浏览代码

电子合同添加手动新增自定义费用2

chenjun 1 月之前
父节点
当前提交
d852a8ef8f

+ 103 - 0
.workbuddy/memory/2026-09-03.md

@@ -60,3 +60,106 @@
 
 ### 补充:手动行「费用名称」也去下划线
 用户纠正「金额和名称都要改」。手动添加行(isManual)的费用名称 FieldItem(`width="140px"` `placeholder="请输入费用名称"`)在预览/打印模式下同样带下划线,已给两个合同组件的名称 FieldItem 补 `no-underline`。系统行名称本就是纯文本 `displayExpenseName(item)`,无下划线,无需改。
+
+## 出院小结模块(全新,apply/discharge-summary)
+
+### 需求
+在 `views/elderly/apply/discharge-summary/` 实现「出院小结」功能列表,参照桌面模板 `1.8-出院小结(第一版).doc`(由医生发起:先选长者 → 按模板逐段填写 → 保存)。
+
+### 模板结构提取技巧
+`.doc` 是 OLE2 复合文档(文件头 D0CF11E0),无 LibreOffice 环境下用 **Python 按 UTF-16LE 解码全文件 + 正则提取连续中文片段**即可拿到完整文本(前 ~60 段有效,后面是乱码噪声)。
+
+模板五段结构:
+1. 基本信息:长者姓名/性别/年龄/居住区域(楼 区 房 床)/入住日期/出院日期
+2. 一、入住基本健康评估状况:既往病史、近期健康问题、身体活动能力(完全自理/部分自理/完全依赖)、认知与精神状态(正常/轻/中/重度)
+3. 二、在院期间发生疾病和治疗情况:疾病治疗与控制、近期就医情况
+4. 三、出院长者状况:出院原因(居家照护/医院就诊/死亡退住院内/死亡退住院外/其他)、生命体征(P 次/分、R 次/分、HP / mmhg)、身体功能
+5. 四、用药情况(死亡出院不需填):药名/剂量/用法/频次 动态表格
+6. 五、出院指导(死亡出院不需填):重点关注问题/疾病变化关注/安全防护/饮食与就医
+7. 签名:医生签名 + 签名日期
+
+### 实现(4 个新文件 + 1 个 API 文件)
+- `src/api/elderly/elder/discharge-summary/index.ts`:枚举(DISCHARGE_REASON/PHYSICAL_ABILITY/COGNITIVE_STATUS)+ `isDeathDischarge()` + 文本映射函数 + 5 个接口(page/get/create/update/delete),url 前缀 `elderly/dischargeSummary/*`(与医嘱 `elderly/longDoctorOrder/*` 同风格)
+- `columns.ts`:9 列。**出院原因/年龄是模块自定义枚举(非系统字典)**,Table2 的 `dictArr` 只支持系统字典,故在 index.vue 的 getList 里预格式化 `dischargeReasonText`/`elderAgeText`,columns 用普通 field 渲染
+- `Form.vue`:Dialog 70%,el-divider 分段。selectElder 选长者 → @elder 回填性别字典/年龄/居住区域(buildName-floorName-roomName-bedName 拼)/入住日期;**不绑 v-model:bed-name**(区域需拼四段,避免与 @elder 覆盖顺序冲突)。出院原因=死亡(3/4)时 v-if 隐藏用药情况+出院指导,提交时清空这些字段。resetForm 逐字段显式重置(禁用 JSON 拷贝)
+- `Print.vue`:A4 portrait + #555 细线 + 弹填 dialog。主表 6 列(标签 12%/值 21.33%),用药情况用**嵌套子表**(4 列 30/20/25/25),最少补 4 空行
+- `index.vue`:搜索(机构+姓名+出院日期范围)+ Table2 + 4 个操作(编辑/详情/删除/打印)+ mock 兜底
+
+### 路由
+路由是**后端菜单动态生成**(医嘱模块也没在前端 router 里注册),前端只需保证组件文件存在。菜单配置:组件路径 `elderly/apply/discharge-summary/index`。操作按钮**暂未加 v-hasPermi**(新模块权限标识待后端定义,加了会导致按钮不显示)
+
+### 环境备注
+- element-plus 2.9.1:`el-radio` 用 `value`(2.6+ 推荐),项目老代码用 `label`(兼容别名,会 deprecation 告警)。新代码统一用 value + 插槽文本
+
+### 用药子表字段名:usageMethod(不是 usage)
+**`USAGE` 是 MySQL 保留字**(`GRANT USAGE`),直接做列名必须反引号包裹,易踩坑。三个文件统一用 `usageMethod`:
+- `api/.../discharge-summary/index.ts` 的 `DischargeMedication.usageMethod`
+- `Form.vue` 的 `emptyMedication()` + 表格 `row.usageMethod`
+- `Print.vue` 的 `paddedMedications` padding + `m.usageMethod`
+(这类"字段名 = SQL 保留字"的坑,做子表字段设计时要先排查)
+
+### 后端接口规格文档(已生成)
+路径:`C:\Users\chenjun\Desktop\出院小结-后端接口规格.md`(桌面,非项目根,方便直接交付后端)
+内容:功能概述 / 主表+子表 DDL / 5 个接口(入参出参 JSON 示例)/ 27 个字段对照表 / 3 组枚举 / 权限标识建议 / 菜单配置 / 注意事项 / 联调检查清单
+
+### 待后端确认
+- 接口路径与字段命名是否与后端一致(当前按项目命名惯例自拟 `elderly/dischargeSummary/*`)
+- 权限标识(`discharge-summary:create/update/delete/print`)待定义后再补 `v-hasPermi`
+- ⚠️ 日期字段必须返回 `YYYY-MM-DD` 字符串,不能是毫秒时间戳(前端 el-date-picker value-format 固定,返时间戳会不回显——项目已踩过此坑)
+
+## 出院小结调整:删除「疾病变化关注」+ 入住日期自动回填只读
+
+### 需求
+1. 出院指导删掉「疾病变化关注」,按文档模板只保留 3 项(重点关注问题 / 安全防护 / 饮食与就医),相关字段与打印同步删除
+2. 入住日期改成选择长者后自动回填(**取长者档案的 `checkInTime` 字段**),且不可编辑
+
+### 实现
+- **删除 diseaseChanges 全链路**(4 处):`api/.../discharge-summary/index.ts` 的 `DischargeSummaryVO`、`Form.vue`(模板表单项 / defaultForm / resetForm / submitForm 死亡清空的 payload)、`Print.vue`(打印行 / defaultForm)。源码 grep `diseaseChanges|疾病变化关注` 已零残留
+- **入住日期**:`el-date-picker` → `el-input :model-value="form.checkInDate" disabled placeholder="选择长者后自动填充"`,与性别/年龄/居住区域三个自动回填字段视觉一致
+- **handleSelectElder** 改为无条件覆盖:`form.checkInDate = formatDate(elder.checkInTime ?? elder.checkInDate ?? elder.inDate ?? '')`
+- **新增 formatDate helper**(dayjs):兼容毫秒时间戳(13位)/秒级时间戳(10位,×1000)/Date/字符串,非法值返回 `''`。避免长者档案 `checkInTime` 返回时间戳时输入框显示一串数字
+- open() 编辑回显时同步格式化 3 个日期字段(checkInDate/dischargeDate/signDate),防后端返时间戳导致 date-picker 不回显
+- **桌面后端规格文档同步**:DDL 删 `disease_changes` 列、两处 JSON 示例删字段、死亡兜底清理说明删该字段、字段对照表 27→26 条并重排序号
+
+### 复用要点
+- 长者档案「入住日期」字段名是 **`checkInTime`**(不是 checkInDate/inDate)——合同模块 `AddForm.vue` 的 `elderUp(e)` 里就是 `dataForm.checkInTime = e.checkInTime` + `dayjs().format('YYYY-MM-DD')` 显示,可作为范式参考
+
+## 出院小结入住日期自动回填修正(本轮回填实际不生效,已修)
+
+### 根因
+上一轮在 `Form.vue handleSelectElder` 写 `form.checkInDate = formatDate(elder.checkInTime ?? ...)`,但 selectElder 在**下拉选择**走 `handleSelect` 分支,emit 的 `elder` 是【分页列表项】(`elderly-Info` 的 `listType`,字段仅 id/elderName/elderSex/buildName/floorName/roomName/bedName/birthday/elderAge/papersType/idCard/socialSecurityCardNumber),**不含 checkInTime**。只有 `upData`(watch 初次回填)路径 emit 的才是 `getElderInfoById` 完整档案、才带 checkInTime。故正常"选长者"后 入住日期 永远回填为空——上一轮"@elder 能直接取到 checkInTime"的复用要点对下拉路径不成立。
+
+### 修复
+`handleSelectElder` 改为 async:拿到 `elder.id` 后 `getElderInfoById(id)`(`elderlyInfo/findDetailById`)拉完整档案取 `checkInTime`,失败回退列表项。居住区域四段同样详情优先、列表项兜底。入住日期 `el-input disabled` 只读不变。导入 `getElderInfoById` from `@/api/elderly/elder/elderly-Info`。
+
+## 出院小结操作栏新增「退住申请」按钮(带入长者,默认选中)
+
+### 需求
+出院小结列表操作栏增加一颗**无需权限**的「退住申请」按钮,功能等同退住申请页面(`elderly/bak/check-out`)的新增:打开弹窗后,选择长者组件**默认选中当前行长者**(带入 `elderId`)。
+
+### 实现(最小侵入,复用退住申请页面组件)
+- **`bak/check-out/ProcessForm.vue`**:
+  - `<selectElder>` 加 `ref="selectElderRef"`(原组件已 `defineExpose({ upData })`,按 id 拉档案并 `emit('elder')` 回填 证件号/入住日期/床位号)
+  - 新增 `selectElderRef` + `setElderById(id)`:`selectElderRef.value?.upData('', id)`(沿用全项目 ~60 处 `upData(res.elderName, res.elderId)` 范式,TS 类型一致)
+  - `defineExpose` 增加 `setElderById`
+- **`bak/check-out/Form.vue`**:`open(tId, id, detail, status, elderId?)` 增加第 5 参。新增且带 elderId 时 `await nextTick()` 后先 `setTenantId(tId)` 再 `setElderById(elderId)`(详情/重新发起路径不变;老调用方只传 4 参,第 5 参 undefined,行为不变)
+- **`apply/discharge-summary/index.vue`**:
+  - `import CheckOutForm from '@/views/elderly/bak/check-out/Form.vue'`,模板加 `<CheckOutForm ref="checkOutRef" @success="getList" />`
+  - 操作栏 `#next` 增加 `<el-button link type="warning" @click="openCheckOut(scope)">退住申请</el-button>`(**无 v-hasPermi**)
+  - `openCheckOut(row)`:解析租户(优先 `row.tenantId`,否则 `queryParams.tenantIds` 单选),无租户则 `message.error('请先选择一个机构')`;调用 `checkOutRef.value.open(tId, undefined, false, undefined, row.elderId)`
+  - 注意 `queryParams.tenantIds` 可能是数组,需 `Array.isArray` 处理
+
+### 验证
+- `selectElder` `upData` 已 emit `'elder'` → ProcessForm `handleSelectElder` 回填 idCard/checkInTime/bedName,长者姓名下拉框也因 `dataForm.elderlyId` 被赋值而默认选中
+- 提交走退住申请真实接口 `elderlyBakAdd`,与退住申请页面新增完全一致
+
+### 实时问题:点击「退住申请」长者未默认选中(9-03 下午,已解决)
+- **用户截图**:退住申请弹窗打开后「长者姓名」下拉是空的;顶部有「服务器错误,请联系管理员!」
+- **根因(已确认)**:
+  1. 出院小结 list 接口未通 → 列表走 mock 兜底,mock 数据 `elderId` 是 801/802/803 这种假值
+  2. `selectElder.upData('', id)` 内部调真实接口 `getElderInfoById(id)`(line 106),mock 假 ID 在真实后端不存在 → 返回失败 → upData 内部 `try/catch` 静默吞掉 → `inputValue` 未赋值 + `emit('elder')` 不触发 → 下拉框空
+- **链路本身是对的**:upData → getElderInfoById → inputValue.value=res.id → emit('elder', res) → ProcessForm.handleSelectElder 回填 idCard/checkInTime/bedName
+- **验证方法**:临时把 openCheckOut 中 `row.elderId` 硬编码成真实 ID `17824`;用户硬刷后点击退住申请 → 下拉自动选中真实长者 + 其余字段回填 ✅(证明 selectElderRef + setElderById 新链路 OK)
+- **已回滚**:验证完把 `17824` 改回 `row.elderId`,并删除 debug console.log,代码恢复正常
+- **接口接通后注意**:columns.ts 没列 `elderId` 字段,列表行对象的 `elderId` 字段名待后端确认(项目惯例用 `elderId` 或 `id`,万一返 `elderlyId` 也常见)——若真实接口接通后预选失效,优先核对 list 返回的字段名是否是 `elderId`
+- 类型检查(vue-tsc 仅筛 discharge-summary/check-out)待跑,范式与既有 AddForm 一致预期零新增错误

+ 154 - 0
src/api/elderly/elder/discharge-summary/index.ts

@@ -0,0 +1,154 @@
+import request from '@/config/axios'
+
+/**
+ * 出院小结
+ * 参照模板:《1.8-出院小结(第一版)》——由医生发起,先选长者,再逐段填写。
+ *
+ * 后端接口前缀:elderly/dischargeSummary/*
+ * (与医嘱模块 elderly/longDoctorOrder/* 保持同一命名风格)
+ */
+
+// ==================== 枚举 ====================
+
+/** 出院原因 */
+export const DISCHARGE_REASON = {
+  HOME_CARE: 1, // 居家照护
+  HOSPITAL: 2, // 医院就诊
+  DEATH_IN: 3, // 死亡退住(院内)
+  DEATH_OUT: 4, // 死亡退住(院外)
+  OTHER: 5 // 其他
+} as const
+
+export const DISCHARGE_REASON_OPTIONS = [
+  { value: DISCHARGE_REASON.HOME_CARE, label: '居家照护' },
+  { value: DISCHARGE_REASON.HOSPITAL, label: '医院就诊' },
+  { value: DISCHARGE_REASON.DEATH_IN, label: '死亡退住(院内)' },
+  { value: DISCHARGE_REASON.DEATH_OUT, label: '死亡退住(院外)' },
+  { value: DISCHARGE_REASON.OTHER, label: '其他' }
+]
+
+/** 死亡出院(用药情况 / 出院指导 不需填写) */
+export const isDeathDischarge = (reason: any): boolean =>
+  Number(reason) === DISCHARGE_REASON.DEATH_IN || Number(reason) === DISCHARGE_REASON.DEATH_OUT
+
+export const getDischargeReasonText = (reason: any): string =>
+  DISCHARGE_REASON_OPTIONS.find((o) => Number(o.value) === Number(reason))?.label || ''
+
+/** 身体活动能力 */
+export const PHYSICAL_ABILITY = {
+  INDEPENDENT: 1, // 完全自理(可独立行走、洗漱、进食)
+  PARTIAL: 2, // 部分自理(需协助穿衣、如厕)
+  DEPENDENT: 3 // 完全依赖(卧床,需全护理)
+} as const
+
+export const PHYSICAL_ABILITY_OPTIONS = [
+  { value: PHYSICAL_ABILITY.INDEPENDENT, label: '完全自理(可独立行走、洗漱、进食)' },
+  { value: PHYSICAL_ABILITY.PARTIAL, label: '部分自理(需协助穿衣、如厕)' },
+  { value: PHYSICAL_ABILITY.DEPENDENT, label: '完全依赖(卧床,需全护理)' }
+]
+
+export const getPhysicalAbilityText = (val: any): string =>
+  PHYSICAL_ABILITY_OPTIONS.find((o) => Number(o.value) === Number(val))?.label || ''
+
+/** 认知与精神状态 */
+export const COGNITIVE_STATUS = {
+  NORMAL: 1, // 认知正常(能清晰交流、记忆良好)
+  MILD: 2, // 轻度认知障碍(偶尔忘事,交流基本正常)
+  MODERATE: 3, // 中度认知障碍(需提醒日常事项,交流偶有困难)
+  SEVERE: 4 // 重度认知障碍(无法正常交流,不认人)
+} as const
+
+export const COGNITIVE_STATUS_OPTIONS = [
+  { value: COGNITIVE_STATUS.NORMAL, label: '认知正常(能清晰交流、记忆良好)' },
+  { value: COGNITIVE_STATUS.MILD, label: '轻度认知障碍(偶尔忘事,交流基本正常)' },
+  { value: COGNITIVE_STATUS.MODERATE, label: '中度认知障碍(需提醒日常事项,交流偶有困难)' },
+  { value: COGNITIVE_STATUS.SEVERE, label: '重度认知障碍(无法正常交流,不认人)' }
+]
+
+export const getCognitiveStatusText = (val: any): string =>
+  COGNITIVE_STATUS_OPTIONS.find((o) => Number(o.value) === Number(val))?.label || ''
+
+// ==================== 类型 ====================
+
+/**
+ * 用药情况子表(药名 / 剂量 / 用法 / 频次)
+ * 注意:字段名用 usageMethod 而非 usage —— usage 是 MySQL 保留字(GRANT USAGE),
+ * 直接做列名需要反引号包裹,易踩坑,这里从 API 层就避开。
+ */
+export interface DischargeMedication {
+  id?: number
+  drugName?: string
+  dosage?: string
+  usageMethod?: string
+  frequency?: string
+}
+
+/** 出院小结 VO(列表 / 详情返回) */
+export interface DischargeSummaryVO {
+  id?: number
+  elderId?: number
+  elderName?: string
+  gender?: number | string
+  genderName?: string
+  elderAge?: number | string
+  bedName?: string // 居住区域:楼 / 区 / 房 / 床
+  checkInDate?: string
+  dischargeDate?: string
+  // 一、入住基本健康评估状况
+  pastMedicalHistory?: string
+  recentHealthProblems?: string
+  physicalAbility?: number
+  cognitiveStatus?: number
+  // 二、在院期间发生疾病和治疗情况
+  diseaseTreatment?: string
+  recentMedicalVisit?: string
+  // 三、出院长者状况
+  dischargeReason?: number
+  dischargeReasonOther?: string
+  pulse?: string | number // P 次/分
+  respiration?: string | number // R 次/分
+  systolicPressure?: string | number // HP 收缩压 mmhg
+  diastolicPressure?: string | number // HP 舒张压 mmhg
+  bodyFunction?: string
+  // 四、用药情况(死亡出院不需要填写)
+  medications?: DischargeMedication[]
+  // 五、出院指导(死亡出院不需要填写)
+  focusProblems?: string
+  safetyProtection?: string
+  dietAndMedical?: string
+  // 签名
+  doctorSign?: string
+  signDate?: string
+  tenantId?: number
+  createTime?: string
+}
+
+/** 提交表单结构(与 VO 一致,medications 为子表) */
+export type DischargeSummaryForm = DischargeSummaryVO
+
+// ==================== 接口 ====================
+
+/** 分页查询 */
+export const getDischargeSummaryPage = (params: any) => {
+  return request.get({ url: 'elderly/dischargeSummary/page', params })
+}
+
+/** 详情 */
+export const getDischargeSummary = (id: number) => {
+  return request.get({ url: `elderly/dischargeSummary/get?id=${id}` })
+}
+
+/** 新增 */
+export const createDischargeSummary = (data: DischargeSummaryForm) => {
+  return request.post({ url: 'elderly/dischargeSummary/create', data })
+}
+
+/** 更新 */
+export const updateDischargeSummary = (data: DischargeSummaryForm) => {
+  return request.put({ url: 'elderly/dischargeSummary/update', data })
+}
+
+/** 删除 */
+export const deleteDischargeSummary = (id: number) => {
+  return request.delete({ url: `elderly/dischargeSummary/delete?id=${id}` })
+}

+ 677 - 0
src/views/elderly/apply/discharge-summary/Form.vue

@@ -0,0 +1,677 @@
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    :title="title"
+    width="70%"
+    class="discharge-summary-dialog"
+    @close="handleClosed"
+  >
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="130px"
+      v-loading="formLoading"
+    >
+      <!-- ============ 基本信息(选择长者后自动回填) ============ -->
+      <el-divider content-position="left">基本信息</el-divider>
+
+      <el-form-item label="长者姓名" prop="elderId">
+        <selectElder
+          v-if="!isDetail"
+          :key="selectKey"
+          v-model="form.elderId"
+          v-model:elder-name="form.elderName"
+          v-model:elder-sex="form.gender"
+          v-model:elder-age="form.elderAge"
+          :disabled="!!form.id"
+          @elder="handleSelectElder"
+        />
+        <el-input v-else :model-value="form.elderName" disabled />
+      </el-form-item>
+
+      <el-row :gutter="16">
+        <el-col :span="6">
+          <el-form-item label="性别">
+            <el-input :model-value="form.genderName" disabled placeholder="选择长者后自动填充" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="年龄">
+            <el-input
+              :model-value="form.elderAge !== '' && form.elderAge !== undefined && form.elderAge !== null ? `${form.elderAge}岁` : ''"
+              disabled
+              placeholder="选择长者后自动填充"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="居住区域">
+            <el-input :model-value="form.bedName" disabled placeholder="选择长者后自动填充" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <el-form-item label="入住日期" prop="checkInDate">
+            <!-- 选择长者后由 checkInTime 自动回填,不可编辑 -->
+            <el-input
+              :model-value="form.checkInDate"
+              disabled
+              placeholder="选择长者后自动填充"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="出院日期" prop="dischargeDate">
+            <el-date-picker
+              v-model="form.dischargeDate"
+              type="date"
+              value-format="YYYY-MM-DD"
+              format="YYYY-MM-DD"
+              placeholder="选择出院日期"
+              style="width: 100%"
+              :disabled="isDetail"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- ============ 一、入住基本健康评估状况 ============ -->
+      <el-divider content-position="left">一、入住基本健康评估状况</el-divider>
+
+      <el-form-item label="既往病史">
+        <el-input
+          v-model="form.pastMedicalHistory"
+          type="textarea"
+          :rows="3"
+          placeholder="请填写入院时已确诊的疾病及症状程度"
+          :disabled="isDetail"
+        />
+      </el-form-item>
+
+      <el-form-item label="近期健康问题">
+        <el-input
+          v-model="form.recentHealthProblems"
+          type="textarea"
+          :rows="3"
+          placeholder="如近期血压、糖尿是否平稳、关节疼痛情况(部位、疼痛性质)等,需明确记录入院时已确诊的疾病及症状程度"
+          :disabled="isDetail"
+        />
+      </el-form-item>
+
+      <el-form-item label="身体活动能力">
+        <el-radio-group v-model="form.physicalAbility" :disabled="isDetail">
+          <el-radio
+            v-for="o in PHYSICAL_ABILITY_OPTIONS"
+            :key="o.value"
+            :value="o.value"
+            class="block-radio"
+          >
+            {{ o.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item label="认知与精神状态">
+        <el-radio-group v-model="form.cognitiveStatus" :disabled="isDetail">
+          <el-radio
+            v-for="o in COGNITIVE_STATUS_OPTIONS"
+            :key="o.value"
+            :value="o.value"
+            class="block-radio"
+          >
+            {{ o.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <!-- ============ 二、在院期间发生疾病和治疗情况 ============ -->
+      <el-divider content-position="left">二、在院期间发生疾病和治疗情况</el-divider>
+
+      <el-form-item label="疾病治疗与控制">
+        <el-input
+          v-model="form.diseaseTreatment"
+          type="textarea"
+          :rows="3"
+          placeholder="记录在院期间针对基础疾病的治疗措施(如分级护理、吸氧、饮食、用药问题)"
+          :disabled="isDetail"
+        />
+      </el-form-item>
+
+      <el-form-item label="近期就医情况">
+        <el-input
+          v-model="form.recentMedicalVisit"
+          type="textarea"
+          :rows="3"
+          placeholder="3-6个月的外出就医情况(如:何时因何原因外出就医,就诊医院,主要诊断,回院日期,回院后长者身体情况)"
+          :disabled="isDetail"
+        />
+      </el-form-item>
+
+      <!-- ============ 三、出院长者状况 ============ -->
+      <el-divider content-position="left">三、出院长者状况</el-divider>
+
+      <el-form-item label="出院原因" prop="dischargeReason">
+        <el-radio-group v-model="form.dischargeReason" :disabled="isDetail">
+          <el-radio
+            v-for="o in DISCHARGE_REASON_OPTIONS"
+            :key="o.value"
+            :value="o.value"
+            class="inline-radio"
+          >
+            {{ o.label }}
+          </el-radio>
+          <el-input
+            v-if="form.dischargeReason === DISCHARGE_REASON.OTHER"
+            v-model="form.dischargeReasonOther"
+            placeholder="请填写其他出院原因"
+            style="width: 240px; margin-left: 8px"
+            :disabled="isDetail"
+          />
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item label="生命体征">
+        <div class="vital-signs">
+          <span class="vital-label">P:</span>
+          <el-input
+            v-model="form.pulse"
+            placeholder="脉搏"
+            style="width: 100px"
+            :disabled="isDetail"
+          />
+          <span class="vital-unit">次/分</span>
+
+          <span class="vital-label">R:</span>
+          <el-input
+            v-model="form.respiration"
+            placeholder="呼吸"
+            style="width: 100px"
+            :disabled="isDetail"
+          />
+          <span class="vital-unit">次/分</span>
+
+          <span class="vital-label">HP:</span>
+          <el-input
+            v-model="form.systolicPressure"
+            placeholder="收缩压"
+            style="width: 90px"
+            :disabled="isDetail"
+          />
+          <span class="vital-unit">/</span>
+          <el-input
+            v-model="form.diastolicPressure"
+            placeholder="舒张压"
+            style="width: 90px"
+            :disabled="isDetail"
+          />
+          <span class="vital-unit">mmhg</span>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="身体功能">
+        <el-input
+          v-model="form.bodyFunction"
+          type="textarea"
+          :rows="3"
+          placeholder="对当前的意识状态、认知能力(如有无痴呆症状加重)、肢体活动能力、吞咽功能、皮肤状况、留置管道等情况进行描述"
+          :disabled="isDetail"
+        />
+      </el-form-item>
+
+      <!-- ============ 四、用药情况(死亡出院不需要填写) ============ -->
+      <template v-if="!isDeathDischarge(form.dischargeReason)">
+        <el-divider content-position="left">
+          四、用药情况(死亡出院不需要填写)
+          <el-button
+            v-if="!isDetail"
+            link
+            type="primary"
+            class="!ml-10px"
+            @click="handleAddMedication"
+          >
+            <Icon icon="ep:plus" class="mr-2px" />新增药品
+          </el-button>
+        </el-divider>
+
+        <el-table :data="form.medications" border style="width: 100%" size="default">
+          <el-table-column label="序号" type="index" width="60" align="center" />
+          <el-table-column label="药名" min-width="160">
+            <template #default="{ row }">
+              <el-input v-model="row.drugName" placeholder="请输入药名" :disabled="isDetail" />
+            </template>
+          </el-table-column>
+          <el-table-column label="剂量" min-width="120">
+            <template #default="{ row }">
+              <el-input v-model="row.dosage" placeholder="如 5mg" :disabled="isDetail" />
+            </template>
+          </el-table-column>
+          <el-table-column label="用法" min-width="140">
+            <template #default="{ row }">
+              <el-input v-model="row.usageMethod" placeholder="如 口服" :disabled="isDetail" />
+            </template>
+          </el-table-column>
+          <el-table-column label="频次" min-width="140">
+            <template #default="{ row }">
+              <el-input v-model="row.frequency" placeholder="如 每日3次" :disabled="isDetail" />
+            </template>
+          </el-table-column>
+          <el-table-column v-if="!isDetail" label="操作" width="90" align="center">
+            <template #default="{ $index }">
+              <el-button link type="danger" @click="handleRemoveMedication($index)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <div v-if="!form.medications || form.medications.length === 0" class="empty-tip">
+          暂无用药记录,点击右上「新增药品」添加
+        </div>
+      </template>
+
+      <!-- ============ 五、出院指导(死亡出院不需填写) ============ -->
+      <template v-if="!isDeathDischarge(form.dischargeReason)">
+        <el-divider content-position="left">五、出院指导(死亡出院不需填写)</el-divider>
+
+        <el-form-item label="重点关注问题">
+          <el-input
+            v-model="form.focusProblems"
+            type="textarea"
+            :rows="3"
+            placeholder="请填写出院后需要重点关注的问题"
+            :disabled="isDetail"
+          />
+        </el-form-item>
+
+        <el-form-item label="安全防护">
+          <el-input
+            v-model="form.safetyProtection"
+            type="textarea"
+            :rows="3"
+            placeholder="防跌倒、防走失、防呛咳、防管道脱落等措施"
+            :disabled="isDetail"
+          />
+        </el-form-item>
+
+        <el-form-item label="饮食与就医">
+          <el-input
+            v-model="form.dietAndMedical"
+            type="textarea"
+            :rows="3"
+            placeholder="饮食指导,出现什么症状建议就医"
+            :disabled="isDetail"
+          />
+        </el-form-item>
+      </template>
+
+      <!-- ============ 签名 ============ -->
+      <el-divider content-position="left">签名</el-divider>
+
+      <el-row :gutter="16">
+        <el-col :span="12">
+          <el-form-item label="医生签名">
+            <el-input
+              v-model="form.doctorSign"
+              placeholder="请输入医生签名"
+              :disabled="isDetail"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="签名日期">
+            <el-date-picker
+              v-model="form.signDate"
+              type="date"
+              value-format="YYYY-MM-DD"
+              format="YYYY-MM-DD"
+              placeholder="选择签名日期"
+              style="width: 100%"
+              :disabled="isDetail"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </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 {
+  getDischargeSummary,
+  createDischargeSummary,
+  updateDischargeSummary,
+  DISCHARGE_REASON,
+  DISCHARGE_REASON_OPTIONS,
+  isDeathDischarge,
+  PHYSICAL_ABILITY_OPTIONS,
+  COGNITIVE_STATUS_OPTIONS,
+  type DischargeSummaryForm,
+  type DischargeMedication
+} from '@/api/elderly/elder/discharge-summary'
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+import dayjs from 'dayjs'
+import { getElderInfoById } from '@/api/elderly/elder/elderly-Info'
+import Print from './Print.vue'
+
+defineOptions({ name: 'DischargeSummaryForm' })
+
+const message = useMessage()
+const { t } = useI18n()
+
+const dialogVisible = ref(false)
+const formLoading = ref(false)
+const isDetail = ref(false)
+const formRef = ref()
+const printRef = ref()
+// 每次开弹窗递增,强制 selectElder 重新挂载,避免其内部 list 残留
+const selectKey = ref(0)
+
+const emptyMedication = (): DischargeMedication => ({
+  drugName: '',
+  dosage: '',
+  usageMethod: '',
+  frequency: ''
+})
+
+/**
+ * 日期统一格式化为 YYYY-MM-DD
+ * 兼容毫秒时间戳(13位) / 秒级时间戳(10位) / Date / 各种日期字符串;非法值返回空串
+ * (长者档案的 checkInTime 后端可能返回时间戳,直接塞给 el-input 会显示成一串数字)
+ */
+const formatDate = (val: any): string => {
+  if (val === '' || val === null || val === undefined) return ''
+  let input: any = val
+  const num = Number(val)
+  if (!Number.isNaN(num) && num > 0 && String(val).length <= 10) {
+    input = num * 1000 // 秒级时间戳
+  }
+  const d = dayjs(input)
+  return d.isValid() ? d.format('YYYY-MM-DD') : ''
+}
+
+const defaultForm: DischargeSummaryForm = {
+  id: undefined,
+  elderId: undefined,
+  elderName: '',
+  gender: '',
+  genderName: '',
+  elderAge: '',
+  bedName: '',
+  checkInDate: '',
+  dischargeDate: '',
+  // 一、入住基本健康评估状况
+  pastMedicalHistory: '',
+  recentHealthProblems: '',
+  physicalAbility: undefined,
+  cognitiveStatus: undefined,
+  // 二、在院期间发生疾病和治疗情况
+  diseaseTreatment: '',
+  recentMedicalVisit: '',
+  // 三、出院长者状况
+  dischargeReason: undefined,
+  dischargeReasonOther: '',
+  pulse: '',
+  respiration: '',
+  systolicPressure: '',
+  diastolicPressure: '',
+  bodyFunction: '',
+  // 四、用药情况
+  medications: [emptyMedication()],
+  // 五、出院指导
+  focusProblems: '',
+  safetyProtection: '',
+  dietAndMedical: '',
+  // 签名
+  doctorSign: '',
+  signDate: ''
+}
+
+const form = reactive(JSON.parse(JSON.stringify(defaultForm)))
+
+const title = computed(() => {
+  if (isDetail.value) return '出院小结详情'
+  return form.id ? '编辑出院小结' : '新增出院小结'
+})
+
+const rules = {
+  elderId: [{ required: true, message: '请选择长者', trigger: 'change' }],
+  dischargeDate: [{ required: true, message: '请选择出院日期', trigger: 'change' }],
+  dischargeReason: [{ required: true, message: '请选择出院原因', trigger: 'change' }],
+  dischargeReasonOther: [
+    {
+      validator: (_rule: any, _value: any, callback: any) => {
+        // 出院原因 = 其他(5) 时,必须补充说明
+        if (
+          Number(form.dischargeReason) === DISCHARGE_REASON.OTHER &&
+          !form.dischargeReasonOther
+        ) {
+          callback(new Error('请填写其他出院原因'))
+        } else {
+          callback()
+        }
+      },
+      trigger: ['change', 'blur']
+    }
+  ]
+}
+
+/**
+ * 选择长者后补全:性别字典、居住区域(楼-区-房-床)、入住日期
+ * 注意:不绑 v-model:bed-name,居住区域需要拼接 楼/区/房/床 四段,
+ * 由本函数在 @elder 事件里统一赋值(selectElder 内部先 emit bed-name 再 emit elder,本函数后执行生效)
+ *
+ * ⚠️ 入住日期来源:selectElder 在下拉选择时 emit 的是【分页列表项】,该对象不含 checkInTime 字段,
+ * 只有详情接口 elderlyInfo/findDetailById 才返回完整档案(含 checkInTime)。
+ * 因此这里必须按 id 拉一次详情来取入住时间,否则入住日期永远回填为空。
+ */
+const handleSelectElder = async (elder: any) => {
+  if (!elder || !elder.id) return
+  // 优先用详情接口补全 checkInTime(下拉列表项不含入住时间字段)
+  let full = elder
+  try {
+    const detail = await getElderInfoById(elder.id)
+    if (detail) full = detail
+  } catch (e) {
+    full = elder
+  }
+  form.gender = full.elderSex ?? elder.elderSex ?? form.gender
+  form.genderName = getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender) || ''
+  form.elderAge = full.elderAge ?? elder.elderAge ?? ''
+  // 居住区域:楼-区-房-床,缺失段落自动跳过(详情优先,列表项兜底)
+  const area = [
+    full.buildName ?? elder.buildName,
+    full.floorName ?? elder.floorName,
+    full.roomName ?? elder.roomName,
+    full.bedName ?? elder.bedName
+  ]
+    .filter(Boolean)
+    .join('-')
+  form.bedName = area || full.bedName || elder.bedName || ''
+  // 入住日期:取长者档案的 checkInTime 自动回填(可能是毫秒时间戳或带时分秒的字符串,统一格式化为 YYYY-MM-DD)
+  // 详情接口字段名 checkInTime;列表项/兜底字段名为 checkInDate / inDate
+  form.checkInDate = formatDate(
+    full.checkInTime ??
+      elder.checkInTime ??
+      full.checkInDate ??
+      elder.checkInDate ??
+      full.inDate ??
+      elder.inDate ??
+      ''
+  )
+}
+
+/** 打开弹窗:detail=true 表示只读(详情场景) */
+const open = async (row: any = {}, detail: boolean = false) => {
+  // 1. 先把内部状态彻底清空,避免 selectElder 拿到旧 modelValue 触发其内部 watch
+  resetForm()
+  // 2. 递增 selectKey,强制 selectElder 重新挂载(防止内部 list/搜索关键字残留)
+  selectKey.value++
+  // 3. 开弹窗
+  dialogVisible.value = true
+  isDetail.value = !!detail
+  await nextTick()
+  if (row?.id) {
+    formLoading.value = true
+    try {
+      const data: any = await getDischargeSummary(row.id)
+      Object.assign(form, defaultForm, data)
+      form.genderName = getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender) || ''
+      // 日期统一格式化,避免后端返回时间戳导致 el-date-picker / 回填输入框显示异常
+      form.checkInDate = formatDate(form.checkInDate)
+      form.dischargeDate = formatDate(form.dischargeDate)
+      form.signDate = formatDate(form.signDate)
+      form.medications = (data.medications || []).map((m: any) => ({ ...emptyMedication(), ...m }))
+      if (form.medications.length === 0) form.medications = [emptyMedication()]
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open })
+
+/**
+ * 表单重置:必须逐字段显式重置!
+ * 不能用 Object.assign(form, JSON.parse(JSON.stringify(defaultForm))):
+ * JSON.stringify 会丢弃值为 undefined 的字段(如 id / elderId / dischargeReason),
+ * 反序列化后 source 没有这些 key,Object.assign 只复制 source 自身可枚举属性,
+ * 导致 form.id 等残留 → Dialog 标题误判为「编辑」、selectElder 被 :disabled="!!form.id" 锁死。
+ */
+const resetForm = () => {
+  form.id = undefined
+  form.elderId = undefined
+  form.elderName = ''
+  form.gender = ''
+  form.genderName = ''
+  form.elderAge = ''
+  form.bedName = ''
+  form.checkInDate = ''
+  form.dischargeDate = ''
+  form.pastMedicalHistory = ''
+  form.recentHealthProblems = ''
+  form.physicalAbility = undefined
+  form.cognitiveStatus = undefined
+  form.diseaseTreatment = ''
+  form.recentMedicalVisit = ''
+  form.dischargeReason = undefined
+  form.dischargeReasonOther = ''
+  form.pulse = ''
+  form.respiration = ''
+  form.systolicPressure = ''
+  form.diastolicPressure = ''
+  form.bodyFunction = ''
+  form.medications = [emptyMedication()]
+  form.focusProblems = ''
+  form.safetyProtection = ''
+  form.dietAndMedical = ''
+  form.doctorSign = ''
+  form.signDate = ''
+  formRef.value?.clearValidate()
+}
+
+const handleClosed = () => {
+  resetForm()
+  dialogVisible.value = false
+}
+
+/** 新增 / 删除用药记录 */
+const handleAddMedication = () => {
+  form.medications.push(emptyMedication())
+}
+
+const handleRemoveMedication = (idx: number) => {
+  if (idx < 0) return
+  form.medications.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 death = isDeathDischarge(form.dischargeReason)
+    const payload: DischargeSummaryForm = {
+      ...form,
+      medications: death
+        ? []
+        : (form.medications || []).filter((m: DischargeMedication) => m.drugName),
+      focusProblems: death ? '' : form.focusProblems,
+      safetyProtection: death ? '' : form.safetyProtection,
+      dietAndMedical: death ? '' : form.dietAndMedical
+    }
+    if (form.id) {
+      await updateDischargeSummary(payload)
+    } else {
+      await createDischargeSummary(payload)
+    }
+    message.success(t(form.id ? 'common.updateSuccess' : 'common.createSuccess'))
+    emit('success')
+    handleClosed()
+  } finally {
+    setTimeout(() => {
+      formLoading.value = false
+    }, 300)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.discharge-summary-dialog :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+// 身体活动能力 / 认知状态 选项文案较长,每项独占一行
+.block-radio {
+  display: flex;
+  margin-right: 0;
+  height: auto;
+  line-height: 24px;
+  white-space: normal;
+}
+.inline-radio {
+  margin-right: 16px;
+}
+.vital-signs {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 4px;
+  .vital-label {
+    font-weight: bold;
+    margin-left: 8px;
+    &:first-child {
+      margin-left: 0;
+    }
+  }
+  .vital-unit {
+    color: #606266;
+  }
+}
+.empty-tip {
+  text-align: center;
+  color: #999;
+  padding: 16px 0;
+  font-size: 14px;
+  border: 1px dashed #ddd;
+  border-top: none;
+}
+</style>

+ 394 - 0
src/views/elderly/apply/discharge-summary/Print.vue

@@ -0,0 +1,394 @@
+<template>
+  <Dialog v-model="dialogVisible" title="出院小结" width="95%" scroll>
+    <div class="discharge-summary-print" id="dischargeSummaryPrintArea" v-loading="loading">
+      <!-- 主表:6 列(标签列 12% / 值列 21.34%,合计 100%) -->
+      <table class="summary-table">
+        <colgroup>
+          <col style="width: 14.5%" />
+          <col style="width: 20.0%" />
+          <col style="width: 12%" />
+          <col style="width: 21.33%" />
+          <col style="width: 12%" />
+          <col style="width: 20.0%" />
+        </colgroup>
+        <thead>
+          <tr>
+            <th colspan="6" class="doc-title">养老院老人出院小结</th>
+          </tr>
+        </thead>
+        <tbody>
+          <!-- ============ 基本信息 ============ -->
+          <tr>
+            <th class="lbl">长者姓名</th>
+            <td>{{ form.elderName || '' }}</td>
+            <th class="lbl">性别</th>
+            <td>{{ genderText }}</td>
+            <th class="lbl">年龄</th>
+            <td>{{ ageText }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">居住区域</th>
+            <td colspan="3">{{ form.bedName || '' }}</td>
+            <th class="lbl">入住日期</th>
+            <td>{{ form.checkInDate || '' }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">出院日期</th>
+            <td colspan="5">{{ form.dischargeDate || '' }}</td>
+          </tr>
+
+          <!-- ============ 一、入住基本健康评估状况 ============ -->
+          <tr class="section-row">
+            <td colspan="6">一、入住基本健康评估状况</td>
+          </tr>
+          <tr>
+            <th class="lbl">既往病史</th>
+            <td colspan="5" class="pre">{{ form.pastMedicalHistory || '' }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">近期健康问题</th>
+            <td colspan="5" class="pre">{{ form.recentHealthProblems || '' }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">身体活动能力</th>
+            <td colspan="5">{{ physicalAbilityText }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">认知与精神状态</th>
+            <td colspan="5">{{ cognitiveStatusText }}</td>
+          </tr>
+
+          <!-- ============ 二、在院期间发生疾病和治疗情况 ============ -->
+          <tr class="section-row">
+            <td colspan="6">二、在院期间发生疾病和治疗情况</td>
+          </tr>
+          <tr>
+            <th class="lbl">疾病治疗与控制</th>
+            <td colspan="5" class="pre">{{ form.diseaseTreatment || '' }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">近期就医情况</th>
+            <td colspan="5" class="pre">{{ form.recentMedicalVisit || '' }}</td>
+          </tr>
+
+          <!-- ============ 三、出院长者状况 ============ -->
+          <tr class="section-row">
+            <td colspan="6">三、出院长者状况</td>
+          </tr>
+          <tr>
+            <th class="lbl">出院原因</th>
+            <td colspan="5">{{ dischargeReasonText }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">生命体征</th>
+            <td colspan="5">{{ vitalSignsText }}</td>
+          </tr>
+          <tr>
+            <th class="lbl">身体功能</th>
+            <td colspan="5" class="pre">{{ form.bodyFunction || '' }}</td>
+          </tr>
+
+          <!-- ============ 四、用药情况(死亡出院不需要填写) ============ -->
+          <template v-if="!isDeath">
+            <tr class="section-row">
+              <td colspan="6">四、用药情况(死亡出院不需要填写)</td>
+            </tr>
+            <tr>
+              <td colspan="6" class="nest-cell">
+                <table class="med-table">
+                  <colgroup>
+                    <col style="width: 30%" />
+                    <col style="width: 20%" />
+                    <col style="width: 25%" />
+                    <col style="width: 25%" />
+                  </colgroup>
+                  <thead>
+                    <tr>
+                      <th>药名</th>
+                      <th>剂量</th>
+                      <th>用法</th>
+                      <th>频次</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr v-for="(m, idx) in paddedMedications" :key="idx">
+                      <td>{{ m.drugName || '' }}</td>
+                      <td>{{ m.dosage || '' }}</td>
+                      <td>{{ m.usageMethod || '' }}</td>
+                      <td>{{ m.frequency || '' }}</td>
+                    </tr>
+                  </tbody>
+                </table>
+              </td>
+            </tr>
+          </template>
+
+          <!-- ============ 五、出院指导(死亡出院不需填写) ============ -->
+          <template v-if="!isDeath">
+            <tr class="section-row">
+              <td colspan="6">五、出院指导(死亡出院不需填写)</td>
+            </tr>
+            <tr>
+              <th class="lbl">重点关注问题</th>
+              <td colspan="5" class="pre">{{ form.focusProblems || '' }}</td>
+            </tr>
+            <tr>
+              <th class="lbl">安全防护</th>
+              <td colspan="5" class="pre">{{ form.safetyProtection || '' }}</td>
+            </tr>
+            <tr>
+              <th class="lbl">饮食与就医</th>
+              <td colspan="5" class="pre">{{ form.dietAndMedical || '' }}</td>
+            </tr>
+          </template>
+
+          <!-- ============ 签名 ============ -->
+          <tr>
+            <th class="lbl">医生签名</th>
+            <td colspan="2">{{ form.doctorSign || '' }}</td>
+            <th class="lbl">签名日期</th>
+            <td colspan="2">{{ form.signDate || '' }}</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 {
+  getDischargeSummary,
+  getDischargeReasonText,
+  getPhysicalAbilityText,
+  getCognitiveStatusText,
+  isDeathDischarge,
+  type DischargeMedication
+} from '@/api/elderly/elder/discharge-summary'
+import { getDictLabel, DICT_TYPE } from '@/utils/dict'
+
+defineOptions({ name: 'DischargeSummaryPrint' })
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+
+const defaultForm = {
+  id: undefined,
+  elderId: undefined,
+  elderName: '',
+  gender: '',
+  elderAge: '',
+  bedName: '',
+  checkInDate: '',
+  dischargeDate: '',
+  pastMedicalHistory: '',
+  recentHealthProblems: '',
+  physicalAbility: undefined,
+  cognitiveStatus: undefined,
+  diseaseTreatment: '',
+  recentMedicalVisit: '',
+  dischargeReason: undefined,
+  dischargeReasonOther: '',
+  pulse: '',
+  respiration: '',
+  systolicPressure: '',
+  diastolicPressure: '',
+  bodyFunction: '',
+  medications: [] as DischargeMedication[],
+  focusProblems: '',
+  safetyProtection: '',
+  dietAndMedical: '',
+  doctorSign: '',
+  signDate: ''
+}
+const form = reactive({ ...defaultForm })
+
+const genderText = computed(() =>
+  form.gender === '' || form.gender === undefined || form.gender === null
+    ? ''
+    : getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, form.gender)
+)
+
+const ageText = computed(() =>
+  form.elderAge === '' || form.elderAge === undefined || form.elderAge === null
+    ? ''
+    : `${form.elderAge} 岁`
+)
+
+const physicalAbilityText = computed(() => getPhysicalAbilityText(form.physicalAbility))
+const cognitiveStatusText = computed(() => getCognitiveStatusText(form.cognitiveStatus))
+
+/** 出院原因:其他(5) 时把补充说明拼在后面 */
+const dischargeReasonText = computed(() => {
+  const base = getDischargeReasonText(form.dischargeReason)
+  if (!base) return ''
+  const other = form.dischargeReasonOther ? `(${form.dischargeReasonOther})` : ''
+  return base + other
+})
+
+/** 死亡出院:用药情况 / 出院指导 不展示 */
+const isDeath = computed(() => isDeathDischarge(form.dischargeReason))
+
+/** 生命体征:P 次/分、R 次/分、HP 收缩压/舒张压 mmhg */
+const vitalSignsText = computed(() => {
+  const parts: string[] = []
+  if (form.pulse) parts.push(`P:${form.pulse} 次/分`)
+  if (form.respiration) parts.push(`R:${form.respiration} 次/分`)
+  const bp = [form.systolicPressure, form.diastolicPressure].filter((v) => v !== '' && v != null)
+  if (bp.length) parts.push(`HP:${bp.join(' / ')} mmhg`)
+  return parts.join('    ')
+})
+
+/** 用药表格最少展示 4 行,不足补空行(与纸质表格留白一致,方便手写补充) */
+const MIN_MED_ROWS = 4
+const paddedMedications = computed<DischargeMedication[]>(() => {
+  const list = form.medications || []
+  const padding: DischargeMedication[] = Array.from(
+    { length: Math.max(0, MIN_MED_ROWS - list.length) },
+    () => ({ drugName: '', dosage: '', usageMethod: '', frequency: '' })
+  )
+  return [...list, ...padding]
+})
+
+/** 打开打印预览 */
+const open = async (row: any = {}) => {
+  dialogVisible.value = true
+  loading.value = true
+  try {
+    Object.assign(form, defaultForm, { medications: [] })
+    if (row?.id) {
+      const data: any = await getDischargeSummary(row.id)
+      Object.assign(form, defaultForm, data)
+      form.medications = (data.medications || []) as DischargeMedication[]
+    }
+  } finally {
+    loading.value = false
+  }
+}
+defineExpose({ open })
+
+const handleClosed = () => {
+  dialogVisible.value = false
+  Object.assign(form, defaultForm, { medications: [] })
+}
+
+// 打印配置(vue3-print-nb)
+const print = {
+  id: 'dischargeSummaryPrintArea',
+  popTitle: '出院小结',
+  extraCss: '',
+  preview: false,
+  zIndex: 20002
+}
+</script>
+
+<style lang="scss" scoped>
+.discharge-summary-print {
+  padding: 4px 0;
+  color: #000;
+  background: #fff;
+  /* 屏幕预览弹填 dialog(width:100%);打印时 @page A4 portrait 自适应 ~194mm 内容宽 */
+  width: 100%;
+  margin: 0;
+  box-sizing: border-box;
+
+  .summary-table {
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+    font-size: 13px;
+
+    th,
+    td {
+      border: 1px solid #555;
+      padding: 6px 8px;
+      vertical-align: middle;
+      word-break: break-word;
+    }
+
+    /* 标题 */
+    .doc-title {
+      text-align: center;
+      font-size: 20px;
+      font-weight: bold;
+      padding: 12px 0;
+      letter-spacing: 2px;
+    }
+
+    /* 字段名(左列):白底、加粗、居中 */
+    th.lbl {
+      background: #fff;
+      font-weight: bold;
+      text-align: center;
+      white-space: nowrap;
+    }
+
+    /* 段落小标题:整行跨列,浅灰底 */
+    .section-row td {
+      background: #f6f6f6;
+      font-weight: bold;
+      text-align: left;
+      padding: 6px 8px;
+    }
+
+    /* 长文本:保留换行、左对齐、上下留白 */
+    .pre {
+      white-space: pre-wrap;
+      text-align: left;
+      min-height: 3em;
+    }
+
+    /* 嵌套用药表格:去掉外层单元格内边距,让子表铺满 */
+    .nest-cell {
+      padding: 0;
+    }
+  }
+
+  .med-table {
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+    font-size: 13px;
+
+    th,
+    td {
+      border: 1px solid #555;
+      padding: 5px 8px;
+      text-align: center;
+      vertical-align: middle;
+      word-break: break-word;
+    }
+
+    thead th {
+      background: #fff;
+      font-weight: bold;
+    }
+
+    tbody td {
+      height: 26px;
+    }
+  }
+}
+</style>
+
+<!--
+  @page 必须 un-scoped(CSS 规范限制),单独放一个 style 块。
+  A4 竖版 210×297mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
+  - tr { page-break-inside: avoid }:单行内容不被切断到两页
+-->
+<style lang="scss">
+@media print {
+  @page {
+    size: A4 portrait;
+    margin: 8mm;
+  }
+  .discharge-summary-print .summary-table tr,
+  .discharge-summary-print .med-table tr {
+    page-break-inside: avoid;
+  }
+}
+</style>

+ 56 - 0
src/views/elderly/apply/discharge-summary/columns.ts

@@ -0,0 +1,56 @@
+import { DICT_TYPE } from '@/utils/dict'
+
+// ==================出院小结====================
+// 说明:出院原因 / 身体活动能力 / 认知状态 是本模块自定义枚举(非系统字典),
+// 因此在 index.vue 的 getList 里预先把文本映射好(dischargeReasonText),
+// 这里直接用普通 field 渲染,避免依赖 Table2 的 dictArr 字典翻译。
+export const DischargeSummaryColumns = reactive([
+  {
+    label: '长者姓名',
+    field: 'elderName',
+    width: 120
+  },
+  {
+    label: '性别',
+    field: 'gender',
+    type: '1',
+    dictArr: DICT_TYPE.SYSTEM_USER_SEX,
+    width: 80
+  },
+  {
+    label: '年龄',
+    field: 'elderAgeText',
+    width: 80
+  },
+  {
+    label: '居住区域',
+    field: 'bedName',
+    width: 180
+  },
+  {
+    label: '入住日期',
+    field: 'checkInDate',
+    width: 120
+  },
+  {
+    label: '出院日期',
+    field: 'dischargeDate',
+    width: 120
+  },
+  {
+    label: '出院原因',
+    field: 'dischargeReasonText',
+    width: 140
+  },
+  {
+    label: '医生签名',
+    field: 'doctorSign',
+    width: 110
+  },
+  {
+    label: '创建时间',
+    field: 'createTime',
+    format: true,
+    width: 170
+  }
+])

+ 286 - 0
src/views/elderly/apply/discharge-summary/index.vue

@@ -0,0 +1,286 @@
+<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="dischargeDate">
+        <el-date-picker
+          style="width: 240px"
+          v-model="queryParams.dischargeDate"
+          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="DischargeSummaryColumns"
+      :queryParams="queryParams"
+      :op-width="280"
+      :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)">编辑</el-button>
+        <el-button link type="primary" @click="openDetail(scope)">详情</el-button>
+        <el-button link type="danger" @click="handleDelete(scope.id)">删除</el-button>
+        <el-button link type="primary" @click="openPrint(scope)">打印</el-button>
+        <el-button link type="warning" @click="openCheckOut(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" />
+
+  <!-- 退住申请弹窗(从出院小结带入长者 id,无需权限) -->
+  <CheckOutForm ref="checkOutRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import {
+  getDischargeSummaryPage,
+  deleteDischargeSummary,
+  getDischargeReasonText,
+  DISCHARGE_REASON
+} from '@/api/elderly/elder/discharge-summary'
+import { DischargeSummaryColumns } from './columns'
+import Form from './Form.vue'
+import Print from './Print.vue'
+import CheckOutForm from '@/views/elderly/bak/check-out/Form.vue'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'DischargeSummary' })
+
+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,
+  dischargeDate: [] as string[] | undefined,
+  tenantIds: userStore.orgTenantId as any
+})
+const queryFormRef = ref()
+
+/** 测试假数据:列表请求失败时填充,方便前端联调 */
+const buildMockDischargeSummary = () => {
+  const base = [
+    {
+      id: 3001,
+      elderId: 801,
+      elderName: '张建国',
+      gender: 1,
+      bedName: '1号楼-3区-301-01',
+      elderAge: 78,
+      checkInDate: '2024-03-15',
+      dischargeDate: '2026-08-20',
+      dischargeReason: DISCHARGE_REASON.HOME_CARE,
+      doctorSign: '李医生',
+      createTime: '2026-08-20 10:30:00'
+    },
+    {
+      id: 3002,
+      elderId: 802,
+      elderName: '刘秀英',
+      gender: 2,
+      bedName: '2号楼-2区-205-03',
+      elderAge: 82,
+      checkInDate: '2023-11-02',
+      dischargeDate: '2026-08-25',
+      dischargeReason: DISCHARGE_REASON.HOSPITAL,
+      doctorSign: '陈医生',
+      createTime: '2026-08-25 15:10:00'
+    },
+    {
+      id: 3003,
+      elderId: 803,
+      elderName: '王德海',
+      gender: 1,
+      bedName: '1号楼-4区-402-02',
+      elderAge: 75,
+      checkInDate: '2025-01-08',
+      dischargeDate: '2026-09-01',
+      dischargeReason: DISCHARGE_REASON.OTHER,
+      dischargeReasonOther: '家属迁居外地',
+      doctorSign: '周医生',
+      createTime: '2026-09-01 09:00:00'
+    }
+  ]
+  return base.map((item: any) => ({ ...item, tenantId: queryParams.tenantIds }))
+}
+
+/**
+ * 查询列表
+ * 说明:出院原因 / 年龄 的展示文本在此统一映射,
+ * 因为它们是模块自定义枚举,Table2 的 dictArr 字典翻译用不上。
+ */
+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.dischargeDate) && queryParams.dischargeDate.length === 2) {
+      params.beginTime = queryParams.dischargeDate[0]
+      params.endTime = queryParams.dischargeDate[1]
+    }
+    const data = await getDischargeSummaryPage(params)
+    list.value = (data.list || []).map((item: any) => ({
+      ...item,
+      dischargeReasonText: getDischargeReasonText(item.dischargeReason),
+      elderAgeText:
+        item.elderAge === undefined || item.elderAge === null || item.elderAge === ''
+          ? ''
+          : `${item.elderAge} 岁`
+    }))
+    total.value = data.total || 0
+  } catch (e) {
+    // 列表请求失败时填充测试假数据,便于前端联调(控制台有提示,不影响页面渲染)
+    console.warn('[出院小结] 列表请求失败,已使用测试假数据。', e)
+    list.value = buildMockDischargeSummary().map((item: any) => ({
+      ...item,
+      dischargeReasonText: getDischargeReasonText(item.dischargeReason),
+      elderAgeText: `${item.elderAge} 岁`
+    }))
+    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.dischargeDate = []
+  handleQuery()
+}
+
+/** 新增 / 编辑 */
+const formRef = ref()
+const printRef = ref()
+const checkOutRef = 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)
+}
+
+/** 退住申请:复用退住申请页面新增功能,带入当前长者 id 并预选该长者(无需权限) */
+const openCheckOut = (row: any = {}) => {
+  // 解析租户 id:优先用行内 tenantId,否则取当前查询选中的机构(只能单选)
+  let tId = row.tenantId
+  if (!tId) {
+    const qt = queryParams.tenantIds
+    if (Array.isArray(qt)) {
+      tId = qt.length === 1 ? qt[0] : undefined
+    } else {
+      tId = qt
+    }
+  }
+  if (!tId) {
+    message.error('请先选择一个机构')
+    return
+  }
+  // elderId 带入当前长者,open 内部会调用 setElderById 默认选中该长者
+  checkOutRef.value.open(tId, undefined, false, undefined, row.elderId)
+}
+
+/** 删除 */
+const handleDelete = async (id: number) => {
+  try {
+    await message.confirm('确定删除该出院小结吗?删除后不可恢复。', '提示')
+    await deleteDischargeSummary(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>

+ 14 - 10
src/views/elderly/bak/check-out/Form.vue

@@ -28,20 +28,24 @@ const processFormRef = ref() // 表单 Ref
 const isDetail = ref(false) // 是否详情打开
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const titleItem = ref('新增')
-/** 打开弹窗 */
-const open = async (tId, id, detail, status) => {
+/** 打开弹窗
+ * @param tId       租户 id(必填)
+ * @param id        退住申请 id(编辑/重新发起时传入)
+ * @param detail    是否详情
+ * @param status    审批状态
+ * @param elderId   外部带入的长者 id(如从出院小结发起退住申请时预选该长者,新增场景)
+ */
+const open = async (tId, id, detail, status, elderId) => {
   dialogVisible.value = true
   isDetail.value = detail
+  await nextTick()
+  processFormRef.value.setTenantId(tId)
   if (id) {
     titleItem.value = '重新发起'
-    nextTick(() => {
-      processFormRef.value.setTenantId(tId)
-      processFormRef.value.init(id, detail, status)
-    })
-  }else{
-    nextTick(()=>{
-      processFormRef.value.setTenantId(tId)
-    })
+    processFormRef.value.init(id, detail, status)
+  } else if (elderId) {
+    // 新增且带入长者 id:打开后默认选中该长者(下拉框显示且其证件号/入住日期/床位号自动回填)
+    processFormRef.value.setElderById(elderId)
   }
 }
 

+ 11 - 2
src/views/elderly/bak/check-out/ProcessForm.vue

@@ -12,7 +12,7 @@
       <el-row>
         <el-col :xs="24" :sm="24" :md="24" :lg="processType == 2 ? 24 : 12">
           <el-form-item label="长者姓名" prop="elderlyId">
-            <selectElder v-model="dataForm.elderlyId" @elder="handleSelectElder" :tId="dataForm.tenantId" :toggleType="isDetail"/>
+            <selectElder ref="selectElderRef" v-model="dataForm.elderlyId" @elder="handleSelectElder" :tId="dataForm.tenantId" :toggleType="isDetail"/>
           </el-form-item>
         </el-col>
         <el-col :xs="24" :sm="24" :md="24" :lg="processType == 2 ? 24 : 12">
@@ -147,6 +147,7 @@ const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const formRef = ref() // 表单 Ref
+const selectElderRef = ref() // 选择长者组件 Ref
 const isDetail = ref(false) // 是否详情打开
 let dataForm = ref({
   // 表单字段
@@ -236,7 +237,15 @@ const setTenantId = (tId) => {
   dataForm.value.tenantId = tId
 }
 
-defineExpose({ init, resetForm, submitForm, getProcess, setTenantId }) // 提供 open 方法,用于打开弹窗
+/** 按长者 id 预选长者(外部带入,如从出院小结发起退住申请) */
+const setElderById = (id) => {
+  if (id) {
+    // selectElder 已暴露 upData(str, id):拉取档案并 emit('elder') 回填 证件号/入住日期/床位号
+    selectElderRef.value?.upData('', id)
+  }
+}
+
+defineExpose({ init, resetForm, submitForm, getProcess, setTenantId, setElderById }) // 提供 open 方法,用于打开弹窗
 </script>
 <style lang="scss" scoped>
 .flex {

+ 2 - 2
src/views/elderly/elder/elder-file/index.vue

@@ -440,8 +440,8 @@ const handleExport = async () => {
     exportLoading.value = true
     const data = await exportElder({
       ...queryParams,
-      pageSize: undefined,
-      pageNo: undefined
+      pageSize: queryParams.pageSize,
+      pageNo: queryParams.pageNo,
     })
     download.excel(data, `长者档案${Date.now()}.xls`)
   } catch {