从零构建「融合健康风险评估的智能健康管理系统」1. 项目缘起作为一名前端开发者我一直想做一个有完整前后端交互、能真正解决实际问题的全栈项目。健康管理这个方向让我产生了兴趣——普通人去医院体检后拿到一堆指标数据却缺乏一个直观的工具来解读这些数字背后意味着什么。这个项目的核心目标很简单用户录入健康数据 → 系统给出五维健康评分 → 图表直观呈现 → 个性化方案推荐。同时包含管理员后台支持用户、档案和报告的管理。技术栈选择了 Vue 3 TypeScript Element Plus ECharts Pinia json-server下面会逐一说明why。2. 技术选型前端框架Vue 3 Composition API选择 Vue 3 而不是 React 有两个原因一是script setup语法比 React Hooks 更简洁逻辑复用的心智负担更低二是配套的 Pinia Element Plus 生态对中文项目的支持更好。TypeScript 严格模式从一开始就开启了strict: true并且vue-tsc --noEmit始终保持零错误。一个值得提的点开启noUnusedLocals和noUnusedParameters后未使用的函数参数必须用_前缀抑制检查这在回调函数中经常遇到。UI 组件库Element Plus企业级中后台最成熟的 Vue 3 组件库。el-table、el-form、el-dialog几乎覆盖了该项目 90% 的 UI 需求。唯一踩过的坑是Tag 的type属性合法值是success | warning | danger | info写成空字符串不会报错但样式完全不生效——后面 Bug 复盘会细说。图表库ECharts雷达图展示五维健康评分对比柱状图按颜色分级折线图加警戒线追踪指标趋势。ECharts 配置式 API 对复杂图表支持非常好但需要特别关注生命周期管理——组件卸载时必须dispose()销毁实例否则内存泄漏。状态管理Pinia persist相比 VuexPinia 不需要 mutationaction 直接改 stateTypeScript 类型推断也更完整。userStore使用pinia-plugin-persistedstate将 token 和 userInfo 持久化到 localStorage刷新页面不丢登录态。Mock 后端json-server零配置即可将db.json转化为完整 REST API。在此基础上通过server.cjs自定义中间件实现了登录鉴权、问卷提交和五维评分引擎等 json-server 不提供的功能。这种方式让前后端分离开发完全独立。3. 核心功能拆解3.1 路由守卫Token 角色双重校验// src/router/index.tsrouter.beforeEach(async(to,_from,next){consttokengetToken()if(token){// 已登录访问 /login重定向到工作台if(to.path/login){next(/dashboard)return}constuserStoreuseUserStore()// 刷新后 userInfo 丢失从 API 恢复if(!userStore.userInfo){try{awaituserStore.fetchUserInfo()}catch{userStore.resetState()next(/login?redirect${to.path})return}}// 角色权限校验if(to.meta.roles){constrequiredRolesto.meta.rolesasstring[]if(!requiredRoles.includes(userStore.userInfo!.role)){next(/dashboard)return}}next()}else{// 未登录只允许白名单路由if(whiteList.includes(to.path)){next()}else{next(/login?redirect${to.path})}}})这段代码覆盖了五种场景已登录访问登录页、刷新后恢复用户信息、token 失效回登录、角色越权拦截、未登录拦截。所有鉴权逻辑集中在一处新增路由只需配置meta.roles不用在每个页面写重复判断。3.2 Axios 封装告别裸请求// src/api/request.tsconstserviceaxios.create({baseURL:import.meta.env.VITE_API_BASE_URL,timeout:15000,})// 请求拦截器 — 自动注入 Tokenservice.interceptors.request.use((config){consttokengetToken()if(token){config.headers.AuthorizationBearer${token}}returnconfig})// 响应拦截器 — 统一错误处理service.interceptors.response.use((response){const{code,message,data}response.dataif(code!200){ElMessage.error(message||请求失败)returnPromise.reject(newError(message))}returndata},(error){if(error.response?.status401){// Token 过期踢回登录constuserStoreuseUserStore()userStore.resetState()router.push(/login)}ElMessage.error(error.message||网络错误)returnPromise.reject(error)})baseURL从环境变量读取开发环境/api走 Vite 代理到localhost:3001生产直接指向后端地址。项目早期有一个 bug 是某删除功能用了原生fetch绕过了拦截器导致没有 Token 注入修复后明确了一条铁律所有请求必须走 Axios 实例API 函数统一在src/api/目录下封装。3.3 动态问卷系统问卷数据的 100 道题存储在db.json的questions表中包含 5 个分类。前端不使用硬编码的步骤标题而是用computed从数据中动态提取constcategoriescomputed((){constsetnewSet(questions.value.map(qq.category))returnArray.from(set)})步骤切换使用v-show而不是v-if——v-if会销毁和重建 DOM用户切到第二步再回到第一步填的内容全没了。v-show只是改displayDOM 一直存在v-model绑定保持有效。更进一步AppLayout.vue中用keep-alive包裹router-view用户填写问卷时切去别的页面再回来问卷状态完全保留。3.4 五维评分引擎基于临床参考值设计的线性加权模型所有实现集中在server.cjs的evaluateScores()函数中functionevaluateScores(systolicBP,diastolicBP,bloodSugar,cholesterol,bmi,sleepHours,exerciseFreq){constcardioMath.max(10,100-(systolicBP-120)*0.8-(diastolicBP-80)*0.5)constmetabolicMath.max(10,100-(bloodSugar-5)*20-(cholesterol-4.5)*15)constnutritionMath.max(10,100-Math.abs(bmi-22)*6)constmentalMath.max(10,80-Math.abs(7-sleepHours)*10)constlifestyleMath.max(10,60(exerciseFreq1?exerciseFreq*8:-20))return{cardio:cardio.toFixed(1),metabolic:metabolic.toFixed(1),nutrition:nutrition.toFixed(1),mental:mental.toFixed(1),lifestyle:lifestyle.toFixed(1),}}functioncalcOverall(scores){return(scores.cardio*0.3scores.metabolic*0.25scores.nutrition*0.2scores.mental*0.15scores.lifestyle*0.1).toFixed(1)}五个维度的指标越偏离参考值扣分越多每维度有 10 分下限保护。综合分加权后映射为四级≥85 优秀 / ≥70 良好 / ≥50 一般 / 50 较差。这个模型的定位很明确——它不是机器学习而是基于医学指南参考值的线性评分。它的优势在于可解释性强每个维度的公式和权重一目了然不需要训练数据结果稳定。3.5 ECharts 报告可视化报告页包含三张图表雷达图五维评分对比一眼看出短板柱状图各维度得分按等级着色绿/蓝/橙/红折线图近 6 个月血压、血糖、胆固醇趋势加markLine警戒线最关键的细节是生命周期管理onMounted(async(){awaitnextTick()initRadarChart()initBarChart()initLineChart()window.addEventListener(resize,handleResize)})onBeforeUnmount((){window.removeEventListener(resize,handleResize)if(debounceTimer)clearTimeout(debounceTimer)radarChart?.dispose()barChart?.dispose()lineChart?.dispose()})注意两点initCharts()前加await nextTick()确保 DOM 渲染完resize事件用 200ms debounce 避免频繁重绘。报告数据在刷新后会丢失Pinia store 是内存态因此onMounted中会检测 store 是否为空为空则从 API 重新拉取保证刷新后图表可恢复。3.6 管理后台管理端包含 4 个页面仪表盘统计卡片 最近报告 系统公告、用户管理CRUD 搜索 角色筛选 分页、档案管理按用户名搜索 分页 删除、报告管理等级筛选 详情弹窗 删除。所有管理路由配置meta: { roles: [admin] }路由守卫统一做角色校验非管理员访问自动跳回工作台。4. 踩坑复盘整个开发过程中修了 14 个 bug挑 4 个最典型的来说。4.1 登录页暴露测试凭据现象登录表单预填了admin / admin123页面底部直接展示所有测试账号和密码。根因开发阶段为方便调试加了预填后面忘了移除。这是典型的安全意识问题——测试数据只应留在文档或数据库初始化脚本中绝不能出现在面向用户的 UI 上。修复清空表单默认值底部改为平台描述文字。4.2 Vue 模板 Slot Props 解构错误现象管理后台报告详情弹窗中的评分列渲染报错r.score is undefined。代码是这样的el-table-columnpropscorelabel评分template#default{ r }el-tag{{ r.score }}/el-tag/template/el-table-column根因Element Plus 的el-table-column的 slot props 结构是{ row, column, $index, store }不存在r这个属性。{ r }等价于{ r: r }解构出undefined。修复#default{ r }→#default{ row: r }。冒号左边是框架给的属性名右边是自定义别名两者不可混淆。4.3 全局 CSS 覆盖 Element Plus 布局现象侧边栏和主内容区垂直堆叠右侧大片空白——但只在特定情况下出现。根因global.scss中设置了flex-direction: column覆盖了 Element Plus 的el-container内置逻辑检测到直接子el-aside时自动水平布局。全局样式的一个属性影响了大面积页面排查时很难第一时间想到是 CSS 的问题。修复移除全局 CSS 中的display: flex和flex-direction交给 Element Plus 内部处理。4.4 Object.assign 共享引用 bug现象新增健康档案时要填家族病史数组。第一次新增正常第二次新增时发现家族病史字段里残留了上次填的数据。代码是这样的constdefaultForm{height:170,weight:65,familyHistory:[],...}functionopenDialog(){Object.assign(form,defaultForm)// form 和 defaultForm 共享同一份引用}根因Object.assign是浅拷贝。form.familyHistory直接指向了defaultForm.familyHistory用户编辑时的修改会反向污染默认值。修复用工厂函数每次返回新对象functiongetDefaultForm(){return{height:170,weight:65,familyHistory:[],...}}5. 架构设计亮点defaultForm 工厂函数防共享引用。新增/编辑对话框的默认值每次用函数返回新对象避免Object.assign浅拷贝导致的引用污染。Axios 统一封装 环境变量驱动。所有请求走 Axios 实例请求拦截器自动注入 Token响应拦截器统一处理 401/403/500baseURL 从环境变量读取。keep-alive 状态保持。AppLayout.vue用keep-alive包裹router-view用户填写问卷时切到其他页面再返回表单状态完整保留。动态问卷步骤。步骤标题不是硬编码而是用computed从 100 道题中提取不重复分类名。数据变了步骤自动更新UI 和数据始终保持同步。6. 项目展望当前项目已能完整跑通用户端和管理端的全部流程TypeScript 零错误。后续几个方向测试这是最大短板。工具函数calcBMI、formatDate、评分引擎、Pinia Store 都需要单元测试核心流程需要 E2E 冒烟测试。计划引入 Vitest Playwright。真实后端目前用 json-server 做 mock可以迁移到 Express/Koa MySQL评分引擎保持不变。Token 自动刷新当前 token 24 小时过期后直接踢回登录。可改为 access token2h refresh token7d401 时静默续期。骨架屏 虚拟滚动大数据量场景下的加载体验优化。7. 项目地址GitHubhttps://github.com/DWZZ-666/health-management-system技术栈Vue 3 TypeScript Vite Element Plus ECharts Pinia json-server如果你也在做类似的 Vue 3 全栈项目希望这篇文章能给你一些参考。欢迎 Star 和 Issue 交流。