6.0 KiB
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):
- 校验诊单存在
- 数据权限:医助 (role_id=2) 仅看
assistant_id == self;DataScope 启用时assistant_id IN visibleAdminIds - 拼装
diagnosis(复用self::detail+AppointmentLogic::enrichDiagnosisLabels) - 取最近一条挂号(用于 PatientInfoCard 显示挂号信息);没有就给个默认空对象
- 取最近 30 天的 blood / diet / exercise 三类记录
- 取医生备注(DoctorNoteLogic)
- 计算 unserved_days(= today − MAX(blood.record_date) 天)
- 返回:
{ 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 上)
- 操作列最前新增
<el-button link>查看</el-button>→router.push({ path: '/tcm/diagnosis/readonly', query: { id: row.id } }) <el-table @row-dblclick="goReadonly">双击同样跳转- 操作列原有按钮加
@click.stop(避免冒泡触发双击)
新建 admin/src/views/tcm/diagnosis/readonly.vue
┌─ Hero:返回 + 患者姓名 + 「未服务 X 天」 badge(color 同 T3)
├─ <PatientInfoCard :apt :diag />
├─ <PatientCaseCard :apt :diag />
├─ <TrackingMatrix>
└─ <NoteTimeline>(可选;先复用 reception 的样式)
新建 admin/src/views/tcm/diagnosis/components/TrackingMatrix.vue
Props:
{
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 新增:
export function diagnosisReadonlyDetail(params: { id: number }) {
return request.get({ url: '/tcm.diagnosis/readonlyDetail', params })
}
不做(边界)
- 不做评论 / 编辑 / 删除按钮 — 整页严格只读
- 不引入
useRouter之外的全局状态(页面无状态) - 不做日期窗口的服务端拉取(窗口切换走前端过滤),后续真有量再做
- 不做表头排序(矩阵本身是日期 desc,无意义)
- 不在菜单里展示路由(不配 system_menu type='C')
- DoctorNote 区块复用 reception 的 NoteTimeline 但不绑 refresh 事件(只读)
- 跟踪矩阵不展示「备注」字段(跟踪场景看血糖/血压数值即可)
验收
- 在诊单列表点「查看」 / 双击行 → 跳到 readonly 页
- PatientInfoCard / PatientCaseCard 视觉与接诊台一致(T4 已确保)
- 跟踪矩阵:同一天血糖三段挤一格,红色 + ↑ 与 reception 视觉一致
- 没有任何打卡时矩阵显示空状态(
el-empty) - 编辑/删除按钮全程不存在 — 页面纯只读
- 越权访问 → 提示无权限,无 500
- 时间窗 7/30/90/365 切换有效
- 操作列其它按钮(编辑/删除/指派)的点击不会触发跳转(
@click.stop)