# T1+T2 · 二诊只读病例页 + 跟踪矩阵 > 来源:~/.claude/plans/6-7-fluffy-sunset.md(T1 + T2 节) > 依赖:T4(已完成 — 提供 PatientInfoCard / PatientCaseCard / blood-thresholds.ts);T3(已完成 — 顺手把 unserved_days 用到 readonly 页头部 badge) > 范围:单 PR,前端 + 后端 + 菜单 SQL,整合进入 fix-diagnosis-20250505 分支 ## 用户故事 二中心医助每天进诊单列表,**双击行**或点**「查看」**按钮 → 直达「只读病例页」: - 患者基本信息 + 完整病例(PatientInfoCard / PatientCaseCard,与接诊台像素级一致) - 跟踪矩阵:日期为列、6 项指标为行(血糖|血压|西药|胰岛素|饮食|运动),同日血糖三段挤一格,超阈值红字 + ↑ - 顶部 hero:未服务 X 天的红/橙/绿 badge ## 后端 ### 接口 `GET /tcm.diagnosis/readonlyDetail?id=:diagnosisId` **Validate**:`DiagnosisValidate::sceneReadonlyDetail` → `only(['id'])` + `id` 必填且 `checkDiagnosis` **Logic** `DiagnosisLogic::readonlyDetail($params, $adminId, $adminInfo)`: 1. 校验诊单存在 2. 数据权限:医助 (role_id=2) 仅看 `assistant_id == self`;DataScope 启用时 `assistant_id IN visibleAdminIds` 3. 拼装 `diagnosis`(复用 `self::detail` + `AppointmentLogic::enrichDiagnosisLabels`) 4. 取最近一条挂号(用于 PatientInfoCard 显示挂号信息);没有就给个默认空对象 5. 取最近 30 天的 blood / diet / exercise 三类记录 6. 取医生备注(DoctorNoteLogic) 7. 计算 unserved_days(= today − MAX(blood.record_date) 天) 8. 返回:`{ appointment, diagnosis, blood_records, diet_records, exercise_records, doctor_notes, unserved_days, last_blood_record_at }` **Controller**:`DiagnosisController::readonlyDetail()` thin wrapper **AppointmentLogic 小改**:`enrichDiagnosisLabels` 由 `private` 改 `public static`,便于 DiagnosisLogic 复用。其它行为不变。 **菜单 SQL**:`server/sql/1.9.20260420/add_diagnosis_readonly_menu.sql` —— 注册 `tcm.diagnosis/readonlyDetail` 权限节点。 ## 前端 ### 路由 `/tcm/diagnosis-readonly?id=:diagnosisId` — 注册在 `admin/src/router/routes.ts` 的 **constantRoutes**(不依赖菜单驱动),用 `LAYOUT` 包裹保留侧边栏;`meta.hidden=true` 不显示在导航;`meta.activeMenu='/tcm/diagnosis'` 高亮诊单父菜单。 > 实践教训:admin 的动态路由是后端菜单表驱动的(`createRouteRecord` 只看 menu API);因此「隐藏路由」**不能仅靠不加菜单 SQL** 解决,否则路径根本没注册到 vue-router。最稳的做法是写 constantRoutes 静态路由 + 用与父菜单不同的路径前缀(这里用连字符 `-readonly` 而非 `/readonly` 子路径,避免和动态菜单同前缀互相影响)。 ### 入口(在 diagnosis/index.vue 上) - 操作列**最前**新增 `查看` → `router.push({ path: '/tcm/diagnosis/readonly', query: { id: row.id } })` - `` 双击同样跳转 - 操作列原有按钮加 `@click.stop`(避免冒泡触发双击) ### 新建 `admin/src/views/tcm/diagnosis/readonly.vue` ``` ┌─ Hero:返回 + 患者姓名 + 「未服务 X 天」 badge(color 同 T3) ├─ ├─ ├─ └─ (可选;先复用 reception 的样式) ``` ### 新建 `admin/src/views/tcm/diagnosis/components/TrackingMatrix.vue` Props: ```ts { diagnosisId: number patientId?: number age?: number | null // 阈值依赖 bloodRecords: BloodRecord[] dietRecords: DietRecord[] exerciseRecords: ExerciseRecord[] } ``` 渲染规则: | 列 | 来源 | cell | |---|---|---| | 日期 (fixed left) | union(blood/diet/exercise.record_date) desc 去重 | `MM-DD(周X)` | | 血糖 | bloodRecords by date | `空 6.5 餐 8.9 其他 7.2`,缺值 `—`;任一段超阈值 → 红字 + ↑ | | 血压 | bloodRecords by date | `135/90`;超 → 红 + ↑ | | 西药 | bloodRecords.western_medicine | tooltip 全文,cell 截断 | | 胰岛素 | bloodRecords.insulin | 同上 | | 饮食 | dietRecords by date | `已打卡` / `—`;点 cell 弹出当日早/午/晚 | | 运动 | exerciseRecords by date | `已打卡 · 30min · 中强度` / `—`;点弹详情 | 时间窗:`7 / 30 / 90 / 365 / 自定义`(默认 30);切换时由父组件重新拉数据或客户端过滤。**本期实现简化**:父组件一次拉 30 天,组件本地过滤切换的窗口。 CSS 类:`.matrix-table` `.cell-blood` `.cell-bp` `.cell-empty` `.cell-high` `.cell-up`(红字 + ↑),文字截断/tooltip 走 el-table 的 `show-overflow-tooltip`。 阈值判定:`@/utils/blood-thresholds` 的 isHigh*(T4 提供)。 ### API `admin/src/api/tcm.ts` 新增: ```ts export function diagnosisReadonlyDetail(params: { id: number }) { return request.get({ url: '/tcm.diagnosis/readonlyDetail', params }) } ``` ## 不做(边界) - 不做评论 / 编辑 / 删除按钮 — 整页**严格只读** - 不引入 `useRouter` 之外的全局状态(页面无状态) - 不做日期窗口的服务端拉取(窗口切换走前端过滤),后续真有量再做 - 不做表头排序(矩阵本身是日期 desc,无意义) - 不在菜单里展示路由(不配 system_menu type='C') - DoctorNote 区块复用 reception 的 NoteTimeline 但不绑 refresh 事件(只读) - 跟踪矩阵不展示「备注」字段(跟踪场景看血糖/血压数值即可) ## 验收 1. 在诊单列表点「查看」 / 双击行 → 跳到 readonly 页 2. PatientInfoCard / PatientCaseCard 视觉与接诊台一致(T4 已确保) 3. 跟踪矩阵:同一天血糖三段挤一格,红色 + ↑ 与 reception 视觉一致 4. 没有任何打卡时矩阵显示空状态(`el-empty`) 5. 编辑/删除按钮全程不存在 — **页面纯只读** 6. 越权访问 → 提示无权限,无 500 7. 时间窗 7/30/90/365 切换有效 8. 操作列其它按钮(编辑/删除/指派)的点击不会触发跳转(`@click.stop`)