chenjun преди 1 месец
родител
ревизия
3949ad24b4

+ 152 - 0
.workbuddy/memory/2026-08-25.md

@@ -0,0 +1,152 @@
+# 2026-08-25 工作记录
+
+## 长期医嘱单页面(long-doctor-order)
+
+按截图新建 `src/views/elderly/elder/doctor-orders/long-doctor-order/` 三件套:
+- `columns.ts`:长期医嘱列表格列定义,复用 `reactive` + `type/dictArr/format/width` 约定,与 `src/views/elderly/elder/column.ts` 风格一致。
+- `Form.vue`:Dialog 弹窗(60% 宽),基于 `selectElder` 自动联动填充姓名/性别/床号/护理等级(注意 Vue3 不推荐 `.sync`,要用 `v-model:propName`)。表单含医嘱内容、开始时间、医生签名、护士签名、停止时间、停止医生签名、停止护士签名、备注。`open(row, detail)` 回显详情。
+- `index.vue`:参考 `bed-change/index.vue` 的标准结构(ContentWrap 筛选 + ContentWrap 列表(TabBarBtn + Table2 + Pagination)+ Form 弹窗引用),监听 `@edit`/`@del` 事件做编辑/删除。删除用 `message.confirm` 二次确认。
+
+接口:`src/api/elderly/elder/bed-change/index.ts` 追加 5 个 CRUD + VO/Form 类型:
+- `getLongDoctorOrderPage / getLongDoctorOrder / createLongDoctorOrder / updateLongDoctorOrder / deleteLongDoctorOrder`
+- 路径:`elderly/longDoctorOrder/{page|get|create|update|delete}`,复用 `request.get/post/put/delete` 模式。
+
+注意:列定义里 `gender` 用 `type:'1'` + `DICT_TYPE.SYSTEM_USER_SEX` 字典。**未引入 `status` 字典**(项目里估计没有 LONG_ORDER_STATUS),等后端落地后再加一列。
+
+---
+
+## 临时医嘱单页面(short-doctor-order)
+
+按截图实现 `src/views/elderly/elder/doctor-orders/short-doctor-order/`,四件套:
+- `columns.ts`:列表列(长者姓名/性别/床位号/护理等级/医嘱条数/最新医嘱时间/最新执行时间/创建时间),无状态列(临时医嘱一次性,无未开始/在用/已停止语义)。
+- `Form.vue`:80% 宽 Dialog,上方长者 picker(`selectElder` 联动),下方 el-table 子表展示**多条医嘱条目**(每条 6 列:医嘱时间/医嘱内容/医生签名/执行时间/护士签名/备注),支持新增/删除条目,至少保留 1 条。`entries` 是数组,对应后端一对多结构。
+- `Print.vue`:完全按截图渲染。**关键样式**:日/月 列表头用 `::before` 画斜线(`linear-gradient` 一条对角线),签名按「有数据显数据、无数据显横线」规则(`.sign-box.empty` 灰底文字),打印最少 6 行(不足补空行)。
+- `index.vue`:与 long-doctor-order 同款结构(ContentWrap 筛选 + ContentWrap 列表 + Pagination + Form/Print 引用),操作列只保留 编辑/删除/打印(**没有停止/启用**,因为临时医嘱是一次性)。列表请求失败时填充 2 条测试假数据。
+
+接口:`bed-change/index.ts` 追加 5 个 + 3 个类型:
+- `getShortDoctorOrderPage / getShortDoctorOrder / createShortDoctorOrder / updateShortDoctorOrder / deleteShortDoctorOrder`
+- 路径:`elderly/shortDoctorOrder/{page|get|create|update|delete}`
+- 类型:`ShortDoctorOrderVO`(含 `entries: ShortDoctorOrderEntry[]`)/ `ShortDoctorOrderForm` / `ShortDoctorOrderEntry`(每条单据子行字段)。
+
+## 通用发现
+- yudao 风格打印:`v-print` 指令全局可用(`vue3-print-nb`),只需配置 `id/popTitle/preview/zIndex`。打印区域 ID 不要和其他医嘱单(`longOrderPrintArea`)冲突。
+- el-table 子表 + el-date-picker 单元格内嵌:`el-date-picker` 加 `style="width: 100%"` 才不溢出。`el-form-item` 包外层 + 子列用 `template #default="{ row }"` 解构。
+- 临时医嘱多 entry 提交 payload 用 `{elderId, entries: [...]}`,后端一对多时直接整个 list 传过去即可。
+
+校验:ESLint 0 错误,vue-tsc 过滤 long-doctor-order/bed-change/index.ts 无报错。
+
+## 项目里标准的列表页结构(备查)
+
+`ContentWrap`(筛选 + 列表 + 分页) + `TabBarBtn @add` + `Table2`(自动渲染操作列:编辑/详情/撤销/删除,事件:edit/detail/del/cancel)+ `Pagination`。
+列定义写在独立 `columns.ts` 用 `reactive([{label,field,type,format,...}])`。
+弹窗:`<Dialog v-model="dialogVisible" :title>` + `defineExpose({ open })`。
+全局组件(无需 import):`ContentWrap / Dialog / TabBarBtn / Table2 / Pagination / Icon / TenantSelect`。
+
+## 长期医嘱单功能增强(同文件,追加)
+
+按需求增量修改 long-doctor-order 三件套 + 接口:
+- **新增「未开始」状态**:状态语义 0=未开始 / 1=在用 / 2=已停止。筛选新增「未开始」选项;`getList` 预计算 `statusText`;`columns.ts` 加「状态」列(渲染 `statusText`,因无后端字典故不走 dict-tag)。
+- **操作列新增 停止/启用 切换 + 打印 + 打开**:Table2 内置 编辑/删除/详情 用 `:show-edit-condition / :show-delete-condition / :show-detail-condition="()=>false"` 隐藏(避免权限串 `elder:*` 导致的重复/不可控),改在 `#next` 插槽自绘 编辑/删除/停止启用/打开/打印 按钮(用正确的 `v-hasPermi="['long-doctor-order:*']"`)。`op-width` 调到 340。
+  - `handleToggleStatus`:status==2(已停止)→显示「启用」置 1;否则→「停止」置 2;`message.confirm` 二次确认后调 `updateLongDoctorOrderStatus(id, status)`。
+  - `openDetail`→`Form.open(row,true)` 只读详情;`openPrint`→`Print.open(row)`。
+- **新建 `Print.vue`**:Dialog 内 `#longOrderPrintArea` 渲染医嘱单详情(基本信息表 + 医嘱内容 + 签名区)。**签名规则**:有值显值、无值显横线(`.sign-box.empty` 仅 `border-bottom` + 浅色「(签名)」占位)供纸质签名。底部 `v-print="print"`(vue3-print-nb,已在 main.ts `app.use(Print)` 全局注册)触发打印。
+- **Form.vue 详情增强**:detail 模式展示「医嘱状态」(只读);footer 加「打印」按钮→`Print.open({id})`。
+- 接口追加:`updateLongDoctorOrderStatus(id,status)`(PUT `elderly/longDoctorOrder/updateStatus`)+ `LONG_ORDER_STATUS` / `LONG_ORDER_STATUS_OPTIONS` / `getLongDoctorOrderStatusText()` 常量。
+
+校验:ESLint 0 错误;vue-tsc 过滤 long-doctor-order/bed-change/index.ts 无报错。
+
+⚠️ 待后端落地确认项:①`updateStatus` 接口 URL/参数名(`?id=&status=`);②新医嘱默认 status(建议后端置 0 未开始);③菜单 path + 权限码 `long-doctor-order:edit/delete/print`;④打印走 `v-print` 会克隆 `#longOrderPrintArea` 到 iframe 打印,遮罩层不会入纸。
+
+---
+
+## 打印页时间格式化 + 签名去横线(长期/临时医嘱统一)
+
+用户要求两个医嘱单打印页:**时间用标准格式化(没有就留空白)+ 表格里签名不需要横线**。
+
+### 长期医嘱 `long-doctor-order/Print.vue`
+- 表头 11 列 → 9 列(序号/医嘱时间/医嘱内容/医生签名/护士签名/停止时间/停止医生签名/停止护士签名/备注)。
+- 去掉「医嘱时间/停止时间」的 colspan 2 + 对角线「日/月·时」表头,改单列直接显示完整时间。
+- 新增 `formatFullTime(val)`:`YYYY-MM-DD HH:mm:ss` → 截到 `YYYY-MM-DD HH:mm`;空/`null`/`undefined`/`''` → 空串;数字时间戳按本地时区解析;其他兜底原样返回。替换原 `formatDateOnly`/`formatTimeOnly`。
+- 签名单元格去掉 `.sign-box`(含 `border-bottom` 横线 + `.empty` 占位灰),直接 `{{ item.xxxSign || '' }}`。
+- 删除 `.diag-cell` 斜线 CSS(`::before` linear-gradient)。
+
+### 临时医嘱 `short-doctor-order/Print.vue`
+- 表头 8 列 → 6 列(医嘱时间/医嘱/医生签名/执行时间/护士签名/备注)。
+- 同样 `formatFullTime` 替换 `formatDateOnly`/`formatTimeOnly`,`orderTime` 与 `executeTime` 两个时间字段都用它。
+- 签名去 `.sign-box` 横线,删除 `.diag-cell` 斜线 CSS。
+
+### 结论
+两张打印单视觉风格现已一致:时间列单列标准 `YYYY-MM-DD HH:mm`,无值留空;签名只显文本不加横线。这是用户明确偏好(「有表格了就不需要横线」),后续新打印页默认沿用此风格。
+
+---
+
+## 长期医嘱 Form.vue 子表新增「启用/停止医嘱」按钮
+
+用户要求新增弹窗里子表操作列也显示「启用医嘱/停止医嘱」(此前只有已保存行 `row.id` 存在才显示)。
+
+改动:
+- 操作列 180px → 220px。
+- 启用/停止按钮显示条件 `v-if="row.id && !isDetail"` → `v-if="!isDetail"`(新增行也可见)。
+- 文案/颜色按 `status`:`IN_USE(1)`→「停止医嘱」warning;`NOT_STARTED(0)`/`STOPPED(2)`→「启用医嘱」success。
+- `emptyEntry()` 默认 `status: LONG_ORDER_STATUS.NOT_STARTED`(未开始),`statusText:'未开始'`。
+- `togglingId` 判定加 `row.id !== undefined` 防止新增行 id=undefined 误判。
+- 未保存行(无 id)点击仍走 `handleToggleEntryStatus` 原提示分支「新添加的条目请先保存后再启用/停止」。
+
+⚠️ 临时医嘱 `short-doctor-order/Form.vue` **未加**启停按钮(临时医嘱一次性,无此语义)。如要加「执行/作废」类按钮再单独处理。
+
+---
+
+## 医嘱给药三要素(频次/时间/途径)下拉选择项
+
+用户要求长期+临时医嘱,每条医嘱新增三个结构化选择项:给药频次、给药时间、给药途径,下拉项最右侧带中文临床场景提示。
+
+### API `bed-change/index.ts`
+- 新增 `MedOption` 接口(`value/label/cn/tip`)。
+- 新增三套选项常量(长期/临时共用):
+  - `MED_FREQUENCY_OPTIONS`:qd/bid/tid/qid/qn/qod/qw/biw/tiw/q2w/q3w/q4w/q8h/q6h/q4h(15 项)。
+  - `MED_TIME_OPTIONS`:ac/pc/hs/st/prn/sos/am/pm(8 项)。
+  - `MED_ROUTE_OPTIONS`:po/iv/ivgtt/im/sc/ih/igtt/inh/ext(9 项,ivgtt 含 iv drip 别名释义)。
+  - `label` = 「缩写 + 中文含义」(如下拉主显),`cn` = 中文含义,`tip` = 临床场景(hover 提示)。
+- `LongDoctorOrderEntry` / `ShortDoctorOrderEntry` 各加 `frequency?`、`medTime?`、`route?`(均存缩写)。
+
+### 两个 `Form.vue`(长期/临时)
+- `emptyEntry()` 补 `frequency/medTime/route: ''`。
+- 子表在「医嘱内容」列后插入三列 `el-select`(clearable + filterable),`v-model` 绑 `row.frequency/medTime/route`,`disabled=isDetail`。
+- `el-option` 用 `:value`(缩写) + `:label`(缩写+中文) + `:title="o.tip"`(原生 hover tooltip 显示临床场景)。选框提交时 payload 自动带三字段。
+
+### 两个 `Print.vue`(长期/临时)
+- 导入三套常量 + 新增 `medLabel(options, val)` helper(缩写→`o.label`,空值返回空串)。
+- 表头/表格在「医嘱内容」后加「频次/给药时间/途径」三列,单元格 `medLabel(...)` 显示中文文案;`paddedEntries` 补空字段。
+- 长期打印 9→12 列;临时打印 6→9 列。
+
+### 结论
+两种医嘱编辑时均可结构化选择给药三要素,打印页同步展示中文;选项字典集中在 API 一处维护。校验:ESLint 0 错误(5 文件)。
+
+⚠️ 待后端确认:①entry 新增三字段的库表列名/序列化(建议 frequency/med_time/route);②前端存的缩写需与后端字典一致;③如后端有独立给药字典接口,需替换本地常量。
+
+---
+
+## 护士执行医嘱页面(execute-doctor-order)—— 新建
+
+用户要开发护士"执行医嘱"功能,数据来源是前两个医嘱。已确认方案:**前端自动展开 + 按频次拆分多次**(护士只做"确认执行+签名",不手动新增执行记录)。
+
+### 设计决策
+- 数据流转:**不新增独立录入**,页面读长期+临时医嘱接口,在前端按频次展开成"当日执行行",护士点「执行」→写一条执行记录。
+- 长期医嘱只取 `status=1(在用)` 的 entry;临时医嘱取 `orderTime` 当日者,以医嘱时间为 planTime(一次性)。
+- 执行记录需持久化(跨刷新可追溯):`getDoctorOrderExecutionPage(planDate)` 拉已执行,按 `${orderType}-${orderEntryId}-${planTime}` 合并标记行 status=已执行。
+- 行操作:待执行→「执行」(弹窗填实际时间+签名) / 「跳过」(写 status=2);已执行/已跳过→「查看」。
+
+### API `bed-change/index.ts` 追加
+- `DoctorOrderExecution` 接口(orderType/orderEntryId/elderId/elderName/bedName/orderContent/frequency/medTime/route/planTime/execTime/execNurseSign/status/remark/tenantId)。
+- `EXEC_STATUS`(0待执行/1已执行/2已跳过) + `EXEC_STATUS_OPTIONS`。
+- `getDoctorOrderExecutionPage(query)` GET `elderly/doctorOrderExecution/page`;`executeDoctorOrder(data)` POST `elderly/doctorOrderExecution/execute`;`skipDoctorOrderExecution(id)` PUT `elderly/doctorOrderExecution/skip`。
+- `getMedCn(options, val)` 选项值→中文。
+- `buildExecutionPlanTimes(frequency, medTime)`:频次→HH:mm 时点数组(日频次多时点;周/间隔频次给 1 个 09:00 代表点;medTime 对单时点微调 hs→21:00 等)。**前端启发式,真实排班以后端执行为准**。
+
+### 文件
+- `execute-doctor-order/index.vue`:ContentWrap 筛选(机构/长者/执行日期默认今天/类型/状态)+ el-table 列表(合并展开)+ 客户端分页 Pagination + 打印执行单(`v-print="'#execPrintArea'"`,离屏克隆打印)+ ExecuteDialog 引用。列表失败各自 catch 用 `buildMockLong/buildMockShort` 兜底(含 bids/tid 多时点,验证展开)。
+- `execute-doctor-order/ExecuteDialog.vue`:摘要描述 + 表单(实际执行时间默认 now、执行护士默认 `userStore.nickname`、备注),保存调 `executeDoctorOrder`。`defineExpose({open(row, view)})`。
+
+校验:ESLint 0 错误(3 文件)。
+
+⚠️ 待后端确认:①执行记录表 `doctor_order_execution` 列名/序列化;②`execute` 接口入参(是否需传 elderId/orderContent 冗余或仅 orderEntryId+planTime+status+execTime+sign);③`page` 查询参数 `planDate` 命名;④权限码 `doctor-order-execute:execute`;⑤频次展开逻辑若后端已有执行计划生成,前端应改为直接读执行表而非自行展开。

+ 48 - 0
.workbuddy/memory/2026-08-26.md

@@ -0,0 +1,48 @@
+# 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 到项目根目录;交付文档走用户指定位置或先询问

+ 18 - 0
.workbuddy/memory/MEMORY.md

@@ -0,0 +1,18 @@
+# 项目长期记忆(kyj-yanglao-web-new)
+
+## 项目身份
+湖北省养老机构系统前端(yudao 框架:Vue3 + TS + Element Plus)。重点模块:九防看板、合同(湖北2024版)、入住登记、长期/临时医嘱单、护士执行医嘱。
+
+## 医嘱模块(2026-08 搭建)
+- 长期医嘱 `long-doctor-order/`:长者 + 多条 entries 子表,条目含 status 启用/停止
+- 临时医嘱 `short-doctor-order/`:一次性,无启停,含 executeTime
+- 护士执行 `execute-doctor-order/`:前端按频次展开待执行行 + 执行/跳过弹窗 + 打印 Dialog(v-print 走可见 Dialog,禁止离屏)
+- API 集中在 `src/api/elderly/elder/bed-change/index.ts`
+- 给药三要素(frequency/medTime/route)存缩写,前端 `MED_*_OPTIONS` 翻译;状态枚举 `LONG_ORDER_STATUS` / `EXEC_STATUS`
+- 后端规格文档(不在项目内):用户侧 `doctor-order-backend-spec.md`,已交付后端
+
+## 用户偏好 / 约定
+- ⚠️ **保持项目目录整洁**:非源码文件(.md 规格、交付物等)不要 Write 到项目根;交付文档走用户指定位置或先问
+- 打印页(v-print)打印区必须在可见 `<Dialog>` 内,禁止 `position:absolute; left:-9999px` 离屏(会空白)
+- 列表/接口未通时填充 mock 数据便于看效果
+- 多项目并行,不愿为单项目切 Node 版本(本地 Node v22 vs Vue2 项目要求 ≤20 冲突已知)

+ 366 - 0
src/api/elderly/elder/bed-change/index.ts

@@ -55,3 +55,369 @@ export const checkPrivateRoom = (id,elderId=undefined) => {
     url: `elderly/bedChangeRecord/checkPrivateRoom?bedId=${id}&elderId=${elderId}`
   })
 }
+
+// ============================================================
+// 长期医嘱单(LongDoctorOrder)相关接口
+// ============================================================
+// 单条长期医嘱条目(对应一条长期医嘱 + 停止信息)
+export interface LongDoctorOrderEntry {
+  id?: number
+  longDoctorOrderId?: number
+  orderTime?: string
+  orderContent?: string
+  // 给药三要素(结构化选择项)
+  frequency?: string // 给药频次,存缩写,如 qd
+  medTime?: string // 给药时间,存缩写,如 ac
+  route?: string // 给药途径,存缩写,如 po
+  doctorSign?: string
+  nurseSign?: string
+  stopTime?: string
+  stopDoctorSign?: string
+  stopNurseSign?: string
+  remark?: string
+  status?: number
+  statusText?: string
+}
+
+// 长期医嘱单 VO:按长者分组的单据,含多条 entry
+export interface LongDoctorOrderVO {
+  id?: number
+  elderId?: number
+  elderName?: string
+  gender?: number | string
+  genderName?: string
+  bedName?: string
+  nurseLevelName?: string
+  tenantId?: number | string
+  // 汇总字段(列表用,后端可冗余返回)
+  entryCount?: number
+  latestOrderTime?: string
+  entries?: LongDoctorOrderEntry[]
+  createTime?: string
+}
+
+export interface LongDoctorOrderForm {
+  id?: number
+  elderId: number
+  entries: LongDoctorOrderEntry[]
+}
+
+// 长期医嘱单分页
+export const getLongDoctorOrderPage = (query: Recordable) => {
+  return request.get({
+    url: 'elderly/longDoctorOrder/page',
+    params: query
+  })
+}
+
+// 长期医嘱单详情
+export const getLongDoctorOrder = (id: number) => {
+  return request.get({
+    url: `elderly/longDoctorOrder/get?id=${id}`
+  })
+}
+
+// 新增长期医嘱单
+export const createLongDoctorOrder = (data: LongDoctorOrderForm) => {
+  return request.post({
+    url: 'elderly/longDoctorOrder/create',
+    data
+  })
+}
+
+// 更新长期医嘱单
+export const updateLongDoctorOrder = (data: LongDoctorOrderForm) => {
+  return request.put({
+    url: 'elderly/longDoctorOrder/update',
+    data
+  })
+}
+
+// 删除长期医嘱单
+export const deleteLongDoctorOrder = (id: number) => {
+  return request.delete({
+    url: `elderly/longDoctorOrder/delete?id=${id}`
+  })
+}
+
+// 单条长期医嘱条目状态切换(停止 / 启用)
+// status: 0=未开始 1=在用 2=已停止
+export const updateLongDoctorOrderEntryStatus = (entryId: number, status: number) => {
+  return request.put({
+    url: `elderly/longDoctorOrder/entry/updateStatus?id=${entryId}&status=${status}`
+  })
+}
+
+// 长期医嘱单状态字典(0 未开始 / 1 在用 / 2 已停止)
+export const LONG_ORDER_STATUS = {
+  NOT_STARTED: 0,
+  IN_USE: 1,
+  STOPPED: 2
+}
+
+export const LONG_ORDER_STATUS_OPTIONS = [
+  { label: '未开始', value: 0 },
+  { label: '在用', value: 1 },
+  { label: '已停止', value: 2 }
+]
+
+export const getLongDoctorOrderStatusText = (status: number | string | undefined): string => {
+  const item = LONG_ORDER_STATUS_OPTIONS.find((o) => o.value === Number(status))
+  return item ? item.label : '未知'
+}
+
+// ==================医嘱给药三要素字典(长期/临时医嘱共用)====================
+export interface MedOption {
+  value: string
+  label: string // 下拉框主显:缩写 + 中文
+  cn: string // 中文含义
+  tip: string // 临床常见使用场景(hover tips)
+}
+
+// 给药频次
+export const MED_FREQUENCY_OPTIONS: MedOption[] = [
+  { value: 'qd', label: 'qd 每日1次', cn: '每日1次', tip: '常规慢病长期口服用药' },
+  { value: 'bid', label: 'bid 每日2次', cn: '每日2次', tip: '多数普通口服药早晚分服' },
+  { value: 'tid', label: 'tid 每日3次', cn: '每日3次', tip: '需随三餐规律服用的药物' },
+  { value: 'qid', label: 'qid 每日4次', cn: '每日4次', tip: '严格间隔6小时的抗感染治疗' },
+  { value: 'qn', label: 'qn 每晚1次', cn: '每晚1次', tip: '助眠、长效降压、降脂类药物' },
+  { value: 'qod', label: 'qod 隔日1次', cn: '隔日1次', tip: '特殊半衰期较长的药物' },
+  { value: 'qw', label: 'qw 每周1次', cn: '每周1次', tip: '长效特殊制剂、慢病维持给药' },
+  { value: 'biw', label: 'biw 每周2次', cn: '每周2次', tip: '康复理疗、部分专科治疗' },
+  { value: 'tiw', label: 'tiw 每周3次', cn: '每周3次', tip: '透析、肌肉注射类治疗' },
+  { value: 'q2w', label: 'q2w 每2周1次', cn: '每2周1次', tip: '化疗、靶向、生物制剂给药' },
+  { value: 'q3w', label: 'q3w 每3周1次', cn: '每3周1次', tip: '化疗、靶向、生物制剂给药' },
+  { value: 'q4w', label: 'q4w 每4周1次', cn: '每4周1次', tip: '化疗、靶向、生物制剂给药' },
+  { value: 'q8h', label: 'q8h 每8小时1次', cn: '每8小时1次', tip: '重症、抗感染精准间隔给药' },
+  { value: 'q6h', label: 'q6h 每6小时1次', cn: '每6小时1次', tip: '重症、抗感染精准间隔给药' },
+  { value: 'q4h', label: 'q4h 每4小时1次', cn: '每4小时1次', tip: '重症、抗感染精准间隔给药' }
+]
+
+// 给药时间
+export const MED_TIME_OPTIONS: MedOption[] = [
+  { value: 'ac', label: 'ac 餐前', cn: '餐前服用', tip: '促胃动力、降糖、胃黏膜保护剂' },
+  { value: 'pc', label: 'pc 餐后', cn: '餐后服用', tip: '对胃肠道有刺激的普通药物' },
+  { value: 'hs', label: 'hs 睡前', cn: '睡前服用', tip: '助眠、长效降压、抗过敏药' },
+  { value: 'st', label: 'st 立即', cn: '立即执行', tip: '急诊急救、紧急处置类医嘱' },
+  { value: 'prn', label: 'prn 必要时', cn: '必要时使用', tip: '止痛、止吐、退热等对症药物' },
+  { value: 'sos', label: 'sos 紧急1次', cn: '紧急需要时1次', tip: '限24小时内仅用1次的应急药' },
+  { value: 'am', label: 'am 上午', cn: '上午', tip: '标注日间常规给药时间' },
+  { value: 'pm', label: 'pm 下午', cn: '下午', tip: '标注午后常规给药时间' }
+]
+
+// 给药途径
+export const MED_ROUTE_OPTIONS: MedOption[] = [
+  { value: 'po', label: 'po 口服', cn: '口服', tip: '绝大多数常规口服药物' },
+  { value: 'iv', label: 'iv 静脉推注', cn: '静脉推注', tip: '小容量药剂快速静脉给药' },
+  { value: 'ivgtt', label: 'ivgtt/iv drip 静脉滴注', cn: '静脉滴注/输液', tip: '大容量补液、抗感染输液' },
+  { value: 'im', label: 'im 肌内注射', cn: '肌内注射', tip: '常规疫苗、止痛针、部分抗生素' },
+  { value: 'sc', label: 'sc 皮下注射', cn: '皮下注射', tip: '胰岛素、低分子肝素等药物' },
+  { value: 'ih', label: 'ih 皮下注射', cn: '皮下注射', tip: '胰岛素、低分子肝素等药物' },
+  { value: 'igtt', label: 'igtt 胃肠滴注', cn: '胃肠滴注', tip: '鼻饲、肠内营养输注' },
+  { value: 'inh', label: 'inh 吸入给药', cn: '吸入给药', tip: '哮喘、慢阻肺的雾化治疗' },
+  { value: 'ext', label: 'ext 外用', cn: '外用', tip: '皮肤药膏、黏膜局部用药' }
+]
+
+// ==================临时医嘱单====================
+// 单条临时医嘱条目(对应打印页一行)
+export interface ShortDoctorOrderEntry {
+  id?: number
+  orderTime?: string
+  orderContent?: string
+  // 给药三要素(结构化选择项)
+  frequency?: string // 给药频次,存缩写,如 qd
+  medTime?: string // 给药时间,存缩写,如 ac
+  route?: string // 给药途径,存缩写,如 po
+  doctorSign?: string
+  executeTime?: string
+  nurseSign?: string
+  remark?: string
+}
+
+// 临时医嘱单 VO(按长者分组的单据,含多条 entry)
+export interface ShortDoctorOrderVO {
+  id?: number
+  elderId?: number
+  elderName?: string
+  gender?: number | string
+  genderName?: string
+  bedName?: string
+  nurseLevelName?: string
+  tenantId?: number | string
+  // 汇总字段(后端可冗余返回,便于列表展示)
+  entryCount?: number
+  latestOrderTime?: string
+  latestExecuteTime?: string
+  entries?: ShortDoctorOrderEntry[]
+  createTime?: string
+}
+
+export interface ShortDoctorOrderForm {
+  id?: number
+  elderId: number
+  entries: ShortDoctorOrderEntry[]
+}
+
+// 临时医嘱单分页
+export const getShortDoctorOrderPage = (query: Recordable) => {
+  return request.get({
+    url: 'elderly/shortDoctorOrder/page',
+    params: query
+  })
+}
+
+// 临时医嘱单详情
+export const getShortDoctorOrder = (id: number) => {
+  return request.get({
+    url: `elderly/shortDoctorOrder/get?id=${id}`
+  })
+}
+
+// 新增临时医嘱单
+export const createShortDoctorOrder = (data: ShortDoctorOrderForm) => {
+  return request.post({
+    url: 'elderly/shortDoctorOrder/create',
+    data
+  })
+}
+
+// 更新临时医嘱单
+export const updateShortDoctorOrder = (data: ShortDoctorOrderForm) => {
+  return request.put({
+    url: 'elderly/shortDoctorOrder/update',
+    data
+  })
+}
+
+// 删除临时医嘱单
+export const deleteShortDoctorOrder = (id: number) => {
+  return request.delete({
+    url: `elderly/shortDoctorOrder/delete?id=${id}`
+  })
+}
+
+// ==================医嘱执行记录(护士执行单)====================
+// 一条"护士执行"记录:前端按医嘱展开生成执行行后,护士点击执行即写入一条
+export interface DoctorOrderExecution {
+  id?: number
+  orderType?: number // 1=长期医嘱 2=临时医嘱
+  orderEntryId?: number // 关联原医嘱条目 id(长期/临时各自的 entry id)
+  elderId?: number
+  elderName?: string
+  bedName?: string
+  orderContent?: string
+  frequency?: string // 给药频次缩写
+  medTime?: string // 给药时间缩写
+  route?: string // 给药途径缩写
+  planTime?: string // 计划执行时间 YYYY-MM-DD HH:mm
+  execTime?: string // 实际执行时间
+  execNurseSign?: string // 执行护士签名
+  status?: number // 0=待执行 1=已执行 2=已跳过
+  remark?: string
+  tenantId?: number | string
+  createTime?: string
+}
+
+export const EXEC_STATUS = {
+  PENDING: 0,
+  DONE: 1,
+  SKIPPED: 2
+}
+
+export const EXEC_STATUS_OPTIONS = [
+  { label: '待执行', value: 0 },
+  { label: '已执行', value: 1 },
+  { label: '已跳过', value: 2 }
+]
+
+// 分页查询某日护士执行记录(已执行的会返回,前端据此标记行"已执行")
+export const getDoctorOrderExecutionPage = (query: Recordable) => {
+  return request.get({
+    url: 'elderly/doctorOrderExecution/page',
+    params: query
+  })
+}
+
+// 护士执行(新增/更新一条执行记录)
+export const executeDoctorOrder = (data: DoctorOrderExecution) => {
+  return request.post({
+    url: 'elderly/doctorOrderExecution/execute',
+    data
+  })
+}
+
+// 跳过/取消某次执行
+export const skipDoctorOrderExecution = (id: number) => {
+  return request.put({
+    url: `elderly/doctorOrderExecution/skip?id=${id}`
+  })
+}
+
+// 选项值 → 中文含义(空值返回空串)
+export const getMedCn = (options: MedOption[], val?: string): string => {
+  if (!val) return ''
+  const item = options.find((o) => o.value === val)
+  return item ? item.cn : val
+}
+
+/**
+ * 根据频次(frequency)与给药时间(medTime),在指定日期展开"计划执行时点"列表(返回 HH:mm 数组)。
+ * 说明:
+ * - 日频次(qd/bid/tid/qid/qn/q8h/q6h/q4h) 按医学常规给出多个时点的 HH:mm;
+ * - 周频次/间隔频次(qod/qw/biw/tiw/q2w/q3w/q4w) 无法对应到一天内的多个时点,统一给 1 个 09:00 代表点;
+ * - medTime 仅对"单时点"频次(如 qd/qn/qod/qw 等)做微调(hs→21:00、am→09:00、pc→08:00 等);
+ * - 该映射为前端启发式,真实排班以后端执行计划为准。
+ */
+export const buildExecutionPlanTimes = (frequency?: string, medTime?: string): string[] => {
+  const single = (base: string): string[] => [base]
+  const byMedTime = (def: string): string => {
+    switch (medTime) {
+      case 'hs':
+        return '21:00'
+      case 'am':
+        return '09:00'
+      case 'pm':
+        return '15:00'
+      case 'ac':
+        return '07:30'
+      case 'pc':
+        return '08:00'
+      default:
+        return def
+    }
+  }
+  switch (frequency) {
+    case 'qd':
+      return single(byMedTime('09:00'))
+    case 'bid':
+      return medTime === 'hs' ? ['09:00', '21:00'] : ['09:00', '15:00']
+    case 'tid':
+      return ['08:00', '12:00', '18:00']
+    case 'qid':
+      return ['06:00', '12:00', '18:00', '22:00']
+    case 'qn':
+      return single('21:00')
+    case 'qod':
+      return single(byMedTime('09:00'))
+    case 'qw':
+      return single(byMedTime('09:00'))
+    case 'biw':
+      return single('09:00')
+    case 'tiw':
+      return single('09:00')
+    case 'q2w':
+      return single('09:00')
+    case 'q3w':
+      return single('09:00')
+    case 'q4w':
+      return single('09:00')
+    case 'q8h':
+      return ['08:00', '16:00', '00:00']
+    case 'q6h':
+      return ['06:00', '12:00', '18:00', '00:00']
+    case 'q4h':
+      return ['04:00', '08:00', '12:00', '16:00', '20:00', '00:00']
+    default:
+      return single('09:00')
+  }
+}

+ 1 - 1
src/components/TenantSelect/src/TenantSelect.vue

@@ -62,7 +62,7 @@ const props = defineProps({
   single: {
     // 针对房源,流程模型只能查单个的这种
     type: Boolean,
-    defalut: false
+    default: false
   },
   disabled: {
     type: Boolean,

+ 135 - 0
src/views/elderly/elder/doctor-orders/execute-doctor-order/ExecuteDialog.vue

@@ -0,0 +1,135 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="isView ? '查看执行记录' : '护士执行医嘱'"
+    width="680px"
+    :close-on-click-modal="false"
+    append-to-body
+  >
+    <el-descriptions :column="2" border size="small" class="mb-16px">
+      <el-descriptions-item label="长者">{{ row.elderName || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="床位">{{ row.bedName || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="医嘱类型">
+        {{ row.orderType === 1 ? '长期医嘱' : '临时医嘱' }}
+      </el-descriptions-item>
+      <el-descriptions-item label="计划执行时间">{{ row.planTime || '-' }}</el-descriptions-item>
+      <el-descriptions-item label="给药频次">{{ freqCn(row.frequency) }}</el-descriptions-item>
+      <el-descriptions-item label="给药时间">{{ timeCn(row.medTime) }}</el-descriptions-item>
+      <el-descriptions-item label="给药途径">{{ routeCn(row.route) }}</el-descriptions-item>
+      <el-descriptions-item label="状态">
+        <el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
+      </el-descriptions-item>
+      <el-descriptions-item label="医嘱内容" :span="2">{{ row.orderContent || '-' }}</el-descriptions-item>
+    </el-descriptions>
+
+    <el-form label-width="120px" :disabled="isView">
+      <el-form-item label="实际执行时间" required>
+        <el-date-picker
+          v-model="form.execTime"
+          type="datetime"
+          format="YYYY-MM-DD HH:mm"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          placeholder="请选择实际执行时间"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="执行护士">
+        <el-input v-model="form.execNurseSign" placeholder="请输入执行护士签名" />
+      </el-form-item>
+      <el-form-item label="备注">
+        <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="visible = false">关闭</el-button>
+      <el-button v-if="!isView" type="primary" :loading="saving" @click="handleSave">
+        确认执行
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useUserStore } from '@/store/modules/user'
+import { formatDate } from '@/utils/formatTime'
+import {
+  executeDoctorOrder,
+  type DoctorOrderExecution,
+  MED_FREQUENCY_OPTIONS,
+  MED_TIME_OPTIONS,
+  MED_ROUTE_OPTIONS,
+  getMedCn
+} from '@/api/elderly/elder/bed-change'
+
+const message = useMessage()
+const userStore = useUserStore()
+
+const visible = ref(false)
+const isView = ref(false)
+const saving = ref(false)
+const row = ref<any>({})
+
+const form = reactive<Partial<DoctorOrderExecution>>({
+  execTime: '',
+  execNurseSign: '',
+  remark: ''
+})
+
+const freqCn = (v?: string) => getMedCn(MED_FREQUENCY_OPTIONS, v)
+const timeCn = (v?: string) => getMedCn(MED_TIME_OPTIONS, v)
+const routeCn = (v?: string) => getMedCn(MED_ROUTE_OPTIONS, v)
+const statusType = (s?: number) => (s === 1 ? 'success' : s === 2 ? 'info' : 'warning')
+const statusText = (s?: number) => (s === 1 ? '已执行' : s === 2 ? '已跳过' : '待执行')
+
+const open = (r: any, view = false) => {
+  row.value = r || {}
+  isView.value = view
+  form.execTime = r?.execTime || formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
+  form.execNurseSign = r?.execNurseSign || userStore.nickname || ''
+  form.remark = r?.remark || ''
+  visible.value = true
+}
+
+const emit = defineEmits<{ (e: 'success'): void }>()
+
+const handleSave = async () => {
+  // if (!form.execNurseSign) {
+  //   message.error('请填写执行护士签名')
+  //   return
+  // }
+  saving.value = true
+  try {
+    const payload: DoctorOrderExecution = {
+      id: row.value.execId,
+      orderType: row.value.orderType,
+      orderEntryId: row.value.orderEntryId,
+      elderId: row.value.elderId,
+      elderName: row.value.elderName,
+      bedName: row.value.bedName,
+      orderContent: row.value.orderContent,
+      frequency: row.value.frequency,
+      medTime: row.value.medTime,
+      route: row.value.route,
+      planTime: row.value.planTime,
+      execTime: form.execTime,
+      execNurseSign: form.execNurseSign,
+      remark: form.remark,
+      status: 1,
+      tenantId: row.value.tenantId
+    }
+    await executeDoctorOrder(payload)
+    message.success('执行成功')
+    visible.value = false
+    emit('success')
+  } catch {
+    // 错误提示由请求拦截器统一处理
+  } finally {
+    saving.value = false
+  }
+}
+
+defineExpose({ open })
+</script>

+ 600 - 0
src/views/elderly/elder/doctor-orders/execute-doctor-order/index.vue

@@ -0,0 +1,600 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
+      <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
+      <el-form-item label="长者姓名" prop="elderName">
+        <el-input
+          @keyup.enter="handleQuery"
+          v-model="queryParams.elderName"
+          placeholder="请输入长者姓名"
+          class="!w-200px"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="执行日期" prop="execDate">
+        <el-date-picker
+          v-model="queryParams.execDate"
+          type="date"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          placeholder="选择执行日期"
+          style="width: 180px"
+        />
+      </el-form-item>
+      <el-form-item label="医嘱类型" prop="orderType">
+        <el-select v-model="queryParams.orderType" placeholder="全部" clearable class="!w-160px">
+          <el-option label="长期医嘱" :value="1" />
+          <el-option label="临时医嘱" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="全部" clearable class="!w-160px">
+          <el-option label="待执行" :value="0" />
+          <el-option label="已执行" :value="1" />
+          <el-option label="已跳过" :value="2" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 查询
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> 重置
+        </el-button>
+        <el-button type="primary" plain @click="openPrintDialog('all')">
+          <Icon icon="ep:printer" class="mr-5px" /> 打印执行单
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="pagedList" border stripe>
+      <el-table-column label="长者" prop="elderName" width="110" fixed />
+      <el-table-column label="床位" prop="bedName" width="120" />
+      <el-table-column label="类型" width="90" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.orderType === 1 ? 'primary' : 'warning'" size="small" effect="plain">
+            {{ row.orderType === 1 ? '长期' : '临时' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="医嘱内容" prop="orderContent" min-width="220" show-overflow-tooltip />
+      <el-table-column label="频次" width="90" align="center">
+        <template #default="{ row }">{{ freqCn(row.frequency) }}</template>
+      </el-table-column>
+      <el-table-column label="给药时间" width="90" align="center">
+        <template #default="{ row }">{{ timeCn(row.medTime) }}</template>
+      </el-table-column>
+      <el-table-column label="途径" width="90" align="center">
+        <template #default="{ row }">{{ routeCn(row.route) }}</template>
+      </el-table-column>
+      <el-table-column label="计划执行时间" prop="planTime" width="160" />
+      <el-table-column label="执行护士" prop="execNurseSign" width="100" />
+      <el-table-column label="实际执行时间" prop="execTime" width="160" />
+      <el-table-column label="状态" width="90" align="center">
+        <template #default="{ row }">
+          <el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="220" align="center" fixed="right">
+        <template #default="{ row }">
+          <el-button
+            v-if="row.status === 0"
+            link
+            type="primary"
+            @click="openExecute(row, false)"
+            v-hasPermi="['doctor-order-execute:execute']"
+            >执行</el-button
+          >
+          <el-button
+            v-if="row.status === 0"
+            link
+            type="danger"
+            @click="handleSkip(row)"
+            v-hasPermi="['doctor-order-execute:skip']"
+            >跳过</el-button
+          >
+          <el-button v-if="row.status !== 0" link type="primary" @click="openExecute(row, true)"
+            >查看</el-button
+          >
+          <el-button link type="warning" @click="openPrintDialog('single', row)" v-hasPermi="['doctor-order-execute:print']"
+            >打印</el-button
+          >
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      :total="total"
+      v-model:page="pageNo"
+      v-model:limit="pageSize"
+      @pagination="() => {}"
+    />
+  </ContentWrap>
+
+  <!-- 执行弹窗 -->
+  <ExecuteDialog ref="execRef" @success="buildRows" />
+
+  <!-- 跳过原因弹窗 -->
+  <el-dialog
+    v-model="skipVisible"
+    title="跳过执行"
+    width="500px"
+    :close-on-click-modal="false"
+    append-to-body
+  >
+    <el-alert
+      :title="`${skipRow.elderName || ''} · ${skipRow.bedName || ''}`"
+      :description="skipRow.orderContent || ''"
+      type="warning"
+      :closable="false"
+      class="mb-16px"
+    />
+    <el-form label-width="90px">
+      <el-form-item label="计划时间">{{ skipRow.planTime || '-' }}</el-form-item>
+      <el-form-item label="跳过原因" required>
+        <el-input
+          v-model="skipForm.remark"
+          type="textarea"
+          :rows="3"
+          maxlength="200"
+          show-word-limit
+          placeholder="请填写本次不执行的原因,如:长者外出 / 拒采 / 当日禁食转院…"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="skipVisible = false">取消</el-button>
+      <el-button type="primary" :loading="skipping" @click="confirmSkip">确认跳过</el-button>
+    </template>
+  </el-dialog>
+
+  <!-- 打印弹窗:打印区在可见状态下由 v-print 克隆,避免离屏 div 导致空白页 -->
+  <el-dialog
+    v-model="printDialogVisible"
+    :title="printMode === 'all' ? '医嘱执行单' : '医嘱执行单(单条)'"
+    width="1080px"
+    append-to-body
+    :close-on-click-modal="false"
+  >
+    <!-- 汇总(当日全量) -->
+    <div v-if="printMode === 'all'" id="execPrintArea" class="exec-print" style="padding: 12px 16px; color: #000; background: #fff">
+      <div style="text-align: center; font-size: 20px; font-weight: bold; margin: 6px 0 18px">
+        医嘱执行单
+      </div>
+      <div style="display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 10px">
+        <span>机构:{{ tenantName }}</span>
+        <span>执行日期:{{ queryParams.execDate }}</span>
+      </div>
+      <table
+        border="1"
+        cellspacing="0"
+        cellpadding="6"
+        style="width: 100%; border-collapse: collapse; font-size: 14px"
+      >
+        <thead>
+          <tr>
+            <th>长者</th>
+            <th>床位</th>
+            <th>类型</th>
+            <th>医嘱内容</th>
+            <th>频次</th>
+            <th>给药时间</th>
+            <th>途径</th>
+            <th>计划时间</th>
+            <th>执行护士</th>
+            <th>实际时间</th>
+            <th>状态</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(item, idx) in list" :key="idx">
+            <td>{{ item.elderName }}</td>
+            <td>{{ item.bedName }}</td>
+            <td>{{ item.orderType === 1 ? '长期' : '临时' }}</td>
+            <td>{{ item.orderContent }}</td>
+            <td>{{ freqCn(item.frequency) }}</td>
+            <td>{{ timeCn(item.medTime) }}</td>
+            <td>{{ routeCn(item.route) }}</td>
+            <td>{{ item.planTime }}</td>
+            <td>{{ item.execNurseSign }}</td>
+            <td>{{ item.execTime }}</td>
+            <td>{{ statusText(item.status) }}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 单条 -->
+    <div v-else id="singlePrintArea" class="exec-print" style="padding: 12px 16px; color: #000; background: #fff">
+      <div v-if="singlePrintRow">
+        <div style="text-align: center; font-size: 20px; font-weight: bold; margin: 6px 0 18px">
+          医嘱执行单(单条)
+        </div>
+        <div
+          style="
+            display: grid;
+            grid-template-columns: repeat(2, 1fr);
+            gap: 4px 16px;
+            font-size: 14px;
+            margin-bottom: 14px;
+            line-height: 1.8;
+          "
+        >
+          <div><strong>长者:</strong>{{ singlePrintRow.elderName }}</div>
+          <div><strong>床位:</strong>{{ singlePrintRow.bedName }}</div>
+          <div><strong>类型:</strong>{{ singlePrintRow.orderType === 1 ? '长期医嘱' : '临时医嘱' }}</div>
+          <div><strong>状态:</strong>{{ statusText(singlePrintRow.status) }}</div>
+          <div><strong>频次:</strong>{{ freqCn(singlePrintRow.frequency) }}</div>
+          <div><strong>给药时间:</strong>{{ timeCn(singlePrintRow.medTime) }}</div>
+          <div><strong>途径:</strong>{{ routeCn(singlePrintRow.route) }}</div>
+          <div><strong>计划执行时间:</strong>{{ singlePrintRow.planTime }}</div>
+          <div><strong>实际执行时间:</strong>{{ singlePrintRow.execTime || '-' }}</div>
+          <div><strong>执行护士:</strong>{{ singlePrintRow.execNurseSign || '-' }}</div>
+          <div style="grid-column: 1 / -1"><strong>医嘱内容:</strong>{{ singlePrintRow.orderContent }}</div>
+          <div v-if="singlePrintRow.remark" style="grid-column: 1 / -1">
+            <strong>{{ singlePrintRow.status === 2 ? '跳过原因:' : '备注:' }}</strong
+            >{{ singlePrintRow.remark }}
+          </div>
+        </div>
+        <div
+          style="
+            margin-top: 40px;
+            display: flex;
+            justify-content: space-between;
+            font-size: 14px;
+          "
+        >
+          <span>执行护士签名:__________________</span>
+          <span>核对护士签名:__________________</span>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <el-button @click="printDialogVisible = false">关闭</el-button>
+      <el-button v-if="printMode === 'all'" type="primary" v-print="printAll">打印</el-button>
+      <el-button v-else type="primary" v-print="printOne">打印</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed } from 'vue'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useUserStore } from '@/store/modules/user'
+import { formatDate } from '@/utils/formatTime'
+import {
+  getLongDoctorOrderPage,
+  getShortDoctorOrderPage,
+  getDoctorOrderExecutionPage,
+  executeDoctorOrder,
+  buildExecutionPlanTimes,
+  MED_FREQUENCY_OPTIONS,
+  MED_TIME_OPTIONS,
+  MED_ROUTE_OPTIONS,
+  getMedCn,
+  type DoctorOrderExecution
+} from '@/api/elderly/elder/bed-change'
+import ExecuteDialog from './ExecuteDialog.vue'
+
+defineOptions({ name: 'DoctorOrderExecute' })
+
+const message = useMessage()
+const userStore = useUserStore()
+const tenantName = computed(() => userStore.tenant?.name || '')
+
+/** v-print 配置(vue3-print-nb 期望传 {id, popTitle, ...} 对象,不是字符串选择器) */
+const printAll = {
+  id: 'execPrintArea',
+  popTitle: '医嘱执行单',
+  extraCss: '',
+  preview: false,
+  zIndex: 20002
+}
+const printOne = {
+  id: 'singlePrintArea',
+  popTitle: '医嘱执行单(单条)',
+  extraCss: '',
+  preview: false,
+  zIndex: 20002
+}
+/** 打印弹窗:可见状态下由 v-print 克隆,避免离屏 div 空白页 */
+const printDialogVisible = ref(false)
+const printMode = ref<'all' | 'single'>('all')
+const singlePrintRow = ref<any>(null)
+/** 打开打印弹窗:all=当日汇总,single=当前行 */
+const openPrintDialog = (mode: 'all' | 'single', row?: any) => {
+  printMode.value = mode
+  singlePrintRow.value = mode === 'single' ? { ...row } : null
+  printDialogVisible.value = true
+}
+
+const loading = ref(false)
+const list = ref<any[]>([])
+const pageNo = ref(1)
+const pageSize = ref(20)
+const total = computed(() => list.value.length)
+const pagedList = computed(() => {
+  const start = (pageNo.value - 1) * pageSize.value
+  return list.value.slice(start, start + pageSize.value)
+})
+
+const queryParams = reactive({
+  tenantIds: userStore.orgTenantId as any,
+  elderName: undefined as string | undefined,
+  execDate: formatDate(new Date(), 'YYYY-MM-DD'),
+  orderType: undefined as number | undefined,
+  status: undefined as number | undefined
+})
+const queryFormRef = ref()
+
+const freqCn = (v?: string) => getMedCn(MED_FREQUENCY_OPTIONS, v)
+const timeCn = (v?: string) => getMedCn(MED_TIME_OPTIONS, v)
+const routeCn = (v?: string) => getMedCn(MED_ROUTE_OPTIONS, v)
+const statusType = (s?: number) => (s === 1 ? 'success' : s === 2 ? 'info' : 'warning')
+const statusText = (s?: number) => (s === 1 ? '已执行' : s === 2 ? '已跳过' : '待执行')
+
+/** 构造一条执行行 */
+const makeRow = (orderType: number, e: any, planTime: string, vo: any) => ({
+  orderType,
+  orderEntryId: e.id,
+  elderId: vo.elderId,
+  elderName: vo.elderName,
+  bedName: vo.bedName,
+  orderContent: e.orderContent,
+  frequency: e.frequency,
+  medTime: e.medTime,
+  route: e.route,
+  planTime,
+  status: 0,
+  execTime: '',
+  execNurseSign: '',
+  remark: '',
+  tenantId: vo.tenantId
+})
+
+/** 聚合:长期(在用) + 临时(当日) → 按频次展开 → 合并已执行记录 */
+const buildRows = async () => {
+  loading.value = true
+  pageNo.value = 1
+  try {
+    const { tenantIds, execDate, elderName } = queryParams
+    // 1) 长期医嘱
+    let longOrders: any[] = []
+    try {
+      const lp = await getLongDoctorOrderPage({
+        pageNo: 1,
+        pageSize: 100,
+        tenantIds,
+        elderName: elderName || undefined
+      })
+      longOrders = lp.list || []
+    } catch (e) {
+      console.warn('[执行医嘱] 长期医嘱列表失败,使用测试数据', e)
+      longOrders = buildMockLong(execDate)
+    }
+    // 2) 临时医嘱
+    let shortOrders: any[] = []
+    try {
+      const sp = await getShortDoctorOrderPage({
+        pageNo: 1,
+        pageSize: 100,
+        tenantIds,
+        elderName: elderName || undefined
+      })
+      shortOrders = sp.list || []
+    } catch (e) {
+      console.warn('[执行医嘱] 临时医嘱列表失败,使用测试数据', e)
+      shortOrders = buildMockShort(execDate)
+    }
+
+    // 3) 展开执行行
+    const rows: any[] = []
+    longOrders.forEach((vo: any) => {
+      ;(vo.entries || []).forEach((e: any) => {
+        if (e.status !== 1) return // 仅在用医嘱需要执行
+        buildExecutionPlanTimes(e.frequency, e.medTime).forEach((t: string) => {
+          rows.push(makeRow(1, e, `${execDate} ${t}`, vo))
+        })
+      })
+    })
+    shortOrders.forEach((vo: any) => {
+      ;(vo.entries || []).forEach((e: any) => {
+        const plan = e.orderTime
+        if (!plan || plan.slice(0, 10) !== execDate) return
+        rows.push(makeRow(2, e, plan, vo))
+      })
+    })
+
+    // 4) 拉取已执行记录,合并标记
+    const execMap = new Map<string, any>()
+    try {
+      const ep = await getDoctorOrderExecutionPage({
+        pageNo: 1,
+        pageSize: 1000,
+        tenantIds,
+        planDate: execDate
+      })
+      ;(ep.list || []).forEach((r: any) => {
+        execMap.set(`${r.orderType}-${r.orderEntryId}-${r.planTime}`, r)
+      })
+    } catch (e) {
+      console.warn('[执行医嘱] 执行记录查询失败,全部按待执行展示', e)
+    }
+    rows.forEach((r) => {
+      const ex = execMap.get(`${r.orderType}-${r.orderEntryId}-${r.planTime}`)
+      if (ex) {
+        r.execId = ex.id
+        r.status = ex.status ?? 1
+        r.execTime = ex.execTime || ''
+        r.execNurseSign = ex.execNurseSign || ''
+        r.remark = ex.remark || ''
+      }
+    })
+
+    // 5) 筛选 + 排序
+    let filtered = rows
+    if (queryParams.orderType != null) {
+      filtered = filtered.filter((r) => r.orderType === queryParams.orderType)
+    }
+    if (queryParams.status != null) {
+      filtered = filtered.filter((r) => r.status === queryParams.status)
+    }
+    filtered.sort((a, b) => (a.planTime || '').localeCompare(b.planTime || ''))
+    list.value = filtered
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 测试假数据:长期医嘱(在用条目,含多频次) */
+const buildMockLong = (execDate: string): any[] => [
+  {
+    id: 1001,
+    elderId: 801,
+    elderName: '张建国',
+    bedName: '1号楼-301-01',
+    tenantId: queryParams.tenantIds,
+    entries: [
+      {
+        id: 9001,
+        orderTime: `${execDate} 09:00:00`,
+        orderContent: '口服苯磺酸氨氯地平片 5mg',
+        frequency: 'qd',
+        medTime: 'am',
+        route: 'po',
+        status: 1,
+        statusText: '在用'
+      },
+      {
+        id: 9002,
+        orderTime: `${execDate} 09:00:00`,
+        orderContent: '二甲双胍缓释片 0.5g 早晚服用',
+        frequency: 'bid',
+        medTime: 'pc',
+        route: 'po',
+        status: 1,
+        statusText: '在用'
+      }
+    ]
+  },
+  {
+    id: 1002,
+    elderId: 802,
+    elderName: '刘秀英',
+    bedName: '2号楼-205-03',
+    tenantId: queryParams.tenantIds,
+    entries: [
+      {
+        id: 9003,
+        orderTime: `${execDate} 08:00:00`,
+        orderContent: '胰岛素皮下注射,餐前 15 分钟',
+        frequency: 'tid',
+        medTime: 'ac',
+        route: 'sc',
+        status: 1,
+        statusText: '在用'
+      }
+    ]
+  }
+]
+
+/** 测试假数据:临时医嘱(医嘱时间为当日) */
+const buildMockShort = (execDate: string): any[] => [
+  {
+    id: 2001,
+    elderId: 803,
+    elderName: '王德海',
+    bedName: '1号楼-402-02',
+    tenantId: queryParams.tenantIds,
+    entries: [
+      {
+        id: 9101,
+        orderTime: `${execDate} 10:30:00`,
+        orderContent: '血常规 + 生化全套(空腹采血)',
+        frequency: 'st',
+        medTime: 'st',
+        route: 'iv',
+        executeTime: '',
+        nurseSign: ''
+      }
+    ]
+  }
+]
+
+/** 查询 */
+const handleQuery = () => {
+  buildRows()
+}
+
+/** 重置 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.elderName = undefined
+  queryParams.execDate = formatDate(new Date(), 'YYYY-MM-DD')
+  queryParams.orderType = undefined
+  queryParams.status = undefined
+  handleQuery()
+}
+
+/** 执行 / 查看 */
+const execRef = ref()
+const openExecute = (row: any, view: boolean) => {
+  execRef.value?.open(row, view)
+}
+
+/** 跳过:打开原因弹窗 */
+const skipVisible = ref(false)
+const skipRow = ref<any>({})
+const skipping = ref(false)
+const skipForm = reactive({ remark: '' })
+
+const handleSkip = (row: any) => {
+  skipRow.value = row
+  skipForm.remark = ''
+  skipVisible.value = true
+}
+
+/** 确认跳过:原因必填,写入一条 status=2 的执行记录 */
+const confirmSkip = async () => {
+  if (!skipForm.remark || !skipForm.remark.trim()) {
+    message.error('请填写跳过原因')
+    return
+  }
+  skipping.value = true
+  try {
+    const r = skipRow.value
+    const payload: DoctorOrderExecution = {
+      orderType: r.orderType,
+      orderEntryId: r.orderEntryId,
+      elderId: r.elderId,
+      elderName: r.elderName,
+      bedName: r.bedName,
+      orderContent: r.orderContent,
+      frequency: r.frequency,
+      medTime: r.medTime,
+      route: r.route,
+      planTime: r.planTime,
+      status: 2,
+      remark: skipForm.remark.trim(),
+      tenantId: r.tenantId
+    }
+    await executeDoctorOrder(payload)
+    message.success('已跳过')
+    skipVisible.value = false
+    buildRows()
+  } catch {
+    // 错误提示由请求拦截器统一处理
+  } finally {
+    skipping.value = false
+  }
+}
+
+onMounted(() => {
+  buildRows()
+})
+</script>

+ 455 - 0
src/views/elderly/elder/doctor-orders/long-doctor-order/Form.vue

@@ -0,0 +1,455 @@
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    :title="form.id ? '编辑长期医嘱单' : '新增长期医嘱单'"
+    width="80%"
+    class="long-doctor-order-dialog"
+    @close="handleClosed"
+  >
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <!-- 长者信息 -->
+      <el-form-item label="长者姓名" prop="elderId">
+        <selectElder
+          v-if="!isDetail"
+          v-model="form.elderId"
+          v-model:elder-name="form.elderName"
+          v-model:elder-sex="form.gender"
+          v-model:bed-name="form.bedName"
+          :disabled="!!form.id"
+          @elder="handleSelectElder"
+        />
+        <el-input v-else :model-value="form.elderName" disabled />
+      </el-form-item>
+
+      <el-row :gutter="16">
+        <el-col :span="8">
+          <el-form-item label="性别">
+            <el-input :model-value="form.genderName" disabled placeholder="选择长者后自动填充" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="床位号">
+            <el-input :model-value="form.bedName" disabled placeholder="选择长者后自动填充" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="护理等级">
+            <el-input
+              :model-value="form.nurseLevelName"
+              disabled
+              placeholder="选择长者后自动填充"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-divider content-position="left">
+        医嘱条目
+        <el-button
+          v-if="!isDetail"
+          link
+          type="primary"
+          class="!ml-10px"
+          @click="handleAddEntry"
+        >
+          <Icon icon="ep:plus" class="mr-2px" />新增条目
+        </el-button>
+      </el-divider>
+
+      <el-table :data="form.entries" border style="width: 100%" size="default">
+        <el-table-column label="序号" type="index" width="55" align="center" />
+        <el-table-column label="医嘱时间" width="190">
+          <template #default="{ row }">
+            <el-date-picker
+              v-model="row.orderTime"
+              type="datetime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              format="YYYY-MM-DD HH:mm"
+              placeholder="选择医嘱时间"
+              style="width: 100%"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="医嘱内容" min-width="200">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.orderContent"
+              type="textarea"
+              :rows="2"
+              placeholder="请输入医嘱内容"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="给药频次" width="140">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.frequency"
+              placeholder="频次"
+              clearable
+              filterable
+              :disabled="isDetail"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="o in MED_FREQUENCY_OPTIONS"
+                :key="o.value"
+                :value="o.value"
+                :label="o.label"
+                :title="o.tip"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="给药时间" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.medTime"
+              placeholder="时间"
+              clearable
+              filterable
+              :disabled="isDetail"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="o in MED_TIME_OPTIONS"
+                :key="o.value"
+                :value="o.value"
+                :label="o.label"
+                :title="o.tip"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="给药途径" width="140">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.route"
+              placeholder="途径"
+              clearable
+              filterable
+              :disabled="isDetail"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="o in MED_ROUTE_OPTIONS"
+                :key="o.value"
+                :value="o.value"
+                :label="o.label"
+                :title="o.tip"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="医生签名" width="130">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.doctorSign"
+              placeholder="医生签名"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="护士签名" width="130">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.nurseSign"
+              placeholder="护士签名"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="停止时间" width="190">
+          <template #default="{ row }">
+            <el-date-picker
+              v-model="row.stopTime"
+              type="datetime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              format="YYYY-MM-DD HH:mm"
+              placeholder="选择停止时间"
+              style="width: 100%"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="停止医生签名" width="140">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.stopDoctorSign"
+              placeholder="停止医生签名"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="停止护士签名" width="140">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.stopNurseSign"
+              placeholder="停止护士签名"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" min-width="160">
+          <template #default="{ row }">
+            <el-input v-model="row.remark" placeholder="备注" :disabled="isDetail" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="220" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              v-if="!isDetail"
+              link
+              :type="row.status === LONG_ORDER_STATUS.IN_USE ? 'warning' : 'success'"
+              :loading="row.id !== undefined && togglingId === row.id"
+              @click="handleToggleEntryStatus(row)"
+              v-hasPermi="['long-doctor-order:edit']"
+              >{{ row.status === LONG_ORDER_STATUS.IN_USE ? '停止医嘱' : '启用医嘱' }}</el-button
+            >
+            <el-button
+              v-if="!isDetail"
+              link
+              type="danger"
+              @click="handleRemoveEntry(getEntryIndex(row))"
+              >删除</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div v-if="!form.entries || form.entries.length === 0" class="empty-tip">
+        暂无医嘱,点击右上「新增条目」添加
+      </div>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button v-if="isDetail" type="primary" @click="handlePrint">打印</el-button>
+      <el-button v-if="!isDetail" v-loading="formLoading" type="primary" @click="submitForm">
+        提交
+      </el-button>
+    </template>
+  </Dialog>
+
+  <!-- 打印(详情页) -->
+  <Print ref="printRef" />
+</template>
+
+<script lang="ts" setup>
+import {
+  getLongDoctorOrder,
+  createLongDoctorOrder,
+  updateLongDoctorOrder,
+  updateLongDoctorOrderEntryStatus,
+  LONG_ORDER_STATUS,
+  MED_FREQUENCY_OPTIONS,
+  MED_TIME_OPTIONS,
+  MED_ROUTE_OPTIONS,
+  type LongDoctorOrderForm,
+  type LongDoctorOrderEntry
+} from '@/api/elderly/elder/bed-change'
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+import Print from './Print.vue'
+
+defineOptions({ name: 'LongDoctorOrderForm' })
+
+const message = useMessage()
+const { t } = useI18n()
+
+const dialogVisible = ref(false)
+const formLoading = ref(false)
+const isDetail = ref(false)
+const formRef = ref()
+const printRef = ref()
+const togglingId = ref<number | undefined>()
+
+const emptyEntry = (): LongDoctorOrderEntry => ({
+  orderTime: '',
+  orderContent: '',
+  frequency: '',
+  medTime: '',
+  route: '',
+  doctorSign: '',
+  nurseSign: '',
+  stopTime: '',
+  stopDoctorSign: '',
+  stopNurseSign: '',
+  remark: '',
+  status: LONG_ORDER_STATUS.NOT_STARTED,
+  statusText: '未开始'
+})
+
+const defaultForm: LongDoctorOrderForm & {
+  elderName?: string
+  gender?: number | string
+  genderName?: string
+  bedName?: string
+  nurseLevelName?: string
+} = {
+  id: undefined,
+  elderId: undefined,
+  elderName: '',
+  gender: '',
+  genderName: '',
+  bedName: '',
+  nurseLevelName: '',
+  entries: [emptyEntry()]
+}
+
+const form = reactive(JSON.parse(JSON.stringify(defaultForm)))
+
+const rules = {
+  elderId: [{ required: true, message: '请选择长者', trigger: 'change' }],
+  entries: [
+    {
+      validator: (_rule: any, _value: any, callback: any) => {
+        if (!form.entries || form.entries.length === 0) {
+          callback(new Error('请至少添加一条医嘱'))
+        } else if (form.entries.some((e) => !e.orderContent)) {
+          callback(new Error('每条医嘱必须填写医嘱内容'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ]
+}
+
+/** 选择长者后补全护理等级、性别字典 */
+const handleSelectElder = (elder: any) => {
+  if (!elder) return
+  form.nurseLevelName = elder.nurseLevelName || elder.nurseLevel || ''
+  form.bedName = elder.bedName || form.bedName || ''
+  form.gender = elder.elderSex ?? elder.gender ?? form.gender
+  form.genderName = getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender) || ''
+}
+
+/** 打开弹窗:detail=true 表示只读(详情场景) */
+const open = async (row: any = {}, detail: boolean = false) => {
+  dialogVisible.value = true
+  isDetail.value = !!detail
+  await nextTick()
+  resetForm()
+  if (row?.id) {
+    formLoading.value = true
+    try {
+      const data: any = await getLongDoctorOrder(row.id)
+      Object.assign(form, defaultForm, data)
+      form.genderName = getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender) || ''
+      form.nurseLevelName = data.nurseLevelName || ''
+      form.bedName = data.bedName || ''
+      form.entries = (data.entries || []).map((e: any) => ({ ...emptyEntry(), ...e }))
+      if (form.entries.length === 0) form.entries = [emptyEntry()]
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open })
+
+const resetForm = () => {
+  Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))
+  form.entries = [emptyEntry()]
+  formRef.value?.clearValidate()
+  togglingId.value = undefined
+}
+
+const handleClosed = () => {
+  resetForm()
+  dialogVisible.value = false
+}
+
+/** 新增 / 删除医嘱条目 */
+const handleAddEntry = () => {
+  form.entries.push(emptyEntry())
+}
+
+const getEntryIndex = (entry: any) => {
+  return form.entries.indexOf(entry)
+}
+
+const handleRemoveEntry = (idx: number) => {
+  if (idx < 0) return
+  if (form.entries.length <= 1) {
+    message.warning('至少保留一条医嘱')
+    return
+  }
+  form.entries.splice(idx, 1)
+}
+
+/** 单条医嘱启用 / 停止切换(走 entries 子表内按钮) */
+const handleToggleEntryStatus = async (row: LongDoctorOrderEntry) => {
+  if (!row.id) {
+    message.warning('新添加的条目请先保存后再启用/停止')
+    return
+  }
+  const isStopped = row.status === LONG_ORDER_STATUS.STOPPED
+  const action = isStopped ? '启用' : '停止'
+  const targetStatus = isStopped ? LONG_ORDER_STATUS.IN_USE : LONG_ORDER_STATUS.STOPPED
+  try {
+    await message.confirm(`确定${action}该长期医嘱吗?`, '提示')
+    togglingId.value = row.id
+    await updateLongDoctorOrderEntryStatus(row.id, targetStatus)
+    message.success(`已${action}`)
+    row.status = targetStatus
+    row.statusText = targetStatus === LONG_ORDER_STATUS.STOPPED ? '已停止' : '在用'
+  } catch {} finally {
+    togglingId.value = undefined
+  }
+}
+
+/** 从详情页打开打印 */
+const handlePrint = () => {
+  printRef.value?.open({ id: form.id })
+}
+
+const emit = defineEmits(['success'])
+
+const submitForm = async () => {
+  if (formLoading.value) return
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+  formLoading.value = true
+  try {
+    const payload: LongDoctorOrderForm = {
+      id: form.id,
+      elderId: form.elderId,
+      entries: form.entries.map((e) => ({ ...e }))
+    }
+    if (form.id) {
+      await updateLongDoctorOrder(payload)
+    } else {
+      await createLongDoctorOrder(payload)
+    }
+    message.success(t(form.id ? 'common.updateSuccess' : 'common.createSuccess'))
+    emit('success')
+    handleClosed()
+  } finally {
+    setTimeout(() => {
+      formLoading.value = false
+    }, 300)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.long-doctor-order-dialog :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 24px 0;
+  font-size: 14px;
+  border: 1px dashed #ddd;
+  margin-top: 8px;
+}
+</style>

+ 267 - 0
src/views/elderly/elder/doctor-orders/long-doctor-order/Print.vue

@@ -0,0 +1,267 @@
+<template>
+  <Dialog v-model="dialogVisible" title="长期医嘱单" width="90%" scroll>
+    <div class="long-order-print" id="longOrderPrintArea" v-loading="loading">
+      <h2 class="doc-title">长期医嘱单</h2>
+
+      <!-- 基本信息 -->
+      <div class="info-row">
+        <div class="info-cell"><span class="lbl">姓名:</span>{{ form.elderName || '' }}</div>
+        <div class="info-cell"><span class="lbl">性别:</span>{{ genderText }}</div>
+        <div class="info-cell"><span class="lbl">床号:</span>{{ form.bedName || '' }}</div>
+        <div class="info-cell">
+          <span class="lbl">护理等级:</span>{{ form.nurseLevelName || '' }}
+        </div>
+      </div>
+
+      <!-- 医嘱表 -->
+      <table class="order-table">
+        <colgroup>
+          <col style="width: 50px" />
+          <col style="width: 160px" />
+          <col />
+          <col style="width: 110px" />
+          <col style="width: 100px" />
+          <col style="width: 110px" />
+          <col style="width: 90px" />
+          <col style="width: 90px" />
+          <col style="width: 160px" />
+          <col style="width: 90px" />
+          <col style="width: 90px" />
+          <col style="width: 130px" />
+        </colgroup>
+        <thead>
+          <tr>
+            <th>序号</th>
+            <th>医嘱时间</th>
+            <th>医嘱内容</th>
+            <th>频次</th>
+            <th>给药时间</th>
+            <th>途径</th>
+            <th>医生<br />签名</th>
+            <th>护士<br />签名</th>
+            <th>停止时间</th>
+            <th>停止<br />医生签名</th>
+            <th>停止<br />护士签名</th>
+            <th>备注</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(item, idx) in paddedEntries" :key="idx">
+            <td>{{ idx + 1 }}</td>
+            <td>
+              <span class="cell-value">{{ formatFullTime(item.orderTime) }}</span>
+            </td>
+            <td>
+              <span class="cell-value pre">{{ item.orderContent || '' }}</span>
+            </td>
+            <td>
+              <span class="cell-value">{{ medLabel(MED_FREQUENCY_OPTIONS, item.frequency) }}</span>
+            </td>
+            <td>
+              <span class="cell-value">{{ medLabel(MED_TIME_OPTIONS, item.medTime) }}</span>
+            </td>
+            <td>
+              <span class="cell-value">{{ medLabel(MED_ROUTE_OPTIONS, item.route) }}</span>
+            </td>
+            <td>{{ item.doctorSign || '' }}</td>
+            <td>{{ item.nurseSign || '' }}</td>
+            <td>
+              <span class="cell-value">{{ formatFullTime(item.stopTime) }}</span>
+            </td>
+            <td>{{ item.stopDoctorSign || '' }}</td>
+            <td>{{ item.stopNurseSign || '' }}</td>
+            <td>
+              <span class="cell-value pre">{{ item.remark || '' }}</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="primary" v-print="print">打印</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import {
+  getLongDoctorOrder,
+  MED_FREQUENCY_OPTIONS,
+  MED_TIME_OPTIONS,
+  MED_ROUTE_OPTIONS,
+  type LongDoctorOrderEntry
+} from '@/api/elderly/elder/bed-change'
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+
+/** 把存取的缩写转成展示文案(如 qd → qd 每日1次),无值返回空串 */
+const medLabel = (options: any[], val?: string): string => {
+  if (!val) return ''
+  const item = options.find((o) => o.value === val)
+  return item ? item.label : val
+}
+
+defineOptions({ name: 'LongDoctorOrderPrint' })
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+
+const defaultForm = {
+  id: undefined,
+  elderId: undefined,
+  elderName: '',
+  gender: '',
+  bedName: '',
+  nurseLevelName: '',
+  entries: [] as LongDoctorOrderEntry[]
+}
+const form = reactive({ ...defaultForm })
+
+const genderText = computed(() =>
+  form.gender === '' || form.gender === undefined || form.gender === null
+    ? ''
+    : getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender)
+)
+
+/** 打印页最少展示 6 行,不足补空行 */
+const MIN_ROWS = 6
+const paddedEntries = computed<LongDoctorOrderEntry[]>(() => {
+  const list = form.entries || []
+  const padding: LongDoctorOrderEntry[] = Array.from(
+    { length: Math.max(0, MIN_ROWS - list.length) },
+    () => ({
+      orderTime: '',
+      orderContent: '',
+      frequency: '',
+      medTime: '',
+      route: '',
+      doctorSign: '',
+      nurseSign: '',
+      stopTime: '',
+      stopDoctorSign: '',
+      stopNurseSign: '',
+      remark: ''
+    })
+  )
+  return [...list, ...padding]
+})
+
+/**
+ * 标准格式化时间为 YYYY-MM-DD HH:mm
+ * - 字符串日期直接匹配;如已是标准格式则原样返回,否则尽量截前 16 位
+ * - 数字时间戳按本地时区解析
+ * - 空值/无效返回空串
+ */
+const formatFullTime = (val?: any): string => {
+  if (val === null || val === undefined || val === '') return ''
+  if (typeof val === 'string') {
+    const m = val.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/)
+    if (m) return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`
+    // 兜底:返回原字符串(不再做任何神秘切分)
+    return val
+  }
+  if (typeof val === 'number') {
+    const d = new Date(val)
+    if (isNaN(d.getTime())) return ''
+    const pad = (n: number) => String(n).padStart(2, '0')
+    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
+  }
+  return ''
+}
+
+/** 打开打印(详情页) */
+const open = async (row: any = {}) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    Object.assign(form, defaultForm, { entries: [] })
+    if (row?.id) {
+      const data: any = await getLongDoctorOrder(row.id)
+      Object.assign(form, defaultForm, data)
+      form.entries = (data.entries || []) as LongDoctorOrderEntry[]
+    }
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open })
+
+const handleClosed = () => {
+  dialogVisible.value = false
+  Object.assign(form, defaultForm, { entries: [] })
+}
+
+// 打印配置(vue3-print-nb)
+const print = {
+  id: 'longOrderPrintArea',
+  popTitle: '长期医嘱单',
+  extraCss: '',
+  preview: false,
+  zIndex: 20002
+}
+</script>
+
+<style lang="scss" scoped>
+.long-order-print {
+  padding: 10px 20px;
+  color: #000;
+  background: #fff;
+
+  .doc-title {
+    text-align: center;
+    font-size: 20px;
+    font-weight: bold;
+    margin: 6px 0 18px;
+  }
+
+  .info-row {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+    font-size: 14px;
+
+    .info-cell {
+      flex: 1;
+      min-width: 0;
+      padding: 0 8px;
+      .lbl {
+        font-weight: bold;
+      }
+    }
+  }
+
+  .order-table {
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+    font-size: 13px;
+
+    th,
+    td {
+      border: 1px solid #333;
+      padding: 6px 8px;
+      text-align: center;
+      vertical-align: middle;
+    }
+
+    thead th {
+      background: #fff;
+      font-weight: bold;
+    }
+
+    tbody td {
+      height: 40px;
+    }
+    .cell-value {
+      display: inline-block;
+      width: 100%;
+      text-align: center;
+    }
+    .cell-value.pre {
+      white-space: pre-wrap;
+      text-align: left;
+    }
+  }
+}
+</style>

+ 45 - 0
src/views/elderly/elder/doctor-orders/long-doctor-order/columns.ts

@@ -0,0 +1,45 @@
+import { DICT_TYPE } from '@/utils/dict'
+
+// ==================长期医嘱单====================
+export const LongDoctorOrderColumns = reactive([
+  {
+    label: '长者姓名',
+    field: 'elderName',
+    width: 120
+  },
+  {
+    label: '性别',
+    field: 'gender',
+    type: '1',
+    dictArr: DICT_TYPE.SYSTEM_USER_SEX,
+    width: 80
+  },
+  {
+    label: '床位号',
+    field: 'bedName',
+    width: 120
+  },
+  {
+    label: '护理等级',
+    field: 'nurseLevelName',
+    width: 120
+  },
+  {
+    label: '医嘱条数',
+    field: 'entryCount',
+    width: 100,
+    align: 'center'
+  },
+  {
+    label: '最新医嘱时间',
+    field: 'latestOrderTime',
+    format: true,
+    width: 170
+  },
+  {
+    label: '创建时间',
+    field: 'createTime',
+    format: true,
+    width: 170
+  }
+])

+ 338 - 0
src/views/elderly/elder/doctor-orders/long-doctor-order/index.vue

@@ -0,0 +1,338 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+    >
+      <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
+      <el-form-item label="长者姓名" prop="elderName">
+        <el-input
+          @keyup.enter="handleQuery"
+          v-model="queryParams.elderName"
+          placeholder="请输入长者姓名"
+          class="!w-240px"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="医嘱时间" prop="orderTime">
+        <el-date-picker
+          style="width: 240px"
+          v-model="queryParams.orderTime"
+          type="daterange"
+          :clearable="true"
+          placeholder="选择开始时间范围"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          date-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> 重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <TabBarBtn @add="openForm()" />
+    <Table2
+      v-loading="loading"
+      :data="list"
+      :columns="LongDoctorOrderColumns"
+      :queryParams="queryParams"
+      :op-width="240"
+      :show-edit-condition="() => false"
+      :show-delete-condition="() => false"
+      :show-detail-condition="() => false"
+      @del="handleDelete"
+    >
+      <template #next="{ scope }">
+        <el-button
+          link
+          type="primary"
+          @click="openForm(scope)"
+          v-hasPermi="['long-doctor-order:edit']"
+          >编辑</el-button
+        >
+        <el-button
+          link
+          type="danger"
+          @click="handleDelete(scope.id)"
+          v-hasPermi="['long-doctor-order:delete']"
+          >删除</el-button
+        >
+        <el-button link type="primary" @click="openPrint(scope)">打印</el-button>
+      </template>
+    </Table2>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 新增 / 编辑弹窗 -->
+  <Form ref="formRef" @success="getList" />
+
+  <!-- 打印(详情页) -->
+  <Print ref="printRef" />
+</template>
+
+<script lang="ts" setup>
+import {
+  getLongDoctorOrderPage,
+  deleteLongDoctorOrder,
+  getLongDoctorOrderStatusText
+} from '@/api/elderly/elder/bed-change'
+import { LongDoctorOrderColumns } from './columns'
+import Form from './Form.vue'
+import Print from './Print.vue'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'LongDoctorOrder' })
+
+const userStore = useUserStore()
+const message = useMessage()
+const { t } = useI18n()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<any[]>([])
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: undefined as string | undefined,
+  orderTime: [] as string[] | undefined,
+  tenantIds: userStore.orgTenantId as any
+})
+const queryFormRef = ref()
+
+/** 测试假数据:列表请求失败时填充,方便前端联调 */
+const buildMockLongDoctorOrders = () => {
+  const base = [
+    {
+      id: 1001,
+      elderId: 801,
+      elderName: '张建国',
+      gender: 1,
+      genderName: '男',
+      bedName: '1号楼-301-01',
+      nurseLevelName: '二级护理',
+      entryCount: 2,
+      latestOrderTime: '2026-08-10 09:00:00',
+      entries: [
+        {
+          id: 9001,
+          orderTime: '2026-08-10 09:00:00',
+          orderContent: '每日晨间测量血压,记录于护理记录单;低盐低脂饮食。',
+          doctorSign: '李医生',
+          nurseSign: '王护士',
+          stopTime: '',
+          stopDoctorSign: '',
+          stopNurseSign: '',
+          remark: '家属已告知注意事项',
+          status: 1,
+          statusText: '在用'
+        },
+        {
+          id: 9002,
+          orderTime: '2026-08-10 09:30:00',
+          orderContent: '口服降压药苯磺酸氨氯地平 5mg,每日一次,晨起服用。',
+          doctorSign: '李医生',
+          nurseSign: '',
+          stopTime: '',
+          stopDoctorSign: '',
+          stopNurseSign: '',
+          remark: '',
+          status: 0,
+          statusText: '未开始'
+        }
+      ]
+    },
+    {
+      id: 1002,
+      elderId: 802,
+      elderName: '刘秀英',
+      gender: 2,
+      genderName: '女',
+      bedName: '2号楼-205-03',
+      nurseLevelName: '一级护理',
+      entryCount: 1,
+      latestOrderTime: '2026-08-12 14:30:00',
+      entries: [
+        {
+          id: 9003,
+          orderTime: '2026-08-12 14:30:00',
+          orderContent: '口服阿司匹林肠溶片 100mg,每日一次,晚饭后服用。',
+          doctorSign: '陈医生',
+          nurseSign: '赵护士',
+          stopTime: '',
+          stopDoctorSign: '',
+          stopNurseSign: '',
+          remark: '',
+          status: 1,
+          statusText: '在用'
+        }
+      ]
+    },
+    {
+      id: 1003,
+      elderId: 803,
+      elderName: '王德海',
+      gender: 1,
+      genderName: '男',
+      bedName: '1号楼-402-02',
+      nurseLevelName: '三级护理',
+      entryCount: 1,
+      latestOrderTime: '2026-07-20 08:15:00',
+      entries: [
+        {
+          id: 9004,
+          orderTime: '2026-07-20 08:15:00',
+          orderContent: '康复科会诊,每周二次肢体功能训练。',
+          doctorSign: '周医生',
+          nurseSign: '孙护士',
+          stopTime: '2026-08-15 10:00:00',
+          stopDoctorSign: '周医生',
+          stopNurseSign: '孙护士',
+          remark: '长者转院,遵医嘱停止',
+          status: 2,
+          statusText: '已停止'
+        }
+      ]
+    },
+    {
+      id: 1004,
+      elderId: 804,
+      elderName: '赵桂芳',
+      gender: 2,
+      genderName: '女',
+      bedName: '3号楼-108-01',
+      nurseLevelName: '一级护理',
+      entryCount: 1,
+      latestOrderTime: '2026-08-18 16:00:00',
+      entries: [
+        {
+          id: 9005,
+          orderTime: '2026-08-18 16:00:00',
+          orderContent: '胰岛素皮下注射,餐前 15 分钟,剂量遵医嘱调整。',
+          doctorSign: '',
+          nurseSign: '',
+          stopTime: '',
+          stopDoctorSign: '',
+          stopNurseSign: '',
+          remark: '待主管医生签字',
+          status: 1,
+          statusText: '在用'
+        }
+      ]
+    }
+  ]
+  return base.map((item: any) => ({
+    ...item,
+    tenantId: queryParams.tenantIds
+  }))
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const params: Recordable = {
+      pageNo: queryParams.pageNo,
+      pageSize: queryParams.pageSize,
+      elderName: queryParams.elderName || undefined,
+      tenantIds: queryParams.tenantIds
+    }
+    // 时间范围
+    if (Array.isArray(queryParams.orderTime) && queryParams.orderTime.length === 2) {
+      params.beginTime = queryParams.orderTime[0]
+      params.endTime = queryParams.orderTime[1]
+    }
+    const data = await getLongDoctorOrderPage(params)
+    list.value = (data.list || []).map((item: any) => ({
+      ...item,
+      entryCount: item.entryCount ?? (item.entries ? item.entries.length : 0)
+    }))
+    total.value = data.total || 0
+  } catch (e) {
+    // 列表请求失败时填充测试假数据,便于前端联调(控制台有提示,不影响页面渲染)
+    console.warn('[长期医嘱单] 列表请求失败,已使用测试假数据。', e)
+    list.value = buildMockLongDoctorOrders()
+    total.value = list.value.length
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索 */
+const handleQuery = async () => {
+  await queryFormRef.value?.validate().catch(() => {})
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.orderTime = []
+  handleQuery()
+}
+
+/** 新增 / 编辑 */
+const formRef = ref()
+const printRef = ref()
+const openForm = (row: any = {}) => {
+  if (
+    !queryParams.tenantIds ||
+    (Array.isArray(queryParams.tenantIds) && queryParams.tenantIds.length === 0) ||
+    (Array.isArray(queryParams.tenantIds) && queryParams.tenantIds.length > 1 && !row.tenantId)
+  ) {
+    message.error('新增只能选择一个机构')
+    return
+  }
+  formRef.value.open(row)
+}
+
+/** 打开详情(只读) */
+const openDetail = (row: any = {}) => {
+  formRef.value.open(row, true)
+}
+
+/** 打印 */
+const openPrint = (row: any = {}) => {
+  printRef.value.open(row)
+}
+
+/** 删除 */
+const handleDelete = async (id: number) => {
+  try {
+    await message.confirm('确定删除该长期医嘱单吗?删除后不可恢复。', '提示')
+    await deleteLongDoctorOrder(id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 路由携带 elderName 时自动带入 */
+const route = useRoute()
+onMounted(() => {
+  if (route.query && route.query.elderName) {
+    queryParams.elderName = route.query.elderName as string
+  }
+  getList()
+})
+</script>

+ 387 - 0
src/views/elderly/elder/doctor-orders/short-doctor-order/Form.vue

@@ -0,0 +1,387 @@
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    :title="form.id ? '编辑临时医嘱单' : '新增临时医嘱单'"
+    width="80%"
+    class="short-doctor-order-dialog"
+    @close="handleClosed"
+  >
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <!-- 长者信息 -->
+      <el-form-item label="长者姓名" prop="elderId">
+        <selectElder
+          v-if="!isDetail"
+          v-model="form.elderId"
+          v-model:elder-name="form.elderName"
+          v-model:elder-sex="form.gender"
+          v-model:bed-name="form.bedName"
+          :disabled="!!form.id"
+          @elder="handleSelectElder"
+        />
+        <el-input v-else :model-value="form.elderName" disabled />
+      </el-form-item>
+
+      <el-row :gutter="16">
+        <el-col :span="8">
+          <el-form-item label="性别">
+            <el-input :model-value="form.genderName" disabled placeholder="选择长者后自动填充" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="床位号">
+            <el-input :model-value="form.bedName" disabled placeholder="选择长者后自动填充" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="8">
+          <el-form-item label="护理等级">
+            <el-input
+              :model-value="form.nurseLevelName"
+              disabled
+              placeholder="选择长者后自动填充"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-divider content-position="left">
+        医嘱条目
+        <el-button
+          v-if="!isDetail"
+          link
+          type="primary"
+          class="!ml-10px"
+          @click="handleAddEntry"
+        >
+          <Icon icon="ep:plus" class="mr-2px" />新增条目
+        </el-button>
+      </el-divider>
+
+      <el-table :data="form.entries" border style="width: 100%" size="default">
+        <el-table-column label="序号" type="index" width="55" align="center" />
+        <el-table-column label="医嘱时间" width="190">
+          <template #default="{ row }">
+            <el-date-picker
+              v-model="row.orderTime"
+              type="datetime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              format="YYYY-MM-DD HH:mm"
+              placeholder="选择医嘱时间"
+              style="width: 100%"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="医嘱内容" min-width="200">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.orderContent"
+              type="textarea"
+              :rows="2"
+              placeholder="请输入医嘱内容"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="给药频次" width="140">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.frequency"
+              placeholder="频次"
+              clearable
+              filterable
+              :disabled="isDetail"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="o in MED_FREQUENCY_OPTIONS"
+                :key="o.value"
+                :value="o.value"
+                :label="o.label"
+                :title="o.tip"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="给药时间" width="130">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.medTime"
+              placeholder="时间"
+              clearable
+              filterable
+              :disabled="isDetail"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="o in MED_TIME_OPTIONS"
+                :key="o.value"
+                :value="o.value"
+                :label="o.label"
+                :title="o.tip"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="给药途径" width="140">
+          <template #default="{ row }">
+            <el-select
+              v-model="row.route"
+              placeholder="途径"
+              clearable
+              filterable
+              :disabled="isDetail"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="o in MED_ROUTE_OPTIONS"
+                :key="o.value"
+                :value="o.value"
+                :label="o.label"
+                :title="o.tip"
+              />
+            </el-select>
+          </template>
+        </el-table-column>
+        <el-table-column label="医生签名" width="140">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.doctorSign"
+              placeholder="医生签名"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="执行时间" width="190">
+          <template #default="{ row }">
+            <el-date-picker
+              v-model="row.executeTime"
+              type="datetime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              format="YYYY-MM-DD HH:mm"
+              placeholder="选择执行时间"
+              style="width: 100%"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="护士签名" width="140">
+          <template #default="{ row }">
+            <el-input
+              v-model="row.nurseSign"
+              placeholder="护士签名"
+              :disabled="isDetail"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" min-width="160">
+          <template #default="{ row }">
+            <el-input v-model="row.remark" placeholder="备注" :disabled="isDetail" />
+          </template>
+        </el-table-column>
+        <el-table-column v-if="!isDetail" label="操作" width="80" align="center" fixed="right">
+          <template #default="{ $index }">
+            <el-button link type="danger" @click="handleRemoveEntry($index)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div v-if="!form.entries || form.entries.length === 0" class="empty-tip">
+        暂无医嘱,点击右上「新增条目」添加
+      </div>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button v-if="isDetail" type="primary" @click="handlePrint">打印</el-button>
+      <el-button v-if="!isDetail" v-loading="formLoading" type="primary" @click="submitForm">
+        提交
+      </el-button>
+    </template>
+  </Dialog>
+
+  <!-- 打印(详情页) -->
+  <Print ref="printRef" />
+</template>
+
+<script lang="ts" setup>
+import {
+  getShortDoctorOrder,
+  createShortDoctorOrder,
+  updateShortDoctorOrder,
+  MED_FREQUENCY_OPTIONS,
+  MED_TIME_OPTIONS,
+  MED_ROUTE_OPTIONS,
+  type ShortDoctorOrderForm,
+  type ShortDoctorOrderEntry
+} from '@/api/elderly/elder/bed-change'
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+import Print from './Print.vue'
+
+defineOptions({ name: 'ShortDoctorOrderForm' })
+
+const message = useMessage()
+const { t } = useI18n()
+
+const dialogVisible = ref(false)
+const formLoading = ref(false)
+const isDetail = ref(false)
+const formRef = ref()
+const printRef = ref()
+
+const emptyEntry = (): ShortDoctorOrderEntry => ({
+  orderTime: '',
+  orderContent: '',
+  frequency: '',
+  medTime: '',
+  route: '',
+  doctorSign: '',
+  executeTime: '',
+  nurseSign: '',
+  remark: ''
+})
+
+const defaultForm: ShortDoctorOrderForm & {
+  elderName?: string
+  gender?: number | string
+  genderName?: string
+  bedName?: string
+  nurseLevelName?: string
+} = {
+  id: undefined,
+  elderId: undefined,
+  elderName: '',
+  gender: '',
+  genderName: '',
+  bedName: '',
+  nurseLevelName: '',
+  entries: [emptyEntry()]
+}
+
+const form = reactive(JSON.parse(JSON.stringify(defaultForm)))
+
+const rules = {
+  elderId: [{ required: true, message: '请选择长者', trigger: 'change' }],
+  entries: [
+    {
+      validator: (_rule: any, _value: any, callback: any) => {
+        if (!form.entries || form.entries.length === 0) {
+          callback(new Error('请至少添加一条医嘱'))
+        } else if (form.entries.some((e) => !e.orderContent)) {
+          callback(new Error('每条医嘱必须填写医嘱内容'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'change'
+    }
+  ]
+}
+
+/** 选择长者后补全护理等级、性别字典 */
+const handleSelectElder = (elder: any) => {
+  if (!elder) return
+  form.nurseLevelName = elder.nurseLevelName || elder.nurseLevel || ''
+  form.bedName = elder.bedName || form.bedName || ''
+  form.gender = elder.elderSex ?? elder.gender ?? form.gender
+  form.genderName = getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender) || ''
+}
+
+/** 打开弹窗:detail=true 表示只读(详情场景) */
+const open = async (row: any = {}, detail: boolean = false) => {
+  dialogVisible.value = true
+  isDetail.value = !!detail
+  await nextTick()
+  resetForm()
+  if (row?.id) {
+    formLoading.value = true
+    try {
+      const data: any = await getShortDoctorOrder(row.id)
+      Object.assign(form, defaultForm, data)
+      form.genderName = getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender) || ''
+      form.nurseLevelName = data.nurseLevelName || ''
+      form.bedName = data.bedName || ''
+      form.entries = (data.entries || []).map((e: any) => ({ ...emptyEntry(), ...e }))
+      if (form.entries.length === 0) form.entries = [emptyEntry()]
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open })
+
+const resetForm = () => {
+  Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))
+  form.entries = [emptyEntry()]
+  formRef.value?.clearValidate()
+}
+
+const handleClosed = () => {
+  resetForm()
+  dialogVisible.value = false
+}
+
+/** 新增 / 删除医嘱条目 */
+const handleAddEntry = () => {
+  form.entries.push(emptyEntry())
+}
+const handleRemoveEntry = (idx: number) => {
+  if (form.entries.length <= 1) {
+    message.warning('至少保留一条医嘱')
+    return
+  }
+  form.entries.splice(idx, 1)
+}
+
+/** 从详情页打开打印 */
+const handlePrint = () => {
+  printRef.value?.open({ id: form.id })
+}
+
+const emit = defineEmits(['success'])
+
+const submitForm = async () => {
+  if (formLoading.value) return
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+  formLoading.value = true
+  try {
+    const payload: ShortDoctorOrderForm = {
+      id: form.id,
+      elderId: form.elderId,
+      entries: form.entries.map((e) => ({ ...e }))
+    }
+    if (form.id) {
+      await updateShortDoctorOrder(payload)
+    } else {
+      await createShortDoctorOrder(payload)
+    }
+    message.success(t(form.id ? 'common.updateSuccess' : 'common.createSuccess'))
+    emit('success')
+    handleClosed()
+  } finally {
+    setTimeout(() => {
+      formLoading.value = false
+    }, 300)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.short-doctor-order-dialog :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 24px 0;
+  font-size: 14px;
+  border: 1px dashed #ddd;
+  margin-top: 8px;
+}
+</style>

+ 255 - 0
src/views/elderly/elder/doctor-orders/short-doctor-order/Print.vue

@@ -0,0 +1,255 @@
+<template>
+  <Dialog v-model="dialogVisible" title="临时医嘱单" width="80%" scroll>
+    <div class="short-order-print" id="shortOrderPrintArea" v-loading="loading">
+      <h2 class="doc-title">临时医嘱单</h2>
+
+      <!-- 基本信息 -->
+      <div class="info-row">
+        <div class="info-cell"><span class="lbl">姓名:</span>{{ form.elderName || '' }}</div>
+        <div class="info-cell"><span class="lbl">性别:</span>{{ genderText }}</div>
+        <div class="info-cell"><span class="lbl">床号:</span>{{ form.bedName || '' }}</div>
+        <div class="info-cell">
+          <span class="lbl">护理等级:</span>{{ form.nurseLevelName || '' }}
+        </div>
+      </div>
+
+      <!-- 医嘱表 -->
+      <table class="order-table">
+        <colgroup>
+          <col style="width: 160px" />
+          <col />
+          <col style="width: 110px" />
+          <col style="width: 100px" />
+          <col style="width: 110px" />
+          <col style="width: 110px" />
+          <col style="width: 160px" />
+          <col style="width: 110px" />
+          <col style="width: 140px" />
+        </colgroup>
+        <thead>
+          <tr>
+            <th>医嘱时间</th>
+            <th>医嘱</th>
+            <th>频次</th>
+            <th>给药时间</th>
+            <th>途径</th>
+            <th>医生<br />签名</th>
+            <th>执行时间</th>
+            <th>护士<br />签名</th>
+            <th>备注</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="(item, idx) in paddedEntries" :key="idx">
+            <td>
+              <span class="cell-value">{{ formatFullTime(item.orderTime) }}</span>
+            </td>
+            <td>
+              <span class="cell-value pre">{{ item.orderContent || '' }}</span>
+            </td>
+            <td>
+              <span class="cell-value">{{ medLabel(MED_FREQUENCY_OPTIONS, item.frequency) }}</span>
+            </td>
+            <td>
+              <span class="cell-value">{{ medLabel(MED_TIME_OPTIONS, item.medTime) }}</span>
+            </td>
+            <td>
+              <span class="cell-value">{{ medLabel(MED_ROUTE_OPTIONS, item.route) }}</span>
+            </td>
+            <td>{{ item.doctorSign || '' }}</td>
+            <td>
+              <span class="cell-value">{{ formatFullTime(item.executeTime) }}</span>
+            </td>
+            <td>{{ item.nurseSign || '' }}</td>
+            <td>
+              <span class="cell-value pre">{{ item.remark || '' }}</span>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <template #footer>
+      <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="primary" v-print="print">打印</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import {
+  getShortDoctorOrder,
+  MED_FREQUENCY_OPTIONS,
+  MED_TIME_OPTIONS,
+  MED_ROUTE_OPTIONS,
+  type ShortDoctorOrderEntry
+} from '@/api/elderly/elder/bed-change'
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+
+/** 把存取的缩写转成展示文案(如 qd → qd 每日1次),无值返回空串 */
+const medLabel = (options: any[], val?: string): string => {
+  if (!val) return ''
+  const item = options.find((o) => o.value === val)
+  return item ? item.label : val
+}
+
+defineOptions({ name: 'ShortDoctorOrderPrint' })
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+
+const defaultForm = {
+  id: undefined,
+  elderId: undefined,
+  elderName: '',
+  gender: '',
+  bedName: '',
+  nurseLevelName: '',
+  entries: [] as ShortDoctorOrderEntry[]
+}
+const form = reactive({ ...defaultForm })
+
+const genderText = computed(() =>
+  form.gender === '' || form.gender === undefined || form.gender === null
+    ? ''
+    : getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender)
+)
+
+/** 打印页最少展示 6 行,不足补空行(与截图默认空白行一致) */
+const MIN_ROWS = 6
+const paddedEntries = computed<ShortDoctorOrderEntry[]>(() => {
+  const list = form.entries || []
+  const padding: ShortDoctorOrderEntry[] = Array.from(
+    { length: Math.max(0, MIN_ROWS - list.length) },
+    () => ({
+      orderTime: '',
+      orderContent: '',
+      frequency: '',
+      medTime: '',
+      route: '',
+      doctorSign: '',
+      executeTime: '',
+      nurseSign: '',
+      remark: ''
+    })
+  )
+  return [...list, ...padding]
+})
+
+/**
+ * 标准格式化时间为 YYYY-MM-DD HH:mm
+ * - 字符串日期直接匹配;如已是标准格式则原样返回,否则尽量截前 16 位
+ * - 数字时间戳按本地时区解析
+ * - 空值/无效返回空串
+ */
+const formatFullTime = (val?: any): string => {
+  if (val === null || val === undefined || val === '') return ''
+  if (typeof val === 'string') {
+    const m = val.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/)
+    if (m) return `${m[1]}-${m[2]}-${m[3]} ${m[4]}:${m[5]}`
+    return val
+  }
+  if (typeof val === 'number') {
+    const d = new Date(val)
+    if (isNaN(d.getTime())) return ''
+    const pad = (n: number) => String(n).padStart(2, '0')
+    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
+  }
+  return ''
+}
+
+/** 打开打印(详情页) */
+const open = async (row: any = {}) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    Object.assign(form, defaultForm, { entries: [] })
+    if (row?.id) {
+      const data: any = await getShortDoctorOrder(row.id)
+      Object.assign(form, defaultForm, data)
+      form.entries = (data.entries || []) as ShortDoctorOrderEntry[]
+    }
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open })
+
+const handleClosed = () => {
+  dialogVisible.value = false
+  Object.assign(form, defaultForm, { entries: [] })
+}
+
+// 打印配置(vue3-print-nb)
+const print = {
+  id: 'shortOrderPrintArea',
+  popTitle: '临时医嘱单',
+  extraCss: '',
+  preview: false,
+  zIndex: 20002
+}
+</script>
+
+<style lang="scss" scoped>
+.short-order-print {
+  padding: 10px 20px;
+  color: #000;
+  background: #fff;
+
+  .doc-title {
+    text-align: center;
+    font-size: 20px;
+    font-weight: bold;
+    margin: 6px 0 18px;
+  }
+
+  .info-row {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+    font-size: 14px;
+
+    .info-cell {
+      flex: 1;
+      min-width: 0;
+      padding: 0 8px;
+      .lbl {
+        font-weight: bold;
+      }
+    }
+  }
+
+  .order-table {
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+    font-size: 14px;
+
+    th,
+    td {
+      border: 1px solid #333;
+      padding: 6px 8px;
+      text-align: center;
+      vertical-align: middle;
+    }
+
+    thead th {
+      background: #fff;
+      font-weight: bold;
+    }
+
+    tbody td {
+      height: 40px;
+    }
+    .cell-value {
+      display: inline-block;
+      width: 100%;
+      text-align: center;
+    }
+    .cell-value.pre {
+      white-space: pre-wrap;
+      text-align: left;
+    }
+  }
+}
+</style>

+ 50 - 0
src/views/elderly/elder/doctor-orders/short-doctor-order/columns.ts

@@ -0,0 +1,50 @@
+import { DICT_TYPE } from '@/utils/dict'
+
+// ==================临时医嘱单====================
+export const ShortDoctorOrderColumns = reactive([
+  {
+    label: '长者姓名',
+    field: 'elderName',
+    width: 120
+  },
+  {
+    label: '性别',
+    field: 'gender',
+    type: '1',
+    dictArr: DICT_TYPE.SYSTEM_USER_SEX,
+    width: 80
+  },
+  {
+    label: '床位号',
+    field: 'bedName',
+    width: 120
+  },
+  {
+    label: '护理等级',
+    field: 'nurseLevelName',
+    width: 120
+  },
+  {
+    label: '医嘱条数',
+    field: 'entryCount',
+    width: 100
+  },
+  {
+    label: '最新医嘱时间',
+    field: 'latestOrderTime',
+    format: true,
+    width: 170
+  },
+  {
+    label: '最新执行时间',
+    field: 'latestExecuteTime',
+    format: true,
+    width: 170
+  },
+  {
+    label: '创建时间',
+    field: 'createTime',
+    format: true,
+    width: 170
+  }
+])

+ 235 - 0
src/views/elderly/elder/doctor-orders/short-doctor-order/index.vue

@@ -0,0 +1,235 @@
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="80px"
+    >
+      <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
+      <el-form-item label="长者姓名" prop="elderName">
+        <el-input
+          @keyup.enter="handleQuery"
+          v-model="queryParams.elderName"
+          placeholder="请输入长者姓名"
+          class="!w-240px"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="医嘱时间" prop="orderTime">
+        <el-date-picker
+          style="width: 240px"
+          v-model="queryParams.orderTime"
+          type="daterange"
+          :clearable="true"
+          placeholder="选择医嘱时间范围"
+          value-format="YYYY-MM-DD"
+          format="YYYY-MM-DD"
+          date-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> 重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <TabBarBtn @add="openForm()" />
+    <Table2
+      v-loading="loading"
+      :data="list"
+      :columns="ShortDoctorOrderColumns"
+      :queryParams="queryParams"
+      :op-width="240"
+      :show-edit-condition="() => false"
+      :show-delete-condition="() => false"
+      :show-detail-condition="() => false"
+      @del="handleDelete"
+    >
+      <template #next="{ scope }">
+        <el-button
+          link
+          type="primary"
+          @click="openForm(scope)"
+          v-hasPermi="['short-doctor-order:edit']"
+          >编辑</el-button
+        >
+        <el-button
+          link
+          type="danger"
+          @click="handleDelete(scope.id)"
+          v-hasPermi="['short-doctor-order:delete']"
+          >删除</el-button
+        >
+        <el-button link type="primary" @click="openPrint(scope)">打印</el-button>
+      </template>
+    </Table2>
+
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 新增 / 编辑弹窗 -->
+  <Form ref="formRef" @success="getList" />
+
+  <!-- 打印(详情页) -->
+  <Print ref="printRef" />
+</template>
+
+<script lang="ts" setup>
+import {
+  getShortDoctorOrderPage,
+  deleteShortDoctorOrder
+} from '@/api/elderly/elder/bed-change'
+import { ShortDoctorOrderColumns } from './columns'
+import Form from './Form.vue'
+import Print from './Print.vue'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'ShortDoctorOrder' })
+
+const userStore = useUserStore()
+const message = useMessage()
+const { t } = useI18n()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<any[]>([])
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  elderName: undefined as string | undefined,
+  orderTime: [] as string[] | undefined,
+  tenantIds: userStore.orgTenantId as any
+})
+const queryFormRef = ref()
+
+/** 测试假数据:列表请求失败时填充 */
+const buildMockShortDoctorOrders = () => {
+  const base = [
+    {
+      id: 2001,
+      elderId: 901,
+      elderName: '陈金凤',
+      gender: 2,
+      genderName: '女',
+      bedName: '1号楼-205-01',
+      nurseLevelName: '二级护理',
+      entryCount: 3,
+      latestOrderTime: '2026-08-22 10:15:00',
+      latestExecuteTime: '2026-08-22 10:30:00',
+      createTime: '2026-08-22 10:15:00',
+      tenantId: queryParams.tenantIds
+    },
+    {
+      id: 2002,
+      elderId: 902,
+      elderName: '孙志远',
+      gender: 1,
+      genderName: '男',
+      bedName: '2号楼-310-02',
+      nurseLevelName: '一级护理',
+      entryCount: 5,
+      latestOrderTime: '2026-08-23 14:00:00',
+      latestExecuteTime: '2026-08-23 14:25:00',
+      createTime: '2026-08-23 14:00:00',
+      tenantId: queryParams.tenantIds
+    }
+  ]
+  return base
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const params: Recordable = {
+      pageNo: queryParams.pageNo,
+      pageSize: queryParams.pageSize,
+      elderName: queryParams.elderName || undefined,
+      tenantIds: queryParams.tenantIds
+    }
+    if (Array.isArray(queryParams.orderTime) && queryParams.orderTime.length === 2) {
+      params.beginTime = queryParams.orderTime[0]
+      params.endTime = queryParams.orderTime[1]
+    }
+    const data = await getShortDoctorOrderPage(params)
+    list.value = data.list || []
+    total.value = data.total || 0
+  } catch (e) {
+    console.warn('[临时医嘱单] 列表请求失败,已使用测试假数据。', e)
+    list.value = buildMockShortDoctorOrders()
+    total.value = list.value.length
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索 */
+const handleQuery = async () => {
+  await queryFormRef.value?.validate().catch(() => {})
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  queryParams.orderTime = []
+  handleQuery()
+}
+
+/** 新增 / 编辑 */
+const formRef = ref()
+const printRef = ref()
+const openForm = (row: any = {}) => {
+  if (
+    !queryParams.tenantIds ||
+    (Array.isArray(queryParams.tenantIds) && queryParams.tenantIds.length === 0) ||
+    (Array.isArray(queryParams.tenantIds) && queryParams.tenantIds.length > 1 && !row.tenantId)
+  ) {
+    message.error('新增只能选择一个机构')
+    return
+  }
+  formRef.value.open(row)
+}
+
+/** 打印 */
+const openPrint = (row: any = {}) => {
+  printRef.value.open(row)
+}
+
+/** 删除 */
+const handleDelete = async (id: number) => {
+  try {
+    await message.confirm('确定删除该临时医嘱单吗?删除后不可恢复。', '提示')
+    await deleteShortDoctorOrder(id)
+    message.success(t('common.delSuccess'))
+    await getList()
+  } catch {}
+}
+
+/** 路由携带 elderName 时自动带入 */
+const route = useRoute()
+onMounted(() => {
+  if (route.query && route.query.elderName) {
+    queryParams.elderName = route.query.elderName as string
+  }
+  getList()
+})
+</script>

+ 5 - 0
src/views/system/official-seal/index.vue

@@ -0,0 +1,5 @@
+<script setup lang="ts"></script>
+
+<template>a</template>
+
+<style scoped lang="scss"></style>