版本: 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(P4-P5 管理端 5 个 admin 接口必须先完成;本计划不新增任何后端接口)
本计划由 AI 代理执行。目标:为 Vue 2 + Element UI Web 管理端(cfc-web/)实现「指标关联对配置页」「指标映射管理页」两个管理页面及对应 API 封装。所有接口复用后端计划 P4-P5 提供的 5 个 /api/admin/indicator/* 端点,不新增、不修改任何后端代码。
执行纪律(违反即失败):
src/api/indicator.js 与 src/utils/indicatorSeeds.js 必须配 axios-mock-adapter 单测(先例:cfc-web/src/__tests__/api/guide.test.js)。.vue 页面组件无组件单测先例 → 不做组件级 jest,用语法校验 + 构建门禁替代。npm run build:package.json 的 prebuild 脚本会执行 bump-version + generate-changelog(污染 git 工作区)。构建门禁一律用 npx vue-cli-service build。禁止 npm run serve(占用端口且不产出验证结果)。cfc-web/src/api/indicator.js,组件内禁止直接 axios / this.$http(cfc-web/AGENTS.md ANTI-PATTERNS)。@/utils/request(res.code === 200 才 resolve、401 → clear+login、504/超时/网络错误分文案)。views/admin/indicators/index.vue 的 this.$http + res.code === 0 旧模式(main.js 未注册 $http,属遗留写法,新页面不得沿用)。2026-08-15T14:30:00),页面统一 formatTime(t) { return t ? t.replace('T', ' ').substring(0, 19) : '-' };禁止 new Date(str).toLocaleString()。/api/admin/indicator/* 5 个端点已在后端计划中记录至 docs/superpowers/api/API_REFERENCE.md,本计划仅前端复用,不再改动 API_REFERENCE;前端 url: '/api/admin/indicator' 必须在 src/api/ 下单一封装(W1 审计确认)。admin-page + page-header + el-table border stripe v-loading + el-pagination + SortMixin(参照 views/admin/EnergyRuleManagement.vue 261 行模板)。| # | 交付物 | 文件 | 说明 |
|---|---|---|---|
| 1 | 指标 API 封装 | cfc-web/src/api/indicator.js(新建) |
6 个封装:listIndicatorDefinitions(复用)/ saveIndicatorCorrelation / listIndicatorCorrelations / calibrateIndicatorCorrelations / importIndicatorMappings / listIndicatorMappings |
| 2 | 指标关联对配置页 | cfc-web/src/views/admin/IndicatorCorrelations.vue(新建) |
关联对列表 + 新增/编辑 dialog(A/B 定义搜索框、方向、人工强度、说明、人工锁定开关)+ 单对校准 + 一键校准 |
| 3 | 指标映射管理页 | cfc-web/src/views/admin/IndicatorMappings.vue(新建) |
映射规则列表(sourceType/rawName/aliasText/关联指标/matchType/priority/status)+ 批量导入 dialog(JSON/CSV)+ 未匹配 raw review(待确认 → 选择定义重新 import 确认) |
| 4 | 种子解析工具 | cfc-web/src/utils/indicatorSeeds.js(新建) |
parseSeedsText(text) 纯函数:JSON 数组或 CSV 两列 → seeds 数组 |
| 5 | 路由注册 | cfc-web/src/router/index.js(改) |
在 L799(indicators 路由后)追加 2 条子路由,perm config:health |
| 6 | 菜单回退项 | cfc-web/src/views/Layout.vue(改) |
硬编码 menuItems「健康配置」组(L325-333)追加 2 项,perm config:health(动态菜单优先,此为回退可见性) |
| 7 | 单测 | cfc-web/src/__tests__/api/indicator.test.js、cfc-web/src/__tests__/utils/indicatorSeeds.test.js(新建) |
axios-mock-adapter 契约测试(先例 guide.test.js)+ 纯函数测试 |
delete 端点再加按钮。/api/admin/menu/tree 返回数据);本计划只保证路由可直达 + 硬编码菜单回退可见,动态菜单数据项需另行在管理端菜单配置或后端菜单种子中补充(编排者与用户确认)。tests/e2e/energy-rules-config.spec.js,http://cfc.iwintrue.com/admin),默认不下发。IndicatorCorrelations.vue
├─ POST /api/admin/indicator/correlation/list { page, size } → { code, message, data:{ records:[...], total } }
│ record 字段:id, definitionIdA, definitionIdB, relationType, direction, manualStrength,
│ computedStrength, method, sampleSize, confidence, effectiveStrength, manualLocked,
│ cohortScope, status, computedAt, createdBy, description, sourceNote, createdAt, updatedAt
├─ POST /api/admin/indicator/correlation/save { definitionIdA, definitionIdB, direction, manualStrength,
│ description, manualLocked, sourceNote }
│ role!='admin' → code!=200 message='无权操作';缺 def → 'definitionIdA/definitionIdB 不能为空';IAE → 其 message
├─ POST /api/admin/indicator/correlation/calibrate { pairId? } → message='校准完成,共更新 N 对'
│ pairId 传了但不存在 → message='关联对不存在'
└─ POST /api/admin/indicator/list { domain?, category?, status? } → List<IndicatorDefinition>(定义搜索框数据源,客户端过滤)
IndicatorMappings.vue
├─ POST /api/admin/indicator/mapping/list { page, size, status?, sourceType? } → Page<IndicatorMappingRule>
│ record 字段:id, sourceType, rawName, aliasText, definitionId, matchType, priority, status(0=待确认/1=生效), createdAt, updatedAt
├─ POST /api/admin/indicator/mapping/import { sourceType, seeds:[{rawName, definitionId?}] }
│ → message='导入完成,共 N 条';sourceType 空 → 'sourceType 不能为空';seeds 空 → 'seeds 不能为空'
└─ POST /api/admin/indicator/list (同上,映射编辑时选择 definitionId)
| 事实 | 依据 |
|---|---|
复用 POST /api/admin/indicator/list 做定义搜索框 |
IndicatorAdminController 既有端点(params domain/category/status → List<IndicatorDefinition>),无 keyword → 前端按 name/code 客户端过滤(参照 views/admin/indicators/index.vue 的 filterKeyword 思路,但请求方式用 @/api 封装) |
IndicatorDefinition 字段 |
id, domain, category, code, name, dataType, options, unit, refRange, sortOrder, status, createdAt, updatedAt |
IndicatorCorrelation 字段 |
见 1.3 契约(列表列 = 实体字段;computedAt 即设计文档的 last_calibrated_at) |
IndicatorMappingRule 字段 |
见 1.3 契约(status=0 为 ai_fallback 待确认;status=1 为生效) |
| saveCorrelation 服务语义 | 后端 L2491:A/B 按 code 排序存储(defA 恒为 code 较小者),已存在则更新;单人工强度/方向/说明可空 |
| calibrate 语义 | 样本量 ≥ 30 时 computed 覆盖 manual;未校准 effectiveStrength = manualStrength;返回文本「校准完成,共更新 N 对」 |
| importSeeds 语义 | seed.rawName 必填(null/空跳过);definitionId 缺省 → lazyRegister;registerRule 按 sourceType+rawName upsert(已存在则更新字段)→ 重新 import 单条 = 覆盖确认 |
| request.js 行为 | res.code === 200 → resolve 整个 Result 体;非 200 → Message.error(res.message) + reject;401 → clear + /login;504/超时/网络错误分文案 |
| jest 先例 | cfc-web/src/__tests__/api/guide.test.js:MockAdapter(axios)(axios=@/utils/request)+ mock.onPost(url).reply(200, {code:200, data:...}) + expect(res.code).toBe(200);jest.config.js testMatch 含 **/__tests__/**/*.test.js、moduleNameMapper @/ → src |
| 路由插入点 | cfc-web/src/router/index.js L794-799 = indicators 路由(perm config:health),L800 起为「虚拟服务商团队管理」注释 → 新路由插在 L799 与 L800 之间 |
| 菜单回退插入点 | cfc-web/src/views/Layout.vue「健康配置」组 L325-333(子项 perm 均为 config:health)→ 追加 2 项 |
| 构建门禁 | npx vue-cli-service build(禁 npm run build,prebuild 含 bump-version + generate-changelog 副作用) |
| 语法校验 | 提取 .vue <script> 块写 /tmp/*.mjs 后 node --check(ESM 语法需 .mjs 扩展名) |
每个 W 任务结尾必须
git commit(message 遵循仓库既有风格,如feat(web): ...)。禁止一次 commit 覆盖多个 W。
W1a(红) 新建 cfc-web/src/__tests__/api/indicator.test.js(照抄 guide.test.js 骨架):
beforeEach mock = new MockAdapter(axios);afterEach mock.restore()mock.history.post[i].url)、method=POST、res.code===200 透传(expect(res.code).toBe(200))listIndicatorDefinitions({domain:'gut', status:1}) → /api/admin/indicator/list,断言 data 数组透传saveIndicatorCorrelation({definitionIdA:1, definitionIdB:2, direction:'positive', manualStrength:0.6, description:'x', manualLocked:false, sourceNote:'y'}) → /api/admin/indicator/correlation/savelistIndicatorCorrelations({page:1, size:10}) → /api/admin/indicator/correlation/list,断言 res.data.records 透传calibrateIndicatorCorrelations({}) → /api/admin/indicator/correlation/calibrate;calibrateIndicatorCorrelations({pairId:1}) 断言 body 含 pairId:1importIndicatorMappings({sourceType:'gut', seeds:[{rawName:'乳酸菌', definitionId:3}]}) → /api/admin/indicator/mapping/importlistIndicatorMappings({page:1, size:10, status:0, sourceType:'gut'}) → /api/admin/indicator/mapping/list,断言 body 含 status/sourceTypenpx jest src/__tests__/api/indicator.test.js → 确认红灯(模块不存在,import 报错)W1b(实现) 新建 cfc-web/src/api/indicator.js:
import request from '@/utils/request'
export function listIndicatorDefinitions(params) {
return request({ url: '/api/admin/indicator/list', method: 'post', data: params })
}
export function saveIndicatorCorrelation(data) {
return request({ url: '/api/admin/indicator/correlation/save', method: 'post', data })
}
export function listIndicatorCorrelations(params) {
return request({ url: '/api/admin/indicator/correlation/list', method: 'post', data: params })
}
export function calibrateIndicatorCorrelations(params) {
return request({ url: '/api/admin/indicator/correlation/calibrate', method: 'post', data: params })
}
export function importIndicatorMappings(data) {
return request({ url: '/api/admin/indicator/mapping/import', method: 'post', data })
}
export function listIndicatorMappings(params) {
return request({ url: '/api/admin/indicator/mapping/list', method: 'post', data: params })
}
W1c(绿) 运行 npx jest src/__tests__/api/indicator.test.js → 全绿。
W1d(审计) grep -rn "indicator" cfc-web/src/api/ cfc-web/src/views/ | grep -v __tests__ → 确认 /api/admin/indicator 仅在本封装出现(views/admin/indicators/index.vue 的 this.$http 遗留调用的 URL 已失效,标记为遗留不改,但本计划新增页面一律走新封装)。commit。
W2a cfc-web/src/router/index.js:在 L799(indicators 路由)与 L800(// ========== 虚拟服务商团队管理 ==========)之间插入:
{
path: 'indicator-correlations',
name: 'IndicatorCorrelations',
component: () => import('@/views/admin/IndicatorCorrelations.vue'),
meta: { title: '指标关联配置', perm: 'config:health' }
},
{
path: 'indicator-mappings',
name: 'IndicatorMappings',
component: () => import('@/views/admin/IndicatorMappings.vue'),
meta: { title: '指标映射管理', perm: 'config:health' }
},
W2b cfc-web/src/views/Layout.vue「健康配置」组(L325-333)children 末尾追加:
{ path: '/indicator-correlations', label: '指标关联配置', icon: 'el-icon-connection', perm: 'config:health' },
{ path: '/indicator-mappings', label: '指标映射管理', icon: 'el-icon-sort', perm: 'config:health' },
W2c(审计) 路由 path 去重:grep -n "path: 'indicator" cfc-web/src/router/index.js → 各 1 条;grep -n "indicator-correlations\|indicator-mappings" cfc-web/src/views/Layout.vue → 各 1 条。commit。
views/admin/IndicatorCorrelations.vue)模板参照 EnergyRuleManagement.vue(admin-page + header + table + pagination + dialog + SortMixin)。结构:
模板区:
admin-page > page-header(标题「指标关联配置」+ 右侧「一键校准」按钮 el-button type=warning)el-table border stripe v-loading,列:ID / 指标A(显示定义名称)/ 指标B / 方向(positive→tag「正向」type=success,negative→tag「负向」type=danger)/ 人工强度 manualStrength / 计算强度 computedStrength / 生效强度 effectiveStrength / 样本量 sampleSize / 置信度 confidence / 最近校准时间 formatTime(row.computedAt) / 人工锁定(manualLocked → tag「锁定」)/ 状态(status tag)/ 操作(编辑 · 单对校准)el-pagination(page/total → listCorrelations() 刷新)el-dialog:
el-select filterable clearable :filter-method="filterDefsA"(选项来自 W3 加载的定义数组,显示 ${name}(${code}),value=id)this.$message.error('指标A与指标B不能相同'))el-select(positive/negative,默认 positive)el-input-number(min 0 / max 1 / step 0.05 / precision 2)el-input type=textarea(description)el-switch(manualLocked,开启时后续校准不覆盖人工值)el-input(sourceNote,可空)脚本区:
list、total、page、size、loading、dialogVisible、form(含 definitionIdA/B/direction/manualStrength/description/manualLocked/sourceNote)、definitions、defMap(id→name)、defOptionsA/B(过滤后的候选数组)、editingIdcreated() { this.loadDefinitions(); this.listCorrelations() }loadDefinitions():listIndicatorDefinitions({ status: 1 }).then(res => { this.definitions = res.data || []; 构建 defMap })——status=1 仅取启用定义;失败静默(列表仍可展示,名称显示 #id)listCorrelations():listIndicatorCorrelations({ page, size }).then(res => { this.list = res.data.records; this.total = res.data.total }),.finally 关 loadingopenCreate() / openEdit(row)(回填 form + editingId)subCorrelations():校验 A≠B(if (!form.definitionIdA || !form.definitionIdB) return message('请选择指标A和指标B'))→ saveIndicatorCorrelation({...form}) → message 成功 → 关 dialog → 刷新列表calibrateOne(row):this.$confirm('确定对该关联对执行校准?', ...) → calibrateIndicatorCorrelations({ pairId: row.id }).then(res => this.$message.success(res.message)) → 刷新calibrateAll():this.$confirm('将基于样本数据重新计算全部关联对的有效强度,确定?') → calibrateIndicatorCorrelations({}).then(res => this.$message.success(res.message)) → 刷新defName(id) { return (this.defMap[id] ||#${id}) }filterDefsA(keyword) / filterDefsB(keyword):按 name/code 包含过滤 → defOptionsA/B(keyword 空则回退全量)不做: 删除按钮(无端点);row 直接编辑(走 dialog)。
验证: node --check 提取 script 块(见 §4 命令)→ commit。
views/admin/IndicatorMappings.vue)W4a(红) 新建 cfc-web/src/__tests__/utils/indicatorSeeds.test.js:
parseSeedsText('') → [];null/undefined → []'[{"rawName":"乳酸菌","definitionId":3},{"rawName":"双歧杆菌"}]' → 2 条,definitionId 缺省为 null'乳酸菌,3\n双歧杆菌\n' → 第 1 条 {rawName:'乳酸菌', definitionId:3},第 2 条 {rawName:'双歧杆菌', definitionId:null}rawName 为空的条目跳过npx jest src/__tests__/utils/indicatorSeeds.test.js → 红灯W4b(实现) 新建 cfc-web/src/utils/indicatorSeeds.js:
parseSeedsText(text):trim 空 → [];先尝试 JSON.parse 为数组 → 逐条规范化 {rawName: String(item.rawName||'').trim(), definitionId: item.definitionId || null},rawName 空跳过;JSON 解析失败 → 按行 CSV 解析:line.split(/[,,]/),col0=rawName(trim,空跳过),col1=definitionId(可空,空 → null,非数字 → null)W4c(绿) jest 全绿 → commit。
W4d 页面模板/views/admin/IndicatorMappings.vue:
模板区:
admin-page > page-header(标题「指标映射管理」+ 右侧「批量导入」按钮)el-select(状态:全部 / 生效 status=1 / 待确认 status=0)+ el-input(来源 sourceType)+ 「查询」按钮 → listMappings()el-table border stripe v-loading,列:ID / 来源 sourceType / 原始名称 rawName / 别名 aliasText / 关联指标 defName(definitionId) / 匹配方式 matchType(ai_fallback → tag「AI 建议」warning,其余原样)/ 优先级 priority / 状态(status=0 → tag「待确认」warning,1 → tag「生效」success)/ 创建时间 formatTime(row.createdAt) / 操作(status=0 时显示「确认映射」)el-pagination + SortMixinel-input(sourceType,必填)el-input type=textarea(支持 JSON 数组 或 CSV 两列,placeholder 示例 [{"rawName":"乳酸菌","definitionId":3}] 或 乳酸菌,3)parseSeedsText(text) → importIndicatorMappings({ sourceType, seeds }) → message 显示返回文本 → 关 dialog → 刷新已确认 AI 建议:defName(aliasText 或 definitionId) 若无关联定义则空)el-select filterable(选项=definitions,同 W3 加载与过滤方式)选择目标定义importIndicatorMappings({ sourceType: row.sourceType, seeds: [{ rawName: row.rawName, definitionId: 选中的 id }] }) → 覆盖 status→1 → message 成功 → 刷新脚本区:
list、total、page、size、loading、statusFilter、sourceTypeFilter、importDialogVisible、importForm{sourceType, text}、confirmDialogVisible、confirmRow、confirmDefId、definitions、defMap、defOptionscreated() { this.loadDefinitions(); this.listMappings() }(loadDefinitions 同 W3,可抽公共但本期不新增公共组件,两页各保留一份轻量实现,注释标注来源即可——遵循「只改计划列出文件」)listMappings():listIndicatorMappings({ page, size, status: this.statusFilter || undefined, sourceType: this.sourceTypeFilter || undefined })submitImport():校验 sourceType/text 非空 → parse → if (!seeds.length) return message('未解析到有效条目') → import → 刷新openConfirm(row) / submitConfirm():校验 confirmDefId 非空 → 单条 import → 刷新defName(id) 同 W3验证: node --check 两页 → commit。
src/api/indicator.js、src/utils/indicatorSeeds.js 及两个 .vue 提取的 script 块执行 node --check(命令见 §4),全部 exit 0。npx jest 全量 → 通过(含既有 guide 测试不得被破坏)。npx vue-cli-service build → BUILD SUCCESSFUL(严禁 npm run build)。grep -n "correlation/save\|correlation/list\|correlation/calibrate\|mapping/import\|mapping/list" cfc-web/src/api/indicator.js → 5 个 URL 与 backend 计划 L3113-3117 完全一致。commit(如需)。/admin#/indicator-correlations、/admin#/indicator-mappings;依赖后端 P4-P5 已部署。# 单测(每步 TDD 红/绿)
cd cfc-web && npx jest src/__tests__/api/indicator.test.js --silent
cd cfc-web && npx jest src/__tests__/utils/indicatorSeeds.test.js --silent
cd cfc-web && npx jest --silent # W5 全量
# .vue script 提取 + 语法校验(ESM 需 .mjs 扩展名)
node -e "const fs=require('fs');const s=fs.readFileSync('cfc-web/src/views/admin/IndicatorCorrelations.vue','utf8');const m=s.match(/<script>([\\s\\S]*?)<\\/script>/);if(!m){console.error('no script');process.exit(1)}fs.writeFileSync('/tmp/IndicatorCorrelations.mjs',m[1])"
node --check /tmp/IndicatorCorrelations.mjs
# 同上对 IndicatorMappings.vue
cp cfc-web/src/api/indicator.js /tmp/indicator-api.mjs && node --check /tmp/indicator-api.mjs
cp cfc-web/src/utils/indicatorSeeds.js /tmp/indicatorSeeds.mjs && node --check /tmp/indicatorSeeds.mjs
# 构建门禁(禁 npm run build)
cd cfc-web && npx vue-cli-service build
# 去重审计
grep -n "path: 'indicator" cfc-web/src/router/index.js
grep -n "indicator-correlations\|indicator-mappings" cfc-web/src/views/Layout.vue
grep -rn "indicator/correlation\|indicator/mapping" cfc-web/src/api/ cfc-web/src/views/ | grep -v __tests__
# 契约对齐抽查
grep -n "correlation/save\|correlation/list\|correlation/calibrate\|mapping/import\|mapping/list" cfc-web/src/api/indicator.js
| # | 验收项 | 判定 |
|---|---|---|
| 1 | 管理端可配置关联对(A/B 定义搜索框、方向、人工强度、说明、manualLocked 开关),保存后列表立即可见 | 手测:保存 → 列表刷新出现新行,字段回显正确 |
| 2 | 列表展示 computed_strength / sample_size / confidence / last_calibrated_at(=computedAt 格式化) | 手测:有真实配对数据显示校准强度+样本量;样本 <30 显示人工值(effectiveStrength=manualStrength) |
| 3 | 一键校准 + 单对校准,返回「校准完成,共更新 N 对」 | 手测:message 提示正确,列表刷新 |
| 4 | 映射规则管理:sourceType + rawName → definitionId;批量导入 JSON/CSV;status=0 待确认行可确认(重新 import 覆盖) | 手测:导入返回「导入完成,共 N 条」;确认后行状态 tag 变「生效」 |
| 5 | node --check 通过、npx jest 通过、npx vue-cli-service build 通过、API 去重审计通过(对应设计 §9 #8) |
§4 命令全部 exit 0 |
| 6 | 只修改计划列出文件;git 提交记录每个 W 独立 commit | git show --stat 抽查 |
/api/admin/menu/tree 驱动,硬编码 menuItems 仅回退。若动态菜单数据未含新页面,入口不可见但路由可直达(URL 直连)。需与用户确认是否同步补充菜单配置数据(超出本计划代码范围)。views/admin/indicators/index.vue 遗留 this.$http:其 URL 实为失效旧模式,不在本计划范围;新页面严格走 @/api/indicator.js。若后续清理该页可另立任务。