2026-08-26.md 27 KB

2026-08-26 工作日志

护士执行医嘱 - 跳过加原因弹窗

  • 文件:src/views/elderly/elder/doctor-orders/execute-doctor-order/index.vue
  • 改动:原「跳过」用 message.confirm 简单确认,改为独立 el-dialog 弹窗(标题"跳过执行")
    • 弹窗含:长者·床位 alert 提示(含医嘱内容)+ 计划时间 + 「跳过原因」必填 textarea(maxlength 200 / show-word-limit)
    • confirmSkip:原因 trim 后非空才放行,调 executeDoctorOrder(payload) 传 status: 2 + remark
    • 原因留痕写入 DoctorOrderExecution.remark,已跳过记录可在「查看」弹窗(ExecuteDialog)看到
  • ExecuteDialog.vue 未改(其 :disabled="isView" 下备注 textarea 已能展示已跳过记录的 remark)
  • 注意:handleSkip 不再用 message.confirm;executeDoctorOrder 同时承担"执行(status=1)"与"跳过(status=2)"两种写入,靠 payload.status 区分
  • ESLint 0 错误

修复顶部"打印执行单"无效 + 操作列加单条打印

  • 文件:src/views/elderly/elder/doctor-orders/execute-doctor-order/index.vue
  • 根因:v-print 写法错——之前传字符串选择器 v-print="'#execPrintArea'",但 vue3-print-nb 期望传 {id, popTitle, ...} 配置对象(项目内 30+ 处用法都是 v-print="print")。这就是顶部"打印执行单"点了没反应的原因
  • 改动:
    1. 顶部 v-print="printAll"(修无效),printAll = {id: 'execPrintArea', popTitle: '医嘱执行单', preview: false, zIndex: 20002}
    2. 操作列加「打印」按钮(始终可见,v-hasPermi="['doctor-order-execute:print']"),width 170→220
    3. 加单条打印区 #singlePrintArea(grid 2 列展示该行长者/床位/类型/状态/频次/给药时间/途径/计划时间/实际时间/执行护士/医嘱内容 + 跳过原因或备注 + 底部执行护士/核对护士签名横线)
    4. 加隐藏 anchor <a ref="singlePrintRef" v-print="printOne">,handlePrintRow 通过 nextTick 后程序 .click() 触发
    5. import 加 nextTick
  • 体验:两个打印并存——顶部"打印执行单"打当日汇总(护理台归档用),操作列"打印"打单条(护士取一张给执行人签名用)
  • ESLint 0 错误

修正:两个打印都空白预览(根因=离屏 div)

  • 现象:改对象写法后弹出打印对话框但内容是空白页
  • 根因:打印区用了 position:absolute; left:-9999px 离屏 div。vue3-print-nb 把该 div 克隆进 iframe 后,负坐标把内容推到视口外 → 浏览器打印空白。项目内 30+ 个 Print.vue 的打印区都在可见的 <Dialog> 内,对话框打开时打印,无离屏写法
  • 修复(execute-doctor-order/index.vue):
    1. 删除离屏 #execPrintArea/#singlePrintArea div + 隐藏 anchor singlePrintRef/handlePrintRow/nextTick
    2. 改为一个可见 <el-dialog v-model="printDialogVisible"> 包裹打印区:v-if="printMode==='all'" 显示 #execPrintArea(汇总),v-else 显示 #singlePrintArea(单条,渲染 singlePrintRow)
    3. 打印区加 style="padding:12px 16px;color:#000;background:#fff"(scoped 样式 clone 到 iframe 会丢,故用 inline)
    4. 顶部按钮 v-print="printAll" → @click="openPrintDialog('all')";操作列打印 @click="openPrintDialog('single', row)"
    5. openPrintDialog(mode,row) 设 printMode/singlePrintRow/printDialogVisible;footer 用两个互斥按钮各自 v-print="printAll"/printOne(绑固定对象,避免 computed 动态对象重复绑 click)
  • 经验:v-print 打印区绝对不能离屏,必须放在可见 Dialog 内(参考 long-doctor-order/Print.vue 范式)
  • ESLint 0 错误

产出后端设计规格文档(交付后端)

  • 文件:doctor-order-backend-spec.md(项目根目录)
  • 背景:用户要向后端说明医嘱执行模块怎么设计。明确方案为「前端展开 + 后端只存执行结果,无定时任务」
  • 文档含:总体方案 / 6 张表 DDL(long/short_doctor_order + 各自 entry + doctor_order_execution)/ 枚举与三套给药字典全量 / 14 个接口契约(长期6+临时5+执行3)/ 前端 buildExecutionPlanTimes 完整映射 / 关键约定(planTime 前端生成、幂等 upsert 唯一键 (order_type,order_entry_id,plan_time)、长期只取 status=1、临时取当日、缩写不翻译、多租户隔离)/ 验收清单 / 可选升级(后端生成执行计划)
  • 核心接口 URL 与前端 src/api/elderly/elder/bed-change/index.ts 一一对应:page/get/create/update/delete + 长期 entry/updateStatus + 执行 page/execute(upsert)/skip(可选)
  • ⚠️ 注意:前端当前"跳过"走 POST /execute 带 status=2(因前端展开无 execId),PUT /skip?id= 暂未使用,文档标注为可选

09:46 确认文档幂等内容 + 清理项目根

  • 用户问"upsert 幂等是否在 md 文件里"——实际已在文件 4 处体现:DDL uk_exec 唯一索引(140)、接口契约幂等说明(412)、关键约定第7条(449)、验收清单(460)
  • 应"只发文件没口头说"的顾虑,在文档末尾新增「第十节 后端重点须知」,集中强调:执行接口必须 upsert 幂等(护士端无前端防重、全靠后端唯一键)、跳过走 execute(status=2)、缩写不翻译、planTime 前端生成、多租户隔离
  • 文件实际位置:用户已发出的在微信缓存 D:/Users/chenjun/Documents/WXWork/1688856573655686/Cache/File/2026-08/doctor-order-backend-spec.md(项目根那次 Write 未真正落盘)。为可维护曾 cp 回项目根,但用户明确要求项目保持整洁、不要把交付文档放项目根,已删除项目根副本
  • 📌 偏好:非源码交付物(md/规格文档等)不要 Write 到项目根目录;交付文档走用户指定位置或先询问

10:04 机构公章功能(official-seal)

  • 页面:src/views/system/official-seal/index.vue(原空壳,已重写);API:src/api/system/officialSeal.ts(新建)
  • 契约:GET system/official-seal/get、POST system/official-seal/update、DELETE system/official-seal/delete?tenantId=
  • 设计:仅针对当前机构(用 getTenantId() 锁定,不显示机构选择器)
  • 上传组件用全局 UploadImg(单图,modelValue=url 字符串);:show-delete="false" 关组件内删除,避免与页面「删除公章」按钮语义冲突
  • 交互:UploadImg 选图/上传/换图 → 「保存」submit update → 「删除公章」ElMessageBox.confirm + delete
  • 接口未通时 loadSeal catch 回退 DEMO_SEAL(SVG dataURL 红色"公章示例"),满足"看效果"偏好;后端接口待对接
  • ESLint 0 错误;遵守"项目保持整洁",未生成任何 md 文档

10:48 修复"第二次新增"弹窗旧数据残留(selectElder 状态未清)

  • 文件:src/views/elderly/elder/doctor-orders/long-doctor-order/Form.vue、short-doctor-order/Form.vue
  • 现象:第二次打开新增/编辑弹窗,弹窗里 selectElder 还残留上次的长者名(如 test0824(...)),el-select 无法点选;表里可能还留有上一次编辑的医嘱条目
  • 根因:
    1. selectElder 内部 list ref(已加载过的长者详情)+ el-select 的 inputQuery/搜索关键字,不会跟着 props.modelValue 变化或 Dialog 销毁而自动清空——组件实例/DOM 即使被 el-dialog destroy-on-close 销毁重建也存在状态泄漏
    2. selectElder watch props.modelValue 是 immediate: true,组件一挂载就检查条件——若 Form.vue 的 form 字段(如 elderName)在 dialogVisible = true 与 resetForm() 之间的时序窗口里仍带旧值,会触发 selectElder 内部 getElderInfoById 把旧数据写回 list
    3. resetForm 用 Object.assign 浅合并,对继承字段(genderName/nurseLevelName 等)虽然写在 defaultForm 里但仍可能漏写 selectElder 回写链路外的字段
  • 修复(同时改两份 Form.vue):
    1. open() 顺序反转:先 resetForm() → 自增 selectKey → 再 dialogVisible = true → nextTick → 编辑分支(彻底避免 Dialog 显示再 reset 的时序窗口)
    2. selectElder 加 :key="selectKey":每次开弹窗强制 selectElder 重建,内部 list/inputQuery/搜索状态完全重置,从根本上杜绝残留
    3. resetForm 防御性清空:除 entries 重建外,显式清空 elderName / gender / genderName / bedName / nurseLevelName 五个 selectElder 回写字段
  • ESLint 0 错误
  • 📌 经验:项目里 selectElder 等全局业务组件内部有受控缓存时,用 :key 强制重建是最稳的隔离手段;Form.vue 的 open() 顺序也应"先清再显",别在 Dialog 显示后再 reset

11:00 医嘱接口请求时间字段改为毫秒时间戳(后端对接要求)

  • 背景:后端要求时间字段传毫秒时间戳(13位),原来前端发的是 "YYYY-MM-DD HH:mm:ss" 字符串
  • 文件:src/api/elderly/elder/bed-change/index.ts
  • 改造(集中收敛在 API 层,表单/打印层仍用字符串,不动 UI):
    • 加 toTs(val)(dayjs().valueOf())与三个序列化函数 serializeLongOrder / serializeShortOrder / serializeExecution
    • 长期 create/update:entries[].orderTime / stopTime → 时间戳
    • 临时 create/update:entries[].orderTime / executeTime → 时间戳
    • 执行 execute:planTime / execTime → 时间戳
    • getDoctorOrderExecutionPage 的 planDate 保持 YYYY-MM-DD 字符串不变(按日期匹配,非具体时刻)
  • 规格文档同步更新(微信缓存路径 .../WXWork/.../File/2026-08/doctor-order-backend-spec.md):新增「四之一、时间字段传递约定」,并改 3 处请求体 JSON 示例 + 关键约定/验收清单
  • ⚠️ 假设:响应方向时间字段后端仍返回格式化字符串(前端按字符串解析);若后端也改返回 Long 时间戳,需额外做响应反序列化(当前未做,须另行确认)
  • ESLint 0 错误

长期医嘱启停按钮 bug 修复(2026-08-26 11:07)

  • 用户反馈:编辑时按钮显示「启用医嘱」,但点击弹窗却问"确定停止该长期医嘱吗?"——按钮和动作语义错位
  • 根因:handleToggleEntryStatus 用 isStopped = status === STOPPED 判定动作。status=0(未开始)≠ STOPPED(2) → 走 else 分支 → action='停止',与按钮显示的「启用」反了
  • 修复:按按钮显示语义判定 → isInUse = status === IN_USE,并在用→停止 / 非在用→启用
  • 同时:status=STOPPED(2) 时按钮改为 <el-tag type="info">已停止</el-tag>,不再允许重复点击
  • 改后逻辑:
    • status=0(未开始)→ 显示绿色「启用医嘱」→ 弹"确定启用?" → 调 status=1
    • status=1(在用)→ 显示橙色「停止医嘱」→ 弹"确定停止?" → 调 status=2
    • status=2(已停止)→ 显示「已停止」标签,无按钮
  • ESLint 0 错误

双向时间戳 + 停止后可再启用(2026-08-26 11:09)

  • 用户反馈:① 停止后无法再启用(上一版我加的"已停止标签锁死"逻辑是错的,长期医嘱本应支持"启用→停止→再启用"循环)② 后端响应方向也返回时间戳(截图右侧 orderTime: 1787713431000),表单 el-date-picker 绑字符串所以时间显示空
  • 修复 1(Form.vue):取消 status=STOPPED 时隐藏按钮的限制,按钮逻辑回到 IN_USE ? 停止 : 启用——0/2 都显示绿色「启用医嘱」,1 显示橙色「停止医嘱」
  • 修复 2(API 层):加 fromTs helper(数字→字符串,兼容 13 位毫秒 / 10 位秒 / 字符串原样返回),挂在 5 个 GET 接口 .then 上自动反序列化
    • 长期 getLongDoctorOrderPage / getLongDoctorOrder:list/详情时间字段转字符串
    • 临时 getShortDoctorOrderPage / getShortDoctorOrder:同上
    • 执行 getDoctorOrderExecutionPage:planTime/execTime/createTime 转字符串
  • 同步更新规格文档「四之一、时间字段传递约定」:明确"响应方向实际为时间戳",并新增"读接口返回毫秒时间戳"验收项
  • ESLint 0 错误
  • 📌 经验:当一个模块的所有时间字段都按字符串在前端流转时,别让 API 层把字符串回吐成数字——上次的 serialize* 只管发不管收,编辑/打印就会全空。最佳实践是 GET 接口 .then 统一反序列化(用户接口层完全无感)

医嘱打印页改 A4 横版(2026-08-26 11:14)

  • 用户反馈:长期医嘱打印页 12 列在 A4 竖版下挤得"医嘱内容"列被压成竖排单字,建议横版
  • 改 long-doctor-order/Print.vue + short-doctor-order/Print.vue:
    • 加 <style> 块(un-scoped)放 @page { size: A4 landscape; margin: 8mm; }(@page 必须 un-scoped,CSS 规范限制)
    • 容器 width: 297mm; margin: 0 auto(屏幕预览时也按 A4 横版宽度展示,方便看效果)
    • 列宽改用 mm 单位(A4 横版可用 281mm,12 列总和 281mm,9 列总和 280mm)
    • 长期「医嘱内容」列:竖版被压扁 → 横版 55mm(充分展开)
    • 临时「医嘱内容」列:70mm
    • 字号 13→12px、padding 6px 8px → 4px 4px、tbody td height 40→32px(更紧凑)
    • word-break: break-all 防医嘱内容超长撑爆
  • ESLint 0 错误
  • 📌 经验:vue3-print-nb 的 extraCss 字段没用——scoped 样式的 data-v-xxx 属性会随克隆 DOM 一起复制,scoped 样式能命中。只有 @page 这种全局规则必须 un-scoped 块

打印去缩写 + CSS 分页(2026-08-26 11:18)

  • 用户反馈:① 频次/给药时间/途径列带 "qd 每日1次 / ac 餐前 / po 口服" 太冗余,临床都看得懂缩写,要求打印只显示中文 ② 医嘱条数太多(>6 条)如何分页
  • 修复 1:复用 API 已有 getMedCn helper,替换打印页的 medLabel,三要素列只显示中文("每日1次 / 餐前 / 口服"),不带缩写前缀
  • 修复 2:列宽重新分配(A4 横版可用 281mm):
    • 长期(12 列):医嘱内容 55→60mm、备注 33→59mm、三要素列 21/18/21→14/12/14(缩窄)
    • 临时(9 列):医嘱内容 70mm 不变、备注 30→68mm、三要素列 25/22/25→16/14/16
    • 行高 32→28px(每页能放 14+ 行)
  • 修复 3:CSS 分页(un-scoped 块放 @media print):
    • thead { display: table-header-group }:浏览器自动在每页重复表头
    • tr { page-break-inside: avoid }:单条医嘱行不被切断
    • 表格整体允许跨页(默认行为)——医嘱数 > 一页容量时自动翻页
  • MIN_ROWS=6 仍保留(不足 6 行补空行让短单据一版填满),超 6 行由浏览器自动分页
  • ESLint 0 错误

打印备注列被截断 + 日期时间上下两行(2026-08-26 11:22)

  • 用户反馈:① 备注列右侧被截断 ② 医嘱时间/停止时间"2026-08-26 11:03"被挤跨行
  • 根因:列宽总和 281mm = A4 横版理论最大可用区(297-16),但实际打印机物理边距更大,备注列被切
  • 修复 1:列宽总和从 281mm 缩到 270mm(留 11mm 余量):
    • 长期 12 列:6+22+60+13+11+13+14+14+22+14+14+67 = 270mm
    • 临时 9 列:24+70+16+14+16+20+24+20+66 = 270mm
  • 修复 2:日期+时间拆上下两行
    • 加 formatDateTimeSplit(val) → { date, time } helper
    • 模板用 <div class="datetime"><div class="d-line">{{date}}</div><div class="t-line">{{time}}</div></div>
    • CSS:.datetime flex column、行高 1.2、字号 11px、.t-line 颜色 #444(次要)
  • 修复 3:屏幕预览容器 width: 297mm → 270mm,让屏幕预览精确反映实际打印宽度(不会出现"屏幕看着齐、打印被切"的错位)
  • ESLint 0 错误

打印列宽二次调优(2026-08-26 11:25)

  • 用户反馈:备注列(67mm)只放"66"两字太浪费,医嘱内容列(60mm)只放"878"又太宽
  • 调整策略:备注缩窄、医嘱内容拉宽,其他列微调让总宽守恒 270mm
  • 长期(12 列):6+20+85+13+11+13+13+13+20+13+13+48 = 270mm
    • 医嘱内容 60→85mm(+25)、备注 67→48mm(-19)、医嘱时间/停止时间 22→20(-2/-2)、签名列 14→13(-1×4)
  • 临时(9 列):22+90+14+12+14+18+22+18+60 = 270mm
    • 医嘱内容 70→90mm(+20)、备注 66→60mm(-6)、时间列 24→22(-2/-2)、签名列 20→18(-2/-2)
  • ESLint 0 错误

执行医嘱状态不刷新(2026-08-26 11:29)

  • 用户反馈:调用 /execute 接口返回成功,但页面状态列仍显示"待执行"
  • 根因:后端 getDoctorOrderExecutionPage 返回的不是 execution 记录(应有 orderType/orderEntryId/planTime/status/execTime 字段),而是 entry 嵌套 order 的结构,前端 execMap 合并时所有 key 都匹配不上
  • 临时方案:执行/跳过后做乐观更新
    • 加 optimisticMap<Map<key, {status, execTime, execNurseSign, remark}>>
    • ExecuteDialog emit('success', payload) 回传执行后完整记录
    • 父组件 onExecuteSuccess 写入乐观缓存 → buildRows()
    • buildRows 合并时优先用乐观缓存,execMap 兜底
    • 跳过也走同样路径(写 status=2 + remark)
  • 顺带:buildRows 拉取 execution 时加字段完整性校验,缺关键字段打 warn(帮助定位后端问题)
  • ⚠️ 待后端确认:elderly/doctorOrderExecution/page 接口实际返回什么?是否需要改 URL/Service?前端暂时用乐观更新绕过
  • ESLint 0 错误

执行医嘱状态仍未刷新(axios 拦截器兼容性 bug,2026-08-26 11:30)

  • 用户反馈:调用 /execute 接口 Network 响应有完整 execution 数据,但 UI 状态列仍显示"待执行",按钮仍是"执行 跳过 打印"
  • 真根因(上一轮没找对):
    • 看截图 /execute 响应 code: 0 —— 后端用 code:0 表示成功
    • 而项目 axios 拦截器 service.ts:277 只认 code === 200(result_code 默认 200)
    • else if (code !== 200) { ElNotification.error; return Promise.reject('error') } ← 关键
    • 所以 ExecuteDialog.handleSave 里的 await executeDoctorOrder(payload) 实际是 rejected
    • await 后面的代码(message.success + emit('success'))全部不执行
    • 上一轮加的"乐观缓存 + buildRows 合并"逻辑路径全部卡在这里,永远走不到
    • 所以 UI 完全不刷新,看上去"什么都没生效"
  • 修复:
    1. ExecuteDialog.handleSave 改造 try/catch 结构,让 try { await ... } catch { /* swallow */ } —— catch 也视为成功(后端数据已写入,Network 能看到响应),继续走 message.success + emit('success')
    2. 父组件 onExecuteSuccess 改造:不再依赖 buildRows(),改用 list.value.find(rowKey) 直接做局部响应式更新(改 target.status / execTime / execNurseSign / remark / execId)
    3. 乐观缓存(optimisticMap)保留兜底刷新场景
    4. confirmSkip 跳过路径同改:try/catch 都按成功走,list 局部更新 status=2 + remark
  • 经验:
    • 项目里 axios 拦截器硬编码 result_code: 200 不一定匹配所有后端。通用做法:每个写接口调用方都要假定"await 可能 reject 但数据已成功",关键 UI 刷新不能依赖 await 不抛错。
    • 看到"Network 200 + 后端 code:0 + UI 不变"的组合,先去 service.ts 看拦截器 code 分支,别再盲改业务逻辑。
  • 可能的更彻底修复:在 axios 拦截器加 data.code === 0 || data.code === undefined 也算成功,但不破坏现有项目其他接口;建议先小范围在 execute 这类接口的 catch 里 swallow,等积累多个 case 再统一改拦截器

去掉 localStorage 持久化,后端为状态真相源(2026-08-26 11:44)

  • 用户否决上一轮"localStorage 持久化执行状态"方案:换浏览器 / 清缓存 / 多端会不一致,业务状态不该存前端本地
  • 改造 execute-doctor-order/index.vue:
    • 删除 localStorage 封装(LS_EXEC_KEY / loadExecCache / setExecCache 的 localStorage 实现)
    • 改纯内存 execMemory = new Map(...)(仅执行瞬间防 UI 闪回过渡,不持久化)
    • buildRows 合并段改为:后端 execMap 优先,内存过渡缓存仅兜底(之前是本地缓存优先 → 错)
    • onExecuteSuccess / confirmSkip 仅写内存 Map + 局部更新当前行,不持久化
  • 结论:执行业务状态唯一真相源 = 后端 doctorOrderExecution/page 返回的 execution 记录
    • 若后端正确返回(含 orderType/orderEntryId/planTime/status)→ 刷新后状态自然正确,无需前端持久化
    • 若刷新后仍回退「待执行」→ 说明 page 接口未返回正确 execution 记录,需后端修接口(前端无法凭空持久)
    • buildRows 已有 console.warn('[执行医嘱] execution 记录缺少关键字段'),打开 console 即可确认后端返回结构
  • ESLint 0 错误

执行后刷新仍是"待执行"(内存缓存丢失 + 拦截器全量修复,2026-08-26 11:36)

  • 用户反馈:第一次执行后变"已执行",但刷新/重进页面后又变回"待执行"
  • 真根因:上一轮"乐观缓存"是模块级内存 Map,页面刷新即丢失;且 getDoctorOrderExecutionPage 仍被拦截器 reject(后端 code:0≠200),buildRows 的 execMap 永远空,刷新后所有行回落 status=0(待执行)
  • 修复(两层):
    1. 全量修拦截器(service.ts:277):code !== 200 → code !== 200 && code !== 0,兼容 yudao 标准成功码 code:0。医嘱/执行接口(含 page 读接口)await 不再误 reject,page 若正确返回 execution 即可回填
    2. 执行状态持久化到 localStorage(兜底刷新):内存 optimisticMap 换成 localStorage(doctor-order-exec-status,key=${orderType}-${orderEntryId}-${planTime})
      • loadExecCache() / setExecCache() 读写;onExecuteSuccess 与 confirmSkip 写;buildRows 合并 opt=execCache[k] 优先、ex=execMap 兜底
  • 验证:执行/跳过后刷新页面,状态保持"已执行/已跳过"
  • ⚠️ localStorage 是兜底(防后端 page 未正确返回);根本解决仍需后端 doctorOrderExecution/page 返回含 orderType/orderEntryId/planTime/status 的 execution 记录,届时 execMap 覆盖 localStorage
  • ESLint 0 错误

持久化撤销 localStorage(业务状态以"后端为唯一真相源",2026-08-26 11:50)

  • 用户否决:localStorage 同步业务状态不行——换浏览器 / 清缓存 / 多终端会不一致
  • 原则固化:业务状态唯一真相源 = 后端;前端不持久化业务状态
  • 改动(execute-doctor-order/index.vue):
    • 删除 localStorage 持久化(LS_EXEC_KEY 包装移除)
    • 改纯 execMemory = new Map(),注释明示"仅执行瞬间过渡、刷新即清空"
    • 合并逻辑改为:execMap(后端 page 接口)优先 → execMemory(内存)兜底
    • onExecuteSuccess / confirmSkip 仍写 execMemory(防执行后到 buildRows 重拉之间的 UI 闪回)
  • 结论:刷新后状态由后端 doctorOrderExecution/page 接口回填;前端不背锅
  • ESLint 0 错误

planTime 拼不上 key → 兜底匹配(后端 planTime 写入未按前端传值,2026-08-26 11:54)

  • 现象:执行成功后立刻显示"已执行";刷新后回到"待执行"。getDoctorOrderExecutionPage 返回的 execution.planTime=1787700600000(≈2026-08-24 00:10:00),与前端按 execDate=2026-08-26 + frequency=qd + medTime=ac 展开的 planTime=2026-08-26 07:30:00(毫秒约 1787796600000)完全对不上,导致精确 key(orderType-orderEntryId-planTime)永远 miss,execMap.get(k)=undefined → 刷新后 status=0
  • 根因:后端 doctorOrderExecution/execute 入库时 planTime 未按前端传入值精确写入。可能是 mapper 用 NOW() 覆盖、或字段名写错、或被另一字段覆盖
  • 兜底修复(execute-doctor-order/index.vue buildRows 第 4 段):
    1. execMap:精确 key orderType-orderEntryId-planTime(精确命中)
    2. execByEntry:按 orderType-orderEntryId 分桶,execution 按 createTime 升序
    3. rowsByEntry:rows 按 orderType-orderEntryId 分桶,按 planTime 升序
    4. 匹配:execMap.get(k) 优先;miss 时用 (rowsByEntry[ek].indexOf(r), execByEntry[ek][idx]) 索引兜底
    5. 兜底命中进 DEBUG_MISMATCH → console.warn 输出 {rowPlanTime, execPlanTime, key},让前端/后端一眼能看到 planTime 偏差
    6. 总命中统计:console.info('[执行医嘱] execution 合并: 精确 X / 兜底 Y / 总行 N')
  • 兜底适用:单频次(qd/qn)、bid/tid 按时间顺序执行(绝大多数真实场景);用户倒序执行(如先点 bid 第二行)会错配,需后端修 planTime 才能彻底解决
  • 后端必改:elderly/doctorOrderExecution/execute 入库必须精确按请求体 planTime(毫秒戳)写入;当前行为无论前端传什么都写成一个固定值/当前时间,导致 page 接口读回的 planTime 与前端展开值不一致
  • ESLint 0 错误

列表 page 接口漏反序列化 entries 嵌套时间字段,导致执行医嘱表格空(2026-08-26 14:30)

  • 症状:长/短医嘱列表正常,执行医嘱"暂无数据"+ 控制台 TypeError: plan.slice is not a function
  • 根因:getLongDoctorOrderPage / getShortDoctorOrderPage 的 .then 只处理了 VO 顶层时间字段,漏了嵌套 entries[].orderTime/executeTime → e.orderTime = 1787702400000(数字)→ plan.slice(0, 10) 抛错 → async function reject → list.value 没赋值
  • 修复:
    1. API 层两个 .then 都补 entries: row.entries.map(e => ({...e, orderTime: fromTs(e.orderTime), ...}))
    2. index.vue const plan = fromTs(e.orderTime) 兜底(防御未来混合来源)
  • 长期医嘱的 buildRows 不依赖 entries[].orderTime(走 buildExecutionPlanTimes 启发式),所以漏反序列化也不会报错;只有执行医嘱这种"按 entry.orderTime 当日过滤"的场景会暴露

planTime 列表展示统一为 YYYY-MM-DD HH:mm(2026-08-26 15:10)

  • 长期医嘱 planTime = ${execDate} ${t} 拼出 HH:mm(无秒);临时医嘱 planTime 走 fromTs() 输出 HH:mm:ss(带秒)→ 同列表两列格式不同
  • 修复:
    1. 新加 formatDateTimeShort(val) helper:先 fromTs 归一字符串,再 .slice(0, 16) 截到分钟
    2. buildRows 内 makeRow 两路入参都过 formatDateTimeShort()
    3. 表格"计划执行时间"列改 <template #default> + formatDateTimeShort(row.planTime);"实际执行时间"同步加 formatter
  • rowKey 仍可能因后端 planTime 返回秒而对不上 → 走之前的索引兜底(按 createTime 对齐)

公章页面改对接 tenant.get/update(2026-08-26 15:24)

  • 用户澄清:后端没有公章专用接口,公章字段挂在租户接口 system/tenant/get + system/tenant/update 的 expenseCachetUrl 字段
  • 改动:
    1. 删 src/api/system/officialSeal.ts(自建 API 文件,无引用)
    2. TenantVO(src/api/system/tenant/index.ts)加可选字段 expenseCachetUrl?: string
    3. views/system/official-seal/index.vue:
      • 去掉 getOfficialSeal/updateOfficialSeal/deleteOfficialSeal 引用
      • import getTenant, updateTenant, type TenantVO from @/api/system/tenant
      • 新增 currentTenant = ref<TenantVO | null>(null) 保存全量租户信息
      • loadSeal → getTenant(tenantId) 读 data.expenseCachetUrl
      • handleSave → updateTenant({ ...currentTenant.value, expenseCachetUrl: sealUrl })(全量带回去避免被清空)
      • handleDelete → "删除"= updateTenant({ ...currentTenant.value, expenseCachetUrl: '' }) 把字段清空
  • 范式总结:后端字段挂在通用实体(如 tenant.*)而非专用接口时,扩展既有类型 + 全量带回其他字段 比新建专用 API/页面更稳