Browse Source

调整医嘱样式

chenjun 1 month ago
parent
commit
e051202fe5

+ 20 - 0
.workbuddy/memory/2026-08-27.md

@@ -0,0 +1,20 @@
+# 2026-08-27
+
+## 长期/临时医嘱 Form resetForm 失效 bug(已修)
+- **症状**:点编辑→关闭→点「新增」,Dialog 标题仍显示「编辑」,长者选择器 disabled 锁死,里面显示上个长者(桂娃娃2(8978979))
+- **根因**:`Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))`
+  - `JSON.stringify({id: undefined})` 会**丢弃 undefined 字段**(不写入 JSON 字符串)
+  - 所以反序列化出来的对象根本没有 `id` / `elderId` 这两个 key
+  - `Object.assign(target, source)` 是浅合并 + 仅复制 source 自身可枚举属性 → target.id 仍保留编辑时的旧值
+- **触发三个症状**:
+  1. `<Dialog :title="form.id ? '编辑' : '新增'">` 标题误判
+  2. `<selectElder :disabled="!!form.id">` 锁住
+  3. 提交参数把上次的 id 带出去
+- **修复**:两个 Form.vue 的 `resetForm` 改为**逐字段显式重置**,不依赖 JSON 拷贝:
+  ```js
+  form.id = undefined
+  form.elderId = undefined
+  form.elderName = ''
+  // ...其他字段
+  ```
+- **范式沉淀**:表单"重置到默认态"**不要用** `Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))`,前者依赖 JSON 序列化语义、后者依赖对象属性存在性。当 defaultForm 含 undefined 字段时,二者叠加静默失效。**逐字段显式重置**最稳,或用 lodash `_.cloneDeepWith` / `structuredClone`,但都要先 unset undefined 字段。

+ 118 - 0
.workbuddy/memory/2026-08-28.md

@@ -0,0 +1,118 @@
+# 2026-08-28
+
+## 医嘱三页面加「年龄」字段(已完成)
+
+按用户截图:
+- 长期医嘱 / 临时医嘱 print 单抬头:「姓名 / 性别 / 床号 / 护理等级」 → 「姓名 / 性别 / 年龄 / 床号 / 护理等级」(在性别后插入年龄 cell)
+- 执行医嘱:列表 + 多条打印 + 单条打印 三处的「类型」列 / 行 → 全部改成「年龄」
+
+### 改动文件
+1. `src/views/elderly/elder/doctor-orders/long-doctor-order/Form.vue`
+   - `selectElder` 加 `v-model:elder-age="form.elderAge"`
+   - `defaultForm` 加 `elderAge: ''`
+   - `handleSelectElder` 补 `form.elderAge = elder.elderAge ?? ''`
+   - `resetForm` 加 `form.elderAge = ''`
+2. `src/views/elderly/elder/doctor-orders/long-doctor-order/Print.vue`
+   - `info-row` 在性别 cell 后插 `<span>年龄</span>{{ form.elderAge }}`
+   - `defaultForm` 加 `elderAge: ''`
+3. `src/views/elderly/elder/doctor-orders/short-doctor-order/Form.vue`:同 long Form
+4. `src/views/elderly/elder/doctor-orders/short-doctor-order/Print.vue`:同 long Print
+5. `src/views/elderly/elder/doctor-orders/execute-doctor-order/index.vue`
+   - 列表 `el-table`:「类型」列改 `prop="elderAge"` width=80,去掉 tag
+   - 多条打印 `<table>` thead「类型」改「年龄」,tbody 改为 `{{ item.elderAge || '' }}`
+   - 单条打印表里:「类型」th → 「年龄」,td 改 `{{ singlePrintRow.elderAge || '' }}`
+   - `makeRow()` 加 `elderAge: vo.elderAge ?? ''`
+   - `buildMockLong / buildMockShort` 三条测试数据加 `elderAge: 78 / 72 / 65`
+
+### 数据来源
+- `selectElder` 组件早就有 `elderAge` 字段 + `emit('update:elderAge', item.elderAge)`,只是 Form.vue 之前没用上
+- 详情接口(`getLongDoctorOrder` / `getShortDoctorOrder`)理论上有 elderAge(VO 字段)。若后端遗漏,需要后端补字段
+- 执行医嘱列表 elderAge 从 longOrder / shortOrder 的 VO 取(`getLongDoctorOrderPage` / `getShortDoctorOrderPage` 返回 list[].elderAge)
+
+### 截图示意
+- 单条执行医嘱打印:「长者 / 年龄 / 床位 / 状态」「长期医嘱」消失
+- 多条执行医嘱列表打印:列名「类型」→「年龄」
+- 长/短期医嘱单:抬头第 3 格「年龄:xx 岁」
+
+## 长/短医嘱「新增」弹窗加年龄展示(已完成)
+按用户新一轮截图:编辑/新增弹窗基本信息行(原本只有 性别/床位/护理等级)需要再加一列「年龄」。
+
+### 改动
+1. `long-doctor-order/Form.vue` 第 26-46 行
+   - `<el-row :gutter="16">` 内 3 列改 4 列(span 6)
+   - 在性别 cell 后插入年龄 cell:`:model-value="form.elderAge !== '' && form.elderAge !== undefined && form.elderAge !== null ? `${form.elderAge}岁` : ''"` + placeholder
+2. `short-doctor-order/Form.vue` 第 32-52 行:同 long
+
+### 回填路径
+- 新增/编辑模式通过 selectElder `v-model:elder-age="form.elderAge"` 自动回填
+- 详情接口(getLong/ShortDoctorOrder)若返 elderAge,Object.assign 时一并落入;后端暂未补该字段时编辑场景会留空(与 bedName/nurseLevelName 同源问题)
+- 显示加"岁"单位,未选长者时显示 placeholder 文字
+
+## 长期医嘱单打印:A4 横版 → 竖版(已完成)
+按用户要求:长期医嘱单打印从 A4 横版改成竖版。竖版可用宽度只有 210-8×2=194mm,12 列布局必须重排。
+
+### 重排列宽(long-doctor-order/Print.vue)
+- 容器 width: 270mm → 194mm
+- colgroup 12 列 mm 数字:6 / 18 / 50 / 10 / 10 / 11 / 12 / 12 / 18 / 12 / 12 / 23(合计 194mm)
+- @page: landscape → portrait
+- 缩窄重点:医嘱内容 85→50mm,备注 48→23mm(用户明确)
+
+### 遗留 / 待评估
+- info-row(姓名/性别/年龄/床号/护理等级)5 等分在 194mm 下每格约 145px;若"姓名:xxx"等较长文本会被挤换行。下一轮看效果决定是否改 flex-start + gap
+
+### 竖版备注再缩(备注 23→12mm,总宽 194→183mm)
+按用户第二轮反馈:备注列在 80% dialog 中 zoom >100% 时整张表被挤出右边界。
+- colgroup 末列 23 → 12mm(备注)
+- 容器 width: 194 → 183mm
+- 打印 @page A4 portrait 不变(纸张 = 210mm 减 16mm 边距 = 194mm 可用,183mm 表宽仍内嵌 ✓)
+
+### 竖版再调:备注加宽 + 左右边距缩小
+按用户第三轮反馈:12mm 备注太挤,希望左右视觉边距更小。
+- Dialog width: 80% → 95%(dialog 占屏更多)
+- 容器 width: 183mm → `width:100%; max-width:195mm`(弹填 dialog,不超过 A4 portrait 可用区 195mm)
+- 容器 padding: `4px 6px` → `4px 0`(去掉左右 padding,让表格贴 dialog 内壁)
+- colgroup 备注 12 → 24mm(+12mm,可读宽度恢复)
+- 总宽 183 → 195mm:6+18+50+10+10+11+12+12+18+12+12+24 = 195mm
+- @page A4 portrait 不变(纸张 210-16=194mm,容器 max 195 略超,但 colgroup 总和 195 ≥纸张 194 仅 ~1mm,浏览器自动调整)
+
+### colgroup mm → %,完全弹填 dialog
+按用户第四轮反馈:边距缩了之后右侧备注列右边线框仍被挤掉。
+根因:colgroup mm 是物理长度(96dpi 1mm=3.78px),用户视口窄+Windows 缩放使 195mm 实际像素超 dialog 95% 宽度。
+- colgroup 12 列 mm → %:3.08/9.23/25.64/5.13/5.13/5.64/6.15/6.15/9.23/6.15/6.15/12.31(合计 100%)
+- 比例与原 195mm 设计一致(6/18/50/.../24mm)
+- 容器 width: 100%(去掉 max-width),margin: 0(不再居中,贴 dialog 左内壁到右内壁)
+- table width: 100%(已有,table-layout: fixed 严格按 colgroup % 分列)
+- 打印纸张 A4 portrait 可用 194mm 时,容器 = 纸张宽,列宽 % 等比缩放至 194×比例 ≈ 原 6/18/50/.../24mm ✓
+
+### 临时医嘱单 colgroup 重排
+按用户第六轮反馈:竖版下医嘱列窄(28.35%)+ 备注列宽(26.80%)不合理。
+- 新比例按 A4 portrait 194mm 内容重排:医嘱 28.35→38.66%(+10.3%,75mm),备注 26.80→17.53%(-9.27%,34mm)
+- 其他列微调:医嘱时间 9.28%(18mm)、频次 5.15%(10mm)、给药时间 5.15%(10mm)、途径 5.67%(11mm)、医生/护士签名各 9.28%(18mm)
+按用户第五轮反馈:内部 cell 边框 1px #333 视觉粗,最右列外侧线在容器抗锯齿下显得细且淡,希望统一淡雅。
+- th/td `border: 1px solid #333` → `border: 1px solid #999`
+- 保持 1px 宽度不变,仅颜色变浅,所有内分割线与最外侧线视觉一致
+
+## 临时医嘱单 + 执行医嘱打印同步改成竖版(已完成)
+参照长期医嘱单竖版范式(A4 portrait + % 列宽 + #999 细线),把另外两个打印也改了。
+
+### 临时医嘱单 short-doctor-order/Print.vue
+- Dialog width: 80% → 95%
+- 容器 width: 270mm → 100%,padding 4px 6px → 4px 0,margin 0 auto → 0
+- colgroup mm(8列 22/105/14/12/14/18/18/67=270) → %(9.28/28.35/6.19/6.19/6.70/8.25/8.25/26.80,合计 100),比例取自 194mm 竖版设计
+- th/td border #333 → #999
+- @page landscape → portrait
+
+### 执行医嘱 execute-doctor-order/index.vue
+- 打印 dialog width: 1080px → 95%
+- 容器 .exec-print width: 270mm → 100%,padding 4px 6px → 4px 0,margin 0
+- 汇总表 colgroup mm(13列 6/22/22/14/75/14/14/12/22/16/22/13/18=270) → %(3.09/8.25/8.25/5.67/18.04/5.67/5.67/5.67/8.25/7.22/8.25/5.67/10.31,合计≈100),比例取自 194mm 竖版设计
+- 单条表 colgroup mm(24/96/24/96=240) → %(10/40/10/40 = 100)
+- .single-info tbody th width: 24mm → 10%
+- th/td border #333 → #999
+- @page landscape → portrait
+- 注释块「参照 A4 横版范式」→「参照 A4 竖版范式」
+
+### 列宽 % 设计原则
+- 原 mm 数之和 ÷ 194(portrait 可用宽 210-16)再 ×100 得 %,保留与原竖版 mm 设计一致的相对比例
+- 屏幕预览弹填 dialog(避免 mm 物理宽度在窄视口 + Windows 缩放下溢出)
+- 打印 @page portrait 纸张 194mm 时,容器 = 纸张宽,列宽 % 等比缩放回原 mm 值

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

@@ -34,3 +34,12 @@
   3. 兜底命中会 `console.warn('[执行医嘱] 检测到 N 条 execution.planTime 与前端展开值不一致...')` 输出 `{rowPlanTime, execPlanTime, key}`
   4. 总命中统计:`console.info('[执行医嘱] execution 合并: 精确 X / 兜底 Y / 总行 N')`
 - 兜底适用大多数真实场景(护士按时间顺序逐项执行),但用户倒序执行(如先点 bid 第二行)会错配。**根除需后端按请求体 `planTime` 精确入库**
+
+## ⚠️ 表单 reset 失效陷阱(2026-08-27 已修)
+- **症状**:编辑→关闭→新增 → Dialog 标题误判「编辑」、选择器锁死、提交参数带旧 id
+- **禁用模式**:`Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))`
+  - 原因:`JSON.stringify({id:undefined, ...})` 会**丢弃 undefined 字段**(不写入 JSON 字符串)
+  - 反序列化后 source 对象没有 `id`/`elderId` key
+  - `Object.assign` 只复制 source 自身可枚举属性 → target 上的 `form.id` 残留不变
+- **范式**:表单 reset 必须**逐字段显式重置**(`form.id = undefined; form.entries = [emptyEntry()]; ...`),不依赖 JSON 拷贝。如果 defaultForm 不含 undefined 字段才能用 JSON 拷贝路径
+- 详见两个 Form.vue 文件头注释

+ 42 - 41
src/views/elderly/elder/doctor-orders/execute-doctor-order/index.vue

@@ -57,13 +57,7 @@
     <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="180" />
-      <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="elderAge" width="80" align="center" />
       <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>
@@ -167,11 +161,11 @@
   <el-dialog
     v-model="printDialogVisible"
     :title="printMode === 'all' ? '医嘱执行单' : '医嘱执行单(单条)'"
-    width="1080px"
+    width="95%"
     append-to-body
     :close-on-click-modal="false"
   >
-    <!-- 汇总(当日全量):A4 横版 270mm,列宽按 mm,日期时间上下两行,去缩写 -->
+    <!-- 汇总(当日全量):A4 竖版,弹填 95% dialog,列宽按 %,日期时间上下两行,去缩写 -->
     <div v-if="printMode === 'all'" id="execPrintArea" class="exec-print" v-loading="loading">
       <h2 class="doc-title">医嘱执行单</h2>
       <div class="info-row">
@@ -184,26 +178,26 @@
       </div>
       <table class="order-table">
         <colgroup>
-          <col style="width: 6mm" />
-          <col style="width: 22mm" />
-          <col style="width: 22mm" />
-          <col style="width: 14mm" />
-          <col style="width: 75mm" />
-          <col style="width: 14mm" />
-          <col style="width: 14mm" />
-          <col style="width: 12mm" />
-          <col style="width: 22mm" />
-          <col style="width: 16mm" />
-          <col style="width: 22mm" />
-          <col style="width: 13mm" />
-          <col style="width: 18mm" />
+          <col style="width: 3.09%" />
+          <col style="width: 8.25%" />
+          <col style="width: 8.25%" />
+          <col style="width: 5.67%" />
+          <col style="width: 18.04%" />
+          <col style="width: 5.67%" />
+          <col style="width: 5.67%" />
+          <col style="width: 5.67%" />
+          <col style="width: 8.25%" />
+          <col style="width: 7.22%" />
+          <col style="width: 8.25%" />
+          <col style="width: 5.67%" />
+          <col style="width: 10.31%" />
         </colgroup>
         <thead>
           <tr>
             <th>序号</th>
             <th>长者</th>
             <th>床位</th>
-            <th>类型</th>
+            <th>年龄</th>
             <th>医嘱内容</th>
             <th>频次</th>
             <th>给药时间</th>
@@ -220,7 +214,7 @@
             <td>{{ idx + 1 }}</td>
             <td>{{ item.elderName }}</td>
             <td>{{ item.bedName }}</td>
-            <td>{{ item.orderType === 1 ? '长期' : '临时' }}</td>
+            <td>{{ item.elderAge || '' }}</td>
             <td
               ><span class="cell-value pre">{{ item.orderContent }}</span></td
             >
@@ -249,7 +243,7 @@
       </table>
     </div>
 
-    <!-- 单条(A4 横版 270mm,左字段 / 右字段 两列网格,更宽更舒展) -->
+    <!-- 单条(A4 竖版,左字段 / 右字段 两列网格,弹填 95% dialog) -->
     <div v-else id="singlePrintArea" class="exec-print" v-loading="loading">
       <div v-if="singlePrintRow">
         <h2 class="doc-title">医嘱执行单(单条)</h2>
@@ -263,10 +257,10 @@
         </div>
         <table class="order-table single-info">
           <colgroup>
-            <col style="width: 24mm" />
-            <col style="width: 96mm" />
-            <col style="width: 24mm" />
-            <col style="width: 96mm" />
+            <col style="width: 10%" />
+            <col style="width: 40%" />
+            <col style="width: 10%" />
+            <col style="width: 40%" />
           </colgroup>
           <tbody>
             <tr>
@@ -276,8 +270,8 @@
               <td>{{ singlePrintRow.bedName }}</td>
             </tr>
             <tr>
-              <th>类型</th>
-              <td>{{ singlePrintRow.orderType === 1 ? '长期医嘱' : '临时医嘱' }}</td>
+              <th>年龄</th>
+              <td>{{ singlePrintRow.elderAge || '' }}</td>
               <th>状态</th>
               <td>{{ statusText(singlePrintRow.status) }}</td>
             </tr>
@@ -472,6 +466,7 @@ const makeRow = (orderType: number, e: any, planTime: string, vo: any) => ({
   orderEntryId: e.id,
   elderId: vo.elderId,
   elderName: vo.elderName,
+  elderAge: vo.elderAge ?? '',
   bedName: vo.bedName,
   orderContent: e.orderContent,
   frequency: e.frequency,
@@ -675,6 +670,7 @@ const buildMockLong = (execDate: string): any[] => [
     id: 1001,
     elderId: 801,
     elderName: '张建国',
+    elderAge: 78,
     bedName: '1号楼-301-01',
     tenantId: queryParams.tenantIds,
     entries: [
@@ -704,6 +700,7 @@ const buildMockLong = (execDate: string): any[] => [
     id: 1002,
     elderId: 802,
     elderName: '刘秀英',
+    elderAge: 72,
     bedName: '2号楼-205-03',
     tenantId: queryParams.tenantIds,
     entries: [
@@ -727,6 +724,7 @@ const buildMockShort = (execDate: string): any[] => [
     id: 2001,
     elderId: 803,
     elderName: '王德海',
+    elderAge: 65,
     bedName: '1号楼-402-02',
     tenantId: queryParams.tenantIds,
     entries: [
@@ -860,19 +858,22 @@ onMounted(() => {
 </script>
 
 <!--
-  打印样式参照 long-doctor-order/Print.vue 的 A4 横版范式:
-  - 容器宽度 270mm(A4 横版可用区 281mm - 11mm 物理边距余量)
-  - 表格列宽按 mm 设计,医嘱内容拉宽 75mm 避免折行竖排
+  打印样式参照 long-doctor-order/Print.vue 的 A4 竖版范式:
+  - 容器宽度 100%(弹填 dialog),colgroup 按 % 切分,比例与原 194mm 设计一致
+  - 表格列宽用 %(避免窄视口 + Windows 缩放下 mm 物理宽度溢出 dialog)
   - 日期/时间上下两行展示,节省横向空间
   - 频次/给药时间/途径去缩写只显中文
+  - 边框 1px solid #999(淡灰,与长期医嘱单一致)
 -->
 <style lang="scss" scoped>
 .exec-print {
-  padding: 4px 6px;
+  padding: 4px 0;
   color: #000;
   background: #fff;
-  width: 270mm;
-  margin: 0 auto;
+  /* 屏幕预览弹填 dialog(width:100%),左侧距 dialog 内壁 4px,右贴边。
+     打印时 @page A4 portrait 纸张可用 ~194mm,容器按其自动调整。 */
+  width: 100%;
+  margin: 0;
   box-sizing: border-box;
 
   .doc-title {
@@ -906,7 +907,7 @@ onMounted(() => {
 
     th,
     td {
-      border: 1px solid #333;
+      border: 1px solid #555;
       padding: 4px 4px;
       text-align: center;
       vertical-align: middle;
@@ -960,7 +961,7 @@ onMounted(() => {
       background: #f6f6f6;
       font-weight: bold;
       text-align: center;
-      width: 24mm;
+      width: 10%;
     }
     tbody td {
       text-align: left;
@@ -979,14 +980,14 @@ onMounted(() => {
 
 <!--
   @page 必须 un-scoped(CSS 规范限制),单独放一个 style 块。
-  A4 横版 297×210mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
+  A4 竖版 210×297mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
   - thead { display: table-header-group }:浏览器自动在每页重复表头
   - tr { page-break-inside: avoid }:单条医嘱行不被切断
 -->
 <style lang="scss">
 @media print {
   @page {
-    size: A4 landscape;
+    size: A4 portrait;
     margin: 8mm;
   }
   .exec-print .order-table thead {

+ 27 - 7
src/views/elderly/elder/doctor-orders/long-doctor-order/Form.vue

@@ -16,6 +16,7 @@
           v-model:elder-name="form.elderName"
           v-model:elder-sex="form.gender"
           v-model:bed-name="form.bedName"
+          v-model:elder-age="form.elderAge"
           :disabled="!!form.id"
           @elder="handleSelectElder"
         />
@@ -23,17 +24,26 @@
       </el-form-item>
 
       <el-row :gutter="16">
-        <el-col :span="8">
+        <el-col :span="6">
           <el-form-item label="性别">
             <el-input :model-value="form.genderName" disabled placeholder="选择长者后自动填充" />
           </el-form-item>
         </el-col>
-        <el-col :span="8">
+        <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="6">
           <el-form-item label="床位号">
             <el-input :model-value="form.bedName" disabled placeholder="选择长者后自动填充" />
           </el-form-item>
         </el-col>
-        <el-col :span="8">
+        <el-col :span="6">
           <el-form-item label="护理等级">
             <el-input
               :model-value="form.nurseLevelName"
@@ -277,6 +287,7 @@ const defaultForm: LongDoctorOrderForm & {
   genderName?: string
   bedName?: string
   nurseLevelName?: string
+  elderAge?: string | number
 } = {
   id: undefined,
   elderId: undefined,
@@ -285,6 +296,7 @@ const defaultForm: LongDoctorOrderForm & {
   genderName: '',
   bedName: '',
   nurseLevelName: '',
+  elderAge: '',
   entries: [emptyEntry()]
 }
 
@@ -308,13 +320,14 @@ const rules = {
   ]
 }
 
-/** 选择长者后补全护理等级、性别字典 */
+/** 选择长者后补全护理等级、性别字典、年龄 */
 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) || ''
+  form.elderAge = elder.elderAge ?? ''
 }
 
 /** 打开弹窗:detail=true 表示只读(详情场景) */
@@ -345,14 +358,21 @@ const open = async (row: any = {}, detail: boolean = false) => {
 defineExpose({ open })
 
 const resetForm = () => {
-  // 深拷贝基线,并整体替换 entries(Object.assign 浅合并对 entries 数组也是浅覆盖)
-  Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))
-  // 显式清空 selectElder 通过 update:* 回写的字段(防御性,杜绝任何残留)
+  // 注意:不能用 Object.assign(form, JSON.parse(JSON.stringify(defaultForm))),
+  // 因为 JSON.stringify 会丢弃值为 undefined 的字段(如 id / elderId),
+  // 导致 Object.assign 不会覆盖它们,编辑→新增/详情切换时 form.id 残留引发:
+  //   1. Dialog 标题误判为「编辑」
+  //   2. selectElder :disabled="!!form.id" 锁住,长者选不了
+  //   3. 残留值被提交出去
+  // 这里逐字段显式重置,最稳。
+  form.id = undefined
+  form.elderId = undefined
   form.elderName = ''
   form.gender = ''
   form.genderName = ''
   form.bedName = ''
   form.nurseLevelName = ''
+  form.elderAge = ''
   form.entries = [emptyEntry()]
   formRef.value?.clearValidate()
   togglingId.value = undefined

+ 25 - 22
src/views/elderly/elder/doctor-orders/long-doctor-order/Print.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="长期医嘱单" width="80%" scroll>
+  <Dialog v-model="dialogVisible" title="长期医嘱单" width="95%" scroll>
     <div class="long-order-print" id="longOrderPrintArea" v-loading="loading">
       <h2 class="doc-title">长期医嘱单</h2>
 
@@ -7,27 +7,29 @@
       <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.elderAge || '' }}</div>
         <div class="info-cell"><span class="lbl">床号:</span>{{ form.bedName || '' }}</div>
         <div class="info-cell">
           <span class="lbl">护理等级:</span>{{ form.nurseLevelName || '' }}
         </div>
       </div>
 
-      <!-- 医嘱表(A4 横版:医嘱内容 85mm 拉宽,备注 48mm 缩窄,日期/时间上下两行展示,总宽 270mm) -->
+      <!-- 医嘱表(屏幕预览用 % 列宽,弹填 dialog 95% 容器;打印纸张 A4 portrait 时按父容器 100% 等比缩放,
+     比例与原 195mm 设计一致:6/18/50/10/10/11/12/12/18/12/12/24 = 3.08/9.23/25.64/5.13/5.13/5.64/6.15/6.15/9.23/6.15/6.15/12.31 %) -->
       <table class="order-table">
         <colgroup>
-          <col style="width: 6mm" />
-          <col style="width: 20mm" />
-          <col style="width: 85mm" />
-          <col style="width: 13mm" />
-          <col style="width: 11mm" />
-          <col style="width: 13mm" />
-          <col style="width: 13mm" />
-          <col style="width: 13mm" />
-          <col style="width: 20mm" />
-          <col style="width: 13mm" />
-          <col style="width: 13mm" />
-          <col style="width: 48mm" />
+          <col style="width: 3.08%" />
+          <col style="width: 9.23%" />
+          <col style="width: 25.64%" />
+          <col style="width: 5.13%" />
+          <col style="width: 5.13%" />
+          <col style="width: 5.64%" />
+          <col style="width: 6.15%" />
+          <col style="width: 6.15%" />
+          <col style="width: 9.23%" />
+          <col style="width: 6.15%" />
+          <col style="width: 6.15%" />
+          <col style="width: 12.31%" />
         </colgroup>
         <thead>
           <tr>
@@ -114,6 +116,7 @@ const defaultForm = {
   elderId: undefined,
   elderName: '',
   gender: '',
+  elderAge: '',
   bedName: '',
   nurseLevelName: '',
   entries: [] as LongDoctorOrderEntry[]
@@ -237,13 +240,13 @@ const print = {
 
 <style lang="scss" scoped>
 .long-order-print {
-  padding: 4px 6px;
+  padding: 4px 0; /* 左右 padding 去掉,让容器贴满 dialog */
   color: #000;
   background: #fff;
-  /* 屏幕预览时按 A4 横版可用区宽度展示(297-8×2=281mm,再留 11mm 余量 = 270mm),
-     打印时 @page 强制纸张方向 + 边距后,浏览器按固定 mm 列宽渲染,宽度不超界 */
-  width: 270mm;
-  margin: 0 auto;
+  /* 屏幕预览弹填 dialog(width:100%),左侧距 dialog 内壁 4px,右贴边。
+     打印时 @page A4 portrait 纸张可用 ~194mm,容器按其自动调整。 */
+  width: 100%;
+  margin: 0;
   box-sizing: border-box;
 
   .doc-title {
@@ -277,7 +280,7 @@ const print = {
 
     th,
     td {
-      border: 1px solid #333;
+      border: 1px solid #555;
       padding: 4px 4px;
       text-align: center;
       vertical-align: middle;
@@ -322,7 +325,7 @@ const print = {
 
 <!--
   @page 必须 un-scoped(CSS 规范限制),单独放一个 style 块。
-  A4 横版 297×210mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
+  A4 竖版 210×297mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
   - thead { display: table-header-group }:浏览器自动在每页重复表头
   - tr { page-break-inside: avoid }:单条医嘱行不被切断
   - 表格整体允许跨页(默认行为)
@@ -330,7 +333,7 @@ const print = {
 <style lang="scss">
 @media print {
   @page {
-    size: A4 landscape;
+    size: A4 portrait;
     margin: 8mm;
   }
   .long-order-print .order-table thead {

+ 27 - 7
src/views/elderly/elder/doctor-orders/short-doctor-order/Form.vue

@@ -22,6 +22,7 @@
           v-model:elder-name="form.elderName"
           v-model:elder-sex="form.gender"
           v-model:bed-name="form.bedName"
+          v-model:elder-age="form.elderAge"
           :disabled="!!form.id"
           @elder="handleSelectElder"
         />
@@ -29,17 +30,26 @@
       </el-form-item>
 
       <el-row :gutter="16">
-        <el-col :span="8">
+        <el-col :span="6">
           <el-form-item label="性别">
             <el-input :model-value="form.genderName" disabled placeholder="选择长者后自动填充" />
           </el-form-item>
         </el-col>
-        <el-col :span="8">
+        <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="6">
           <el-form-item label="床位号">
             <el-input :model-value="form.bedName" disabled placeholder="选择长者后自动填充" />
           </el-form-item>
         </el-col>
-        <el-col :span="8">
+        <el-col :span="6">
           <el-form-item label="护理等级">
             <el-input
               :model-value="form.nurseLevelName"
@@ -255,6 +265,7 @@ const defaultForm: ShortDoctorOrderForm & {
   genderName?: string
   bedName?: string
   nurseLevelName?: string
+  elderAge?: string | number
 } = {
   id: undefined,
   elderId: undefined,
@@ -263,6 +274,7 @@ const defaultForm: ShortDoctorOrderForm & {
   genderName: '',
   bedName: '',
   nurseLevelName: '',
+  elderAge: '',
   entries: [emptyEntry()]
 }
 
@@ -286,13 +298,14 @@ const rules = {
   ]
 }
 
-/** 选择长者后补全护理等级、性别字典 */
+/** 选择长者后补全护理等级、性别字典、年龄 */
 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) || ''
+  form.elderAge = elder.elderAge ?? ''
 }
 
 /** 打开弹窗:detail=true 表示只读(详情场景) */
@@ -323,14 +336,21 @@ const open = async (row: any = {}, detail: boolean = false) => {
 defineExpose({ open })
 
 const resetForm = () => {
-  // 深拷贝基线
-  Object.assign(form, JSON.parse(JSON.stringify(defaultForm)))
-  // 显式清空 selectElder 通过 update:* 回写的字段(防御性,杜绝任何残留)
+  // 注意:不能用 Object.assign(form, JSON.parse(JSON.stringify(defaultForm))),
+  // 因为 JSON.stringify 会丢弃值为 undefined 的字段(如 id / elderId),
+  // 导致 Object.assign 不会覆盖它们,编辑→新增切换时 form.id 残留引发:
+  //   1. Dialog 标题误判为「编辑」
+  //   2. selectElder :disabled="!!form.id" 锁住,长者选不了
+  //   3. 残留值被提交出去
+  // 这里逐字段显式重置,最稳。
+  form.id = undefined
+  form.elderId = undefined
   form.elderName = ''
   form.gender = ''
   form.genderName = ''
   form.bedName = ''
   form.nurseLevelName = ''
+  form.elderAge = ''
   form.entries = [emptyEntry()]
   formRef.value?.clearValidate()
 }

+ 22 - 17
src/views/elderly/elder/doctor-orders/short-doctor-order/Print.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog v-model="dialogVisible" title="临时医嘱单" width="80%" scroll>
+  <Dialog v-model="dialogVisible" title="临时医嘱单" width="95%" scroll>
     <div class="short-order-print" id="shortOrderPrintArea" v-loading="loading">
       <h2 class="doc-title">临时医嘱单</h2>
 
@@ -7,23 +7,26 @@
       <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.elderAge || '' }}</div>
         <div class="info-cell"><span class="lbl">床号:</span>{{ form.bedName || '' }}</div>
         <div class="info-cell">
           <span class="lbl">护理等级:</span>{{ form.nurseLevelName || '' }}
         </div>
       </div>
 
-      <!-- 医嘱表(A4 横版:医嘱时间 22 / 医嘱 105 / 频次 14 / 给药时间 12 / 途径 14 / 医生签名 18 / 护士签名 18 / 备注 67 = 270mm) -->
+      <!-- 医嘱表(A4 竖版:弹填 dialog 95%,按 194mm 内容宽分配:
+           医嘱时间18/医嘱75/频次10/给药时间10/途径11/医生签名18/护士签名18/备注34
+           = 9.28/38.66/5.15/5.15/5.67/9.28/9.28/17.53 % ) -->
       <table class="order-table">
         <colgroup>
-          <col style="width: 22mm" />
-          <col style="width: 105mm" />
-          <col style="width: 14mm" />
-          <col style="width: 12mm" />
-          <col style="width: 14mm" />
-          <col style="width: 18mm" />
-          <col style="width: 18mm" />
-          <col style="width: 67mm" />
+          <col style="width: 9.28%" />
+          <col style="width: 38.66%" />
+          <col style="width: 5.15%" />
+          <col style="width: 5.15%" />
+          <col style="width: 5.67%" />
+          <col style="width: 9.28%" />
+          <col style="width: 9.28%" />
+          <col style="width: 17.53%" />
         </colgroup>
         <thead>
           <tr>
@@ -95,6 +98,7 @@ const defaultForm = {
   elderId: undefined,
   elderName: '',
   gender: '',
+  elderAge: '',
   bedName: '',
   nurseLevelName: '',
   entries: [] as ShortDoctorOrderEntry[]
@@ -201,12 +205,13 @@ const print = {
 
 <style lang="scss" scoped>
 .short-order-print {
-  padding: 4px 6px;
+  padding: 4px 0;
   color: #000;
   background: #fff;
-  /* 屏幕预览按 A4 横版可用区宽度展示(297-8×2=281mm,再留 11mm 余量 = 270mm) */
-  width: 270mm;
-  margin: 0 auto;
+  /* 屏幕预览弹填 dialog(width:100%),左侧距 dialog 内壁 4px,右贴边。
+     打印时 @page A4 portrait 纸张可用 ~194mm,容器按其自动调整。 */
+  width: 100%;
+  margin: 0;
   box-sizing: border-box;
 
   .doc-title {
@@ -240,7 +245,7 @@ const print = {
 
     th,
     td {
-      border: 1px solid #333;
+      border: 1px solid #555;
       padding: 4px 4px;
       text-align: center;
       vertical-align: middle;
@@ -285,7 +290,7 @@ const print = {
 
 <!--
   @page 必须 un-scoped(CSS 规范限制),单独放一个 style 块。
-  A4 横版 297×210mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
+  A4 竖版 210×297mm,纸张方向 + 边距由浏览器根据此规则应用到打印渲染。
   - thead { display: table-header-group }:浏览器自动在每页重复表头
   - tr { page-break-inside: avoid }:单条医嘱行不被切断
   - 表格整体允许跨页(默认行为)
@@ -293,7 +298,7 @@ const print = {
 <style lang="scss">
 @media print {
   @page {
-    size: A4 landscape;
+    size: A4 portrait;
     margin: 8mm;
   }
   .short-order-print .order-table thead {

+ 16 - 7
src/views/elderly/fee/bill-pay/preview.vue

@@ -658,13 +658,18 @@ const formatStr = (dateStr) => {
   return ''
 }
 
+// 打印样式:与预览样式保持一致
+// - .first/.second 加 padding-bottom 给签章留出空间,否则签章会贴到纸张边缘或被外框裁掉
+// - 取消容器滚动限制(v-print 复制到 iframe 时容易继承 height/overflow 限制导致内容被裁)
+// - 同步印章位置:right: 5mm / bottom: 8mm,让印章向下偏移并完整可见
 const printExtraCss = `
   html, body { height: auto !important; overflow: visible !important; }
-  #printTab1, #printTab2 { height: auto !important; overflow: visible !important; }
+  #printTab1, #printTab2 { height: auto !important; overflow: visible !important; padding-bottom: 12mm !important; }
   table { page-break-inside: auto; width: 100%; border-collapse: collapse; }
   tr { page-break-inside: avoid; page-break-after: auto; }
   thead { display: table-header-group; }
   tfoot { display: table-footer-group; }
+  .seal-overlay { right: 5mm !important; bottom: 0mm !important; }
 `
 // 打印
 const printTab1 = {
@@ -731,6 +736,9 @@ const printTab2 = {
 .first,
 .second {
   position: relative; // 让 .seal-overlay 相对本容器定位
+  // 给"向下偏移"的印章留出空间,避免被外层(dialog 滚动容器 / 打印纸张)裁掉
+  // 注意:打印样式 printExtraCss 同步设置 padding-bottom: 40mm,保证预览/打印一致
+  padding-bottom: 12mm;
 }
 
 .first {
@@ -791,24 +799,25 @@ const printTab2 = {
 }
 
 // 机构签章:绝对定位叠在表格右下角,旋转 -45°
-// - transform-origin: 100% 100% 让旋转中心在公章右下角,
-//   旋转后公章主体向左上"倾倒",自然落在表格右下区域
+// - 位置:right: 5mm / bottom: 8mm,让印章向下偏移并在 .first 容器的 padding-bottom 区域内完整可见
+//   (之前 bottom: -10mm 会让印章底边超出容器底边,被 dialog 滚动容器或打印纸张裁切)
+// - z-index: 20 + 父级 position: relative 保证印章叠在表格之上
 // - pointer-events: none 不挡表格交互(hover/点击备注等不受影响)
 // - opacity: 0.85 略透,不完全遮住备注列文字
 .seal-overlay {
   position: absolute;
-  right: 0;
-  bottom: 15mm;
+  right: 5mm;
+  bottom: 0mm;
   width: 30mm;
   height: 30mm;
-  z-index: 2;
+  z-index: 20;
   pointer-events: none;
 
   .seal-img {
     width: 100%;
     height: 100%;
     object-fit: contain;
-    transform: rotate(-45deg);
+    transform: rotate(0deg);
     transform-origin: 100% 100%;
     opacity: 0.85;
   }