# 指标变化曲线 · 指标速查 - 小程序实施计划(P4) **版本:** v1.0 **日期:** 2026-10-09 **状态:** 待执行 **关联设计:** `docs/superpowers/specs/2026-10-09-indicator-change-curve-design.md`(v1.0 已确认) **依赖后端:** `docs/superpowers/plans/2026-10-09-indicator-change-curve-backend.md`(P1-P3 视图 API 必须先完成) --- ## 面向 AI 代理的工作者 本计划由 AI 代理执行。目标:为 uni-app 微信小程序(`cfc-frontend/`,Vue 2 Options API)实现「指标变化曲线页」「指标速查页」「菌群报告详情页变化区块」及通用 `TrendChart` 折线组件。所有 API 依赖后端 P1-P3。 **执行纪律(违反即失败):** 1. **单一写者**:每个任务必须是「写测试 → 运行确认失败 → 实现 → 运行确认通过 → commit」的 TDD 闭环。测试永远先行。 2. **禁止打包**:Agent 禁止执行 `npm run build:mp-weixin` / `npm run dev:mp-weixin`。只做语法/结构校验(`node --check`)、审计(`audit-duplicate-api-calls.js`)、jest 单测。最终打包由用户在 HBuilderX 完成。 3. **前端硬规则硬编码进每一步**: - 禁止可选链 `?.` → 用 `&&` - 禁止 CSS Grid → flexbox - 禁止 `:key` 表达式 → methods 方法 `getItemKey(item)` - 禁止 `:class` 绑定方法调用 → 内联表达式 `:class="'xxx-' + item._cssClass"`;`_cssClass` 由前端 computed/后端字段映射为英文 - 禁止直接 `new Date(string)` → `utils/format.js` 的 `parseDate()` - 禁止中文 CSS 类名 → 英文 - 禁止 `uni.getSystemInfoSync()` → `uni.getWindowInfo()` - 组件接收数组 prop 必须 `|| []` + `Array.isArray` 兜底(三态兼容) - 页面静态标题在 `pages.json` 的 `navigationBarTitleText` 配置,模板内禁止手写 `` 4. **API 去重规范**(`cfc-frontend/AGENTS.md`):`_onLoadFired` 标志位;`utils/api.js` 每个 URL 单一规范入口,别名委托;禁止循环内请求;CI 门禁 `node scripts/audit-duplicate-api-calls.js` 必须 exit 0。 5. **新增 API 封装前查重 API_REFERENCE**;封装改动同步更新 `docs/superpowers/api/API_REFERENCE.md`(接口校准不存在时留待后端实现后补)。 6. **只修改本计划列出的文件**,禁止顺手改动其他文件;不改样式设计体系 token。 --- ## 1. 目标与范围 ### 1.1 本期范围(P4) | # | 交付物 | 文件 | 说明 | |---|--------|------|------| | 1 | 通用折线组件 | `cfc-frontend/components/TrendChart.vue` | 多序列折线 + 数据点 + 数值标注,手写 canvas(参照 `RadarChart.vue` type=2d + DPR + 重试初始化模式),**不引入图表库** | | 2 | 指标变化曲线页 | `cfc-frontend/pages/health/indicator-curve.vue` | 顶部筛选(成员/报告类型/时间范围)+ 曲线区(指标选择器 → TrendChart)+ 变化排名区(Tab:菌群/食材/指标 × 排序:升幅/降幅/绝对幅度;adjacent 相邻两次报告 Δ;按 good_direction 着色) | | 3 | 指标速查页 | `cfc-frontend/pages/health/indicator-quicklook.vue` | 分类 Tab + 分页(≤30/页),每项:名称 · 最新值 · 参考区间 · ↑↓→ 趋势 · 最近报告日期;独立小节五维/七维评分趋势;点击单项 → 跳转曲线页携带 definitionId | | 4 | 菌群详情页变化区块 | `cfc-frontend/pages/health/gut-flora-detail.vue`(改) | 最近两次同类型报告的菌属 Δ,红/绿标注(一期仅此详情页) | | 5 | 页面注册 + API 封装 | `cfc-frontend/pages.json`(改)、`cfc-frontend/utils/api.js`(改) | 注册 2 个新页面路径;新增 trend / change-ranking / quick-lookup / correlation/list 4 个封装 | | 6 | 单测 | `cfc-frontend/__tests__/utils/indicator-change.test.js`、`cfc-frontend/__tests__/api/api.indicator.test.js` | 变化规则纯函数 + API 封装契约 | ### 1.2 明确不做(二期) - DAN / 体检报告详情页的「变化」区块(一期仅菌群报告详情页) - 跨报告类型同曲线叠加对比 - 曲线页知识库弹出框内嵌(复用既有 `health-knowledge-popup` 组件已有能力,如需展示弹出框直接复用,不新增接口) - 关联关系在小程序的展示页(`correlation/list` 封装本期提供,展示留二期;`quick-lookup` 页面不渲染关联区块) ### 1.3 数据流(依赖后端 API 契约) ``` indicator-curve.vue ├─ POST /api/indicator/trend { subjectId, definitionIds:[...] } │ → { items:[{ definitionId, name, points:[{reportDate,value,unit,status}] }] } ├─ POST /api/indicator/change-ranking { subjectId, reportType?, category?, scope:"adjacent", orderBy:"up|down|magnitude", limit:50 } │ → { items:[{ definitionId, name, category, sourceKind, unit, before, after, delta, deltaPct, movement, trendLabel }], overall:{...} } └─ POST /api/family/member/list (成员切换,复用既有封装 getFamilyMemberList) indicator-quicklook.vue ├─ POST /api/indicator/quick-lookup { subjectId, category?, page:1, pageSize:30 } │ → { categories:[{name,total}], items:[{definitionId,name,category,value,unit,refRange,status,trend,trendLabel,latestReportDate,pointsCount}], dimensionScores:[...] } └─ 跳转 → indicator-curve.vue?definitionId=X gut-flora-detail.vue └─ POST /api/indicator/change-ranking { subjectId, reportType:'gut_flora', scope:"adjacent", orderBy:"magnitude", limit:6 } → 渲染最近两次报告的菌属 Δ 区块(红/绿标注) ``` --- ## 2. 技术栈与关键约定 | 项 | 约定 | |----|------| | 框架 | uni-app Vue 2(Options API),微信小程序 | | 图表 | `TrendChart.vue` 手写 canvas,`` + `uni.createSelectorQuery().in(this).select('#'+canvasId).fields({node:true,size:true,rect:true})` + `uni.getWindowInfo().pixelRatio` DPR 适配 + 重试初始化(参考 `components/RadarChart.vue`) | | 单测 | jest(`tests/jest.config.js`:babel-jest + `@/` → `cfc-frontend/` + setup mock uni 全局 + `getCurrentPages`)。运行:`cd cfc-frontend && npx jest __tests__/...` 或 `cd tests && npx jest ../cfc-frontend/__tests__/...`(plan 内统一使用 `cd cfc-frontend && npx jest `) | | 语法校验 | 提取 ` ``` > 注:上述 `selfShowValues`/`selfFmtValue` 是占位,实现时需改为直接引用组件方法(Vue 2 methods 内通过 `this.showValues` / `this._fmtValue` 访问)。**交付时必须替换为实际代码,无占位符。** **验证(通过才 commit)**: ```bash # 提取 ``` > 注:上述脚本中 `loadCurve` 委托 `loadRankingThenCurve` 已满足单 URL 单请求;`onLoad` 只触发 `loadMembers` 一次(`_onLoadFired` 跳过 onShow 首刷),成员选中后并行 `loadCurve`+`loadRanking`。**实现时若发现 `loadMembers` 内部还有默认成员触发多次 ranking,需用 `_loading` 标志防重。** **验证**: ```bash # 提取 ``` **验证**:与 M4 相同的语法校验 + audit 脚本 + jest 已有 M1/M2 保障纯函数。 **commit**:`feat(miniapp): 指标速查页(分类分页 + 五维/七维小节 + 跳转曲线)` --- ### 任务 M6:gut-flora-detail.vue 增加「变化」区块 **目标**:报告详情页展示最近两次同类型报告的菌属 Δ(红/绿标注),一期仅此页。 **文件**:`cfc-frontend/pages/health/gut-flora-detail.vue`(改) **插入位置**:`quick-nav` 区块(L72 后)与 `菌群功能分析`(L74 前)之间插入「变化」区块。 **实现**:在 template 的 quick-nav 之后插入: ```vue 📈 指标变化 与上次报告对比 {{ item.name }} {{ item.before ? item.before.value : '--' }} {{ getChangeStyle(item).arrow }} {{ item.after ? item.after.value : '--' }} {{ formatDelta(item.delta, item.deltaPct) }} {{ getChangeStyle(item).trendLabel }} 查看完整变化曲线 › ``` **script 新增**(data + methods): ```js // data 内新增: // changeItems: [], _changeLoaded: false // methods 内新增: loadChangeBlock: function() { var self = this // subjectId 取当前详情页已加载的 report.subjectId(报告头部已有 personName 等) if (!this.report || !this.report.subjectId) return getIndicatorChangeRanking({ subjectId: this.report.subjectId, reportType: 'gut_flora', scope: 'adjacent', orderBy: 'magnitude', limit: 6 }).then(function(res) { self.changeItems = (res.data && res.data.items) || [] }) }, getChangeStyle: function(item) { return resolveChangeStyle(item.goodDirection, item.movement) }, getChangeKey: function(item, i) { return item.definitionId || i }, goCurve: function() { uni.navigateTo({ url: '/pages/health/indicator-curve?subjectId=' + this.report.subjectId + '&reportType=gut_flora' }) } ``` **接入点**:在该页 `onLoad`/数据加载完成的回调末尾调用一次 `loadChangeBlock()`(在 report 数据就绪后),并遵守 `_onLoadFired` 模式避免 onLoad/onShow 双发。若该页本身已用 `_onLoadFired` 模式,在数据加载函数的尾部追加一次调用即可。 **imports 追加**: ```js import { getIndicatorChangeRanking } from '@/utils/api' import { resolveChangeStyle, formatDelta, colorClassFor } from '@/utils/indicator-change' ``` **验证**:语法校验(node --check 提取 script)+ audit 脚本 exit 0。 **commit**:`feat(miniapp): 菌群报告详情页增加指标变化区块(最近两次对比)` --- ### 任务 M7:pages.json 注册 2 个新页面 **目标**:health 分包注册 `indicator-curve` 与 `indicator-quicklook`,配 navigationBarTitleText。 **文件**:`cfc-frontend/pages.json`(改) **改动**:找到 health 分包(`"root": "pages/health"`)的 pages 数组,追加两项(保持数组末尾不含多余逗号): ```json { "path": "indicator-curve", "style": { "navigationBarTitleText": "指标变化" } }, { "path": "indicator-quicklook", "style": { "navigationBarTitleText": "指标速查" } } ``` **验证**: ```bash # JSON 合法性 node -e "const p=require('./pages.json');const s=p.subPackages.find(x=>x.root==='pages/health');if(!s.pages.some(x=>x.path==='indicator-curve')){throw new Error('missing indicator-curve')}if(!s.pages.some(x=>x.path==='indicator-quicklook')){throw new Error('missing indicator-quicklook')}console.log('pages.json OK')" ``` **commit**:`feat(miniapp): pages.json 注册 indicator-curve / indicator-quicklook` --- ### 任务 M8:API_REFERENCE 同步 + 全量门禁验证 **文件**:`docs/superpowers/api/API_REFERENCE.md`(改) **改动**:在指标模块章节新增 4 项接口记录(若 API_REFERENCE 尚无「指标」章节则新增): | 接口 | 方法 | 请求 | 响应 | 状态 | |------|------|------|------|------| | `/api/indicator/trend` | POST | `{subjectId, definitionIds:[]}` | `{items:[{definitionId,name,points:[{reportDate,value,unit,status}]}]}` | 新增(本期) | | `/api/indicator/change-ranking` | POST | `{subjectId, reportType?, category?, scope, orderBy, limit}` | `{items:[...], overall:{improvedCount,worsenedCount,flatCount}}` | 新增(本期) | | `/api/indicator/quick-lookup` | POST | `{subjectId, category?, page, pageSize}` | `{categories:[], items:[], dimensionScores:[]}` | 新增(本期) | | `/api/indicator/correlation/list` | POST | `{subjectId, definitionId}` | `{relations:[...]}` | 新增(本期,展示二期) | **全量门禁**: ```bash cd cfc-frontend && npx jest __tests__/utils/indicator-change.test.js __tests__/api/api.indicator.test.js --no-coverage node scripts/audit-duplicate-api-calls.js && echo "AUDIT PASS" # 三个新增 .vue 全部 node --check script 块(TrendChart / curve / quicklook / gut-flora-detail) ``` **commit**:`docs(api): 同步指标 4 接口至 API_REFERENCE` --- ### 任务 M9:HBuilderX 打包验证(用户在 HBuilderX 完成,非 Agent) **目标**:HBuilderX 打开 `cfc-frontend/` → 运行到微信开发者工具 → 真机/模拟器人工验证。 **人工验收清单(对应 design.md §9 验收 #1/#2/#3):** - [ ] 上传第二份同类型(gut_flora)报告后,曲线页显示完整前后对比(多数据点曲线) - [ ] 变化排名按幅度排序正确(绝对幅度 / 升幅 / 降幅) - [ ] 变化箭头颜色符合 good_direction:炎症类 lower_is_better 下降=绿;双歧杆菌 higher_is_better 上升=绿 - [ ] 速查页分类分页正常(每类 ≤30 项),含五维/七维小节 - [ ] 速查页点击单项 → 跳转曲线页并展示该指标 - [ ] 菌群详情页「变化」区块显示最近两次 Δ(红/绿),点击「查看完整变化曲线」跳转曲线页 - [ ] 无控制台报错(canvas 初始化成功日志可见) --- ## 5. 自检清单(全计划完成前逐项过) - [ ] 5 个前端文件新建/修改完成、无占位符代码(尤其 TrendChart 的 `selfShowValues` 占位已替换) - [ ] jest 单测全绿:`indicator-change.test.js` + `api.indicator.test.js` - [ ] `node scripts/audit-duplicate-api-calls.js` exit 0(API 去重规范) - [ ] 2 个新 .vue 页面 + 1 个组件 + 改动页面的 script 块 `node --check` 通过 - [ ] `pages.json` JSON 合法且 2 页已注册(任务 M7 校验命令通过) - [ ] 前端硬规则逐条过:无可选链 / 无 CSS Grid / 无 `:key` 表达式 / 无 `:class` 方法调用 / 无中文 CSS 类 / 无 `uni.getSystemInfoSync` / 数组 prop 有兜底 / 静态标题在 pages.json - [ ] `_onLoadFired` 模式在 2 个新页面 + 详情页改动中落实,无 onLoad/onShow 双发 - [ ] `docs/superpowers/api/API_REFERENCE.md` 已同步 4 接口 - [ ] 未触碰计划外文件(除 API_REFERENCE 外) - [ ] commit 历史清晰:M1-M8 每个任务一个 commit - [ ] HBuilderX 人工验收清单已由用户确认(任务 M9) --- ## 6. 风险与缓解 | 风险 | 缓解 | |------|------| | 后端 P1-P3 未就绪时前端联调受阻 | 前端严格按 design.md §6 契约开发;接口先以 mock 数据(本地 fixture)跑通渲染,后端就绪后替换为真实调用(仅替换数据源,不改变页面结构) | | canvas 在部分机型初始化失败 | 参照 RadarChart 的重试机制(5 次 × 60ms)+ `uni.getWindowInfo()` DPR 兜底 | | 曲线页 ranking+trend 两次请求时序 | `onIndicatorTap` 用 `theIndicatorId` 比较防重复;series 每次 `loadTrendFor` 全量覆盖,无竞态 | | 指标候选过多(200+ 菌属) | 曲线页指标选择器取 ranking 前 10 作为候选,不一次性拉全量;速查页分类分页 30/页 | | good_direction 缺失时颜色误判 | `resolveChangeStyle` 对 `neutral` 兜底中性色;后端保证返回 goodDirection | --- ## 7. 依赖与顺序 1. 依赖后端 `2026-10-09-indicator-change-curve-backend.md` 的 P3(视图 API)先行或并行(前端用 mock 先行)。 2. 任务顺序:M1(纯函数)→ M2(API 封装)→ M3(组件)→ M4/M5/M6(页面,可并行)→ M7(注册)→ M8(文档+门禁)→ M9(人工验收)。 3. M4/M5/M6 相互独立,可并行执行(若子代理驱动),但都依赖 M1/M2。 --- ## 8. 完工定义(Definition of Done) - 所有任务 M1-M8 commit 完成,M9 人工验收清单逐项勾选 - jest 单测 + audit + node --check 三门禁全绿 - 与 design.md §7.1 / §9 验收 #1-#3 对齐