--- title: 从模拟数据到接口化 — heart-rate.vue 到 physical.vue 的逐项差异与迁移细节 date: 2025-11-12 --- ## 概要 本文件对比并总结了 `src/pages/patient/health/details/heart-rate.vue`(当前仍使用模拟数据)与 `src/pages/patient/health/details/physical.vue`(已改为调用后端接口)的改动点,力求捕捉每一个显性与隐性差异,包括结构、数据流、交互、边界检查、UI 细节与工程实践层面的改进。 目的: - 帮助开发者理解如何将页面从本地随机/静态数据迁移到真实接口数据。 - 列出可复用的实践与需要注意的陷阱(例如时区/ISO 时间、错误处理、登录态处理、canvas 初始化等)。 > 说明:文中引用的文件基于工作区当前文件内容对比生成,所有路径均以项目根目录为基准。 --- ## 高层差异概览(先读这里) - 数据来源: - `heart-rate.vue` 使用 `generateMockRecords()` 在前端生成随机记录; - `physical.vue` 通过 `uni.request` 向 `https://wx.baiyun.work/physical/list` 获取记录,并对新增/删除也调用对应 API(`/physical/add`、`/physical/delete`)。 - 认证与登录处理: - `physical.vue` 在请求前将 `token` 从 `uni.getStorageSync('token')` 添加到 `Authorization` header;对 401 响应会清除本地 token 并 `uni.reLaunch` 到登录页; - `heart-rate.vue` 没有任何认证逻辑(因为使用本地数据)。 - 时间与周期范围计算: - `physical.vue` 精确计算 `startTime` 和 `endTime`(结束时间设置为当天 23:59:59.999)并使用 ISO 字符串发送给后端,避免跨日/时区错误; - `heart-rate.vue` 只在本地生成随机日期,无需与后端对齐。 - 数据格式化与容错: - `physical.vue` 将后端字段 `height`/`weight`/`bmi` 映射为数字、处理空值或不合法数据、优先使用后端 bmi 否则在客户端计算并保留 1 位小数; - `heart-rate.vue` 内部记录已是干净的 JS 对象。 - Chart 初始化与生命周期: - `physical.vue` 将 chart 的创建延迟,支持按 `selectedMetric` 切换并在变化时销毁重建;`createUChart` 的调用使用动态 series 配置; - `heart-rate.vue` 在 `onMounted` 直接基于固定的 `createUChart` 配置创建单序列图表。 - 用户输入与校验: - `physical.vue` 在添加记录时校验未来日期、校验必填字段、对 BMI 阈值弹窗预警,并在成功后重新拉取并重建图表; - `heart-rate.vue` 仅在前端校验并添加本地数据,同样做了未来日期检查,但没有 API 相关的错误处理。 --- ## 逐行/逐节关键差异(详尽提取) 下面按关注点罗列两个文件的差异,并尽可能抓住“看似微不足道但重要”的细节: ### 1) 引入与声明 - `physical.vue` 引入额外的模块: - `onShow`(用于页面显示时做 token 校验); - `useUChart` 与 `getCurrentInstance` 的获取相似,但 `physical.vue` 里对 `vm`/chart 实例管理更小心(延迟创建、销毁、rebuild、update); - `heart-rate.vue` 的 chart 是直接创建: - createUChart(...) 返回单一 `hrChart`,没有在 metric 切换或不同 series 情况下销毁重建的逻辑。 为什么重要:延迟创建和可重建的图表可防止 canvas/DOM 未就绪导致的异常,且支持运行时切换展示序列(例如身高/体重/BMI)。 ### 2) Canvas 和尺寸计算 - 两者都定义 `canvasWidth`、`canvasHeight` 并在 `onMounted` 使用 `getWindowWidth()` 计算实际大小(根据宽高比),但: - `physical.vue` 的默认 canvasHeight 是 320,心率是 280,均通过比例计算:physical 使用 `Math.round((320 / 750) * width)`。 - `physical.vue` 在 `onMounted` 包裹在 setTimeout(…,500) 并 await nextTick(),然后再创建 chart;`heart-rate.vue` 同样有延迟但较少的后续复杂性。 细节说明:使用 setTimeout+nextTick 的双保险(确保样式与容器尺寸完成)在不同平台(H5/小程序)能减少 canvas 绘制错位的概率。 ### 3) 数据获取(核心差异) - `physical.vue` 中 `fetchRecords()`: - 区分月视图与周视图,构造 `startTime` 与 `endTime`;对于月视图,`endTime` 会被设为当月最后一天并设置时分秒为 23:59:59.999; - 显示 `uni.showLoading` / `uni.hideLoading`(放在 try/finally),提升加载体验并保证隐藏调用; - 在请求时添加 `Authorization: Bearer ${token}`,并对 401 做统一处理(清 token + 跳转登录); - 处理后端返回结构:当 res.data.code===200 时取 data.records;映射时对 height/weight 做 Number() 转换并处理 BMI(优先使用后端 bmi,否则自己根据身高体重计算并保留 1 位小数); - 成功后设置 records.value 并调用 rebuildChart()。 - `heart-rate.vue` 使用 `generateMockRecords()`: - 根据当前月份或周随机生成若干条记录作为页面数据; - 在切换 period 时直接重新生成并更新 records; - 没有网络请求、loading、鉴权或 401 处理。 关键点:真实接口需要考虑网络异常、鉴权失效、后端字段缺失与类型不一致问题,并保证日期范围和时区不会导致漏取或重复取数据。 ### 4) 后端响应字段的健壮处理 - `physical.vue` 对后端字段做了以下细致处理: - `height`/`weight` 可能为 null 或空字符串,先检查再 Number(),默认 0; - `bmi` 如果存在且能 parse 为 number,则取一位小数;如果不存在且 h/w 有效则本地计算; - id 强制转为字符串 `String(item.id)`,并使用 `formatDisplayDate(new Date(item.measureTime))` 格式化日期以统一前端展示。 为什么重要:后端可能返回字符串类型或数值,也可能遗漏字段,前端需要统一并容错。强制 id 为字符串避免 v-for keyed 类型不一致导致的渲染问题。 ### 5) 增删逻辑与 API 调用 - `physical.vue`: - `confirmAdd()` 会先校验输入,提示 BMI 超标(bmi >= 25)通过 `uni.showModal`,但无取消分支(showCancel: false); - then 使用 `uni.request` POST 到 `/physical/add`,携带 Authorization; - 判断 401、判断返回 code===200,成功后 `uni.showToast`,`closeAdd()`、`await fetchRecords()` 并 rebuildChart; - `confirmDeleteRecord(id)` 弹窗确认后通过 `/physical/delete` 接口删除,处理 401 和返回 code,成功则从 records 中剔除并重建图表。 - `heart-rate.vue`: - `confirmAdd()` 只在本地 `records.value = [item, ...records.value]`(若在当前周期则加入),提示并 rebuildChart; - 删除仅修改本地数组。 注意:physical 的实现处理了网络返回错误并保证 UI 状态(loading、弹窗、toast)与后端一致。 ### 6) 页面显示/路由/参数读取 - `physical.vue` 包含 `initMetricFromRoute()`:尝试从 `getCurrentPages()` 的 options 中读取 `metric` 参数以设置初始 `selectedMetric`(all/height/weight/bmi),从而决定 chart 的 series 配置; - include 对 metric 的合法性判断并将其赋给 selectedMetric; - `heart-rate.vue` 没有 route-driven metric 切换逻辑。 细节:从 route 读取参数允许外部页面 link 到该页面并预设 metric,提高灵活性。getCurrentPages 在不同平台的可用性也被安全地封装(try/catch)。 ### 7) watch / reactive 更新策略 - `physical.vue`: - watch current 与 records 的数组变化(deep: true),并在变化时 setTimeout(100) 调用 bpChart.update,添加 try/catch 以避免 chart update 异常阻断; - watch selectedMetric 并在变化时销毁旧 chart、创建新 chart、draw 新数据; - `heart-rate.vue`: - 对 current 与 records 使用 watch 并触发 hrChart.update,同样延迟 100ms,但没有 metric 切换重建逻辑。 这说明 physical 的实现考虑了多序列、实例生命周期和潜在的异步异常。 ### 8) UI 小细节与样式差异 - 两文件的 template/样式非常接近,但 `physical.vue` 在 header 的 period-controls 内额外包含了 `metric-toggle`(按钮组用于切换全部/身高/体重/BMI)并对按钮 click 使用 `.prevent` 来阻止 picker 的冒泡或默认行为; - `chart-canvas` 的样式在两处有相同的 margin-left: -10rpx、background-color 与 display: block,但高度与 canvasHeight 初始化不同(physical 320rpx vs heart-rate 280rpx)。 微小但关键的细节:metric-toggle 使用 @click.prevent 可避免点击时触发 picker 的默认选择行为(这在嵌套交互时会引发误操作)。 ### 9) 边界和时间相关校验 - `physical.vue` 和 `heart-rate.vue` 都使用 `isAfterTodayDate()`、`isMonthAfterToday()`、`isWeekAfterToday()` 来阻止选择或导航到未来日期;但: - `physical.vue` 在 nextPeriod() 时提前检查并触发 `uni.showToast` 并 return,避免设置 current 为未来值并触发不必要的请求; - `onPickerChange()` 在 physical 中会将 pickerValue 修正为当前月份(today)并显示 toast,说明更友好地处理了用户选择未来月份的行为。 小细节:替换为今天月份时会同时更新 pickerValue,使 UI 和内部 state 保持一致,防止 picker 与 current 不一致的情况。 ### 10) 日志、异常处理与容错 - `physical.vue` 在多处使用 try/catch 并在控制台打印警告(console.warn / console.error),但对用户则显示 toast 或跳转登录以处理常见问题; - `heart-rate.vue` 较少对异常做显式捕获(因为只有本地操作),主要用 try/catch 在 chart destroy 等地方包裹。 重要性:生产环境要把异常对用户友好地提示并在控制台保留足够信息用于排查。 --- ## 可直接复用的模式 & 建议(提炼) 以下模式在 `physical.vue` 中体现,建议在其他页面迁移到接口时复用: 1. 请求范围与时间边界:后端请求传递 ISO 时间,结束时间设为 23:59:59.999,避免跨日遗漏。 2. 显示弱提示:请求前调用 `uni.showLoading()`,finally 中 `uni.hideLoading()`。 3. 401 统一处理:若 statusCode === 401,则清理 token/role 并 reLaunch 到登录页。 4. 后端字段健壮化:统一把数字字段 Number() 化并提供默认值,优先使用后端计算字段(如 bmi),否则客户端计算并格式化小数位。 5. Chart 生命周期管理:为每种展示配置创建 chart,切换时销毁旧实例并创建新实例,避免状态混用。 6. UI/交互小细节:使用 `@click.prevent` 阻止 picker 与父容器的冲突;修改 pickerValue 与 current 保持同步。 --- ## 验证清单(迁移后需人工或自动验证) 1. 切换月份/周时,网络请求的 startTime/endTime 与预期一致(检查接口日志或抓包)。 2. 当后端返回空 height/weight 或字符串类型时,前端能正常展示并且不崩溃。 3. 添加/删除数据时,token 失效(401)能够触发登录页跳转。 4. 切换 metric(全部/身高/体重/BMI)时 chart 能正确重建并绘制数据。 5. 在 H5、微信小程序、Android/iOS 端均能正确计算 canvas 尺寸并正确绘制(注意像素比)。 --- ## 后续改进建议(非必须但推荐) - 将 API 调用抽离为单独的 service 层(例如 `src/api/physical.ts`),方便重用与单元测试。 - 增加对网络错误的重试/退避策略(尤其是在数据请求失败导致图表无数据时)。 - 对 chart 的 draw/update 方法增加参数校验与幂等性保证,避免并发更新竞争条件。 - 在调用 `uni.request` 前检查 token 有效性(本地),并在多个页面之间共享统一的鉴权中间件。 --- ## 结语 本文档在根目录 `docs/` 下保存为 `从模拟数据到接口化-心率到体格数据迁移详解.md`,目标是为团队成员提供一份完整且可操作的迁移参考。若需要我可以将其中的建议抽象成可复用的代码片段(如 `api` 层、utils 或 chart 管理器),并为 `heart-rate.vue` 实现接口化改造。