Browse Source

Merge branch 'master' of http://47.107.245.0:3000/xiongxing/kyj-yanglao-web-new

xiongxing 1 week ago
parent
commit
b680bb1983

+ 13 - 1
.workbuddy/memory/2026-08-04.md

@@ -26,4 +26,16 @@
 补充(约 11:15):vue-tsc 类型检查报两处 TS6133 死代码,已清理——
 - `index.vue` 中未使用的 `levelNames` 常量(疑似日历遗留);
 - `nine-safety.ts` 的 `getNineRiskCorrelation` 中未使用的 `elders` 局部变量(`generateElders` 在 `getRiskRadar` 仍使用,不受影响)。
-重新检查 nine-safety 相关文件:NO_NINE_SAFETY_ERRORS,删除干净无编译错误。
+重新检查 nine-safety 相关文件:NO_NINE_SAFETY_ERRORS,删除干净无编译错误。
+
+## 排查 src/views/ykCenter/... vue-tsc 报错(约 11:55)
+整体 vue-tsc 在 ykCenter 相关约 9 个文件报 ~70 条错误,分组根因:
+1. **遗留 mock 数据(最大块,~20 条)**:各视图 `(await import('@/api/ykCenter')).mockData` 直接拿被注释掉的假数据对象做本地增删改。`mockData` 原在 `src/api/ykCenter/indexFetch.ts:286` 整段被 `//` 注释(伪后端脚手架),真实 API 已接好。需用户决定:恢复导出 / 重构为真实 API / 暂不处理。
+2. **未注明泛型的 `ref([])` → `never[]`**(delivery-staff、food-delivered-history,~6 条):给 ref 补 `<T[]>` 泛型即可,低风险。
+3. **局部 interface 字段缺失/错名**(delivery-staff `elderName/personalPhone`、elderly-manage `supportStatus/startDate/endDate/desc`、finance-verify `paymentType/confirmStatus/elderlyName`、food-delivered-history `ykElderName/mealStandard`):扩宽 interface,但 `elderlyName` vs `elderName` 涉及接口字段名正确性,需谨慎。
+4. **缺 `$modal` 全局类型**(delivery-staff:336):`proxy?.$modal` 报错,需在 `ComponentCustomProperties` 增补类型声明。
+5. **TS6133 未使用变量**(全 ykCenter ~20 条):安全可删,但需确认模板未引用。
+6. **`null` 不能赋给 `string`**(delivery-staff:355):ref 类型加 `| null`。
+
+注意:`src/types/auto-components.d.ts` 仅声明 GlobalComponents 且有 `@ts-nocheck`,未声明 `$modal` 等 ComponentCustomProperties。
+→ 已向用户给出分组诊断,并询问处理方式(先修安全项 / 全修含 mockData / 仅诊断)。

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

@@ -0,0 +1,25 @@
+# 2026-08-05
+
+## nine-safety-measures/index.vue — 红名单筛选下拉框
+
+用户要求把「重点人群红名单」卡片标题里的提示文案「建议:纳入每日护士长巡床必查清单」去掉,改成按九防筛选的下拉框,默认「全部」。
+
+改动:
+- 删除了 `.tip danger` 提示 span,原位置(`.card-title` 右侧,`margin-left:auto`)替换为 `el-select`(v-model `redRiskFilter`,`clearable`,选项 `nineRiskOptions`)。
+- 新增 `redRiskFilter` ref(默认 `''` = 全部)、`nineRiskOptions` computed(全部 + 九防)、`filteredRedList` computed(按 `r.risks.includes(value)` 过滤)。
+- 表格 `v-for` 与计数改用 `filteredRedList`。
+- `src/api/data-board/nine-safety.ts`:`NINE_RISKS` 由 `const` 改为 `export const`,页面用 `echartsApi.NINE_RISKS` 复用(单一来源)。
+- 选项 value 用 `r.name.replace('防','')`,**因为红名单 `risks` 字段存的是去「防」后的名**(API 第 291 行 `r.name.replace('防','')`,如「噎食」),否则筛不出;label 仍显示完整「防噎食」更直观。
+- 复用页面已有 `:deep(.el-select__wrapper)` 暗色适配,无需额外主题。
+
+注意:清空(clearable)时 v-model 变 `undefined`,`!redRiskFilter.value` 判断为 falsy → 回退显示全部,逻辑正确。
+vue-tsc 过滤 nine-safety:NO_NINE_SAFETY_ERRORS,编译通过。
+
+## bpm/task/process-list/index.vue — 回车刷新页面 fix
+
+用户问:审批内容输入框按回车为什么导致整页刷新。
+
+根因:`<el-input>` 包在 `<el-form>`(渲染成原生 `<form>`)里,且表单内只有一个文本输入框 + 一个 `native-type="button"` 的「取消」按钮。HTML 规范:单个输入框按回车会触发原生表单提交 → 当前 URL 重新 GET → 整页刷新。`@change="handleQuery"` 其实已触发,但被紧随的原生提交冲掉。
+
+修法:在 `<el-form>` 上加 `@submit.prevent`(第 37 行),拦掉原生提交;回车仍通过 `@change` 触发 `handleQuery`,页面不再重载。Vue 3 事件透传能正确作用到原生 form。
+

+ 283 - 0
.workbuddy/memory/2026-08-06.md

@@ -0,0 +1,283 @@
+# 2026-08-06
+
+## elderly/apply/nine-precautions/risk-disclosure-statement/Form.vue — secondElderSign 字符串/数组互转
+
+用户要求:用 `TableUpload` 的 `secondElderSign` 字段,提交时转成字符串、回显时转成数组(参照 check-in ProcessForm.vue 的做法)。
+
+背景:`TableUpload`(`src/components/UploadFile/src/TableUpload.vue`)的 `modelValue` 必须是数组 `FileItem[]`(内部 `fileList = props.modelValue || []`,并 `push`/`splice`),而后端以 JSON 字符串存储该字段。原 `open()` 用 `Object.assign(dataForm, res)` 直接把后端字符串塞进 `secondElderSign`,编辑/查看时组件会拿到字符串而崩。
+
+改动:
+- 新增两个辅助函数 `toFileArray(val)`(字符串→数组,带 Array.isArray/JSON.parse 兜底)与 `toFileString(val)`(空数组→'',否则 JSON.stringify)。
+- `open()`:`Object.assign` 后加 `dataForm.secondElderSign = toFileArray(res.secondElderSign)`(回显转数组)。
+- `submitForm()`:`submitData` 加 `secondElderSign: toFileString(dataForm.secondElderSign)`(提交转字符串)。
+- 因 `reactive` 里 `secondElderSign: []` 被推成 `never[]`,在声明处和 `resetForm` 处改为 `[] as any[]`,消除 TS2322。
+
+类型检查:引入的 TS2322 已消除。Form.vue 仍剩 441/819 两处 TS6133(未使用变量 updateRiskCheckboxes / nextAssessDate),为改动前既有的,与本次无关、不影响运行。
+
+## elderly/apply/nine-precautions/risk-disclosure-statement/index.vue — 导出 secondElderSign 为图片
+
+用户要求:导出(handleImportCard / ButtonImport)时把 `secondElderSign`(后端存 JSON 字符串,形如 `[{fileUrl, fileName, fileSize, createTime}]`,存的是 OSS 图片 URL)导出成真实图片嵌入 Excel。
+
+根因/决策:原导出用 `exportWithExpandedObjectArrays`(`src/utils/excel-export.ts`,底层 SheetJS/xlsx 社区版)**无法往 Excel 嵌图片**,且原代码压根没导出该字段。改用项目已有的 `exceljs`(^4.4.0,已在 node_modules;`social-worker/floor-activity-record/photo/index.vue` 已有可复用的「ExcelJS + addImage + fetch 二进制」范式)。
+
+改动:
+- 移除 `exportWithExpandedObjectArrays` 导入,新增 `import ExcelJS from 'exceljs'`、`import { getAccessToken } from '@/utils/auth'`、`import { saveAs } from 'file-saver'`。
+- 重写 `handleImportCard`:拉取列表后交给新函数 `exportRiskDisclosureToExcel(exportList)`。
+- 新增 `exportRiskDisclosureToExcel`:建 workbook/worksheet,18 个文本列 + 动态「二次告知签名」图片列(列数=所有行签名图最大数量,无图则不生成图片列);逐行 addRow,对每行 `secondElderSign` 用 `parseFileList` 解析出 fileUrl 数组,fetch 成 ArrayBuffer 后 `workbook.addImage({buffer, extension})` + `worksheet.addImage(imageId, {tl, ext:{width:90,height:90}})` 嵌入;失败则单元格写「图片加载失败」。
+- 新增 `parseFileList`(JSON 字符串/数组→`{fileUrl,fileName}[]`,带兜底)、`fetchImageAsBuffer`(复用 photo 页写法,fetch 带 `Authorization: Bearer token`)、`getImageExtension`(png→png,其余→jpeg;注意 ExcelJS 不支持 'jpg' 字面量,必须映射到 'jpeg')。
+
+类型检查:引入的 `getImageExtension` 返回类型含 'jpg' 导致 TS2322,已改为返回 `'png' | 'jpeg'` 修正。index.vue 其余报错(模板 el-tag :type 返回 string、handleEdit/handleView 多传参、若干 TS6133 未用变量)均为 385 行之前的既有问题,与本次无关。
+
+## data-board/nine-safety-measures/index.vue — 对接真实看板接口(前两个对象)
+
+用户要求:后台接口 `nineRiskBoardGet`(`src/api/data-board/nine-safety.ts` 第一个接口)已写好,先对接返回的 `overview` 和 `itemRiskDistributions` 两个对象,其余字段后续再对接。
+
+改动:
+- `loadAllData`:
+  - 在参数里补 `tenantIds`(从 `userStore.orgTenantId` 取,数组化后传入)。
+  - 把 `getNineSafetyOverview` 和 `getNineRiskDistribution` 两个 mock 调用合并成一次真实请求 `echartsApi.nineRiskBoardGet(params)`。
+  - 其余 11 个 mock 接口继续保留(月度趋势、热力图、散点、箱线图、签署状态、红名单、评估人工作量、护理级别、相关性、雷达、3D 柱状图)。
+- 字段映射:
+  - `overview`:
+    - `disclosureTotal` → `totalElders`(知情书总数)
+    - `highRiskTimes` → `highRiskCount`(高风险总人次)
+    - `highRiskTimesRatio` → `highRiskRatio`(高风险人次占比 %)
+    - `disclosureUnsignedCount` → `pendingSignatures`(待签署知情书)
+    - `100 - disclosureUnsignedRatio` → `signedRate`(签署率 %)
+    - `assessCompleteRate` → `evalCompletionRate`(评估完成率 %)
+    - `assessorCount` → `assessorCount`(评估人员数)
+  - `itemRiskDistributions`:
+    - `itemName` → `name`
+    - `highCount` → `high`
+    - `middleCount` → `medium`
+    - `lowCount` → `low`
+    - `unknownCount` → `none`
+    - `totalCount` → `total`
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — highRiskHeatMap 结构变更适配
+
+用户反馈:`highRiskHeatMap` 字段结构有变化(顶部加了楼栋/楼层筛选后),`cells` 改为对象数组。
+
+新结构:
+- `items`:`{code: itemCode, name: 九防名}`
+- `builds`:`{code: buildId, name: 楼栋名}`
+- `cells`:`{buildId, buildName, itemCode, itemName, highRiskElderCount, maxValue}`
+
+改动:
+- 热力图映射不再依赖隐式 `x/y` 下标,改为通过 `itemCode` / `buildId` 在 `items` / `builds` 里查找索引。
+- `heatCells` 过滤掉找不到对应索引的异常单元格。
+- 顺带把 `maxValue` 存到 `floorHeatmap` / `bar3D` 对象里,供后续色阶归一化使用(当前渲染仍用自身计算的最大值,兼容安全)。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 对接真实看板接口(剩余全部字段)
+
+用户发了 12 张接口字段截图,要求把剩余字段全部对接。
+
+改动:
+- 新增 `riskLevelRatios` ref,`renderOverallPie` 改为从该 ref 读取(不再从 `nineRiskDist` 聚合)。
+- `loadAllData`:
+  - 只保留 `getRiskRadar` 一个 mock 调用(真实接口未提供"护理级别 × 九防高危占比"雷达图明细)。
+  - 其余 10 个 mock 接口全部移除,统一从 `nineRiskBoardGet` 一次返回的数据里映射。
+- 字段映射:
+  - `riskLevelRatios` → `renderOverallPie`:`riskLevelName/riskLevel` → `name`,`count` → `value`,按 `riskLevel` 映射颜色。
+  - `monthlyTrends` → `riskTrend`:`month/highCount/middleCount/lowCount/unknownCount` → `month/high/medium/low/none`。
+  - `highRiskHeatMap` → `floorHeatmap` / `bar3D`:items → risks,builds → floors,cells → `[buildIndex, itemIndex, value]`。
+  - `highRiskElders` → `ageScatter`:`elderName/elderAge/elderSex/highRiskCount/nurseLevelName` → `name/age/sex/highCount/nurseLevel`。
+  - `itemScoreDistributions` → `scoreDist`:真实返回 buckets,按 count 累计计算五数概括(min/q1/median/q3/max)供箱线图使用。
+  - `disclosureSignRate` → `signature`:`signedCount/unsignedCount/total/signRate` → `signed/unsigned/total/signedRate`,`firstSignedOnly` 给 0。
+  - `nurseLevelHighRiskElderCounts` → `nurseLevelRisk`:`nurseLevelName/count` → `name/high`。
+  - `assessorStats` → `assessorWorkload`:`assessor/assessedElderCount/highRiskElderCount` → `name/count/highCount`。
+  - `multiHighRiskElders` → `redList`:`elderId/elderName/elderSex/elderAge/bedName/buildName/nurseLevelName/assessor/highRiskItems/highRiskCount/assessDate` → 页面字段;`highRiskItems` 按逗号拆分并去掉"防"字。
+- 模板:红名单表格"评估日期"列从写死的 `2022-12-12` 改为 `r.assessDate`。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — highRiskHeatMap 结构变更适配
+
+用户反馈:`highRiskHeatMap` 字段结构有变化(顶部加了楼栋/楼层筛选后),`cells` 改为对象数组。
+
+新结构:
+- `items`:`{code: itemCode, name: 九防名}`
+- `builds`:`{code: buildId, name: 楼栋名}`
+- `cells`:`{buildId, buildName, itemCode, itemName, highRiskElderCount, maxValue}`
+
+改动:
+- 热力图映射不再依赖隐式 `x/y` 下标,改为通过 `itemCode` / `buildId` 在 `items` / `builds` 里查找索引。
+- `heatCells` 过滤掉找不到对应索引的异常单元格。
+- 顺带把 `maxValue` 存到 `floorHeatmap` / `bar3D` 对象里,供后续色阶归一化使用(当前渲染仍用自身计算的最大值,兼容安全)。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 雷达图改用 nurseLevelHighRiskRecordCounts
+
+用户要求:「各护理级别 · 九防高危占比雷达」使用 `nurseLevelHighRiskRecordCounts` 数组。
+
+背景:该数组只有 `{nurseLevelId, nurseLevelName, count, ratio}`,没有按九防拆分。原 mock 雷达图是「护理级别 × 九防」矩阵,真实数据无法直接还原该形态。
+
+改动:
+- `loadAllData`:移除最后一个 mock 调用 `getRiskRadar`,`nineRiskBoardGet` 成为唯一数据源。
+- 护理级别数据映射:从 `bd.nurseLevelHighRiskRecordCounts` 读取,给 `nurseLevelRisk` 增加 `count`/`ratio`,保留 `high`(=count)供漏斗图继续使用。
+- `renderRiskRadar`:改为读 `nurseLevelRisk`;indicator = 各护理级别名称;series 两个:「高危占比(%)」用真实 ratio,「记录数(归一化)」用 `count/maxCount*100` 避免数量级失真;tooltip 显示真实记录数和占比。
+- 删除 `riskRadar` ref。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — highRiskHeatMap 结构变更适配
+
+用户反馈:`highRiskHeatMap` 字段结构有变化(顶部加了楼栋/楼层筛选后),`cells` 改为对象数组。
+
+新结构:
+- `items`:`{code: itemCode, name: 九防名}`
+- `builds`:`{code: buildId, name: 楼栋名}`
+- `cells`:`{buildId, buildName, itemCode, itemName, highRiskElderCount, maxValue}`
+
+改动:
+- 热力图映射不再依赖隐式 `x/y` 下标,改为通过 `itemCode` / `buildId` 在 `items` / `builds` 里查找索引。
+- `heatCells` 过滤掉找不到对应索引的异常单元格。
+- 顺带把 `maxValue` 存到 `floorHeatmap` / `bar3D` 对象里,供后续色阶归一化使用(当前渲染仍用自身计算的最大值,兼容安全)。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 评估得分分布箱线图数据映射修正
+
+用户反馈:「评估得分分布(箱线图)」渲染异常,大部分项目被拉成 0~100 的细长线条,只有防跌倒、防噎食等少数项目能看出箱子。
+
+根因:后台 `itemScoreDistributions` 返回了真实的 `minScore / maxScore`,也返回了 `buckets`(分桶统计)。原映射用 `buckets[0].min` 和 `buckets[last].max` 作为箱线图的 min/max,导致每个项目的上下须都钉死在 0 和 100,箱子被压扁;四分位数也只用桶下边界,没有插值。
+
+改动:
+- min/max 改为优先使用后台真实 `minScore / maxScore`,缺失时才回退到桶边界。
+- 四分位数 `q1 / median / q3` 在桶内做线性插值,而不是直接取桶下边界。
+- 空桶跳过,避免异常点干扰累计。
+- 五数概括统一保留 2 位小数,避免浮点精度导致十几位小数。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 按护理级别 · 高危项漏斗字段修正
+
+用户反馈:「按护理级别 · 高危项漏斗」图表为空,后台真实字段是 `nurseLevelHighRiskElderCounts`。
+
+根因:`loadAllData` 里护理级别数据仍从 `bd.nurseLevelHighRiskRecordCounts` 读取,与后台实际返回字段不一致,导致 `nurseLevelRisk` 为空,漏斗图没有数据。
+
+改动:
+- `nurseLevelRisk` 改为优先读取 `bd.nurseLevelHighRiskElderCounts`,同时兼容旧名 `bd.nurseLevelHighRiskRecordCounts` 作为 fallback。
+- 漏斗图和雷达图共用 `nurseLevelRisk`,结构保持 `{name, count, ratio, none, low, medium, high}` 不变。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 雷达图适配 nurseLevelHighRiskRecordCounts 新结构
+
+用户反馈:`nurseLevelHighRiskRecordCounts` 数据结构已变化,需要按新结构重新渲染「各护理级别 · 九防高危占比雷达」。
+
+新结构:
+```js
+[
+  {
+    nurseLevelId,
+    nurseLevelName,
+    ratio,
+    totalCount,
+    items: [
+      { itemCode, itemName, count, ratio }
+    ]
+  }
+]
+```
+
+改动:
+- 新增 `nurseLevelRadarData` ref,结构为 `{ indicators: [{name, max}], series: [{name, value, count, lineStyle, itemStyle, areaStyle}] }`。
+- `loadAllData`:从 `bd.nurseLevelHighRiskRecordCounts` 解析雷达数据;indicator 取第一条护理级别的 `items.itemName`;每个护理级别作为一条 radar series,value 为各九防项的 `ratio`(保留 2 位小数),count 为原始人数供 tooltip 使用。
+- `renderRiskRadar`:改为读取 `nurseLevelRadarData`;indicator = 九防项目名称;legend 显示各护理级别名称;tooltip 展示该护理级别下每个九防项的占比和人数。
+- 漏斗图继续使用 `nurseLevelRisk`(来自 `nurseLevelHighRiskElderCounts`),与雷达图数据源解耦。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 评估人工作量统计 X 轴防拥挤
+
+用户反馈:「评估人工作量统计」图表 X 轴标签太多、互相重叠。
+
+改动:
+- `renderAssessorWorkload`:
+  - `grid.bottom` 从 30 加大到 90,给旋转标签留出垂直空间。
+  - `xAxis.axisLabel.rotate` 从 40 度加大到 60 度,`fontSize` 从 11 降到 10,`margin` 从 8 加到 12。
+  - 新增 `dataZoom` slider,默认展示前 12 条(或 40%,取较小者),底部出现横向滚动条,可拖动查看其余评估人。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 修复 3D 楼栋图 tooltip 显示 undefined
+
+问题:3D 楼栋×九防高危人数图表 hover 时 tooltip 显示 `undefined - undefined`,`高危人数:undefined 人`。
+
+根因:ECharts `bar3D` 的 tooltip formatter 参数里,数据坐标/值位于 `params.value`,而 `params.data` 是 `{ value: [...] }` 对象。原代码用 `p.data[0]` / `p.data[2]` 访问,拿到的是 undefined。
+
+改动:`render3DBar` 的 tooltip formatter 改为从 `p.value` 读取 `[楼栋索引, 九防索引, 高危人数]`,并用 `floors` / `risks` 数组反查名称;同时加 `?? '-'` 兜底,避免极端情况再次显示 undefined。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 红名单九防筛选生效 + 评估日期格式化
+
+问题:
+1. 重点人群红名单的九防风险下拉筛选没有过滤效果。
+2. 评估日期显示为数组形式 `[2026,7,28]`,需要格式化为正常日期。
+
+根因:
+- 之前 `nineRiskOptions` 的 value 已去掉"防"前缀,但 `redList.risks` 存储格式与下拉 value 的对应关系存在歧义,导致筛选不直观;同时 `el-select` 的 `clearable` 可能让空值判断不够健壮。
+- `assessDate` 后台直接返回了 `[年,月,日]` 数组,原样渲染到表格。
+
+改动:
+- 统一使用带"防"前缀的风险名称:`redList.risks` 保留原始值(不再 `.replace('防', '')`),`nineRiskOptions` 的 value 也使用 `r.name`。
+- 风险标签颜色计算时去掉"防"前缀以匹配 `riskTagColor` 的 key:`riskTagColor(rk.replace('防', ''))`。
+- 新增 `formatAssessDate` 工具函数,支持数组、Date、字符串,统一输出 `yyyy-MM-dd`。
+- `redList` 映射时 `assessDate` 使用 `formatAssessDate(e.assessDate)`。
+- 模板中评估日期直接显示 `r.assessDate`(函数已返回 '-' 兜底)。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 红名单风险筛选仍显示 0 人
+
+问题:选择"防噎食"后,红名单显示"共 0 人",但数据中应包含该风险项的长者。
+
+根因:
+- `e.highRiskItems` 可能以数组形式返回,原代码只按字符串 `.split(',')` 解析,导致数组被序列化为 `"防噎食,防压疮"` 这样的单一项。
+- 即使解析正确,精确匹配 `===` 对空格、前缀不一致等边界情况不够健壮。
+
+改动:
+- `redList.risks` 解析改为同时支持数组和字符串:数组逐项 `String().trim()`,字符串按逗号拆分。
+- `filteredRedList` 匹配逻辑增强:
+  - 精确相等 `s === redRiskFilter.value`
+  - 去掉"防"前缀后相等 `s.replace('防', '') === keyword`
+  - 互相包含匹配 `s.includes(redRiskFilter.value) || redRiskFilter.value.includes(s)`
+- 空字符串风险项会被过滤掉。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 简易精神状态映射为防误食
+
+问题:后端把「防误食」这条风险命名为「简易精神状态」,导致红名单选「防误食」过滤不出数据,且页面各图表会显示「简易精神状态」。
+
+改动:
+- 新增 `normalizeRiskName` 工具函数:输入 `"简易精神状态"` 时返回 `"防误食"`,其他名称原样返回。
+- 在 `loadAllData` 中所有涉及九防项目名称的地方统一应用该映射:
+  - `nineRiskDist`(九防风险等级分布)
+  - `heatItems`(热力图/3D 图的九防轴标签)
+  - `scoreDist`(评估得分分布箱线图)
+  - `nurseLevelRadarData`(各护理级别雷达 indicator)
+  - `redList.risks`(红名单高风险项标签与下拉筛选)
+- 红名单下拉选项本身来自 `NINE_RISKS`,已固定为「防误食」,映射后筛选可正常命中。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## ContractBodyHubei.vue — 按《湖北省养老机构服务合同(2024版)》.docx 重写正文
+
+用户要求:以桌面上的湖北 2024 标准合同 docx 为准,改造 `src/views/elderly/contracts/components/ContractBodyHubei.vue`,保留原组件设计模式(a4-page / section-title / content-text / FieldItem / SignatureItem)与功能,仅替换正文内容。
+
+关键点(用户特别强调):
+- 费用章节**保持动态生成**,不要写死:monthlyExpenses / oneTimeExpenses / stageExpenses 三块列表渲染原样保留(template 第 1418/1471/1562 行附近)。
+- 争议解决条款误用 accountName/bank 字段,已改为独立字段 disputeCourt / disputeArbitration。
+
+新增字段(contractForm):partyAEmail、partyAContactPhone、elderCensusAddr、elderPhone、elderEmail、roomFloor、bedNo、trialDays(默认7)、trialStart/End、renewDays、emergencyContact*、memberFeeAmt/Cn/RefundMode、penaltyPercent、disputeMode/Court/Arbitration、copyCount。
+- fillContractForm / parse 回填均补了映射,memberFeeAmt 加中文大写 watch(同 deposit)。
+- 正文覆盖:使用说明、甲乙丙方信息、第一条~第十四条(服务内容与方式、费用、期限与变更、护理等级、甲方/乙方权利义务、丙方责任、信息保护、保密、违约责任、免责、争议解决、合同生效附件)。
+- 验证:ESLint 0 错误;vue-tsc 过滤 ContractBodyHubei 无报错。

+ 129 - 0
.workbuddy/memory/2026-08-07.md

@@ -0,0 +1,129 @@
+# 2026-08-07 工作日志
+
+## data-board/nine-safety-measures/index.vue — 修复 getMonthList 报错与红名单过滤
+
+问题:
+1. 控制台报错 `echartsApi.getMonthList is not a function`,阻塞页面后续加载。
+2. 红名单 `highRiskItems` 中后端仍返回「简易精神状态」,选择「防误食」后过滤为空。
+
+改动:
+- 月份筛选相关代码已整体移除:`filter.month`、`monthOptions` ref、`onMounted` 中的 `echartsApi.getMonthList()` 调用、`handleReset` 中的 `filter.month = ''`。月份下拉在模板中已不存在,属于残留代码。
+- 增强 `normalizeRiskName`:对 trim 后的名称做精确或包含匹配,把「简易精神状态」统一映射为「防误食」。
+- 增强 `filteredRedList`:过滤时对每个 risk 再次调用 `normalizeRiskName`,并同时支持带「防」前缀、去前缀、互相包含等多种匹配方式,避免后端字段不统一导致过滤失败。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 红名单数据源头替换「简易精神状态」
+
+用户要求:不要复杂的过滤兼容逻辑,直接把 `multiHighRiskElders[i].highRiskItems` 字符串里的「简易精神状态」替换成「防误食」。
+
+改动:
+- `redList` 映射中,`highRiskItems` 先整体 `replace(/简易精神状态/g, '防误食')`,再按逗号拆分,不再依赖 `normalizeRiskName` 逐元素映射。
+- `filteredRedList` 恢复为简单的 `r.risks.includes(redRiskFilter.value)`,因为数据源头已经统一为「防误食」。
+- `normalizeRiskName` 仍保留,继续用于图表等其他接口返回的九防名称映射。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## data-board/nine-safety-measures/index.vue — 红名单筛选"只有全部有数据"根因
+
+现象:选「全部」有数据,选任何具体九防风险都 0 条。
+
+根因:后端 `highRiskItems` 字符串里风险项的分隔符不是 ASCII 逗号,而是**顿号「、」或全角逗号「,」**。原 `.split(',')` 拆不开,整串变成一个数组元素,导致数组 `includes('防误食')` 恒为 false(数组里只有一个长串 ≠ '防误食')。这也解释了上一版 normalize 逐元素版本同样失败。
+
+改动:
+- `risks` 解析 `split` 改为正则 `/[,,、\s]+/`,支持中英文逗号、顿号、空白统一拆分。
+- `filteredRedList` 改为宽松匹配:精确 / 去「防」前缀 / 子串包含,进一步兜底命名差异。
+
+类型检查:NO_NINE_SAFETY_ERRORS。
+
+## elderly/contracts — 合同主体增加合同类型下拉与湖北合同组件
+
+需求:
+- 合同主体分隔栏左侧文字、右侧增加「合同类型」下拉:广州地区合同 / 湖北地区合同 / 天河老人院合同(默认广州地区合同)。
+- 选中类型随首次保存写入 `contractFormUpdate.value.contractInfo`(JSON)。
+- 下次打开详情时解析该字段,据此渲染不同合同组件。
+- 复制一份合同组件作为「湖北合同」。
+
+改动:
+- `components/ContractBody.vue`:
+  - `contractForm` reactive 新增 `contractType: '广州地区合同'`(首字段)。
+  - 新增 prop `contractType?: string` 和 emit `'update:contractType'`。
+  - 新增 watch:父级下拉变化时同步到 `contractForm.contractType`。
+  - `fetchContractData` 末尾 emit `update:contractType`(用 `contractForm.contractType || '广州地区合同'`)。
+  - 保存(`saveContract` 与无 contractInfo 的兜底分支)均 `JSON.stringify({ ...contractForm })`,自动包含 contractType。
+- 新建 `components/ContractBodyHubei.vue` = ContractBody.vue 的副本(含上述 contractType 支持)。
+- `ContractManageForm.vue`:
+  - 新增 `contractType` ref(默认'广州地区合同')与 `contractTypeOptions`。
+  - `合同主体` 分隔栏改为 flex:左标题、右 `el-select`,新增 `.contract-type-bar` 等样式。
+  - `ContractBody` / `ContractBodyHubei` 用 `v-if="contractType !== '湖北地区合同'"` 条件渲染,二者共用 `ref="contractBodyRef"`,都接收 `:contract-type` 并监听 `@update:contract-type` 回写。
+  - 打印 Teleport 容器内同样条件渲染两个组件。
+  - `open()` 中重置 `contractType` 为默认,避免上一次选择残留;实际类型由详情加载后子组件回传再切换。
+
+注意:ContractBody.vue 中 `contractForm.attachments`、`contractFormUpdate.value.bedName/contractInfo` 等为既有类型告警(非本次引入),ContractBodyHubei.vue 因复制同样存在;本次 contractType 改动未引入任何新类型错误。
+
+## elderly/contracts — 修复合同组件运行时初始化报错
+
+报错:
+1. `ReferenceError: Cannot access 'contractForm' before initialization`(ContractBody.vue:91)
+2. `TypeError: Cannot read properties of null (reading 'emitsOptions')`(ContractManageForm.vue:428)
+
+根因:上一版在 `ContractBody.vue` 顶部添加了 `watch(() => props.contractType, ..., { immediate: true })`,回调里访问了 `contractForm.contractType`,但 `contractForm` 的 `reactive(...)` 声明在文件更下方。`<script setup>` 中变量存在暂时性死区(TDZ),`immediate: true` 的 watch 在 setup 执行期间立即触发,导致 `contractForm` 还未初始化就被访问。
+
+第二个 `emitsOptions` 错误是 Vue 在子组件 setup 失败后内部更新逻辑抛出的连锁错误。
+
+改动:
+- 在 `ContractBody.vue` 和 `ContractBodyHubei.vue` 中,把该 `watch` 块从 `contractForm` 声明之前移到 `contractForm` reactive 声明之后(即 `stageExpenses` 字段下方)。
+- 保持 `immediate: true` 逻辑不变:父组件下拉切换合同类型时仍同步写入本地表单。
+
+类型检查:本次修复未引入新类型错误;剩余告警均为原文件既有问题。
+
+## elderly/contracts — 修复合同类型切换回弹 + 二次修改确认
+
+现象:
+1. 默认广州合同 → 切湖北并保存 → 重开显示湖北 → 再选广州,会自动跳回湖北。
+2. 用户希望二次修改(已有保存记录)时弹确认框,确认后再保存。
+
+根因:子组件 `fetchContractData` 末尾 `emit('update:contractType', 存储的类型)`,且父组件 `@update:contract-type="contractType = $event"` 无差别覆盖。手动切换后 v-if 重挂载子组件,又会回传存储的旧类型,把下拉框拽回旧值(回弹)。
+
+改动:
+- `ContractManageForm.vue`:
+  - 新增 `savedContractType`(初次解析出的已保存类型)、`hasSavedContract`(是否已存在保存记录)、`contractTypeResolved`(是否已解析)三个 ref。
+  - 新增 `handleContractTypeEmit`:仅当 `!contractTypeResolved` 时接受子组件回传类型并解析,其余回传忽略(消除手动切换重挂载导致的回弹)。
+  - 新增 `handleContractExists(exists)`:记录合同是否已存在。
+  - 新增 `handleContractTypeChange`(async):下拉 `@change` 触发;若 `hasSavedContract` 且类型与 `savedContractType` 不同,弹 `ElMessageBox.confirm`;确认则把 `savedContractType` 更新为新选择,取消则把 `contractType` 回退到 `savedContractType`。首次创建(无保存)不弹框。
+  - `open()` 重置三个状态;两个 ContractBody/ContractBodyHubei(含打印 Teleport 内)均绑定 `:lock-contract-type="contractTypeResolved"` 并改 `@update:contract-type`/`@update:contract-exists` 到新 handler。
+  - import `ElMessageBox` from 'element-plus'。
+- `components/ContractBody.vue` 与 `ContractBodyHubei.vue`:
+  - defineEmits 增加 `update:contractExists`;props 增加 `lockContractType?: boolean`。
+  - `fetchContractData` 末尾:若 `props.lockContractType && props.contractType` 则 `contractForm.contractType = props.contractType`(手动切换时用父组件选中值,保证保存值正确);emit `update:contractExists`(依据 `contractFormUpdate.value.contractInfo` 是否存在);再 emit `update:contractType`。
+
+注意:初次加载(lock=false)用存储类型驱动下拉;手动切换(lock=true)用父组件选中值并忽略旧回传,避免回弹且保证保存为新类型。类型检查无新增错误。
+
+## elderly/fee/comprehensive-setting — 合同区域配置改为 value2 字段并放到合同模块配置内
+
+需求:合同默认区域配置项放在「合同模块配置」卡片内部,而不是独立成新的卡片;字段使用 `value2` 而不是 `orgType`。
+
+改动:
+- `src/views/elderly/fee/comprehensive-setting/index.vue`:
+  - 将「合同区域设置」作为第二个 child 放在「合同过期通知设置」同一个 `children` 数组内。
+  - 把标识字段从 `orgTypeField` 改为 `value2Field`。
+  - `handleEdit` 中:保存时 `value2Field` 项传 `value: ''`、`value2: c.value`。
+  - `getList` 中:回显时 `value2Field` 项读取 `r.value2`。
+- `src/views/elderly/contracts/ContractManageForm.vue`:
+  - `loadContractTypeConfig` 读取 `config.value2` 作为 `defaultContractType`。
+
+类型检查:`comprehensive-setting/index.vue` 与 `ContractManageForm.vue` 无新增错误;子组件剩余告警均为既有问题。
+
+## elderly/contracts — 修复保存时 "[object Object] is not valid JSON" 控制台报错
+
+现象:点击保存合同,控制台报 `SyntaxError: "[object Object]" is not valid JSON`(ContractBody.vue:820),但保存成功。
+
+根因:`saveContract` 中遍历 `props.attachmentIds` 时对每个 `row` 直接 `JSON.parse(row)`。当 `row` 已经是对象而非字符串时,`JSON.parse` 会把它隐式转成 `"[object Object]"` 再解析,导致 SyntaxError。该错误被内层 try-catch 捕获并 console.log,不影响外层保存流程,所以保存仍能成功。
+
+改动:
+- `src/views/elderly/contracts/components/ContractBody.vue`:
+  - 第 811 行改为 `const data = typeof row === 'string' ? JSON.parse(row) : row`,对象直接复用。
+- `src/views/elderly/contracts/components/ContractBodyHubei.vue`:
+  - 同步上述修复。
+
+类型检查:未引入新类型错误;剩余告警均为原文件既有问题。

+ 29 - 0
.workbuddy/memory/2026-08-10.md

@@ -0,0 +1,29 @@
+# 2026-08-10
+
+## ProcessForm.vue — 出生日期回显为空(负数时间戳)
+
+问题:`TgDatePicker`(`src/components/ToggleForm/src/TgDatePicker.vue`)设置了 `value-format="YYYY-MM-DD"`,要求 `modelValue` 为对应格式的字符串。后端返回的 `birthday` 是毫秒时间戳(如 `-1086249600000`,小于 1970 年为负数),`ProcessForm.vue` 里 `init()` / `open()` 直接用 `Object.assign(dataForm, res)` 把负数时间戳塞进 `dataForm.birthday`,导致日期选择器无法解析、回显为空。
+
+修复:在 `init()` 和 `open()` 中 `Object.assign` 后加一行格式化:
+```js
+dataForm.birthday = res.birthday ? formatTime(res.birthday, 'yyyy-MM-dd') : ''
+```
+与 `getInfo()` 身份证解析返回的 `'yyyy-MM-dd'` 字符串格式保持一致,也符合 `TgDatePicker` 的 `value-format`。
+
+验证:ESLint 0 错误;vue-tsc 过滤 ProcessForm 无报错。
+
+## ContractBody.vue / ContractBodyHubei.vue — 丙方信息残留上一份合同数据
+
+问题:用户反馈丙方「证件号码」上一个人有数据、下一个人没有时不会清除。检查 `fillContractForm` 发现以下字段都是「条件赋值」或「从未赋值」,上一份合同的数据会残留:
+- `partyCGenderText`、`partyCIdCard`、`partyCPapersType`、`partyCBirth`(仅在对应字段存在时才赋值)
+- `partyCZip`、`partyCCommAddr`、`partyCAccountName`、`partyCBank`、`partyCAccount`(从未从家属数据回填)
+- `partyCSign`、`partyCDate`(仅外部签名数据传入时才赋值)
+- 同类风险还包括乙方条件字段 `elderGenderText`/`elderIdCard`/`elderPapersType`/`elderBirth`、日期 `startDate`/`endDate`、费用 `totalFee`/`deposit*`、房间 `roomType`/`roomNo`。
+
+修复:在两个组件的 `fillContractForm` 开头统一清空可能残留的字段,再按原有逻辑重新回填。这样新合同缺少对应数据时,字段会回到空值而非保留旧值。
+
+涉及文件:
+- `src/views/elderly/contracts/components/ContractBody.vue`
+- `src/views/elderly/contracts/components/ContractBodyHubei.vue`
+
+验证:ESLint 0 错误;vue-tsc 过滤 ContractBody 无报错。

+ 0 - 35
debug-pdf-upload-unknown-error.md

@@ -1,35 +0,0 @@
-# Debug Session: PDF Upload UnknownErrorException
-
-## Session ID
-pdf-upload-unknown-error
-
-## Status
-[OPEN]
-
-## Symptom
-上传 PDF 文件时,前端提示"PDF 解析失败 UnknownErrorException",错误位置在 DocumentAttachmentBody.vue:85(catch 块)。
-
-## Environment
-- Browser DevTools Network 显示 `pdf.worker.mjs` 请求返回 304 Not Modified
-- 请求 URL: `http://localhost/pdf.worker.mjs`
-
-## Hypotheses
-1. `pdfjs-dist` worker 路径 `/pdf.worker.mjs` 与实际部署路径不匹配,导致 worker 加载后执行失败
-2. 当前 PDF 文件本身损坏或格式特殊,触发 pdfjs 解析异常
-3. pdfjs-dist 版本与 public 目录下的 `pdf.worker.mjs` 文件版本不一致
-4. Worker 跨域或 MIME 类型问题导致 pdfjs 无法正确实例化 worker
-5. 动态 import('pdfjs-dist') 时 GlobalWorkerOptions 未正确设置,导致 worker 未生效
-
-## Evidence
-- `node_modules/pdfjs-dist/package.json` 版本为 `4.10.38`
-- `public/pdf.worker.mjs` 原文件中 `workerVersion` 与 `pdfjsVersion` 为旧版本,与安装的 `pdfjs-dist` 不匹配
-- 版本不一致会导致 worker 与主库通信协议不兼容,从而抛出 `UnknownErrorException`
-
-## Fix Applied
-将 `node_modules/pdfjs-dist/build/pdf.worker.mjs` 复制到 `public/pdf.worker.mjs`,确保 worker 版本与主库完全一致(均为 `4.10.38`)。
-
-## Verification
-请重新上传 PDF 文件验证。如仍报错,请提供新的控制台错误信息。
-
-## Created At
-2026-07-20

+ 13 - 396
src/api/data-board/nine-safety.ts

@@ -1,3 +1,14 @@
+import request from '@/config/axios'
+
+// 获取九防安全风险综合看板数据
+export const nineRiskBoardGet = (params:any) => {
+  return request.get({
+    url: `/nine-risk-board/get`,
+    params: params
+  })
+}
+
+
 /**
  * 九防安全看板 - Mock API
  * 基于风险知情书 + 九防项目列表的数据结构,模拟后端返回的聚合统计数据。
@@ -5,7 +16,7 @@
  */
 
 // ========================= 基础配置 =========================
-const NINE_RISKS = [
+export const NINE_RISKS = [
   { key: 'asphyxiation', name: '防噎食' },
   { key: 'pressureUlcer', name: '防压疮' },
   { key: 'fall', name: '防跌倒' },
@@ -14,404 +25,10 @@ const NINE_RISKS = [
   { key: 'wandering', name: '防走失' },
   { key: 'selfHarm', name: '防自杀' },
   { key: 'foodDrug', name: '防误食' },
-  { key: 'entertainment', name: '防文娱意外' }
-]
-
-const FLOORS = ['1号楼', '2号楼', '3号楼', '4号楼', '5号楼', '6号楼']
-const NURSE_LEVELS = ['特级护理', '一级护理', '二级护理', '三级护理']
-const ASSESSORS = ['张护士', '李护士', '王护士', '赵护士', '刘护士', '陈护士']
-const LEVELS = ['none', 'low', 'medium', 'high']
-const ELDER_NAMES = [
-  '张大爷', '王奶奶', '李爷爷', '赵奶奶', '刘大爷', '陈奶奶', '杨爷爷', '黄奶奶',
-  '吴大爷', '周奶奶', '徐爷爷', '孙奶奶', '马大爷', '朱奶奶', '胡爷爷', '郭奶奶',
-  '何大爷', '高奶奶', '林爷爷', '罗奶奶', '郑大爷', '梁奶奶', '宋爷爷', '谢奶奶'
+  { key: 'entertainment', name: '防文娱活动意外' }
 ]
 
-// ========================= 简易随机数(带 seed,保证筛选一致性) =========================
-const seededRandom = (seed: number) => {
-  let s = seed
-  return () => {
-    s = (s * 9301 + 49297) % 233280
-    return s / 233280
-  }
-}
-
-const pick = <T>(arr: T[], rnd: () => number): T => arr[Math.floor(rnd() * arr.length)]
-const randInt = (min: number, max: number, rnd: () => number) =>
-  Math.floor(rnd() * (max - min + 1)) + min
-
-/** 根据筛选条件生成种子 */
-const makeSeed = (month?: string, buildId?: string | number, floorId?: string | number) => {
-  let seed = 20260724
-  if (month) seed += month.split('-').reduce((s, v) => s + parseInt(v), 0) * 31
-  if (buildId) seed += Number(buildId) * 7
-  if (floorId) seed += Number(floorId) * 13
-  return seed
-}
-
-// ========================= 模拟延迟 =========================
-const delay = <T>(data: T, ms = 200): Promise<T> =>
-  new Promise((resolve) => setTimeout(() => resolve(JSON.parse(JSON.stringify(data))), ms))
-
-// ========================= 生成原始长者数据 =========================
-const generateElders = (month?: string, buildId?: string | number, floorId?: string | number) => {
-  const rnd = seededRandom(makeSeed(month, buildId, floorId))
-  const targetFloors = buildId ? [FLOORS[Number(buildId) % FLOORS.length]] : FLOORS
-  // 选了楼层时人数减少(模拟单层数据)
-  const count = floorId ? randInt(15, 35, rnd) : randInt(60, 120, rnd)
-  const elders: any[] = []
-
-  for (let i = 0; i < count; i++) {
-    const fl = pick(targetFloors, rnd)
-    const roomNum = randInt(101, 320, rnd)
-    const bedNo = randInt(1, 6, rnd)
-    const name = pick(ELDER_NAMES, rnd) + (i % 3 === 0 ? `(${i})` : '')
-    const sex = rnd() > 0.45 ? 1 : 2
-    const age = randInt(65, 98, rnd)
-    const nurseLevel = pick(NURSE_LEVELS, rnd)
-    const assessor = pick(ASSESSORS, rnd)
-
-    // 九防风险数据
-    const riskData: any = {}
-    NINE_RISKS.forEach((r) => {
-      // 年龄越大、护理级别越高,风险越高
-      let levelWeight = 0.3
-      if (age > 85) levelWeight += 0.2
-      if (nurseLevel === '特级护理' || nurseLevel === '一级护理') levelWeight += 0.2
-      const roll = rnd()
-      let level: string
-      if (roll < levelWeight * 0.15) level = 'high'
-      else if (roll < levelWeight * 0.5) level = 'medium'
-      else if (roll < levelWeight) level = 'low'
-      else level = 'none'
-
-      riskData[r.key] = {
-        id: randInt(1, 9999, rnd),
-        level,
-        remark: { low: false, medium: false, high: false }
-      }
-    })
-
-    // 签署状态
-    const signed = rnd() > 0.25
-    const firstRiskDate = new Date(2026, randInt(0, 6, rnd), randInt(1, 28, rnd))
-
-    elders.push({
-      id: 10000 + i,
-      elderId: 19000 + i,
-      elderName: name,
-      elderSex: sex,
-      elderAge: age,
-      floorName: fl,
-      bedName: `${fl}-${roomNum}-${String(bedNo).padStart(2, '0')}`,
-      nurseLevelName: nurseLevel,
-      riskData: JSON.stringify(riskData),
-      firstRisk: firstRiskDate.toISOString().slice(0, 10),
-      signDate: signed ? firstRiskDate.toISOString().slice(0, 10) : null,
-      assessor,
-      assessScore: randInt(5, 30, rnd),
-      riskLevel: pick(LEVELS, rnd),
-      createTime: firstRiskDate.getTime(),
-      tenantId: 195
-    })
-  }
-
-  return elders
-}
-
-// ========================= 聚合统计接口 =========================
-
-/** 1. KPI 总览 */
-export const getNineSafetyOverview = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  let highCount = 0
-  let assessedItems = 0
-  let signedCount = 0
-  elders.forEach((e) => {
-    const rd = JSON.parse(e.riskData)
-    NINE_RISKS.forEach((r) => {
-      const node = rd[r.key]
-      if (node?.id) assessedItems++
-      if (node?.level === 'high') highCount++
-    })
-    if (e.signDate) signedCount++
-  })
-  const total = elders.length
-  const expected = total * 9
-  return delay({
-    totalElders: total,
-    assessedElders: total,
-    highRiskCount: highCount,
-    highRiskRatio: expected > 0 ? +((highCount / expected) * 100).toFixed(1) : 0,
-    signedCount,
-    pendingSignatures: total - signedCount,
-    signedRate: total > 0 ? +((signedCount / total) * 100).toFixed(1) : 0,
-    evalCompletionRate: expected > 0 ? +((assessedItems / expected) * 100).toFixed(1) : 0,
-    assessorCount: ASSESSORS.length
-  })
-}
-
-/** 2. 九防各项风险等级分布 */
-export const getNineRiskDistribution = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const result = NINE_RISKS.map((r) => {
-    const row = { name: r.name, key: r.key, none: 0, low: 0, medium: 0, high: 0, total: 0 }
-    elders.forEach((e) => {
-      const node = JSON.parse(e.riskData)[r.key]
-      if (node?.level && row.hasOwnProperty(node.level)) {
-        row[node.level]++
-        row.total++
-      }
-    })
-    return row
-  }).sort((a, b) => b.high - a.high)
-  return delay(result)
-}
-
-/** 3. 风险等级月度趋势(近 6 个月) */
-export const getRiskLevelTrend = (params?: { buildId?: string | number }) => {
-  const months: string[] = []
-  const now = new Date(2026, 6, 1) // 2026-07
-  for (let i = 5; i >= 0; i--) {
-    const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
-    months.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`)
-  }
-  const trend = months.map((m) => {
-    const elders = generateElders(m, params?.buildId)
-    const counts = { none: 0, low: 0, medium: 0, high: 0 }
-    elders.forEach((e) => {
-      const rd = JSON.parse(e.riskData)
-      NINE_RISKS.forEach((r) => {
-        const level = rd[r.key]?.level
-        if (level && counts.hasOwnProperty(level)) counts[level]++
-      })
-    })
-    return { month: m, ...counts }
-  })
-  return delay(trend)
-}
-
-/** 4. 楼栋 × 九防风险热力图 */
-export const getFloorRiskHeatmap = (params?: { month?: string }) => {
-  const floors = FLOORS
-  const risks = NINE_RISKS.map((r) => r.name)
-  const data: [number, number, number][] = []
-  floors.forEach((_, fi) => {
-    risks.forEach((_, ri) => {
-      const elders = generateElders(params?.month, FLOORS[fi])
-      let high = 0
-      elders.forEach((e) => {
-        const node = JSON.parse(e.riskData)[NINE_RISKS[ri].key]
-        if (node?.level === 'high') high++
-      })
-      data.push([fi, ri, high])
-    })
-  })
-  return delay({ floors, risks, data })
-}
-
-/** 5. 年龄-高风险散点图 */
-export const getAgeRiskScatter = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const data = elders
-    .map((e) => {
-      const rd = JSON.parse(e.riskData)
-      let high = 0
-      NINE_RISKS.forEach((r) => {
-        if (rd[r.key]?.level === 'high') high++
-      })
-      return {
-        name: e.elderName,
-        age: e.elderAge,
-        sex: e.elderSex,
-        highCount: high,
-        room: e.bedName,
-        nurseLevel: e.nurseLevelName
-      }
-    })
-    .filter((d) => d.highCount > 0)
-  return delay(data)
-}
-
-/** 6. 评估得分分布(箱线图) */
-export const getAssessScoreDistribution = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const result = NINE_RISKS.map((r) => {
-    const scores: number[] = []
-    elders.forEach((e) => {
-      const node = JSON.parse(e.riskData)[r.key]
-      if (node?.id) scores.push(randInt(5, 30, seededRandom(e.id + r.key.length)))
-    })
-    scores.sort((a, b) => a - b)
-    const q = (p: number) => {
-      if (scores.length === 0) return 0
-      const idx = Math.floor(scores.length * p)
-      return scores[idx]
-    }
-    return {
-      name: r.name,
-      min: scores[0] || 0,
-      q1: q(0.25),
-      median: q(0.5),
-      q3: q(0.75),
-      max: scores[scores.length - 1] || 0
-    }
-  })
-  return delay(result)
-}
-
-/** 7. 签署状态统计 */
-export const getSignatureStatus = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  let signed = 0
-  let firstSignedOnly = 0
-  let unsigned = 0
-  elders.forEach((e) => {
-    if (e.signDate) signed++
-    else if (e.firstRisk) firstSignedOnly++
-    else unsigned++
-  })
-  return delay({
-    signed,
-    firstSignedOnly,
-    unsigned,
-    total: elders.length,
-    signedRate: elders.length > 0 ? +((signed / elders.length) * 100).toFixed(1) : 0
-  })
-}
 
-/** 8. 高风险红名单 */
-export const getHighRiskRedList = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const list = elders
-    .map((e) => {
-      const rd = JSON.parse(e.riskData)
-      const risks: string[] = []
-      NINE_RISKS.forEach((r) => {
-        if (rd[r.key]?.level === 'high') risks.push(r.name.replace('防', ''))
-      })
-      return {
-        id: e.id,
-        name: e.elderName,
-        sex: e.elderSex === 1 ? '男' : '女',
-        age: e.elderAge,
-        room: e.bedName,
-        floor: e.floorName,
-        nurseLevel: e.nurseLevelName,
-        risks,
-        highCount: risks.length,
-        assessor: e.assessor
-      }
-    })
-    .filter((d) => d.highCount >= 2)
-    .sort((a, b) => b.highCount - a.highCount)
-  return delay(list)
-}
-
-/** 9. 评估人工作量 */
-export const getAssessorWorkload = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const map = new Map<string, { count: number; highCount: number }>()
-  elders.forEach((e) => {
-    if (!map.has(e.assessor)) map.set(e.assessor, { count: 0, highCount: 0 })
-    const item = map.get(e.assessor)!
-    item.count++
-    const rd = JSON.parse(e.riskData)
-    NINE_RISKS.forEach((r) => {
-      if (rd[r.key]?.level === 'high') item.highCount++
-    })
-  })
-  const result = Array.from(map.entries())
-    .map(([name, v]) => ({ name, count: v.count, highCount: v.highCount }))
-    .sort((a, b) => b.count - a.count)
-  return delay(result)
-}
-
-/** 10. 按护理级别统计风险 */
-export const getRiskByNurseLevel = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const result = NURSE_LEVELS.map((level) => {
-    const row = { name: level, none: 0, low: 0, medium: 0, high: 0 }
-    elders.filter((e) => e.nurseLevelName === level).forEach((e) => {
-      const rd = JSON.parse(e.riskData)
-      NINE_RISKS.forEach((r) => {
-        const lv = rd[r.key]?.level
-        if (lv && row.hasOwnProperty(lv)) row[lv]++
-      })
-    })
-    return row
-  })
-  return delay(result)
-}
 
-/** 11. 九防风险相关性矩阵 */
-export const getNineRiskCorrelation = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const risks = NINE_RISKS.map((r) => r.name)
-  const matrix: number[][] = []
-  for (let i = 0; i < NINE_RISKS.length; i++) {
-    matrix[i] = []
-    for (let j = 0; j < NINE_RISKS.length; j++) {
-      if (i === j) {
-        matrix[i][j] = 1
-      } else {
-        // 模拟相关性:同类型风险相关性较高
-        const baseCorr = Math.abs(i - j) <= 2 ? 0.4 : 0.15
-        const noise = (seededRandom(makeSeed(params?.month, params?.buildId, params?.floorId) + i * 9 + j)() - 0.5) * 0.3
-        matrix[i][j] = +Math.max(0, Math.min(1, baseCorr + noise)).toFixed(2)
-      }
-    }
-  }
-  return delay({ risks, matrix })
-}
 
-/** 12. 九防风险雷达图(各护理级别对比) */
-export const getRiskRadar = (params?: { month?: string; buildId?: string | number; floorId?: string | number }) => {
-  const elders = generateElders(params?.month, params?.buildId, params?.floorId)
-  const indicators = NINE_RISKS.map((r) => ({ name: r.name, max: 100 }))
-  const series = NURSE_LEVELS.map((level) => {
-    const data = NINE_RISKS.map((r) => {
-      let total = 0
-      let high = 0
-      elders.filter((e) => e.nurseLevelName === level).forEach((e) => {
-        const node = JSON.parse(e.riskData)[r.key]
-        if (node) {
-          total++
-          if (node.level === 'high') high++
-        }
-      })
-      return total > 0 ? Math.round((high / total) * 100) : 0
-    })
-    return { name: level, value: data }
-  })
-  return delay({ indicators, series })
-}
 
-/** 13. 3D 楼栋 × 九防 × 风险等级 */
-export const get3DRiskBar = (params?: { month?: string }) => {
-  const floors = FLOORS
-  const risks = NINE_RISKS.map((r) => r.name)
-  const data: [number, number, number][] = []
-  floors.forEach((_, fi) => {
-    risks.forEach((_, ri) => {
-      const elders = generateElders(params?.month, FLOORS[fi])
-      let high = 0
-      elders.forEach((e) => {
-        const node = JSON.parse(e.riskData)[NINE_RISKS[ri].key]
-        if (node?.level === 'high') high++
-      })
-      data.push([fi, ri, high])
-    })
-  })
-  return delay({ floors, risks, data })
-}
-
-/** 15. 月份列表(近 6 个月) */
-export const getMonthList = () => {
-  const months: string[] = []
-  const now = new Date(2026, 6, 1)
-  for (let i = 5; i >= 0; i--) {
-    const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
-    months.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`)
-  }
-  return delay(months)
-}

+ 2 - 2
src/components/UploadFile/src/TableUpload.vue

@@ -11,8 +11,8 @@
             v-if="isImageFile(row.fileUrl)"
             :src="row.fileUrl"
             :preview-src-list="[row.fileUrl]"
-            fit="cover"
-            style="width: 40px; height: 40px; cursor: pointer"
+            fit="contain"
+            style="width: 100%; height: 42px; cursor: pointer;display: flex;align-items: center;justify-content: center;"
           />
           <Icon v-else icon="ep:document" style="font-size: 20px; color: #909399" />
         </template>

+ 7 - 9
src/components/business-components/selectElder.vue

@@ -14,7 +14,7 @@
     <el-option v-for="item in list" :key="item.id" :label="formatLabel(item)" :value="item.id">
       <div class="flex items-center">
         <span>{{ item.elderName }}</span>
-        <span>{{ item.idCard }}</span>
+        <span>{{ item.idCard }}{{ (item.inStatus||0) == 2 ? ' - 已退住' : '' }}</span>
       </div>
     </el-option>
   </el-select>
@@ -100,8 +100,8 @@ const toggleTypeFmt = computed(() => {
   return formToggleType.value || false
 })
 
-const upData = async (str,id?:any, triggerEvent = true) => {
-  if(id && id!='NaN'){
+const upData = async (str, id?: any, triggerEvent = true) => {
+  if (id && id != 'NaN') {
     try {
       const res = await getElderInfoById(id)
       inputValue.value = res.id
@@ -109,12 +109,10 @@ const upData = async (str,id?:any, triggerEvent = true) => {
         emit('elder', res)
       }
       return res
-    }catch (e) {}
+    } catch (e) {}
   }
-
 }
 
-
 interface listType {
   id: number
   elderName: string
@@ -180,10 +178,11 @@ const getList = async () => {
         ? await getElderlyPage(queryParams)
         : await getElderlybedChangeRecordPage({
             ...queryParams,
-            identification: props.type,
+            identification: props.type
           })
     list.value = res.list
-  } catch (e) {}finally {
+  } catch (e) {
+  } finally {
     loading.value = false
   }
 }
@@ -225,7 +224,6 @@ const handleGetList = () => {
 }
 
 defineExpose({ upData })
-
 </script>
 <style scoped lang="scss">
 .flex {

+ 1 - 1
src/store/modules/setting.ts

@@ -47,7 +47,7 @@ export const useSettingStore = defineStore('setting', {
       }
       const item2 = res.find(item=>item.configName == '入院账单月度费用向上取整设置')
       if(item2){
-        this.admissionBillRoundUp = item2.value == '2' ? true : false
+        this.admissionBillRoundUp = item2.value == '2'
         this.admissionBill = item2.value
       }
       const item3 = res.find(item=>item.configName == '消费券抹零设置')

+ 1 - 1
src/views/bpm/task/process-list/index.vue

@@ -34,7 +34,7 @@
           </el-col>
         </el-row>
       </div>
-      <el-form v-else :inline="true" :model="queryParams" ref="queryFormRef" class="searchContent">
+      <el-form v-else :inline="true" :model="queryParams" ref="queryFormRef" class="searchContent" @submit.prevent>
         <el-row>
           <el-col :span="20">
             <el-input

+ 371 - 95
src/views/data-board/nine-safety-measures/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
 import echarts from '@/plugins/echarts'
 import 'echarts-gl'
 import {
@@ -12,11 +12,7 @@ import {
   SankeyChart,
   ThemeRiverChart
 } from 'echarts/charts'
-import {
-  DataZoomComponent,
-  BrushComponent,
-  RadarComponent
-} from 'echarts/components'
+import { DataZoomComponent, BrushComponent, RadarComponent } from 'echarts/components'
 import * as echartsApi from '@/api/data-board/nine-safety'
 import { getBuildList } from '@/api/system/badManage'
 import { useUserStore } from '@/store/modules/user'
@@ -40,11 +36,9 @@ echarts.use([
 
 // ========================= 筛选条件 =========================
 const filter = reactive({
-  month: '' as string,
   buildId: '' as string | number,
   floorId: '' as string | number
 })
-const monthOptions = ref<string[]>([])
 // 楼栋、楼层使用真实接口
 const buildList = ref<any[]>([])
 const floorList = ref<any[]>([])
@@ -58,16 +52,46 @@ const handleBuildChange = (buildId: any) => {
 const loading = ref(false)
 const overview = ref<any>({})
 const nineRiskDist = ref<any[]>([])
+const riskLevelRatios = ref<any[]>([])
 const riskTrend = ref<any[]>([])
 const floorHeatmap = ref<any>({})
 const ageScatter = ref<any[]>([])
 const scoreDist = ref<any[]>([])
 const signature = ref<any>({})
 const redList = ref<any[]>([])
+// 红名单按九防筛选:'' = 全部
+const redRiskFilter = ref<string>('')
+const nineRiskOptions = computed(() =>
+  [{ label: '全部', value: '' }].concat(
+    echartsApi.NINE_RISKS.map((r: any) => ({
+      label: r.name,
+      value: r.name
+    }))
+  )
+)
+const filteredRedList = computed(() => {
+  if (!redRiskFilter.value) return redList.value
+  const filterValue = redRiskFilter.value.trim()
+  const keyword = filterValue.replace(/^防/, '')
+  return redList.value.filter(
+    (r: any) =>
+      Array.isArray(r.risks) &&
+      r.risks.some((risk: any) => {
+        const s = String(risk || '').trim()
+        if (!s) return false
+        return (
+          s === filterValue ||
+          s.replace(/^防/, '') === keyword ||
+          s.includes(filterValue) ||
+          filterValue.includes(s)
+        )
+      })
+  )
+})
 const assessorWorkload = ref<any[]>([])
 const nurseLevelRisk = ref<any[]>([])
+const nurseLevelRadarData = ref<any>({ indicators: [], series: [] })
 const riskCorrelation = ref<any>({})
-const riskRadar = ref<any>({})
 const bar3D = ref<any>({})
 
 // ========================= 图表实例管理 =========================
@@ -208,19 +232,13 @@ const renderNineRiskDist = () => {
 
 /** 2. 风险等级整体占比 - 玫瑰饼图 */
 const renderOverallPie = () => {
-  const counts = { none: 0, low: 0, medium: 0, high: 0 }
-  nineRiskDist.value.forEach((r) => {
-    counts.none += r.none
-    counts.low += r.low
-    counts.medium += r.medium
-    counts.high += r.high
-  })
-  const data = [
-    { name: '无风险', value: counts.none, color: COLORS.none },
-    { name: '低危', value: counts.low, color: COLORS.low },
-    { name: '中危', value: counts.medium, color: COLORS.medium },
-    { name: '高危', value: counts.high, color: COLORS.high }
-  ].filter((d) => d.value > 0)
+  const data = riskLevelRatios.value
+    .map((r: any) => ({
+      name: r.name,
+      value: r.value,
+      itemStyle: { color: r.color }
+    }))
+    .filter((d: any) => d.value > 0)
   createChart('overallPie', {
     backgroundColor: 'transparent',
     tooltip: {
@@ -243,7 +261,7 @@ const renderOverallPie = () => {
         itemStyle: { borderRadius: 8, borderColor: '#05132b', borderWidth: 2 },
         label: { color: '#fff', formatter: '{b}\n{d}%', fontSize: 12 },
         labelLine: { lineStyle: { color: '#557a9c' } },
-        data: data.map((d) => ({ name: d.name, value: d.value, itemStyle: { color: d.color } }))
+        data
       }
     ]
   })
@@ -518,8 +536,8 @@ const renderNurseLevelFunnel = () => {
       {
         type: 'funnel',
         left: '10%',
-        top: 40,
-        bottom: 20,
+        top: 50,
+        bottom: 10,
         width: '80%',
         label: { color: '#fff', fontSize: 12, formatter: '{b}: {c}' },
         itemStyle: { borderColor: '#05132b', borderWidth: 1 },
@@ -533,15 +551,29 @@ const renderNurseLevelFunnel = () => {
   })
 }
 
-/** 9. 九防风险雷达图 */
+/** 9. 各护理级别 · 九防高危占比雷达 */
 const renderRiskRadar = () => {
-  const { indicators, series } = riskRadar.value
-  if (!indicators || !series) return
-  const colors = ['#ff5a3c', '#ff9966', '#ffd85a', '#00e0a7']
+  const { indicators, series } = nurseLevelRadarData.value
+  if (!indicators.length || !series.length) return
   createChart('riskRadar', {
     backgroundColor: 'transparent',
-    tooltip: { textStyle: { color: '#fff' } },
-    legend: { data: series.map((s: any) => s.name), textStyle: { color: COLORS.text }, top: 0 },
+    tooltip: {
+      textStyle: { color: '#fff' },
+      formatter: (p: any) => {
+        const s = series[p.dataIndex]
+        if (!s) return p.name
+        const lines = s.value.map((v: number, i: number) => {
+          return `${indicators[i].name}:${v}%(${s.count[i] ?? '-'} 人)`
+        })
+        return [`<strong>${s.name}</strong>`, ...lines].join('<br/>')
+      }
+    },
+    legend: {
+      data: series.map((s: any) => s.name),
+      textStyle: { color: COLORS.text },
+      top: 0,
+      type: 'scroll'
+    },
     radar: {
       indicator: indicators,
       shape: 'polygon',
@@ -554,14 +586,7 @@ const renderRiskRadar = () => {
     series: [
       {
         type: 'radar',
-        areaStyle: { opacity: 0.15 },
-        data: series.map((s: any, i: number) => ({
-          name: s.name,
-          value: s.value,
-          lineStyle: { color: colors[i], width: 2 },
-          itemStyle: { color: colors[i] },
-          areaStyle: { color: colors[i], opacity: 0.15 }
-        }))
+        data: series
       }
     ]
   })
@@ -575,12 +600,28 @@ const renderAssessorWorkload = () => {
     backgroundColor: 'transparent',
     tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, textStyle: { color: '#fff' } },
     legend: { data: ['评估人数', '发现高危'], textStyle: { color: COLORS.text }, top: 0 },
-    grid: { left: 50, right: 30, top: 40, bottom: 30 },
+    grid: { left: 50, right: 30, top: 40, bottom: 90 },
+    dataZoom: [
+      {
+        type: 'slider',
+        show: true,
+        xAxisIndex: [0],
+        start: 0,
+        end: Math.min(40, (20 / data.length) * 100),
+        height: 14,
+        bottom: 8,
+        borderColor: 'transparent',
+        backgroundColor: 'rgba(10,47,94,0.3)',
+        fillerColor: 'rgba(0,170,255,0.25)',
+        handleStyle: { color: '#00aaff' },
+        textStyle: { color: COLORS.text }
+      }
+    ],
     xAxis: {
       type: 'category',
       data: data.map((d) => d.name),
       axisLine: { lineStyle: { color: COLORS.axisLine } },
-      axisLabel: { color: COLORS.text }
+      axisLabel: { color: COLORS.text, rotate: 60, fontSize: 10, margin: 12 }
     },
     yAxis: {
       type: 'value',
@@ -684,8 +725,10 @@ const render3DBar = () => {
   createChart('bar3D', {
     backgroundColor: 'transparent',
     tooltip: {
-      formatter: (p: any) =>
-        `${floors[p.data[0]]} - ${risks[p.data[1]]}<br/>高危人数:${p.data[2]} 人`
+      formatter: (p: any) => {
+        const v = Array.isArray(p.value) ? p.value : p.data?.value || []
+        return `${floors[v[0]] ?? '-'} - ${risks[v[1]] ?? '-'}<br/>高危人数:${v[2] ?? '-'} 人`
+      }
     },
     visualMap: {
       max: maxVal,
@@ -737,7 +780,6 @@ const render3DBar = () => {
   })
 }
 
-
 /** 15. 评估完成情况 - 柱状图 */
 const renderAssessCompletion = () => {
   const data = nineRiskDist.value
@@ -835,41 +877,244 @@ const renderAllCharts = () => {
 const loadAllData = async () => {
   loading.value = true
   disposeAll()
+  const tenantId = userStore.orgTenantId
   const params = {
-    month: filter.month || undefined,
+    trendMonths: 6,
     buildId: filter.buildId || undefined,
-    floorId: filter.floorId || undefined
+    floorId: filter.floorId || undefined,
+    tenantIds: tenantId ? (Array.isArray(tenantId) ? tenantId : [tenantId]) : undefined,
+    multiHighRiskThreshold: 1
   }
   try {
-    const [ov, dist, trend, fhm, asc, scd, sig, red, awl, nlr, corr, radar, b3d] =
-      await Promise.all([
-        echartsApi.getNineSafetyOverview(params),
-        echartsApi.getNineRiskDistribution(params),
-        echartsApi.getRiskLevelTrend({ buildId: params.buildId }),
-        echartsApi.getFloorRiskHeatmap({ month: params.month }),
-        echartsApi.getAgeRiskScatter(params),
-        echartsApi.getAssessScoreDistribution(params),
-        echartsApi.getSignatureStatus(params),
-        echartsApi.getHighRiskRedList(params),
-        echartsApi.getAssessorWorkload(params),
-        echartsApi.getRiskByNurseLevel(params),
-        echartsApi.getNineRiskCorrelation(params),
-        echartsApi.getRiskRadar(params),
-        echartsApi.get3DRiskBar({ month: params.month })
-      ])
-    overview.value = ov
-    nineRiskDist.value = dist
-    riskTrend.value = trend
-    floorHeatmap.value = fhm
-    ageScatter.value = asc
-    scoreDist.value = scd
-    signature.value = sig
-    redList.value = red
-    assessorWorkload.value = awl
-    nurseLevelRisk.value = nlr
-    riskCorrelation.value = corr
-    riskRadar.value = radar
-    bar3D.value = b3d
+    // 真实接口:九防安全风险综合看板
+    const bd = (await echartsApi.nineRiskBoardGet(params)) || {}
+    const ov = bd.overview || {}
+
+    // 1. 顶部核心指标映射(保持页面模板字段名不变)
+    overview.value = {
+      totalElders: ov.disclosureTotal ?? 0,
+      highRiskCount: ov.highRiskTimes ?? 0,
+      highRiskRatio: ov.highRiskTimesRatio ?? 0,
+      pendingSignatures: ov.disclosureUnsignedCount ?? 0,
+      signedRate:
+        typeof ov.disclosureUnsignedRatio === 'number'
+          ? +(100 - ov.disclosureUnsignedRatio).toFixed(2)
+          : 100,
+      evalCompletionRate: ov.assessCompleteRate ?? 0,
+      assessorCount: ov.assessorCount ?? 0
+    }
+
+    // 2. 九防风险等级分布映射(真实字段 → 图表内部字段)
+    nineRiskDist.value = (bd.itemRiskDistributions || []).map((item: any) => ({
+      name: normalizeRiskName(item.itemName),
+      high: item.highCount ?? 0,
+      medium: item.middleCount ?? 0,
+      low: item.lowCount ?? 0,
+      none: item.unknownCount ?? 0,
+      total: item.totalCount ?? 0
+    }))
+
+    // 3. 风险等级整体占比
+    const levelNameMap: Record<string, string> = {
+      none: '无风险',
+      low: '低危',
+      medium: '中危',
+      high: '高危',
+      unknown: '无风险'
+    }
+    const levelColorMap: Record<string, string> = {
+      none: COLORS.none,
+      low: COLORS.low,
+      medium: COLORS.medium,
+      high: COLORS.high,
+      unknown: COLORS.none
+    }
+    riskLevelRatios.value = (bd.riskLevelRatios || []).map((r: any) => ({
+      name: r.riskLevelName || levelNameMap[r.riskLevel] || r.riskLevel,
+      value: r.count ?? 0,
+      color: levelColorMap[r.riskLevel] || COLORS.none
+    }))
+
+    // 4. 风险等级月度趋势
+    riskTrend.value = (bd.monthlyTrends || []).map((t: any) => ({
+      month: t.month,
+      high: t.highCount ?? 0,
+      medium: t.middleCount ?? 0,
+      low: t.lowCount ?? 0,
+      none: t.unknownCount ?? 0
+    }))
+
+    // 5. 九防 × 楼栋高危人数热力图(同时供 3D 柱状图复用)
+    const hm = bd.highRiskHeatMap || {}
+    const heatItems = (hm.items || []).map((i: any) => normalizeRiskName(i.name || i.itemName || ''))
+    const heatBuilds = (hm.builds || []).map((b: any) => b.name || b.buildName || '')
+
+    // 新结构通过 code/id 找索引,不再依赖隐式下标
+    const itemCodeToIndex = new Map<string, number>()
+    const buildIdToIndex = new Map<string | number, number>()
+    ;(hm.items || []).forEach((i: any, idx: number) => {
+      if (i.code) itemCodeToIndex.set(String(i.code), idx)
+    })
+    ;(hm.builds || []).forEach((b: any, idx: number) => {
+      if (b.code) buildIdToIndex.set(String(b.code), idx)
+    })
+
+    const heatCells = (hm.cells || [])
+      .map((c: any) => {
+        const x =
+          c.itemIndex ??
+          itemCodeToIndex.get(String(c.itemCode ?? '')) ??
+          c.x ??
+          c.column ??
+          -1
+        const y =
+          c.buildIndex ??
+          buildIdToIndex.get(String(c.buildId ?? '')) ??
+          c.y ??
+          c.row ??
+          -1
+        if (x < 0 || y < 0) return null
+        const v = c.highRiskElderCount ?? c.value ?? c.count ?? c.highCount ?? 0
+        // 热力图与 3D 柱状图均使用 [楼栋索引, 九防索引, 值]
+        return [y, x, v]
+      })
+      .filter(Boolean)
+
+    floorHeatmap.value = {
+      floors: heatBuilds,
+      risks: heatItems,
+      data: heatCells,
+      maxValue: hm.maxValue ?? 0
+    }
+    bar3D.value = {
+      floors: heatBuilds,
+      risks: heatItems,
+      data: heatCells,
+      maxValue: hm.maxValue ?? 0
+    }
+
+    // 6. 年龄 × 高风险项散点
+    ageScatter.value = (bd.highRiskElders || []).map((e: any) => ({
+      name: e.elderName,
+      age: e.elderAge,
+      sex: e.elderSex,
+      highCount: e.highRiskCount ?? 0,
+      room: '',
+      nurseLevel: e.nurseLevelName
+    }))
+
+    // 7. 评估得分分布(真实返回 minScore/maxScore + buckets,转成箱线图五数概括)
+    scoreDist.value = (bd.itemScoreDistributions || []).map((item: any) => {
+      const buckets = [...(item.buckets || [])].sort(
+        (a: any, b: any) => (a.min ?? 0) - (b.min ?? 0)
+      )
+      const total = buckets.reduce((s: number, b: any) => s + (b.count ?? 0), 0)
+      // 在桶内做线性插值,估算四分位数
+      const boundary = (p: number) => {
+        if (total === 0 || buckets.length === 0) return item.minScore ?? 0
+        const target = total * p
+        let cumulative = 0
+        for (const b of buckets) {
+          const count = b.count ?? 0
+          if (count === 0) continue
+          if (cumulative + count >= target) {
+            const ratio = (target - cumulative) / count
+            return (b.min ?? 0) + ((b.max ?? b.min ?? 0) - (b.min ?? 0)) * ratio
+          }
+          cumulative += count
+        }
+        return buckets[buckets.length - 1].max ?? item.maxScore ?? 0
+      }
+      const round2 = (n: number) => +n.toFixed(2)
+      return {
+        name: normalizeRiskName(item.itemName),
+        min: round2(item.minScore ?? buckets[0]?.min ?? 0),
+        q1: round2(boundary(0.25)),
+        median: round2(boundary(0.5)),
+        q3: round2(boundary(0.75)),
+        max: round2(item.maxScore ?? buckets[buckets.length - 1]?.max ?? 0)
+      }
+    })
+
+    // 8. 知情书签署状态
+    const sr = bd.disclosureSignRate || {}
+    signature.value = {
+      signed: sr.signedCount ?? 0,
+      firstSignedOnly: 0,
+      unsigned: sr.unsignedCount ?? 0,
+      total: sr.total ?? 0,
+      signedRate: sr.signRate ?? 0
+    }
+
+    // 9. 按护理级别 · 高危项漏斗 / 雷达
+    // 后端字段为 nurseLevelHighRiskElderCounts(漏斗),优先读取;兼容旧名 nurseLevelHighRiskRecordCounts
+    nurseLevelRisk.value = (
+      bd.nurseLevelHighRiskElderCounts ||
+      bd.nurseLevelHighRiskRecordCounts || []
+    ).map((n: any) => ({
+      name: n.nurseLevelName,
+      count: n.count ?? 0,
+      ratio: n.ratio ?? 0,
+      none: 0,
+      low: 0,
+      medium: 0,
+      high: n.count ?? 0
+    }))
+
+    // 9.1 各护理级别 · 九防高危占比雷达(新结构:每项护理级别带 items 数组)
+    const radarRecords = bd.nurseLevelHighRiskRecordCounts || []
+    if (radarRecords.length && Array.isArray(radarRecords[0].items)) {
+      // 以第一条护理级别的 items 顺序作为雷达 indicator
+      const indicators = radarRecords[0].items.map((it: any) => ({
+        name: normalizeRiskName(it.itemName),
+        max: 100
+      }))
+      const colors = ['#ff5a3c', '#00e0a7', '#ffd85a', '#00aaff', '#ff9966', '#5578ff', '#b380ff']
+      nurseLevelRadarData.value = {
+        indicators,
+        series: radarRecords.map((n: any, idx: number) => ({
+          name: n.nurseLevelName,
+          value: n.items.map((it: any) => +(it.ratio ?? 0).toFixed(2)),
+          count: n.items.map((it: any) => it.count ?? 0),
+          lineStyle: { color: colors[idx % colors.length], width: 2 },
+          itemStyle: { color: colors[idx % colors.length] },
+          areaStyle: { color: colors[idx % colors.length], opacity: 0.1 }
+        }))
+      }
+    }
+
+    // 10. 评估人工作量
+    assessorWorkload.value = (bd.assessorStats || []).map((a: any) => ({
+      name: a.assessor,
+      count: a.assessedElderCount ?? 0,
+      highCount: a.highRiskElderCount ?? 0
+    }))
+
+    // 12. 2 项及以上高风险长者列表(红名单)
+    redList.value = (bd.multiHighRiskElders || []).map((e: any) => ({
+      id: e.elderId,
+      name: e.elderName,
+      sex: e.elderSex === 1 ? '男' : '女',
+      age: e.elderAge,
+      room: e.bedName || '',
+      floor: e.buildName || '',
+      nurseLevel: e.nurseLevelName,
+      assessor: e.assessor,
+      risks: (() => {
+        // 直接把原始数据里的「简易精神状态」替换为「防误食」,再做拆分
+        // 风险项之间可能是英文逗号 / 中文逗号 / 顿号 / 空白,统一拆分
+        const raw = e.highRiskItems
+        const str = Array.isArray(raw) ? raw.join(',') : String(raw || '')
+        return str
+          .replace(/简易精神状态/g, '防误食')
+          .split(/[,,、\s]+/)
+          .map((s: string) => s.trim())
+          .filter(Boolean)
+      })(),
+      highCount: e.highRiskCount ?? 0,
+      assessDate: formatAssessDate(e.assessDate)
+    }))
+
     await nextTick()
     renderAllCharts()
   } catch (e) {
@@ -884,7 +1129,6 @@ const handleFilterChange = () => {
   loadAllData()
 }
 const handleReset = () => {
-  filter.month = ''
   filter.buildId = ''
   filter.floorId = ''
   floorList.value = []
@@ -892,6 +1136,29 @@ const handleReset = () => {
 }
 
 // ========================= 红名单风险标签颜色 =========================
+const formatAssessDate = (v: any): string => {
+  if (!v) return '-'
+  if (Array.isArray(v)) {
+    const [y, m, d] = v
+    if (!y) return '-'
+    return `${y}-${String(m ?? 1).padStart(2, '0')}-${String(d ?? 1).padStart(2, '0')}`
+  }
+  if (v instanceof Date) {
+    const y = v.getFullYear()
+    const m = v.getMonth() + 1
+    const d = v.getDate()
+    return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`
+  }
+  if (typeof v === 'string') return v.split(' ')[0] || '-'
+  return String(v)
+}
+// 后端把「防误食」命名为「简易精神状态」,前端统一做临时映射
+const normalizeRiskName = (name: string): string => {
+  const s = String(name || '').trim()
+  if (!s) return s
+  if (s === '简易精神状态' || s.includes('简易精神状态')) return '防误食'
+  return s
+}
 const riskTagColor = (name: string): string => {
   const map: Record<string, string> = {
     噎食: '#ff9966',
@@ -909,8 +1176,6 @@ const riskTagColor = (name: string): string => {
 
 // ========================= 生命周期 =========================
 onMounted(async () => {
-  const months = await echartsApi.getMonthList()
-  monthOptions.value = months
   // 楼栋使用真实接口
   try {
     const tenantIds = userStore.orgTenantId
@@ -1128,16 +1393,16 @@ onUnmounted(() => {
         <div :ref="setRef('riskRadar')" class="chart"></div>
       </div>
       <!-- 评估人工作量 -->
-      <div class="card">
+      <div class="card span-2">
         <div class="card-title"><span class="dot dot-low"></span>评估人工作量统计</div>
         <div :ref="setRef('assessorWorkload')" class="chart"></div>
       </div>
 
       <!-- 相关性矩阵 -->
-      <div class="card">
-        <div class="card-title"><span class="dot dot-mid"></span>九防风险相关性矩阵</div>
-        <div :ref="setRef('riskCorrelation')" class="chart"></div>
-      </div>
+      <!--      <div class="card">-->
+      <!--        <div class="card-title"><span class="dot dot-mid"></span>九防风险相关性矩阵</div>-->
+      <!--        <div :ref="setRef('riskCorrelation')" class="chart"></div>-->
+      <!--      </div>-->
       <!-- 评估完成率 -->
       <div class="card">
         <div class="card-title"><span class="dot dot-mid"></span>九防高危占比</div>
@@ -1168,42 +1433,49 @@ onUnmounted(() => {
           />
           <path d="M12 9v4M12 17h.01" />
         </svg>
-        重点人群红名单 · 2 项及以上高风险(共 {{ redList.length }} 人)
-        <span class="tip danger">建议:纳入每日护士长巡床必查清单</span>
+        重点人群红名单 · 1 项及以上高风险(共 {{ filteredRedList.length }} 人)
+        <el-select v-model="redRiskFilter" placeholder="全部" clearable class="red-risk-select">
+          <el-option
+            v-for="opt in nineRiskOptions"
+            :key="opt.value"
+            :label="opt.label"
+            :value="opt.value"
+          />
+        </el-select>
       </div>
-      <div class="table-wrap" v-if="redList.length > 0">
+      <div class="table-wrap" v-if="filteredRedList.length > 0">
         <table class="red-table">
           <thead>
             <tr>
-              <th>档案编号</th><th>姓名</th><th>性别</th><th>年龄</th> <th>楼栋</th><th>床号</th
-              ><th>护理级别</th><th>评估人</th> <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="r in redList" :key="r.id">
+            <tr v-for="r in filteredRedList" :key="r.id">
               <td class="mono">{{ r.id }}</td>
               <td class="name">{{ r.name }}</td>
               <td>{{ r.sex }}</td>
               <td>{{ r.age }} 岁</td>
-              <td>{{ r.floor }}</td>
+
               <td class="mono">{{ r.room }}</td>
               <td>{{ r.nurseLevel }}</td>
-              <td>{{ r.assessor }}</td>
+              <td style="max-width: 15vw">{{ r.assessor }}</td>
               <td>
                 <span
                   v-for="rk in r.risks"
                   :key="rk"
                   class="risk-tag"
                   :style="{
-                    background: riskTagColor(rk) + '33',
-                    color: riskTagColor(rk),
-                    borderColor: riskTagColor(rk) + '66'
+                    background: riskTagColor(rk.replace('防', '')) + '33',
+                    color: riskTagColor(rk.replace('防', '')),
+                    borderColor: riskTagColor(rk.replace('防', '')) + '66'
                   }"
                   >{{ rk }}</span
                 >
               </td>
               <td
-                ><span class="high-count">{{ r.highCount }}</span></td
+                ><span class="high-count">{{ r.assessDate }}</span></td
               >
             </tr>
           </tbody>
@@ -1500,6 +1772,10 @@ onUnmounted(() => {
       color: #ff9c8c;
     }
   }
+  .red-risk-select {
+    margin-left: auto;
+    width: 150px;
+  }
 }
 .danger-title {
   border-left-color: #ff5a3c;

+ 2 - 0
src/views/elderly/apply/check-in-request/ProcessForm.vue

@@ -1406,6 +1406,7 @@ const init = async (id, detail, type) => {
       dataForm.custodianFiles = []
       dataForm.idCard2 = res.papersType != 1 ? res.idCard : ''
       dataForm.idCard = res.papersType == 1 ? res.idCard : ''
+      dataForm.birthday = res.birthday ? formatTime(res.birthday, 'yyyy-MM-dd') : ''
     }
   }
 }
@@ -1442,6 +1443,7 @@ const open = async (res, detail, type, tId) => {
     dataForm.otherFiles = [..._elderFiles, ..._custodianFiles, ..._otherFiles]
     dataForm.elderFiles = []
     dataForm.custodianFiles = []
+    dataForm.birthday = res.birthday ? formatTime(res.birthday, 'yyyy-MM-dd') : ''
     dataForm.birthdayCareDate = res.birthdayCareDate ? new Date(res.birthdayCareDate) : ''
     dataForm.tenantId = tId
     dataForm.elderCover = res.elderCover ? res.elderCover : []

+ 269 - 61
src/views/elderly/apply/nine-precautions/risk-disclosure-statement/Form.vue

@@ -32,19 +32,25 @@
             />
           </el-form-item>
         </el-col>
-<!--        <el-col :span="12">-->
-<!--          <el-form-item label="证件号码">-->
-<!--            <TgInput v-model="dataForm.idCard" disabled />-->
-<!--          </el-form-item>-->
-<!--        </el-col>-->
+        <!--        <el-col :span="12">-->
+        <!--          <el-form-item label="证件号码">-->
+        <!--            <TgInput v-model="dataForm.idCard" disabled />-->
+        <!--          </el-form-item>-->
+        <!--        </el-col>-->
       </el-row>
 
       <!-- 长者详情展示 - 对应图片中的表一表头信息 -->
-      <div  class="elder-detail-wrap">
+      <div class="elder-detail-wrap">
         <el-descriptions :column="2" border size="small">
-          <el-descriptions-item label="姓名">{{ elderDetail.elderName || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="性别">{{ getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, elderDetail.elderSex) || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="年龄">{{ elderDetail.elderAge || '-' }}岁</el-descriptions-item>
+          <el-descriptions-item label="姓名">{{
+            elderDetail.elderName || '-'
+          }}</el-descriptions-item>
+          <el-descriptions-item label="性别">{{
+            getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, elderDetail.elderSex) || '-'
+          }}</el-descriptions-item>
+          <el-descriptions-item label="年龄"
+            >{{ elderDetail.elderAge || '-' }}岁</el-descriptions-item
+          >
           <el-descriptions-item label="床号">{{ elderDetail.bedName || '-' }}</el-descriptions-item>
         </el-descriptions>
       </div>
@@ -53,9 +59,7 @@
       <div class="section-title">服务安全风险知情告知书</div>
       <div class="notice-content">
         <p class="notice-header">尊敬的老年人/相关第三方/监护人:</p>
-        <p class="notice-text">
-          您好!感谢您对本机构的信任和支持。
-        </p>
+        <p class="notice-text"> 您好!感谢您对本机构的信任和支持。 </p>
         <p class="notice-text">
           经我院评估您或您相关第三方的身体状况,您或您亲属在我院养老期间,存在下列服务安全风险。为保证对您或您相关第三方的服务质量,特向您告知!我院将针对老年人的情况做好相关防范措施,请您理解、配合、并支持相关工作措施的落实。
         </p>
@@ -93,9 +97,15 @@
                 </td>
                 <td class="col-remark">
                   <span class="remark-text">风险程度:</span>
-                  <el-checkbox v-model="item.lowChecked" :disabled="isView" size="small">低危</el-checkbox>
-                  <el-checkbox v-model="item.mediumChecked" :disabled="isView" size="small">中危</el-checkbox>
-                  <el-checkbox v-model="item.highChecked" :disabled="isView" size="small">高危</el-checkbox>
+                  <el-checkbox v-model="item.lowChecked" :disabled="isView" size="small"
+                    >低危</el-checkbox
+                  >
+                  <el-checkbox v-model="item.mediumChecked" :disabled="isView" size="small"
+                    >中危</el-checkbox
+                  >
+                  <el-checkbox v-model="item.highChecked" :disabled="isView" size="small"
+                    >高危</el-checkbox
+                  >
                 </td>
               </tr>
             </tbody>
@@ -113,7 +123,9 @@
             style="width: 150px"
             :disabled="isView"
           />
-          <span class="date-value">{{ dataForm.firstRisk ? formatDate(dataForm.firstRisk) : '无' }}</span>
+          <span class="date-value">{{
+            dataForm.firstRisk ? formatDate(dataForm.firstRisk) : '无'
+          }}</span>
         </div>
 
         <!-- 告知签名区域 -->
@@ -122,28 +134,33 @@
           <p class="signature-desc">
             通过对老年人的整体评估,老年人有可能发生以上风险,其风险及防范措施已向老年人或相关第三方进行了如实告知。
           </p>
-          <p class="signature-declare">
-            (老年人或相关第三方声明:本人已知悉并签字确认。)
-          </p>
-          
+          <p class="signature-declare"> (老年人或相关第三方声明:本人已知悉并签字确认。) </p>
+
           <div class="signature-item">
             <span class="signature-label">经办人(护理人员):</span>
-
           </div>
-          
+
           <div class="signature-item">
             <span class="signature-label">老年人或相关第三方签名:</span>
+          </div>
 
+          <div class="signature-item">
+            <span class="signature-label">签名日期:</span>
           </div>
         </div>
       </div>
 
- 
+      <div style="background-color: #fafafa;padding: 10px 20px;border-radius: 8px">
+        <div style="padding: 10px 0; font-weight: bold; color: #333333">打印并签名后上传纸质版</div>
+        <TableUpload :limit="1" fun-name="其他附件" v-model="dataForm.secondElderSign" :isDetail="isView" />
+      </div>
     </el-form>
 
     <template #footer>
       <el-button v-if="!isView" @click="handleClosed">取消</el-button>
-      <el-button v-if="!isView" type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
+      <el-button v-if="!isView" type="primary" @click="submitForm" :loading="submitLoading"
+        >确定</el-button
+      >
       <el-button v-if="isView" @click="handleClosed">关闭</el-button>
       <el-button v-if="isView" type="success" @click="handleExport">打印</el-button>
     </template>
@@ -176,6 +193,23 @@ const userStore = useUserStore()
 
 defineOptions({ name: 'RiskDisclosureStatementForm' })
 
+// TableUpload 的 v-model 必须是数组(FileItem[]);后端以 JSON 字符串存储。
+// 回显时把字符串转数组,提交时把数组转字符串,与 check-in ProcessForm 一致。
+const toFileArray = (val: any): any[] => {
+  if (!val) return []
+  if (Array.isArray(val)) return val
+  try {
+    const parsed = JSON.parse(val)
+    return Array.isArray(parsed) ? parsed : []
+  } catch {
+    return []
+  }
+}
+const toFileString = (val: any): string => {
+  if (!val || !Array.isArray(val) || val.length === 0) return ''
+  return JSON.stringify(val)
+}
+
 const emit = defineEmits(['success'])
 
 const dialogVisible = ref(false)
@@ -192,15 +226,96 @@ const elderDetail = ref<any>({})
 
 // 风险项目表格数据 - name 用于匹配接口
 const riskTableItems = ref([
-  { key: 'asphyxiation', name: '防噎食评估', api: asphyxiationPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'pressureUlcer', name: '防压疮评估', api: pressureSoresPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'fall', name: '防跌倒风险评估', api: fallDownPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'bedFall', name: '防坠床评估', api: fallPreventionPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'scald', name: '防烫伤评估', api: burnPreventionPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'wandering', name: '防走失风险评估', api: wanderAwayPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'selfHarm', name: '防自伤和他伤', api: ngasrPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'foodDrug', name: '防食品药品误食评估', api: mmseGetPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-  { key: 'entertainment', name: '防文娱活动意外', api: entertainmentPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false }
+  {
+    key: 'asphyxiation',
+    name: '防噎食评估',
+    api: asphyxiationPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'pressureUlcer',
+    name: '防压疮评估',
+    api: pressureSoresPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'fall',
+    name: '防跌倒风险评估',
+    api: fallDownPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'bedFall',
+    name: '防坠床评估',
+    api: fallPreventionPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'scald',
+    name: '防烫伤评估',
+    api: burnPreventionPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'wandering',
+    name: '防走失风险评估',
+    api: wanderAwayPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'selfHarm',
+    name: '防自伤和他伤',
+    api: ngasrPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'foodDrug',
+    name: '防食品药品误食评估',
+    api: mmseGetPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  },
+  {
+    key: 'entertainment',
+    name: '防文娱活动意外',
+    api: entertainmentPage,
+    recordId: null as any,
+    riskLevel: '',
+    lowChecked: false,
+    mediumChecked: false,
+    highChecked: false
+  }
 ])
 
 // 接口返回的风险数据选项(模拟数据,实际从接口获取)
@@ -313,11 +428,16 @@ const loadElderRiskData = async (elderId: string, preserveSelection = false) =>
 // 获取风险等级标签
 const getRiskLevelLabel = (riskLevel: string) => {
   switch (riskLevel) {
-    case 'none': return '无风险'
-    case 'low': return '低风险'
-    case 'medium': return '中风险'
-    case 'high': return '高风险'
-    default: return '无风险'
+    case 'none':
+      return '无风险'
+    case 'low':
+      return '低风险'
+    case 'medium':
+      return '中风险'
+    case 'high':
+      return '高风险'
+    default:
+      return '无风险'
   }
 }
 
@@ -359,7 +479,7 @@ const dataForm = reactive({
   // 第二次告知签名
   secondRisk: '',
   secondNurseSign: '',
-  secondElderSign: '',
+  secondElderSign: [] as any[],
   secondSignDate: '',
   secondChangeRisk: '',
   secondRiskLevel: '',
@@ -392,7 +512,7 @@ const handleSelectElder = async (item: any) => {
       foodDrug: [],
       entertainment: []
     }
-    riskTableItems.value.forEach(tableItem => {
+    riskTableItems.value.forEach((tableItem) => {
       tableItem.recordId = null
       tableItem.riskLevel = ''
       tableItem.lowChecked = false
@@ -427,11 +547,13 @@ const open = async (row?: any, viewMode = false) => {
     try {
       const res = await getSafetyRiskNoticeById(row.id)
       Object.assign(dataForm, res)
+      // 回显:secondElderSign 后端为字符串,转数组供 TableUpload 展示
+      dataForm.secondElderSign = toFileArray(res.secondElderSign)
       // 解析风险项目
       if (res.riskData) {
         const parsedData = JSON.parse(res.riskData)
         // 解析风险表格数据
-        riskTableItems.value = riskTableItems.value.map(item => {
+        riskTableItems.value = riskTableItems.value.map((item) => {
           const saved = parsedData[item.key]
           if (saved) {
             return {
@@ -491,7 +613,7 @@ const resetForm = () => {
     firstSignDate: '',
     secondRisk: '',
     secondNurseSign: '',
-    secondElderSign: '',
+    secondElderSign: [] as any[],
     secondSignDate: '',
     secondChangeRisk: '',
     secondRiskLevel: '',
@@ -502,15 +624,96 @@ const resetForm = () => {
   })
   elderDetail.value = {}
   riskTableItems.value = [
-    { key: 'asphyxiation', name: '防噎食评估', api: asphyxiationPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'pressureUlcer', name: '防压疮评估', api: pressureSoresPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'fall', name: '防跌倒风险评估', api: fallDownPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'bedFall', name: '防坠床评估', api: fallPreventionPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'scald', name: '防烫伤评估', api: burnPreventionPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'wandering', name: '防走失风险评估', api: wanderAwayPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'selfHarm', name: '防自伤和他伤', api: ngasrPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'foodDrug', name: '防食品药品误食评估', api: mmseGetPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false },
-    { key: 'entertainment', name: '防文娱活动意外', api: entertainmentPage, recordId: null as any, riskLevel: '', lowChecked: false, mediumChecked: false, highChecked: false }
+    {
+      key: 'asphyxiation',
+      name: '防噎食评估',
+      api: asphyxiationPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'pressureUlcer',
+      name: '防压疮评估',
+      api: pressureSoresPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'fall',
+      name: '防跌倒风险评估',
+      api: fallDownPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'bedFall',
+      name: '防坠床评估',
+      api: fallPreventionPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'scald',
+      name: '防烫伤评估',
+      api: burnPreventionPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'wandering',
+      name: '防走失风险评估',
+      api: wanderAwayPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'selfHarm',
+      name: '防自伤和他伤',
+      api: ngasrPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'foodDrug',
+      name: '防食品药品误食评估',
+      api: mmseGetPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    },
+    {
+      key: 'entertainment',
+      name: '防文娱活动意外',
+      api: entertainmentPage,
+      recordId: null as any,
+      riskLevel: '',
+      lowChecked: false,
+      mediumChecked: false,
+      highChecked: false
+    }
   ]
   riskOptionsMap.value = {
     asphyxiation: [],
@@ -541,7 +744,7 @@ const submitForm = async () => {
   try {
     // 处理风险项目数据 - 将表格数据转换为存储格式
     const riskDataObj: Record<string, any> = {}
-    riskTableItems.value.forEach(item => {
+    riskTableItems.value.forEach((item) => {
       riskDataObj[item.key] = {
         id: item.recordId, // 选中的评估记录ID
         level: item.riskLevel, // 风险等级
@@ -553,10 +756,12 @@ const submitForm = async () => {
         }
       }
     })
-    
+
     const submitData = {
       ...dataForm,
-      riskData: JSON.stringify(riskDataObj)
+      riskData: JSON.stringify(riskDataObj),
+      // 提交:secondElderSign 转成 JSON 字符串给后端
+      secondElderSign: toFileString(dataForm.secondElderSign)
     }
 
     if (isEdit.value) {
@@ -598,7 +803,7 @@ const handleExport = () => {
     const lowChecked = item.lowChecked ? '☑' : '☐'
     const mediumChecked = item.mediumChecked ? '☑' : '☐'
     const highChecked = item.highChecked ? '☑' : '☐'
-    
+
     riskTableHtml += `
       <tr>
         <td style="border: 1px solid #333; padding: 8px; text-align: center;">${index + 1}</td>
@@ -698,7 +903,9 @@ const handleExport = () => {
           
           <div class="signature-item">
             <span>老年人或相关第三方签名:</span>
-           
+          </div>
+           <div class="signature-item">
+            <span class="signature-label">签名日期:</span>
           </div>
         </div>
 
@@ -722,10 +929,10 @@ const handleExport = () => {
 // 获取风险等级文本
 const getRiskLevelText = (level: string): string => {
   const levelMap: Record<string, string> = {
-    'none': '无风险',
-    'low': '低风险',
-    'medium': '中风险',
-    'high': '高风险'
+    none: '无风险',
+    low: '低风险',
+    medium: '中风险',
+    high: '高风险'
   }
   return levelMap[level] || level || '-'
 }
@@ -778,7 +985,8 @@ defineExpose({ open })
     border-collapse: collapse;
     font-size: 14px;
 
-    th, td {
+    th,
+    td {
       border: 1px solid #dcdfe6;
       padding: 10px 8px;
       text-align: center;

+ 189 - 54
src/views/elderly/apply/nine-precautions/risk-disclosure-statement/index.vue

@@ -243,7 +243,9 @@ import NutritionalRiskForm from '@/views/elderly/apply/nine-precautions/nutritio
 import ButtonImport from '@/components/ButtonImport/src/ButtonImport.vue'
 import { useUserStore } from '@/store/modules/user'
 import { formatToDateTime } from '@/utils/dateUtil'
-import { exportWithExpandedObjectArrays } from '@/utils/excel-export'
+import ExcelJS from 'exceljs'
+import { getAccessToken } from '@/utils/auth'
+import { saveAs } from 'file-saver'
 const userStore = useUserStore()
 defineOptions({ name: 'RiskDisclosureStatement' })
 
@@ -387,8 +389,8 @@ const handleImportCard = async () => {
   exportLoading.value = true
   try {
     const queryP = {
-      pageNo: 1,
-      pageSize: 9999,
+      pageNo: queryParams.pageNo,
+      pageSize: queryParams.pageSize,
       elderName: queryParams.elderName,
       floorName: queryParams.floorName,
       tenantIds: queryParams.tenantIds
@@ -404,57 +406,7 @@ const handleImportCard = async () => {
       return
     }
 
-    const processedData = exportList.map((row: any, index: number) => {
-      const rd = row.riskData || {}
-      return {
-        index: index + 1,
-        elderName: row.elderName || '',
-        elderSex: getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, row.elderSex) || '',
-        elderAge: row.elderAge ?? '',
-        bedName: row.bedName || '',
-        contractNumber: row.contractNumber || '',
-        nurseLevelName: row.nurseLevelName || '',
-        asphyxiationRisk: getRiskLevelText(rd.asphyxiation?.level || '-'),
-        pressureUlcerRisk: getRiskLevelText(rd.pressureUlcer?.level || '-'),
-        fallRisk: getRiskLevelText(rd.fall?.level || '-'),
-        bedFallRisk: getRiskLevelText(rd.bedFall?.level || '-'),
-        scaldRisk: getRiskLevelText(rd.scald?.level || '-'),
-        wanderingRisk: getRiskLevelText(rd.wandering?.level || '-'),
-        selfHarmRisk: getRiskLevelText(rd.selfHarm?.level || '-'),
-        foodDrugRisk: getRiskLevelText(rd.foodDrug?.level || '-'),
-        entertainmentRisk: getRiskLevelText(rd.entertainment?.level || '-'),
-        createTime: row.createTime ? formatToDateTime(row.createTime) : '',
-        creator: row.creator || ''
-      }
-    })
-
-    const headers = [
-      { key: 'index', title: '序号' },
-      { key: 'elderName', title: '长者姓名' },
-      { key: 'elderSex', title: '性别' },
-      { key: 'elderAge', title: '年龄' },
-      { key: 'bedName', title: '床号' },
-      { key: 'contractNumber', title: '合同号' },
-      { key: 'nurseLevelName', title: '护理级别' },
-      { key: 'asphyxiationRisk', title: '防噎食评估' },
-      { key: 'pressureUlcerRisk', title: '防压疮评估' },
-      { key: 'fallRisk', title: '防跌倒评估' },
-      { key: 'bedFallRisk', title: '防坠床评估' },
-      { key: 'scaldRisk', title: '防烫伤评估' },
-      { key: 'wanderingRisk', title: '防走失评估' },
-      { key: 'selfHarmRisk', title: '防自伤他伤评估' },
-      { key: 'foodDrugRisk', title: '防食品药品误食评估' },
-      { key: 'entertainmentRisk', title: '防文娱活动意外' },
-      { key: 'createTime', title: '记录时间' },
-      { key: 'creator', title: '记录人' }
-    ]
-
-    exportWithExpandedObjectArrays(
-      processedData,
-      headers,
-      `安全风险告知书_${new Date().getTime()}.xlsx`,
-      '安全风险告知书'
-    )
+    await exportRiskDisclosureToExcel(exportList)
     message.success('导出成功')
   } catch (error) {
     console.error('导出失败:', error)
@@ -464,6 +416,189 @@ const handleImportCard = async () => {
   }
 }
 
+/**
+ * 解析 secondElderSign 字段(后端为 JSON 字符串)为文件数组
+ * 形如:[{"fileUrl":"...","fileName":"二维码.png","fileSize":7443,"createTime":"..."}]
+ */
+const parseFileList = (val: any): { fileUrl: string; fileName: string }[] => {
+  if (!val) return []
+  try {
+    const arr = typeof val === 'string' ? JSON.parse(val) : val
+    return Array.isArray(arr) ? arr.filter((x: any) => x && x.fileUrl) : []
+  } catch (e) {
+    return []
+  }
+}
+
+/**
+ * 导出安全风险告知书(带「二次告知签名」图片列)
+ * @param data 已解析 riskData 的列表数据
+ */
+const exportRiskDisclosureToExcel = async (data: any[]) => {
+  const workbook = new ExcelJS.Workbook()
+  const worksheet = workbook.addWorksheet('安全风险告知书')
+
+  // 每行解析出签名图片数组
+  const rows = data.map((row, index) => {
+    const rd = row.riskData || {}
+    return {
+      index: index + 1,
+      elderName: row.elderName || '',
+      elderSex: getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, row.elderSex) || '',
+      elderAge: row.elderAge ?? '',
+      bedName: row.bedName || '',
+      contractNumber: row.contractNumber || '',
+      nurseLevelName: row.nurseLevelName || '',
+      asphyxiationRisk: getRiskLevelText(rd.asphyxiation?.level || '-'),
+      pressureUlcerRisk: getRiskLevelText(rd.pressureUlcer?.level || '-'),
+      fallRisk: getRiskLevelText(rd.fall?.level || '-'),
+      bedFallRisk: getRiskLevelText(rd.bedFall?.level || '-'),
+      scaldRisk: getRiskLevelText(rd.scald?.level || '-'),
+      wanderingRisk: getRiskLevelText(rd.wandering?.level || '-'),
+      selfHarmRisk: getRiskLevelText(rd.selfHarm?.level || '-'),
+      foodDrugRisk: getRiskLevelText(rd.foodDrug?.level || '-'),
+      entertainmentRisk: getRiskLevelText(rd.entertainment?.level || '-'),
+      createTime: row.createTime ? formatToDateTime(row.createTime) : '',
+      creator: row.creator || '',
+      files: parseFileList(row.secondElderSign)
+    }
+  })
+
+  // 图片列数 = 所有行中签名图片的最大数量(无图片则不生成图片列)
+  const maxImages = rows.reduce((m, r) => Math.max(m, r.files.length), 0)
+  const imageColumns =
+    maxImages > 0
+      ? Array.from({ length: maxImages }, (_, i) => ({
+          header: maxImages > 1 ? `纸质版签名图片${i + 1}` : '纸质版签名图片',
+          key: `sign${i}`,
+          width: 22
+        }))
+      : []
+
+  // 文本列
+  worksheet.columns = [
+    { header: '序号', key: 'index', width: 8 },
+    { header: '长者姓名', key: 'elderName', width: 12 },
+    { header: '性别', key: 'elderSex', width: 8 },
+    { header: '年龄', key: 'elderAge', width: 8 },
+    { header: '床号', key: 'bedName', width: 14 },
+    { header: '合同号', key: 'contractNumber', width: 16 },
+    { header: '护理级别', key: 'nurseLevelName', width: 14 },
+    { header: '防噎食评估', key: 'asphyxiationRisk', width: 14 },
+    { header: '防压疮评估', key: 'pressureUlcerRisk', width: 14 },
+    { header: '防跌倒评估', key: 'fallRisk', width: 14 },
+    { header: '防坠床评估', key: 'bedFallRisk', width: 14 },
+    { header: '防烫伤评估', key: 'scaldRisk', width: 14 },
+    { header: '防走失评估', key: 'wanderingRisk', width: 14 },
+    { header: '防自伤他伤评估', key: 'selfHarmRisk', width: 16 },
+    { header: '防食品药品误食评估', key: 'foodDrugRisk', width: 18 },
+    { header: '防文娱活动意外', key: 'entertainmentRisk', width: 14 },
+    { header: '记录时间', key: 'createTime', width: 20 },
+    { header: '记录人', key: 'creator', width: 14 },
+    ...imageColumns
+  ]
+
+  // 表头样式
+  const headerRow = worksheet.getRow(1)
+  headerRow.font = { bold: true, size: 12 }
+  headerRow.fill = {
+    type: 'pattern',
+    pattern: 'solid',
+    fgColor: { argb: 'FFE0E0E0' }
+  }
+  headerRow.alignment = { horizontal: 'center', vertical: 'middle' }
+
+  const textColCount = 18 // 文本列数量,图片列从此之后开始
+  const imgStartCol0 = textColCount // 第一张图片列的 0-based 列号
+
+  for (let i = 0; i < rows.length; i++) {
+    const r = rows[i]
+    const row = worksheet.addRow({
+      index: r.index,
+      elderName: r.elderName,
+      elderSex: r.elderSex,
+      elderAge: r.elderAge,
+      bedName: r.bedName,
+      contractNumber: r.contractNumber,
+      nurseLevelName: r.nurseLevelName,
+      asphyxiationRisk: r.asphyxiationRisk,
+      pressureUlcerRisk: r.pressureUlcerRisk,
+      fallRisk: r.fallRisk,
+      bedFallRisk: r.bedFallRisk,
+      scaldRisk: r.scaldRisk,
+      wanderingRisk: r.wanderingRisk,
+      selfHarmRisk: r.selfHarmRisk,
+      foodDrugRisk: r.foodDrugRisk,
+      entertainmentRisk: r.entertainmentRisk,
+      createTime: r.createTime,
+      creator: r.creator
+    })
+    row.alignment = { vertical: 'middle', wrapText: true }
+
+    // 嵌入签名图片
+    if (r.files.length > 0) {
+      row.height = 100
+      for (let j = 0; j < r.files.length; j++) {
+        const file = r.files[j]
+        try {
+          const buf = await fetchImageAsBuffer(file.fileUrl)
+          if (buf) {
+            const imageId = workbook.addImage({
+              buffer: buf,
+              extension: getImageExtension(file.fileUrl)
+            })
+            await worksheet.addImage(imageId, {
+              tl: { col: imgStartCol0 + j + 0.1, row: i + 1 + 0.1 },
+              ext: { width: 90, height: 90 }
+            })
+          } else {
+            row.getCell(textColCount + j + 1).value = '图片加载失败'
+          }
+        } catch (e) {
+          console.error('加载签名图片失败:', file.fileUrl, e)
+          row.getCell(textColCount + j + 1).value = '图片加载失败'
+        }
+      }
+    }
+  }
+
+  const buffer = await workbook.xlsx.writeBuffer()
+  const blob = new Blob([buffer], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+  })
+  saveAs(blob, `安全风险告知书_${new Date().getTime()}.xlsx`)
+}
+
+/**
+ * 获取图片数据为 ArrayBuffer(用于嵌入 Excel)
+ */
+const fetchImageAsBuffer = async (url: string): Promise<ArrayBuffer | null> => {
+  try {
+    const fullUrl = url.startsWith('http') ? url : `${import.meta.env.VITE_BASE_URL}${url}`
+    const response = await fetch(fullUrl, {
+      headers: {
+        Authorization: 'Bearer ' + getAccessToken()
+      }
+    })
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`)
+    }
+    return await response.arrayBuffer()
+  } catch (e) {
+    console.error('获取图片失败:', url, e)
+    return null
+  }
+}
+
+/**
+ * 根据图片 URL 推断 Excel 支持的图片扩展名
+ */
+const getImageExtension = (url: string): 'png' | 'jpeg' => {
+  const ext = url.split('.').pop()?.toLowerCase()
+  if (ext === 'png') return 'png'
+  return 'jpeg'
+}
+
 /**
  * 根据风险等级获取按钮类型(颜色)
  * @param riskLevel 风险等级:none(无风险)、low(低风险)、medium(中风险)、high(高风险)

+ 165 - 13
src/views/elderly/contracts/ContractManageForm.vue

@@ -1,6 +1,8 @@
 <script setup lang="ts">
 import { reactive, ref, computed, nextTick } from 'vue'
+import { ElMessageBox } from 'element-plus'
 import ContractBody from './components/ContractBody.vue'
+import ContractBodyHubei from './components/ContractBodyHubei.vue'
 import AttachmentPage from './components/AttachmentPage.vue'
 import AttachmentSelector from './components/AttachmentSelector.vue'
 import RiskDisclosureBody from './components/RiskDisclosureBody.vue'
@@ -35,6 +37,7 @@ import {
   firstServiceProjectConfirmation_getById,
   changeConfirmationForm_getById
 } from '@/api/elderly/contracts'
+import { getBusinessParamsConfig } from '@/api/elderly/fee'
 
 const attachmentDetailMap: Record<string, (id: number | string) => Promise<any>> = {
   attach_1: riskDisclosure_getById,
@@ -60,6 +63,67 @@ const message = useMessage()
 const visible = ref(false)
 const formLoading = ref(false)
 
+// 合同类型:默认广州地区合同;保存到 contractInfo 后,下次打开据此渲染对应合同组件
+const contractType = ref('广州地区合同')
+// 从综合设置读取的默认合同区域
+const defaultContractType = ref('广州地区合同')
+const contractTypeOptions = [
+  { label: '广州地区合同', value: '广州地区合同' },
+  { label: '湖北地区合同', value: '湖北地区合同' },
+
+]
+// 初次加载时由子组件回传的合同类型(已保存的类型)
+const savedContractType = ref('广州地区合同')
+// 当前合同是否已存在(已有保存记录,二次修改需确认)
+const hasSavedContract = ref(false)
+// 是否已根据详情回传解析出合同类型:防止手动切换导致的子组件重挂载回传覆盖用户选择
+const contractTypeResolved = ref(false)
+
+// 子组件初次加载回传合同类型时,仅首次生效(解析保存的类型);其余回传忽略,避免回弹
+const handleContractTypeEmit = (val: string) => {
+  if (!contractTypeResolved.value) {
+    contractType.value = val
+    savedContractType.value = val
+    contractTypeResolved.value = true
+  }
+}
+
+// 子组件回传当前合同是否已存在
+const handleContractExists = (exists: boolean) => {
+  hasSavedContract.value = exists
+}
+
+// 用户手动切换合同类型:若已存在保存记录,需二次确认;取消则回退
+const handleContractTypeChange = async () => {
+  if (!hasSavedContract.value) return
+  if (contractType.value === savedContractType.value) return
+  try {
+    await ElMessageBox.confirm(
+      `当前合同已保存为「${savedContractType.value}」,确定要修改为「${contractType.value}」吗?修改后需要点击‘保存’才会生效。`,
+      '修改合同类型',
+      { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
+    )
+    // 确认:以新选择为准
+    savedContractType.value = contractType.value
+  } catch {
+    // 取消:回退到已保存类型
+    contractType.value = savedContractType.value
+  }
+}
+
+// 加载综合设置中的合同区域配置
+const loadContractTypeConfig = async () => {
+  try {
+    const res = await getBusinessParamsConfig()
+    const config = (res || []).find((item: any) => item.configName === '合同区域设置')
+    if (config && config.value2) {
+      defaultContractType.value = config.value2
+    }
+  } catch (e) {
+    console.error('加载合同区域配置失败', e)
+  }
+}
+
 const isTextMode = ref(false)
 const isPrint = ref(false)
 const contractBodyRef = ref<any>(null)
@@ -397,12 +461,19 @@ const selectorVisible = ref<{
 })
 
 const type_c = ref(1)
-const open = (row?: any, type = 1) => {
+const open = async (row?: any, type = 1) => {
   recordRow.value = row || null
   type_c.value = type
   isTextMode.value = type_c.value != 1
   formLoading.value = true
   isPrint.value = false
+  // 先加载综合设置中的默认合同区域
+  await loadContractTypeConfig()
+  // 每次打开重置合同类型状态,实际类型由详情加载后子组件回传解析
+  contractType.value = defaultContractType.value
+  savedContractType.value = defaultContractType.value
+  hasSavedContract.value = false
+  contractTypeResolved.value = false
   attachmentList.forEach((it) => {
     // 固定内容附件(3=入住须知)默认展示,不需要从列表选择
     if (it.no === 3) {
@@ -629,19 +700,58 @@ const renderAttachmentContent = (key: string) => {
 
       <div class="content-body">
         <!-- ============ 合同主体 ============ -->
-        <div class="section-divider">
-          <span>合同主体</span>
+        <div class="section-divider contract-type-bar">
+          <span class="bar-title">合同主体</span>
+          <div class="bar-action">
+            <span
+                class="bar-label"
+                style="color: #000000;background-color: #fff;border-radius: 4px;padding: 7px 12px;font-size: 12px;font-weight: bold;">合同类型:</span>
+            <el-select
+              v-model="contractType"
+              size="default"
+              style="width: 160px"
+              @change="handleContractTypeChange"
+            >
+              <el-option
+                v-for="opt in contractTypeOptions"
+                :key="opt.value"
+                :label="opt.label"
+                :value="opt.value"
+              />
+            </el-select>
+          </div>
         </div>
         <ContractBody
+          v-if="contractType !== '湖北地区合同'"
           ref="contractBodyRef"
           :record-row="recordRow"
           :is-text-mode="isTextMode"
           v-model="contractSignatureData"
           :attachment-ids="attachments"
           :visible="visible"
+          :contract-type="contractType"
+          :lock-contract-type="contractTypeResolved"
           @update:form-data="handFormData"
           @update:data="handleContractDataChange"
           @update:attachments="handleAttachmentsLoad"
+          @update:contract-type="handleContractTypeEmit"
+          @update:contract-exists="handleContractExists"
+        />
+        <ContractBodyHubei
+          v-else
+          ref="contractBodyRef"
+          :record-row="recordRow"
+          :is-text-mode="isTextMode"
+          v-model="contractSignatureData"
+          :attachment-ids="attachments"
+          :visible="visible"
+          :contract-type="contractType"
+          :lock-contract-type="contractTypeResolved"
+          @update:form-data="handFormData"
+          @update:data="handleContractDataChange"
+          @update:attachments="handleAttachmentsLoad"
+          @update:contract-type="handleContractTypeEmit"
+          @update:contract-exists="handleContractExists"
         />
 
         <!-- ============ 16个附件 ============ -->
@@ -767,15 +877,28 @@ const renderAttachmentContent = (key: string) => {
 
   <!-- ========== 打印容器:Teleport 到 body,确保是 body 直接子元素 ========== -->
   <Teleport to="body">
-    <div v-if="isPrint" class="print-container">
-      <!-- 合同主体 -->
-      <ContractBody
-        :record-row="recordRow"
-        :is-text-mode="true"
-        v-model="contractSignatureData"
-        :attachment-ids="attachments"
-        :visible="true"
-      />
+      <div v-if="isPrint" class="print-container">
+        <!-- 合同主体 -->
+        <ContractBody
+          v-if="contractType !== '湖北地区合同'"
+          :record-row="recordRow"
+          :is-text-mode="true"
+          v-model="contractSignatureData"
+          :attachment-ids="attachments"
+          :visible="true"
+          :contract-type="contractType"
+          :lock-contract-type="contractTypeResolved"
+        />
+        <ContractBodyHubei
+          v-else
+          :record-row="recordRow"
+          :is-text-mode="true"
+          v-model="contractSignatureData"
+          :attachment-ids="attachments"
+          :visible="true"
+          :contract-type="contractType"
+          :lock-contract-type="contractTypeResolved"
+        />
 
       <!-- 已选的附件 -->
       <template v-if="selectedAttachments.length > 0">
@@ -1061,7 +1184,6 @@ const renderAttachmentContent = (key: string) => {
   font-size: 15px;
   font-weight: bold;
   color: #666;
-  border-top: 1px dashed #ccc;
   position: relative;
 }
 
@@ -1073,6 +1195,36 @@ const renderAttachmentContent = (key: string) => {
   border: 1px solid #e4e7ed;
 }
 
+/* 合同主体栏:左侧标题、右侧合同类型下拉 */
+.section-divider.contract-type-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  text-align: left;
+}
+
+.section-divider.contract-type-bar .bar-title {
+  font-size: 15px;
+  font-weight: bold;
+  color: #666;
+}
+
+.section-divider.contract-type-bar .bar-action {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 13px;
+  font-weight: normal;
+  color: #555;
+}
+
+.section-divider.contract-type-bar .bar-label {
+  display: inline;
+  padding: 0;
+  background: none;
+  border: none;
+}
+
 .content-body {
   flex: 1;
   overflow-y: auto;

+ 71 - 6
src/views/elderly/contracts/components/ContractBody.vue

@@ -73,6 +73,9 @@ const props = defineProps<{
   modelValue?: any
   attachmentIds?: Record<string, any>
   visible?: boolean
+  contractType?: string
+  // 手动切换合同类型后,form.contractType 以父组件当前选择为准;初次加载时用存储的类型
+  lockContractType?: boolean
 }>()
 
 const emit = defineEmits<{
@@ -80,6 +83,8 @@ const emit = defineEmits<{
   (e: 'update:modelValue', val: any): void
   (e: 'update:attachments', val: any): void
   (e: 'update:formData', val: any): void
+  (e: 'update:contractType', val: string): void
+  (e: 'update:contractExists', val: boolean): void
 }>()
 
 const loading = ref(false)
@@ -91,6 +96,9 @@ const elderId = computed(() => {
 })
 
 const contractForm = reactive({
+  // 合同类型:广州地区合同 / 湖北地区合同 / 天河老人院合同(保存到 contractInfo 中,下次打开据此渲染对应合同组件)
+  // 默认从父组件传入的合同区域配置读取,未传入时兜底广州
+  contractType: props.contractType || '广州地区合同',
   contractNumber: '',
   elderName: '',
   elderGenderText: '',
@@ -190,6 +198,15 @@ const contractForm = reactive({
   stageExpenses: [] as any[]
 })
 
+// 父组件下拉切换合同类型时,同步到本地表单(保存时会写入 contractInfo)
+watch(
+  () => props.contractType,
+  (val) => {
+    if (val) contractForm.contractType = val
+  },
+  { immediate: true }
+)
+
 const parseIdCardBirth = (idCard: string): string => {
   if (!idCard) return ''
   const len = idCard.length
@@ -285,6 +302,42 @@ const fillContractForm = (data: any) => {
   detailData.value = data
   const contract = data.elderlyContractDO || {}
 
+  // 先清空可能从上份合同残留的数据,再重新回填
+  contractForm.elderGenderText = ''
+  contractForm.elderIdCard = ''
+  contractForm.elderPapersType = ''
+  contractForm.elderBirth = ''
+  contractForm.startDate = ''
+  contractForm.endDate = ''
+  contractForm.roomType = ''
+  contractForm.roomNo = ''
+  contractForm.totalFee = ''
+  contractForm.depositFee = ''
+  contractForm.depositAmt = ''
+  contractForm.depositAmtCn = ''
+  contractForm.depositAmt1 = ''
+  contractForm.depositAmtCn1 = ''
+  contractForm.depositAmt2 = ''
+  contractForm.depositAmtCn2 = ''
+  contractForm.depositType = ''
+  contractForm.partyCName = ''
+  contractForm.partyCGenderText = ''
+  contractForm.partyCBirth = ''
+  contractForm.partyCPapersType = ''
+  contractForm.partyCRelation = ''
+  contractForm.partyCIdCard = ''
+  contractForm.partyCPhone = ''
+  contractForm.partyCAddress = ''
+  contractForm.partyCHomeAddr = ''
+  contractForm.partyCZip = ''
+  contractForm.partyCWorkUnit = ''
+  contractForm.partyCCommAddr = ''
+  contractForm.partyCAccountName = ''
+  contractForm.partyCBank = ''
+  contractForm.partyCAccount = ''
+  contractForm.partyCSign = ''
+  contractForm.partyCDate = ''
+
   contractForm.contractNumber = contract.contractNumber || ''
   contractForm.elderName = contract.elderName || data.elderName || ''
 
@@ -541,6 +594,17 @@ const fetchContractData = async (row: any) => {
         }
       }
     }
+    // 手动切换合同类型后(父组件已解析完成),以父组件当前选择为准,避免沿用旧合同类型
+    if (props.lockContractType && props.contractType) {
+      contractForm.contractType = props.contractType
+    }
+    // 回传合同是否已存在(已有保存记录,父组件据此决定是否二次确认)
+    emit(
+      'update:contractExists',
+      !!(contractFormUpdate.value && contractFormUpdate.value.contractInfo)
+    )
+    // 回传合同类型给父组件,父组件据此切换渲染对应合同组件(广州/湖北/天河)
+    emit('update:contractType', contractForm.contractType || '广州地区合同')
     console.log('接口获取到的合同详情:', contractFormUpdate.value)
   } catch (e) {
     console.log(e)
@@ -682,9 +746,9 @@ const displayExpenseName = (item: any): string => {
   const category = item?.itemCategoryName || ''
   const name = item?.itemName || ''
   if (category && name && category !== name) {
-    return `${category}(${name})`
+    return `${category}(${name})`
   }
-  return category || name || ''
+  return category + '¥' || name + '¥' || ''
 }
 
 const bedList = ref<{ chargeName: string; id: string; price: number }[]>([])
@@ -780,7 +844,7 @@ const saveContract = async () => {
 
         if (row) {
           try {
-            const data = JSON.parse(row)
+            const data = typeof row === 'string' ? JSON.parse(row) : row
 
             if (data.id) {
               contractForm.attachments[key] = { id: data.id }
@@ -1304,7 +1368,7 @@ watch(
           3.根据本合同第一条、第二条乙方选择的房间、照护等级及服务项目,各方共同确认,乙方入住甲方需支付的养老服务费用具体如下:
         </p>
         <p class="content-text">
-          &nbsp;&nbsp;&nbsp;&nbsp;(1)养老服务费用为
+          &nbsp;&nbsp;&nbsp;&nbsp;(1)养老服务费用为:¥
           <FieldItem
             v-model="contractForm.totalFee"
             :is-text-mode="isTextMode"
@@ -1315,7 +1379,7 @@ watch(
         </p>
         <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;其中: </p>
         <p v-for="item in monthlyExpensesList" :key="item.id" class="content-text">
-          {{ item.itemCategoryName || item.itemName }}:
+          {{ item.itemCategoryName+'¥' || item.itemName+'¥' }}:
           <FieldItem
             v-model="item.actualAmount"
             :is-text-mode="isTextMode"
@@ -1417,8 +1481,9 @@ watch(
         </p>
         <p class="content-text"> 4.乙方应支付的阶段性费用及支付方式如下: </p>
         <div v-for="item in stageExpensesList" :key="item.id" class="content-text">
-          &nbsp;&nbsp;&nbsp;&nbsp;{{ displayExpenseName(item) }}
+          &nbsp;&nbsp;&nbsp;&nbsp;{{ displayExpenseName(item) }}:
           <template v-if="item.isFreeGift == 1">
+            ({{ item.amount }})
             赠送开始时间
             <FieldItem
               v-model="item.freeStartTime"

+ 2637 - 0
src/views/elderly/contracts/components/ContractBodyHubei.vue

@@ -0,0 +1,2637 @@
+<script setup lang="ts">
+import { reactive, computed, ref, watch, onMounted } from 'vue'
+import { formatBackendDateTime } from '@/utils/formatTime'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { getTenantId } from '@/utils/auth'
+import { getCheckInDetailByContractId } from '@/api/elderly/apply/check-in'
+import { getOverheadListByType } from '@/api/elderly/fee/overheadCharge'
+import FieldItem from './FieldItem.vue'
+import SignatureItem from './SignatureItem.vue'
+import { useUserStore } from '@/store/modules/user'
+import { getElderlyContractById } from '@/api/elderly/elder/contract'
+import { updateContractDate } from '@/api/elderly/fee/leave-settle'
+import {
+  riskDisclosure_getById,
+  checkInRegistration_getById,
+  elderSafetyCommitment_getById,
+  outingConfirmation_getById,
+  signatureProxyApplication_getById,
+  powerOfAttorney_getById,
+  delegationAgentConfirmation_getById,
+  serviceChargingStandards_getById,
+  roomFacilitiesEquipment_getById,
+  explanationExaminationReport_getById,
+  partyBIdentificationDocument_getById,
+  guardianPartyB_getById,
+  registrationCertificateCivilAffairs_getById,
+  comprehensiveAssessmentCapabilities_getById,
+  firstServiceProjectConfirmation_getById,
+  changeConfirmationForm_getById
+} from '@/api/elderly/contracts'
+const userStore = useUserStore()
+
+const attachmentConfig: Record<
+  string,
+  { idField: string; getDetail: (id: number | string) => Promise<any> }
+> = {
+  attach_1: { idField: 'riskDisclosureId', getDetail: riskDisclosure_getById },
+  attach_2: { idField: 'checkInTabId', getDetail: checkInRegistration_getById },
+  attach_4: { idField: 'safetyCommitmentId', getDetail: elderSafetyCommitment_getById },
+  attach_5: { idField: 'outingConfirmationFormId', getDetail: outingConfirmation_getById },
+  attach_6: {
+    idField: 'signatureProxyApplicationId',
+    getDetail: signatureProxyApplication_getById
+  },
+  attach_7: { idField: 'powerOfAttorneyId', getDetail: powerOfAttorney_getById },
+  attach_8: {
+    idField: 'authorizedAgentConfirmationId',
+    getDetail: delegationAgentConfirmation_getById
+  },
+  attach_9: { idField: 'orgServiceScopeFeeId', getDetail: serviceChargingStandards_getById },
+  attach_10: { idField: 'roomFacilityListId', getDetail: roomFacilitiesEquipment_getById },
+  attach_11: { idField: 'physicalExamReportId', getDetail: explanationExaminationReport_getById },
+  attach_12: { idField: 'partyBCertificateId', getDetail: partyBIdentificationDocument_getById },
+  attach_13: { idField: 'partyBGuardianCertificateId', getDetail: guardianPartyB_getById },
+  attach_14: {
+    idField: 'partyACertificateId',
+    getDetail: registrationCertificateCivilAffairs_getById
+  },
+  attach_15: {
+    idField: 'elderlyAbilityEvaluationId',
+    getDetail: comprehensiveAssessmentCapabilities_getById
+  },
+  attach_16: {
+    idField: 'firstServiceConfirmationId',
+    getDetail: firstServiceProjectConfirmation_getById
+  },
+  attach_17: { idField: 'changeConfirmationId', getDetail: changeConfirmationForm_getById }
+}
+const props = defineProps<{
+  recordRow?: any
+  isTextMode?: boolean
+  signatureData?: any
+  modelValue?: any
+  attachmentIds?: Record<string, any>
+  visible?: boolean
+  contractType?: string
+  // 手动切换合同类型后,form.contractType 以父组件当前选择为准;初次加载时用存储的类型
+  lockContractType?: boolean
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:data', val: any): void
+  (e: 'update:modelValue', val: any): void
+  (e: 'update:attachments', val: any): void
+  (e: 'update:formData', val: any): void
+  (e: 'update:contractType', val: string): void
+  (e: 'update:contractExists', val: boolean): void
+}>()
+
+const loading = ref(false)
+const detailData = ref<any>(null)
+
+const elderId = computed(() => {
+  if (!props.recordRow) return ''
+  return props.recordRow.elderId || props.recordRow.id || ''
+})
+
+const contractForm = reactive({
+  // 合同类型:广州地区合同 / 湖北地区合同 / 天河老人院合同(保存到 contractInfo 中,下次打开据此渲染对应合同组件)
+  // 默认从父组件传入的合同区域配置读取,未传入时兜底广州
+  contractType: props.contractType || '广州地区合同',
+  contractNumber: '',
+  elderName: '',
+  elderGenderText: '',
+  elderBirth: '',
+  elderPapersType: '',
+  elderIdCard: '',
+  elderAge: '',
+  elderAddress: '',
+  elderZip: '',
+  partyAName: '颐年健康产业(集团)有限公司',
+  partyACode: '',
+  partyAAddr: '',
+  partyAZip: '',
+  partyALegal: '',
+  partyAPhone: '',
+  partyCName: '',
+  partyCGenderText: '',
+  partyCBirth: '',
+  partyCPapersType: '',
+  partyCRelation: '',
+  partyCIdCard: '',
+  partyCPhone: '',
+  partyCAddress: '',
+  partyCHomeAddr: '',
+  partyCZip: '',
+  partyCWorkUnit: '',
+  partyCCommAddr: '',
+  nurseLevelName: '',
+  contractMonths: '',
+  startDate: '',
+  endDate: '',
+  servicePlace: userStore.getTenantName || '',
+  bedFee: '',
+  nurseFee: '',
+  mealFee: '',
+  manageFee: '',
+  totalFee: '',
+  // 费用相关扩展字段
+  roomType: '',
+  roomNo: '',
+  freeElectric: '',
+  freeWater: '',
+  hotWaterFee: '',
+  coldWaterFee: '',
+  electricFee: '',
+  diagnosisFee: '',
+  oneTimeEquipFee: '',
+  oneTimeEquipStart: '',
+  oneTimeEquipEnd: '',
+  depositFee: '',
+  depositType: '',
+  depositInterest: '',
+  depositInterestType: '0',
+  payMode: '1',
+  payMonthDay: '5',
+  paySeasonMonth: '1',
+  paySeasonDay: '5',
+  partyAAccountName: '',
+  partyABank: '',
+  partyAAccount: '',
+  partyCAccountName: '',
+  partyCBank: '',
+  partyCAccount: '',
+  depositRefund: '',
+  otherPayMode: '',
+  otherPayMode2: '',
+  // 合同续签/变更/解除
+  renewNoticeDays: '30',
+  moveOutDays: '3',
+  changeConfirmDays: '7',
+  priceAdjustNoticeDays: '30',
+  priceThreshold: '6',
+  priceObjDays: '15',
+  priceConfirmDays: '15',
+  leaveDays: '60',
+  depositAmt: '',
+  depositAmtCn: '',
+  depositAmt1: '',
+  depositAmtCn1: '',
+  depositAmt2: '',
+  depositAmtCn2: '',
+  disputeMode: '1',
+  disputeCourt: '',
+  disputeArbitration: '',
+  copyCount: '叁',
+  partyACopy: '壹',
+  partyBCopy: '壹',
+  partyCCopy: '壹',
+  partyASign: '',
+  partyALegalSign: '',
+  partyADate: '',
+  partyBSign: '',
+  partyBDate: '',
+  partyCSign: '',
+  partyCDate: '',
+  // 费用明细列表
+  monthlyExpenses: [] as any[],
+  oneTimeExpenses: [] as any[],
+  stageExpenses: [] as any[],
+  // ===== 2024 湖北版新增字段 =====
+  // 甲方扩展信息
+  partyARecordNo: '',
+  partyAEmail: '',
+  partyAContactPhone: '',
+  // 乙方扩展信息(户籍所在地与家庭住址分离、联系方式)
+  elderCensusAddr: '',
+  elderPhone: '',
+  elderEmail: '',
+  // 房间楼层 / 床位号
+  roomFloor: '',
+  bedNo: '',
+  // 合同期限 / 试住期 / 续约
+  trialDays: '7',
+  trialStart: '',
+  trialEnd: '',
+  renewDays: '15',
+  // 重大事项:紧急联系人
+  emergencyContactName: '',
+  emergencyContactPhone: '',
+  emergencyContactAddr: '',
+  // 会员费(3.5,可选)
+  memberFeeAmt: '',
+  memberFeeCn: '',
+  memberDepositBank: '',
+  memberDepositAccount: '',
+  memberFeeRefundMode: '',
+  // 违约责任(10.5 / 10.6 中的可填比例)
+  penaltyRate: '',
+  penaltyPercent: ''
+})
+
+// 父组件下拉切换合同类型时,同步到本地表单(保存时会写入 contractInfo)
+watch(
+  () => props.contractType,
+  (val) => {
+    if (val) contractForm.contractType = val
+  },
+  { immediate: true }
+)
+
+const parseIdCardBirth = (idCard: string): string => {
+  if (!idCard) return ''
+  const len = idCard.length
+  if (len === 18) {
+    return `${idCard.substring(6, 10)}年${idCard.substring(10, 12)}月${idCard.substring(12, 14)}日`
+  }
+  if (len === 15) {
+    return `19${idCard.substring(6, 8)}年${idCard.substring(8, 10)}月${idCard.substring(10, 12)}日`
+  }
+  return ''
+}
+
+const parseChineseDate = (dateStr: string): Date | null => {
+  if (!dateStr) return null
+  const match = dateStr.match(/^(\d{4})年(\d{2})月(\d{2})日$/)
+  if (!match) return null
+  const year = Number(match[1])
+  const month = Number(match[2]) - 1
+  const day = Number(match[3])
+  return new Date(year, month, day)
+}
+
+const calcMonthsBetween = (startStr: string, endStr: string): number | null => {
+  const start = parseChineseDate(startStr)
+  const end = parseChineseDate(endStr)
+  if (!start || !end || end < start) return null
+  // 直接按"年月差"计算整月数,末尾差几天不影响(如 2026-08-15 → 2028-08-14 = 24 个月)
+  return (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth())
+}
+
+const isIdCardType = (papersType: any): boolean => {
+  const label = getDictLabel(DICT_TYPE.PAPERS_TYPE, papersType)
+  return !!label && (label.includes('身份证') || label.includes('居民身份'))
+}
+
+const numToChinese = (num: number | string): string => {
+  const n = Number(num)
+  if (isNaN(n) || n < 0) return ''
+  if (n === 0) return '零元整'
+  const fractions = ['角', '分']
+  const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
+  const radices = ['', '拾', '佰', '仟']
+  const bigRadices = ['', '万', '亿']
+  const dStr = n.toFixed(2)
+  const parts = dStr.split('.')
+  let intPart = parts[0]
+  let decPart = parts[1]
+  let chinese = ''
+  let zeroFlag = false
+  for (let i = 0; i < intPart.length; i++) {
+    const ch = intPart.charAt(i)
+    const numDigit = parseInt(ch)
+    const pos = intPart.length - i - 1
+    const radixIdx = pos % 4
+    const bigRadixIdx = Math.floor(pos / 4)
+    if (numDigit === 0) {
+      zeroFlag = true
+    } else {
+      if (zeroFlag) {
+        chinese += '零'
+        zeroFlag = false
+      }
+      chinese += digit[numDigit] + radices[radixIdx]
+    }
+    if (radixIdx === 0 && numDigit !== 0) {
+      chinese += bigRadices[bigRadixIdx]
+    } else if (radixIdx === 0 && intPart.length > 4) {
+      const segStart = Math.max(0, intPart.length - (bigRadixIdx + 1) * 4)
+      const seg = intPart.substring(segStart, intPart.length - bigRadixIdx * 4)
+      if (parseInt(seg) > 0) {
+        chinese += bigRadices[bigRadixIdx]
+      }
+    }
+  }
+  chinese = chinese.replace(/零+/g, '零').replace(/零$/, '')
+  chinese += '元'
+  if (decPart === '00') {
+    chinese += '整'
+  } else {
+    const jiao = parseInt(decPart.charAt(0))
+    const fen = parseInt(decPart.charAt(1))
+    if (jiao > 0) chinese += digit[jiao] + fractions[0]
+    if (fen > 0) chinese += digit[fen] + fractions[1]
+    if (jiao === 0 && fen > 0) {
+      // 零元几分的情况
+    }
+  }
+  return chinese
+}
+
+const fillContractForm = (data: any) => {
+  if (!data) return
+  detailData.value = data
+  const contract = data.elderlyContractDO || {}
+
+  // 先清空可能从上份合同残留的数据,再重新回填
+  contractForm.elderGenderText = ''
+  contractForm.elderIdCard = ''
+  contractForm.elderPapersType = ''
+  contractForm.elderBirth = ''
+  contractForm.startDate = ''
+  contractForm.endDate = ''
+  contractForm.roomType = ''
+  contractForm.roomNo = ''
+  contractForm.totalFee = ''
+  contractForm.depositFee = ''
+  contractForm.depositAmt = ''
+  contractForm.depositAmtCn = ''
+  contractForm.depositAmt1 = ''
+  contractForm.depositAmtCn1 = ''
+  contractForm.depositAmt2 = ''
+  contractForm.depositAmtCn2 = ''
+  contractForm.depositType = ''
+  contractForm.partyCName = ''
+  contractForm.partyCGenderText = ''
+  contractForm.partyCBirth = ''
+  contractForm.partyCPapersType = ''
+  contractForm.partyCRelation = ''
+  contractForm.partyCIdCard = ''
+  contractForm.partyCPhone = ''
+  contractForm.partyCAddress = ''
+  contractForm.partyCHomeAddr = ''
+  contractForm.partyCZip = ''
+  contractForm.partyCWorkUnit = ''
+  contractForm.partyCCommAddr = ''
+  contractForm.partyCAccountName = ''
+  contractForm.partyCBank = ''
+  contractForm.partyCAccount = ''
+  contractForm.partyCSign = ''
+  contractForm.partyCDate = ''
+
+  contractForm.contractNumber = contract.contractNumber || ''
+  contractForm.elderName = contract.elderName || data.elderName || ''
+
+  if (data.elderSex) {
+    contractForm.elderGenderText = Number(data.elderSex) === 1 ? '男' : '女'
+  }
+
+  if (data.idCard) {
+    contractForm.elderIdCard = data.idCard
+  }
+  if (data.papersType != null && data.papersType !== '' && data.papersType !== undefined) {
+    const papersNum = Number(data.papersType)
+    contractForm.elderPapersType = Number.isNaN(papersNum) ? data.papersType : papersNum
+  }
+  if (data.idCard && isIdCardType(contractForm.elderPapersType)) {
+    contractForm.elderBirth = parseIdCardBirth(data.idCard)
+  }
+
+  contractForm.elderAge = data.elderAge || ''
+  contractForm.elderAddress = data.elderAddress || ''
+  contractForm.elderZip = data.elderZip || ''
+
+  contractForm.nurseLevelName = data.nurseLevelName || ''
+
+  if (data.checkInTime) {
+    contractForm.startDate = formatBackendDateTime(data.checkInTime, 'YYYY年MM月DD日')
+  }
+  if (data.checkInDeadlineTime) {
+    contractForm.endDate = formatBackendDateTime(data.checkInDeadlineTime, 'YYYY年MM月DD日')
+  }
+
+  if (contract.beginTime) {
+    contractForm.startDate = formatBackendDateTime(contract.beginTime, 'YYYY年MM月DD日')
+  }
+  if (contract.expireTime) {
+    contractForm.endDate = formatBackendDateTime(contract.expireTime, 'YYYY年MM月DD日')
+  }
+
+  const monthly = data.monthlyExpenses || []
+  const findItem = (list: any[], key: string): any => {
+    return list.find(
+      (x: any) => (x.itemName || '').includes(key) || (x.itemCategoryName || '').includes(key)
+    )
+  }
+  const findAmt = (list: any[], key: string): string => {
+    const item = findItem(list, key)
+    return item ? String(item.actualAmount || item.amount || '') : ''
+  }
+  contractForm.bedFee = findAmt(monthly, '床位') || ''
+  contractForm.nurseFee = findAmt(monthly, '护理') || ''
+  contractForm.mealFee = findAmt(monthly, '膳食') || ''
+  contractForm.manageFee = findAmt(monthly, '服务') || ''
+
+  const total = monthly.reduce(
+    (sum: number, it: any) => sum + (Number(it.actualAmount) || Number(it.amount) || 0),
+    0
+  )
+  contractForm.totalFee = total > 0 ? String(total) : ''
+
+  const bedItem = findItem(monthly, '床位')
+  if (bedItem && bedItem.itemName) {
+    const m = bedItem.itemName.match(/^(.+?)\(/)
+    contractForm.roomType = m ? m[1] : bedItem.itemName
+  }
+  console.log('ADDD', data)
+  if (data.bedName) {
+    contractForm.roomNo = data.bedName
+  } else if (data.bedId) {
+    contractForm.roomNo = contractFormUpdate.value.bedName
+  }
+  if (data.bedType) {
+    contractForm.roomType = data.bedType
+  }
+
+  contractForm.monthlyExpenses = data.monthlyExpenses || []
+  //contractForm.oneTimeExpenses = data.oneTimeExpenses || []
+  contractForm.stageExpenses = data.stageExpenses || []
+  //天河颐养院需要过滤掉‘一次性设施费’
+  const oneTime = data.oneTimeExpenses || []
+  const filteredOneTime = oneTime.filter((item: { itemName: string; itemCategoryName: string }) => {
+    const name = item.itemName || ''
+    const category = item.itemCategoryName || ''
+    return !name.includes('一次性设施费') && !category.includes('一次性设施费')
+  })
+  contractForm.oneTimeExpenses = filteredOneTime || []
+  //console.log('一次性:', contractForm.oneTimeExpenses)
+
+  //const deposit = findItem(oneTime, '押金')
+  const firstItem = filteredOneTime.length > 0 ? filteredOneTime[0] : undefined // 取出数组第一个元素(对象)
+  if (firstItem) {
+    const depositAmt = Number(firstItem.actualAmount || firstItem.amount || 0)
+    contractForm.depositFee = String(depositAmt)
+    contractForm.depositAmt = String(depositAmt)
+    contractForm.depositAmtCn = numToChinese(depositAmt)
+    contractForm.depositAmt1 = String(depositAmt)
+    contractForm.depositAmtCn1 = numToChinese(depositAmt)
+    contractForm.depositAmt2 = String(depositAmt)
+    contractForm.depositAmtCn2 = numToChinese(depositAmt)
+    contractForm.depositType = firstItem.itemName || ''
+  }
+
+  const relative = (data.relativesList && data.relativesList[0]) || {}
+  contractForm.partyCName = relative.name || ''
+  if (relative.gender) {
+    contractForm.partyCGenderText = Number(relative.gender) === 1 ? '男' : '女'
+  }
+  if (relative.idCard) {
+    contractForm.partyCIdCard = relative.idCard
+  }
+  if (
+    relative.papersType != null &&
+    relative.papersType !== '' &&
+    relative.papersType !== undefined
+  ) {
+    const papersNum = Number(relative.papersType)
+    contractForm.partyCPapersType = Number.isNaN(papersNum) ? relative.papersType : papersNum
+  }
+  if (relative.idCard && isIdCardType(contractForm.partyCPapersType)) {
+    contractForm.partyCBirth = parseIdCardBirth(relative.idCard)
+  }
+  contractForm.partyCRelation = relative.relation || ''
+  contractForm.partyCPhone = relative.phone || ''
+  contractForm.partyCAddress = relative.address || relative.nowAddress || ''
+  contractForm.partyCHomeAddr = relative.nowAddress || ''
+  contractForm.partyCWorkUnit = relative.workUnit || ''
+
+  // ===== 2024 湖北版新增字段映射(尽量从后端/已存合同信息回填) =====
+  contractForm.partyARecordNo = data.partyARecordNo || contract.partyARecordNo || ''
+  contractForm.partyAEmail = data.partyAEmail || contract.partyAEmail || ''
+  contractForm.partyAContactPhone = data.partyAContactPhone || contract.partyAContactPhone || ''
+  contractForm.elderCensusAddr =
+    data.elderCensusAddr || contract.elderCensusAddr || data.censusAddr || ''
+  contractForm.elderPhone = data.elderPhone || contract.elderPhone || ''
+  contractForm.elderEmail = data.elderEmail || contract.elderEmail || ''
+  contractForm.roomFloor = data.roomFloor || contract.roomFloor || ''
+  contractForm.bedNo = data.bedNo || contract.bedNo || ''
+  contractForm.trialDays = data.trialDays || contract.trialDays || '7'
+  contractForm.trialStart = data.trialStart || contract.trialStart || ''
+  contractForm.trialEnd = data.trialEnd || contract.trialEnd || ''
+  contractForm.renewDays = data.renewDays || contract.renewDays || '15'
+  contractForm.emergencyContactName =
+    data.emergencyContactName || contract.emergencyContactName || ''
+  contractForm.emergencyContactPhone =
+    data.emergencyContactPhone || contract.emergencyContactPhone || ''
+  contractForm.emergencyContactAddr =
+    data.emergencyContactAddr || contract.emergencyContactAddr || ''
+  contractForm.memberFeeAmt = data.memberFeeAmt || contract.memberFeeAmt || ''
+  contractForm.memberFeeCn = data.memberFeeCn || contract.memberFeeCn || ''
+  contractForm.memberDepositBank = data.memberDepositBank || contract.memberDepositBank || ''
+  contractForm.memberDepositAccount =
+    data.memberDepositAccount || contract.memberDepositAccount || ''
+  contractForm.memberFeeRefundMode = data.memberFeeRefundMode || contract.memberFeeRefundMode || ''
+  contractForm.penaltyRate = data.penaltyRate || contract.penaltyRate || ''
+  contractForm.penaltyPercent = data.penaltyPercent || contract.penaltyPercent || ''
+  contractForm.disputeCourt = data.disputeCourt || contract.disputeCourt || ''
+  contractForm.disputeArbitration = data.disputeArbitration || contract.disputeArbitration || ''
+
+  // 如果外部传入了签名数据(如打印时),覆盖合同表单中的签名字段
+  // 优先使用 v-model 传入的 modelValue,其次是 signatureData
+  const externalSignData = props.modelValue || props.signatureData
+  if (externalSignData) {
+    if (externalSignData.partyASign != null && externalSignData.partyASign !== '')
+      contractForm.partyASign = externalSignData.partyASign
+    if (externalSignData.partyALegalSign != null && externalSignData.partyALegalSign !== '')
+      contractForm.partyALegalSign = externalSignData.partyALegalSign
+    if (externalSignData.partyADate != null && externalSignData.partyADate !== '')
+      contractForm.partyADate = externalSignData.partyADate
+    if (externalSignData.partyBSign != null && externalSignData.partyBSign !== '')
+      contractForm.partyBSign = externalSignData.partyBSign
+    if (externalSignData.partyBDate != null && externalSignData.partyBDate !== '')
+      contractForm.partyBDate = externalSignData.partyBDate
+    if (externalSignData.partyCSign != null && externalSignData.partyCSign !== '')
+      contractForm.partyCSign = externalSignData.partyCSign
+    if (externalSignData.partyCDate != null && externalSignData.partyCDate !== '')
+      contractForm.partyCDate = externalSignData.partyCDate
+  }
+
+  emit('update:data', { ...contractForm })
+}
+
+const fetchContractData = async (row: any) => {
+  if (!row) return
+  // 切换人员/重新打开抽屉时,先清空上一个人的附件缓存,避免残留到当前合同
+  contractForm.attachments = {}
+  // // 如果已经有完整数据(如直接返回了elderlyContractDO),直接填充
+  // if (row.elderlyContractDO) {
+  //   console.log('有历史完整数据,直接填充')
+  //   fillContractForm(row)
+  //   return
+  // }
+  // console.log('合同id', row)
+  // // 否则调用接口获取
+  // const recordId = row.id
+  // if (!recordId) {
+  //   console.log('没有id,使用历史数据填充?')
+  //   fillContractForm(row)
+  //   return
+  // }
+  loading.value = true
+  try {
+    const res = await getElderlyContractById(props.recordRow.id)
+    //console.log('合同信息接口:', res)
+    //console.log('合同信息接口:', JSON.parse(res.contractInfo))
+    if (res) {
+      contractFormUpdate.value = res
+      // console.log('接口获取合同信息:', contractFormUpdate.value.contractInfo)
+      contractFormUpdate.value.id = props.recordRow.id
+      if (contractFormUpdate.value.contractInfo) {
+        try {
+          const parsedData = JSON.parse(contractFormUpdate.value.contractInfo)
+          //天河颐养院需要过滤掉‘一次性设施费’
+          const oneTime = parsedData.oneTimeExpenses || []
+          const filteredOneTime = oneTime.filter(
+            (item: { itemName: string; itemCategoryName: string }) => {
+              const name = item.itemName || ''
+              const category = item.itemCategoryName || ''
+              return !name.includes('一次性设施费') && !category.includes('一次性设施费')
+            }
+          )
+          parsedData.oneTimeExpenses = filteredOneTime || []
+          // console.log('一次性的:', parsedData.oneTimeExpenses)
+
+          if (parsedData) {
+            Object.assign(contractForm, parsedData)
+          }
+        } catch (e) {
+          console.error('解析合同信息失败', e)
+        }
+        const attachmentsMap: Record<string, any> = {}
+        const detailPromises: Promise<any>[] = []
+        Object.keys(attachmentConfig).forEach((key) => {
+          const config = attachmentConfig[key]
+          //console.log('config', config, key)
+          let id = undefined
+          try {
+            id = contractForm.attachments[key].id
+          } catch (e) {}
+          //console.log('id', id)
+          if (id) {
+            detailPromises.push(
+              config
+                .getDetail(id)
+                .then((detail) => {
+                  //console.log('获取详情', detail)
+                  attachmentsMap[key] = JSON.parse(detail.content)
+                })
+                .catch((err) => {
+                  console.error(`获取附件详情失败: ${key}`, err)
+                  attachmentsMap[key] = { id }
+                })
+            )
+          } else {
+            attachmentsMap[key] = null
+          }
+        })
+        if (detailPromises.length > 0) {
+          await Promise.all(detailPromises)
+        }
+
+        emit('update:attachments', attachmentsMap)
+        loading.value = false
+      } else {
+        console.log('保存')
+        try {
+          const checkInRes = await getCheckInDetailByContractId(row.id)
+          if (checkInRes) {
+            if (checkInRes) {
+            }
+            fillContractForm(checkInRes)
+          }
+          contractForm.roomNo = contractFormUpdate.value.bedName
+
+          try {
+            contractFormUpdate.value.contractInfo = JSON.stringify({ ...contractForm })
+            const saveRes = await updateContractDate(contractFormUpdate.value)
+            if (saveRes) {
+            }
+          } catch (error) {
+            console.log(error)
+          }
+        } catch (e) {
+          console.error('获取合同详情失败', e)
+        } finally {
+          loading.value = false
+        }
+      }
+    }
+    // 手动切换合同类型后(父组件已解析完成),以父组件当前选择为准,避免沿用旧合同类型
+    if (props.lockContractType && props.contractType) {
+      contractForm.contractType = props.contractType
+    }
+    // 回传合同是否已存在(已有保存记录,父组件据此决定是否二次确认)
+    emit(
+      'update:contractExists',
+      !!(contractFormUpdate.value && contractFormUpdate.value.contractInfo)
+    )
+    // 回传合同类型给父组件,父组件据此切换渲染对应合同组件(广州/湖北/天河)
+    emit('update:contractType', contractForm.contractType || '广州地区合同')
+    console.log('接口获取到的合同详情:', contractFormUpdate.value)
+  } catch (e) {
+    console.log(e)
+  }
+}
+
+// 用于避免抽屉关闭/打开相同数据时重复请求,同时在重新打开时强制刷新
+let lastFetchedKey: string | number | undefined
+
+const tryFetchContractData = () => {
+  const row = props.recordRow
+  if (!row || !props.visible) return
+  const key = row.id ?? row.elderId
+  if (key === lastFetchedKey) return
+  lastFetchedKey = key
+  fetchContractData(row)
+}
+
+watch(() => props.recordRow, tryFetchContractData, { immediate: true })
+watch(
+  () => props.visible,
+  (val) => {
+    if (!val) {
+      // 抽屉关闭后重置标记,下次打开同一条数据会重新请求接口
+      lastFetchedKey = undefined
+    } else {
+      tryFetchContractData()
+    }
+  }
+)
+
+watch(
+  () => props.signatureData,
+  (val) => {
+    if (!val) return
+    if (val.partyASign) contractForm.partyASign = val.partyASign
+    if (val.partyALegalSign) contractForm.partyALegalSign = val.partyALegalSign
+    if (val.partyADate) contractForm.partyADate = val.partyADate
+    if (val.partyBSign) contractForm.partyBSign = val.partyBSign
+    if (val.partyBDate) contractForm.partyBDate = val.partyBDate
+    if (val.partyCSign) contractForm.partyCSign = val.partyCSign
+    if (val.partyCDate) contractForm.partyCDate = val.partyCDate
+  },
+  { deep: true }
+)
+
+watch(
+  () => contractForm.depositAmt1,
+  (val) => {
+    const n = Number(val)
+    if (!isNaN(n) && n >= 0) {
+      contractForm.depositAmtCn1 = numToChinese(n)
+    }
+  }
+)
+
+watch(
+  () => contractForm.depositAmt2,
+  (val) => {
+    const n = Number(val)
+    if (!isNaN(n) && n >= 0) {
+      contractForm.depositAmtCn2 = numToChinese(n)
+    }
+  }
+)
+
+// 会员费金额变化时,同步大写金额
+watch(
+  () => contractForm.memberFeeAmt,
+  (val) => {
+    const n = Number(val)
+    if (!isNaN(n) && n >= 0) {
+      contractForm.memberFeeCn = numToChinese(n)
+    }
+  }
+)
+
+// 外部 modelValue 变化时,同步签名和日期字段到 contractForm
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (!val) return
+    if (val.partyASign != null && val.partyASign !== '') contractForm.partyASign = val.partyASign
+    if (val.partyALegalSign != null && val.partyALegalSign !== '')
+      contractForm.partyALegalSign = val.partyALegalSign
+    if (val.partyADate != null && val.partyADate !== '') contractForm.partyADate = val.partyADate
+    if (val.partyBSign != null && val.partyBSign !== '') contractForm.partyBSign = val.partyBSign
+    if (val.partyBDate != null && val.partyBDate !== '') contractForm.partyBDate = val.partyBDate
+    if (val.partyCSign != null && val.partyCSign !== '') contractForm.partyCSign = val.partyCSign
+    if (val.partyCDate != null && val.partyCDate !== '') contractForm.partyCDate = val.partyCDate
+  },
+  { immediate: true, deep: true }
+)
+
+// 签名或日期变化时,同步给父组件(用于打印时共享签名数据)
+watch(
+  () => [
+    contractForm.partyASign,
+    contractForm.partyALegalSign,
+    contractForm.partyADate,
+    contractForm.partyBSign,
+    contractForm.partyBDate,
+    contractForm.partyCSign,
+    contractForm.partyCDate
+  ],
+  () => {
+    emit('update:data', { ...contractForm })
+    emit('update:modelValue', {
+      partyASign: contractForm.partyASign,
+      partyALegalSign: contractForm.partyALegalSign,
+      partyADate: contractForm.partyADate,
+      partyBSign: contractForm.partyBSign,
+      partyBDate: contractForm.partyBDate,
+      partyCSign: contractForm.partyCSign,
+      partyCDate: contractForm.partyCDate
+    })
+  }
+)
+
+const displayDate = computed(() => {
+  const today = new Date()
+  return `${today.getFullYear()}年${today.getMonth() + 1}月${today.getDate()}日`
+})
+
+// 费用相关动态列表
+const monthlyExpensesList = computed(() => contractForm.monthlyExpenses || [])
+const oneTimeExpensesList = computed(() => contractForm.oneTimeExpenses || [])
+const stageExpensesList = computed(() => contractForm.stageExpenses || [])
+
+const formatExpenseAmount = (val: any): string => {
+  if (val == null || val === '') return ''
+  return String(val)
+}
+
+const isDepositFee = (item: any): boolean => {
+  return (
+    item.isDeposit === 1 ||
+    (item.itemCategoryName || '').includes('押金') ||
+    (item.itemName || '').includes('押金')
+  )
+}
+
+const numberToCircle = (num: number): string => {
+  const circles = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩']
+  return circles[num - 1] || `${num}`
+}
+
+// 费用项显示名称:类别名(项目名)
+const displayExpenseName = (item: any): string => {
+  const category = item?.itemCategoryName || ''
+  const name = item?.itemName || ''
+  if (category && name && category !== name) {
+    return `${category}(${name})¥`
+  }
+  return category + '¥' || name + '¥' || ''
+}
+
+const bedList = ref<{ chargeName: string; id: string; price: number }[]>([])
+
+const loadBedTypes = async () => {
+  try {
+    const tenantId = getTenantId()
+    const res = await getOverheadListByType(1, tenantId)
+    bedList.value = res || []
+  } catch (e) {
+    bedList.value = []
+  }
+}
+
+const roomTypeOptions = computed(() => {
+  return bedList.value.map((item) => ({ label: item.chargeName, value: item.chargeName }))
+})
+
+const isAttachmentSelected = (no: number) => !!props.attachmentIds?.[`attach_${no}`]
+
+const attachmentTitleMap: Record<number, { title: string; subTitle?: string }> = {
+  1: { title: '《知情同意书》' },
+  2: { title: '《入住登记表》' },
+  3: { title: '《入住须知》' },
+  4: { title: '《长者安全承诺书》' },
+  5: { title: '《长者外出情况确认书》' },
+  6: { title: '《签名代理申请》' },
+  7: { title: '《授权委托书》' },
+  8: { title: '《委托代理人确认表》' },
+  9: { title: '《机构服务范围及收费标准》' },
+  10: { title: '《房间设施设备清单》' },
+  11: {
+    title: '二级甲等以上医院出具的《体检报告项目说明》',
+    subTitle: '(体检时间应在签订服务合同前30日以内)'
+  },
+  12: { title: '乙方有效证件(身份证、户口本)复印件' },
+  13: { title: '乙方监护人(丙方)身份证、户口本复印件' },
+  14: { title: '甲方登记证书及民政部门备案回执' },
+  15: { title: '老年人能力综合评估结果' },
+  16: { title: '首次服务项目确认表' },
+  17: { title: '变更事项确认表' }
+}
+
+const toChineseNumber = (num: number): string => {
+  const chars = [
+    '零',
+    '一',
+    '二',
+    '三',
+    '四',
+    '五',
+    '六',
+    '七',
+    '八',
+    '九',
+    '十',
+    '十一',
+    '十二',
+    '十三',
+    '十四',
+    '十五',
+    '十六',
+    '十七'
+  ]
+  return chars[num] || String(num)
+}
+
+const selectedAttachmentItems = computed(() => {
+  const items: { no: number; title: string; subTitle?: string }[] = []
+  for (let no = 1; no <= 17; no++) {
+    if (isAttachmentSelected(no)) {
+      const config = attachmentTitleMap[no]
+      if (config) {
+        items.push({ no, ...config })
+      }
+    }
+  }
+  return items
+})
+
+const page24AttachmentItems = computed(() => selectedAttachmentItems.value.slice(0, 14))
+const page25AttachmentItems = computed(() => selectedAttachmentItems.value.slice(14))
+
+const contractFormUpdate = ref({})
+
+const saveContract = async () => {
+  try {
+    // 保存时只保留当前选中的附件,防止历史/上一个人的附件残留
+    contractForm.attachments = {}
+    if (props.attachmentIds) {
+      Object.keys(props.attachmentIds).forEach((key) => {
+        const row = props.attachmentIds![key]
+
+        if (row) {
+          try {
+            const data = typeof row === 'string' ? JSON.parse(row) : row
+
+            if (data.id) {
+              contractForm.attachments[key] = { id: data.id }
+              const config = attachmentConfig[key]
+
+              contractFormUpdate.value[config.idField] = data.id
+            }
+          } catch (e) {
+            console.log(e)
+          }
+        } else {
+        }
+      })
+    }
+    contractFormUpdate.value.contractInfo = JSON.stringify({
+      ...contractForm
+      // attachments: contractForm.attachments
+    })
+    const saveRes = await updateContractDate(contractFormUpdate.value)
+    return saveRes
+  } catch (error) {
+    console.error('保存合同失败', error)
+    throw error
+  }
+}
+
+defineExpose({ saveContract })
+
+onMounted(async () => {
+  await loadBedTypes()
+})
+
+// 月度费用总额随明细自动汇总
+watch(
+  () => monthlyExpensesList.value.map((item: any) => item.actualAmount),
+  () => {
+    contractForm.totalFee = monthlyExpensesList.value
+      .reduce((sum: number, item: any) => sum + (Number(item.actualAmount) || 0), 0)
+      .toString()
+  },
+  { deep: true }
+)
+
+// 合同期限根据起止日期自动计算
+watch(
+  [() => contractForm.startDate, () => contractForm.endDate],
+  ([start, end]) => {
+    const months = calcMonthsBetween(start, end)
+    if (months !== null) {
+      contractForm.contractMonths = String(months)
+    }
+  },
+  { immediate: true }
+)
+</script>
+
+<template>
+  <div class="contract-body">
+    <!-- ========== 第1页:封面 ========== -->
+    <div class="a4-page cover-a4">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">-1-</span>
+      </div>
+      <div class="page-content cover-page">
+        <div class="cover-fields">
+          <p>
+            <span class="label">合同编号:</span>
+            <FieldItem
+              v-model="contractForm.contractNumber"
+              :is-text-mode="isTextMode"
+              width="180px"
+              placeholder="请输入合同编号"
+            />
+          </p>
+          <p>
+            <span class="label">长者姓名:</span>
+            <FieldItem
+              v-model="contractForm.elderName"
+              :is-text-mode="isTextMode"
+              width="180px"
+              placeholder="请输入长者姓名"
+            />
+          </p>
+        </div>
+        <h1 class="doc-title">湖北省养老机构服务合同</h1>
+        <p class="cover-issuer">
+          湖北省民政厅<br />
+          湖北省市场监督管理局&nbsp;&nbsp;制定
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第2页:使用说明 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">2</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">使用说明</h2>
+        <p class="content-text">
+          一、本合同文本为示范文本,供本省养老机构与入住老年人及第三方(入住老年人家属或监护人、担保人以及委托代理的个人或组织)之间签订养老服务合同使用。特困供养老年人的委托照料不适用本合同。
+        </p>
+        <p class="content-text">
+          二、本合同所称养老机构是指依法办理登记并在民政部门备案,为老年人提供全日集中住宿和照料护理服务,床位数在(含)10张以上的机构。提供短中期集中住宿和照料护理服务的居家社区养老服务机构可参照本合同。
+        </p>
+        <p class="content-text">
+          三、养老机构应当就合同内容对入住老年人及其家属或其他付款人、保证人、联系人等第三方尽到提示义务。入住老年人及第三方应当审慎签订合同,注意查看养老机构的经营资质、收费情况、服务质量。在签订本合同前仔细阅读合同条款,特别是其中具有选择性、补充性、修改性的内容,注意防范潜在的风险。
+        </p>
+        <p class="content-text">
+          四、本合同文本"□"中选择内容、空格部位或划线处填写内容及其他需要删除或添加的内容,双方当事人应当协商确定。"□"中选择内容,以划√方式选定;对于实际情况未发生或双方当事人不作约定时,应当在空格部位打×或划线处写"无",以示删除。
+        </p>
+        <p class="content-text">
+          五、各方当事人可参照本示范文本签订服务合同,也可在有关法律法规规定范围内,针对合同中未约定或约定不明确的内容,根据养老服务内容的具体情况在相关条款后的空白处中进行补充约定或另行签订补充协议,补充协议内容不得与主合同条款冲突,且补充的内容不得减轻或者免除应当由养老机构承担的责任。
+        </p>
+        <p class="content-text">
+          六、当事人可以根据实际情况决定本合同原件的份数,并在签订合同时认真核对,以确保各份合同内容一致;在任何情况下,各方当事人都有权持有至少一份合同原件。
+        </p>
+        <p class="content-text">
+          七、本合同约定了各方的民事权利义务关系,产生纠纷时,各方可依据《中华人民共和国民法典》和本合同的约定协商解决,也可以通过诉讼或仲裁解决。
+        </p>
+        <p class="content-text">
+          八、本合同示范文本,由湖北省民政厅、湖北省市场监督管理局共同制定。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第3页:甲乙丙方信息 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">3</span>
+      </div>
+      <div class="page-content">
+        <h3 class="subsection-title">甲方(养老机构):</h3>
+        <p class="content-text">
+          名称:
+          <FieldItem
+            v-model="contractForm.partyAName"
+            :is-text-mode="isTextMode"
+            width="240px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          统一社会信用代码:
+          <FieldItem
+            v-model="contractForm.partyACode"
+            :is-text-mode="isTextMode"
+            width="220px"
+            placeholder="请输入"
+          />
+          机构备案号:
+          <FieldItem
+            v-model="contractForm.partyARecordNo"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          地址:
+          <FieldItem
+            v-model="contractForm.partyAAddr"
+            :is-text-mode="isTextMode"
+            width="220px"
+            placeholder="请输入"
+          />
+          邮编:
+          <FieldItem
+            v-model="contractForm.partyAZip"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          机构电话:
+          <FieldItem
+            v-model="contractForm.partyAPhone"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="请输入"
+          />
+          机构邮箱:
+          <FieldItem
+            v-model="contractForm.partyAEmail"
+            :is-text-mode="isTextMode"
+            width="200px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          法定代表人:
+          <FieldItem
+            v-model="contractForm.partyALegal"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="请输入"
+          />
+          联系电话:
+          <FieldItem
+            v-model="contractForm.partyAContactPhone"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="请输入"
+          />
+        </p>
+
+        <h3 class="subsection-title" style="margin-top: 16px">乙方(入住老年人):</h3>
+        <div class="content-text">
+          姓名:
+          <FieldItem
+            v-model="contractForm.elderName"
+            :is-text-mode="isTextMode"
+            width="140px"
+            placeholder="请输入"
+          />
+          &nbsp;&nbsp;性别:
+          <FieldItem
+            v-model="contractForm.elderGenderText"
+            :is-text-mode="isTextMode"
+            width="60px"
+            placeholder="男/女"
+          />
+          &nbsp;&nbsp;出生年月:
+          <FieldItem
+            v-model="contractForm.elderBirth"
+            :is-text-mode="isTextMode"
+            type="date"
+            width="200px"
+            placeholder="请选择日期"
+          />
+        </div>
+        <p class="content-text">
+          证件类型:
+          <FieldItem
+            v-model="contractForm.elderPapersType"
+            :is-text-mode="isTextMode"
+            type="dict"
+            dictType="PAPERS_TYPE"
+            width="200px"
+            placeholder="请选择证件类型"
+          />
+          证件号码:
+          <FieldItem
+            v-model="contractForm.elderIdCard"
+            :is-text-mode="isTextMode"
+            width="220px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          户籍所在地:
+          <FieldItem
+            v-model="contractForm.elderCensusAddr"
+            :is-text-mode="isTextMode"
+            width="260px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          家庭住址:
+          <FieldItem
+            v-model="contractForm.elderAddress"
+            :is-text-mode="isTextMode"
+            width="260px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          手机号码:
+          <FieldItem
+            v-model="contractForm.elderPhone"
+            :is-text-mode="isTextMode"
+            width="180px"
+            placeholder="请输入"
+          />
+          电子邮箱:
+          <FieldItem
+            v-model="contractForm.elderEmail"
+            :is-text-mode="isTextMode"
+            width="200px"
+            placeholder="请输入"
+          />
+        </p>
+
+        <h3 class="subsection-title" style="margin-top: 16px"
+          >丙方(老年人家属/监护人,担保人/委托代理的个人或组织):</h3
+        >
+        <div class="content-text">
+          姓名:
+          <FieldItem
+            v-model="contractForm.partyCName"
+            :is-text-mode="isTextMode"
+            width="140px"
+            placeholder="请输入"
+          />
+          &nbsp;&nbsp;性别:
+          <FieldItem
+            v-model="contractForm.partyCGenderText"
+            :is-text-mode="isTextMode"
+            width="60px"
+            placeholder="男/女"
+          />
+          &nbsp;&nbsp;出生年月:
+          <FieldItem
+            v-model="contractForm.partyCBirth"
+            :is-text-mode="isTextMode"
+            type="date"
+            width="200px"
+            placeholder="请选择日期"
+          />
+        </div>
+        <p class="content-text">
+          与乙方关系:
+          <FieldItem
+            v-model="contractForm.partyCRelation"
+            :is-text-mode="isTextMode"
+            width="200px"
+            placeholder="请输入"
+          />
+          联系方式:
+          <FieldItem
+            v-model="contractForm.partyCPhone"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          证件类型:
+          <FieldItem
+            v-model="contractForm.partyCPapersType"
+            :is-text-mode="isTextMode"
+            type="dict"
+            dictType="PAPERS_TYPE"
+            width="200px"
+            placeholder="请选择证件类型"
+          />
+          证件号码:
+          <FieldItem
+            v-model="contractForm.partyCIdCard"
+            :is-text-mode="isTextMode"
+            width="220px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          住所(址):
+          <FieldItem
+            v-model="contractForm.partyCAddress"
+            :is-text-mode="isTextMode"
+            width="280px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          户籍所在地:
+          <FieldItem
+            v-model="contractForm.partyCHomeAddr"
+            :is-text-mode="isTextMode"
+            width="260px"
+            placeholder="请输入"
+          />
+          邮编:
+          <FieldItem
+            v-model="contractForm.partyCZip"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          工作单位:
+          <FieldItem
+            v-model="contractForm.partyCWorkUnit"
+            :is-text-mode="isTextMode"
+            width="280px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          通信地址:
+          <FieldItem
+            v-model="contractForm.partyCCommAddr"
+            :is-text-mode="isTextMode"
+            width="280px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text" style="margin-top: 14px; font-size: 13px">
+          (若乙方属于限制民事行为能力或者无民事行为能力的入住老年人,须由监护人签字确认。入住老年人的代理人作为连带保证人,就入住老年人在本合同项下对养老机构所负的债务承担连带保证责任。)
+        </p>
+        <p class="content-text" style="margin-top: 12px">
+          根据《中华人民共和国民法典》《中华人民共和国老年人权益保障法》《养老机构管理办法》等法律法规规定,甲方、乙方、丙方本着平等、自愿、诚实守信的原则,经过友好协商,就甲方向乙方提供养老服务事宜达成一致,签订本合同。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第4页:第一条 服务地点及服务设施 + 第二条 服务内容与质量标准 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">4</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第一条&nbsp;&nbsp;服务地点及服务设施</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;1.1&nbsp;甲方为乙方提供养老服务的地点为:
+          <FieldItem
+            v-model="contractForm.servicePlace"
+            :is-text-mode="isTextMode"
+            width="260px"
+            placeholder="具体到门牌号"
+          />
+          。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;1.2&nbsp;乙方选择入住的房间类型为:
+          <span v-if="!isTextMode" style="display: inline-block; vertical-align: middle">
+            <el-select v-model="contractForm.roomType" placeholder="请选择" style="width: 200px">
+              <el-option
+                v-for="item in roomTypeOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </span>
+          <span v-else class="fill-text">{{ contractForm.roomType || '' }}</span>
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;1.3&nbsp;乙方选择的房间,具体楼层为:
+          <FieldItem
+            v-model="contractForm.roomFloor"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="楼层"
+          />
+          ,房间号为:
+          <FieldItem
+            v-model="contractForm.roomNo"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="房间号"
+          />
+          ,床位号为:
+          <FieldItem
+            v-model="contractForm.bedNo"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="床位号"
+          />
+          。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;1.4&nbsp;乙方、丙方基于正当理由要求调整房间的,甲方在条件许可的范围内应尽量满足;甲方基于照料护理等级的改变而需要调整房间的,应事先经乙方、丙方同意。若因房间变化,需要相应调整费用的,应经各方协商一致并书面确认,如各方不能达成一致,则仍按本合同约定的房间履行。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;1.5&nbsp;甲方提供的服务设施设备除了住宿的房屋外,还包括房间内设施设备和公共设施设备。
+        </p>
+
+        <h2 class="section-title">第二条&nbsp;&nbsp;服务内容与质量标准</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;2.1&nbsp;甲方根据《老年人能力评估规范》国家标准(GB/T42195-2022)、《入住登记表》《体检报告》及对乙方的能力综合评估结果,经甲方与乙方、丙方商定,确定甲方向乙方提供照料护理等级和服务项目,签署《首次服务项目确认表》。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;2.2&nbsp;在本合同履行过程中,如乙方身心状况发生变化,需要变更照料护理等级及相关服务项目的,甲方应当及时重新对乙方进行老年人能力综合评估,甲方确定或者变更乙方照料护理等级,应当经乙方或丙方同意并签署《变更事项确认表》。乙方、丙方对重新评估结果有异议的,可与甲方协商进行复评,根据复评结果,仍需调整乙方相应的照料护理等级和服务项目的,乙方、丙方应当签署《变更事项确认表》。其中,经评估确定的乙方护理级别为:
+          <FieldItem
+            v-model="contractForm.nurseLevelName"
+            :is-text-mode="isTextMode"
+            width="180px"
+            placeholder="请输入"
+          />
+          ,甲方向乙方提供的护理等级服务项目详见本合同附件《护理等级与服务项目》。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第5页:第三条 收费标准及费用的支付(动态费用明细) ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">5</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第三条&nbsp;&nbsp;收费标准及费用的支付</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.1&nbsp;甲方的收费标准和调整应遵守养老机构服务收费管理的相关规定,若甲方有收取会员费行为,应遵循《湖北省养老机构会员费管理办法(试行)》相关要求执行。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.2&nbsp;甲方的《服务项目及收费标准》和收费依据应在养老机构内公共区域显著位置进行公示,并作为本合同附件。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.3&nbsp;甲方在收到费用后应当给乙方出具符合政府相关部门要求的等额收费凭证或发票。
+        </p>
+        <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;3.4&nbsp;养老服务费用 </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.1&nbsp;根据乙方入住时所选择的房间、照料护理等级及服务项目收费标准,乙方入住甲方的养老服务总费用为:¥
+          <FieldItem
+            v-model="contractForm.totalFee"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="0"
+          />
+          元/月,包括:
+        </p>
+        <p v-for="item in monthlyExpensesList" :key="item.id" class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;{{ displayExpenseName(item) }}:
+          <FieldItem
+            v-model="item.actualAmount"
+            :is-text-mode="isTextMode"
+            width="120px"
+            placeholder="0"
+          />
+          元/月;
+        </p>
+        <p class="content-text"> 其他费用: </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;(1)水电费:超出床位费包含的每人每月
+          <FieldItem
+            v-model="contractForm.freeElectric"
+            :is-text-mode="isTextMode"
+            width="80px"
+            placeholder="0"
+          />
+          千瓦时电和
+          <FieldItem
+            v-model="contractForm.freeWater"
+            :is-text-mode="isTextMode"
+            width="80px"
+            placeholder="0"
+          />
+          立方冷水的免费额度后,超出部分按实际支出进行对应房型具体入住床数进行均摊收取,超出部分收费标准为:热水
+          <FieldItem
+            v-model="contractForm.hotWaterFee"
+            :is-text-mode="isTextMode"
+            width="80px"
+            placeholder="0"
+          />
+          元/立方;冷水
+          <FieldItem
+            v-model="contractForm.coldWaterFee"
+            :is-text-mode="isTextMode"
+            width="80px"
+            placeholder="0"
+          />
+          元/立方;电
+          <FieldItem
+            v-model="contractForm.electricFee"
+            :is-text-mode="isTextMode"
+            width="80px"
+            placeholder="0"
+          />
+          元/千瓦时;
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          出现合同中止或终止情形的,扣除实际入住天数产生的费用后予以退款。
+        </p>
+        <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;(2)一次性收取的费用(如有请填写)。 </p>
+        <div v-for="(item, index) in oneTimeExpensesList" :key="item.id" class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;{{ numberToCircle(index + 1) }}{{ displayExpenseName(item) }}
+          <FieldItem
+            v-model="item.actualAmount"
+            :is-text-mode="isTextMode"
+            width="100px"
+            placeholder="0"
+          />
+          元。
+          <template v-if="isDepositFee(item)">
+            出现合同中止情形的,予以全额退还(
+            <el-radio-group
+              style="margin-left: 4mm"
+              v-if="!isTextMode"
+              v-model="contractForm.depositInterestType"
+              size="small"
+            >
+              <el-radio label="0">押金不计利息</el-radio>
+              <el-radio label="1">押金计利息</el-radio>
+            </el-radio-group>
+            <template v-else>
+              <span v-if="contractForm.depositInterestType === '0'"
+                >☑押金不计利息&nbsp;&nbsp;□押金计利息</span
+              >
+              <span v-else>□押金不计利息&nbsp;&nbsp;☑押金计利息</span>
+            </template>
+            <template v-if="contractForm.depositInterestType === '1'">
+              ,计息标准为:
+              <FieldItem
+                v-model="contractForm.depositInterest"
+                :is-text-mode="isTextMode"
+                width="160px"
+                placeholder="无"
+              />
+              &nbsp;&nbsp;
+            </template>
+            )。
+          </template>
+        </div>
+        <p v-if="!oneTimeExpensesList.length" class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;无。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;合同期限内出现押金不足时,乙方或丙方应在接到甲方通知之日起5日内补足。涉及退款的,在出现合同中止或终止情形后,双方进行结算,经双方确认无误之日起15个工作日内将资金退给付款方。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.2 费用支付方式(原则上一次性收取费用不超过12个月):
+        </p>
+        <p class="content-text">
+          <span v-if="!isTextMode" style="display: inline-block; vertical-align: middle">
+            <el-radio-group v-model="contractForm.payMode" size="small">
+              <el-radio label="1">按月支付</el-radio>
+              <el-radio label="2">其它相关约定</el-radio>
+            </el-radio-group>
+          </span>
+          <template v-else>
+            <span>{{ contractForm.payMode === '1' ? '☑按月支付' : '□按月支付' }}</span>
+            &nbsp;&nbsp;
+            <span>{{ contractForm.payMode === '2' ? '☑其它相关约定' : '□其它相关约定' }}</span>
+          </template>
+          <template v-if="contractForm.payMode === '1'">
+            (每月
+            <FieldItem
+              v-model="contractForm.payMonthDay"
+              :is-text-mode="isTextMode"
+              width="60px"
+              placeholder="5"
+            />
+            日前支付下一个月的费用);
+          </template>
+          <template v-else>
+            (其它相关约定:
+            <FieldItem
+              v-model="contractForm.otherPayMode"
+              :is-text-mode="isTextMode"
+              width="200px"
+              placeholder="请填写"
+            />);
+          </template>
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.3&nbsp;乙方因就医或者其他原因暂时不在甲方入住,但是应乙方、丙方的要求保留房间或者床位的,应当向甲方支付床位费;服务费、膳食费等其他费用可在3.4.2"其他相关约定中"进行商议约定。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.4&nbsp;因食品、材料、劳动力等市场价格发生重大变化,致使甲方服务成本急剧上升(单项价格涨幅达到10%)的,甲方可要求按照价格涨幅增加相应费用;服务成本显著下降(单项价格跌幅达到10%)的,乙方可要求按照价格跌幅减少费用。前述价格涨跌指数,以政府统计部门公布的数据为准。费用调整前30日,甲方书面通知乙方、丙方。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.5&nbsp;乙方因公共卫生突发事件被采取隔离措施的,所发生的隔离费用按照乙方被隔离的原因确定。如果乙方被隔离的原因与甲方无关,则隔离费用全部由乙方承担;如果乙方在养老机构外隔离,为其保留床位的,床位费由乙方承担。如果乙方被隔离是由于甲方原因所致,则隔离费用全部由甲方承担。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.6&nbsp;在本合同约定的期限届满后,如各方未续订书面合同,但乙方继续入住甲方的,在甲方提供实际服务的期间,乙方应当按照该期间的甲方收费标准支付费用并承担违约责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.4.7 乙方应支付的阶段性费用及支付方式如下:
+        </p>
+        <div v-for="item in stageExpensesList" :key="item.id" class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;{{ displayExpenseName(item) }}:
+          <template v-if="item.isFreeGift == 1">
+            ({{ item.amount }})
+            赠送开始时间
+            <FieldItem
+              v-model="item.freeStartTime"
+              :is-text-mode="isTextMode"
+              type="date"
+              width="160px"
+              placeholder="请选择日期"
+            />
+            ,赠送结束时间
+            <FieldItem
+              v-model="item.freeEndTime"
+              :is-text-mode="isTextMode"
+              type="date"
+              width="160px"
+              placeholder="请选择日期"
+            />
+            ;
+          </template>
+          <template v-else>
+            费用总额
+            <FieldItem
+              v-model="item.totalAmount"
+              :is-text-mode="isTextMode"
+              width="100px"
+              placeholder="0"
+            />
+            元,分
+            <FieldItem
+              v-model="item.hirePurchaseNumber"
+              :is-text-mode="isTextMode"
+              width="60px"
+              placeholder="0"
+            />
+            期支付,每期
+            <FieldItem
+              v-model="item.hirePurchaseAmount"
+              :is-text-mode="isTextMode"
+              width="100px"
+              placeholder="0"
+            />
+            元;
+          </template>
+        </div>
+        <p v-if="!stageExpensesList.length" class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;无。 </p>
+      </div>
+    </div>
+
+    <!-- ========== 第6页:第三条 会员费 + 其他事宜(冷静期) ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">6</span>
+      </div>
+      <div class="page-content">
+        <h3 class="subsection-title">3.5&nbsp;&nbsp;会员费</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.5.1&nbsp;甲方收取会员费的,需符合《湖北省养老机构会员费管理办法(试行)》规定的收取会员费的必要条件,且应在属地民政部门备案。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.5.2&nbsp;甲方收取的单个会员费金额不得超过本机构平均养老服务费月收费标准的12倍。收取的会员费总额不得超过本机构扣除银行贷款和其他债务后可抵押物估值的60%。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.5.3&nbsp;经甲方、乙方、丙方协商一致,甲方一次性收取会员费金额为人民币
+          <FieldItem
+            v-model="contractForm.memberFeeAmt"
+            :is-text-mode="isTextMode"
+            width="120px"
+            placeholder="0"
+          />
+          元(大写:
+          <FieldItem
+            v-model="contractForm.memberFeeCn"
+            :is-text-mode="isTextMode"
+            width="200px"
+            placeholder="零元整"
+          />
+          圆整)。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.5.4&nbsp;会员费存入以下专用资金存管账户:
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;开户银行:
+          <FieldItem
+            v-model="contractForm.memberDepositBank"
+            :is-text-mode="isTextMode"
+            width="280px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;银行账号:
+          <FieldItem
+            v-model="contractForm.memberDepositAccount"
+            :is-text-mode="isTextMode"
+            width="280px"
+            placeholder="请输入"
+          />
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.5.5&nbsp;甲方收取的会员费仅限用于弥补本机构设施建设资金不足及主营业务,不得用于不动产、股权、证券、债券、期货等投资及其他借贷用途。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.5.6&nbsp;乙方或丙方提出退还会员费要求时,甲方不得以任何理由拒绝,应在提出退款要求后1个月内全额退还会员费,除工本费以外不得收取其他任何费用。
+        </p>
+
+        <h3 class="subsection-title" style="margin-top: 14px">3.6&nbsp;&nbsp;其他事宜</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.6.1&nbsp;乙方自签署本合同的次日起,有7日冷静期,冷静期期间,在未入住机构的情况下,有权无条件解除本合同,乙方经与甲方以
+          <FieldItem
+            v-model="contractForm.otherPayMode2"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="书面/电子邮件/短信/微信/其他"
+          />
+          方式确认退费申请后,甲方应于30日内一次性返还全部费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;3.6.2&nbsp;本合同终止、解除,涉及甲方应当给乙方、丙方退费的,直接按
+          <FieldItem
+            v-model="contractForm.memberFeeRefundMode"
+            :is-text-mode="isTextMode"
+            width="200px"
+            placeholder="请填写退款方式"
+          />
+          方式退回。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第7页:第四条 合同期限及合同期满的处理 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">7</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第四条&nbsp;&nbsp;合同期限及合同期满的处理</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;4.1&nbsp;本合同服务期限为
+          <FieldItem
+            v-model="contractForm.contractMonths"
+            :is-text-mode="true"
+            width="60px"
+            placeholder="12"
+          />
+          个月,自
+          <FieldItem
+            v-model="contractForm.startDate"
+            :is-text-mode="isTextMode"
+            type="date"
+            width="180px"
+            placeholder="请选择日期"
+          />
+          起至
+          <FieldItem
+            v-model="contractForm.endDate"
+            :is-text-mode="isTextMode"
+            type="date"
+            width="180px"
+            placeholder="请选择日期"
+          />
+          止。其中,试住期为
+          <FieldItem
+            v-model="contractForm.trialDays"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="7"
+          />
+          日,自
+          <FieldItem
+            v-model="contractForm.trialStart"
+            :is-text-mode="isTextMode"
+            type="date"
+            width="160px"
+            placeholder="请选择日期"
+          />
+          至
+          <FieldItem
+            v-model="contractForm.trialEnd"
+            :is-text-mode="isTextMode"
+            type="date"
+            width="160px"
+            placeholder="请选择日期"
+          />
+          。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;4.2&nbsp;试住期:乙方按本合同约定的收费标准,根据实际入住天数结算费用,不足一天的按一天计算。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;4.3&nbsp;甲方、乙方、丙方在本合同期满前
+          <FieldItem
+            v-model="contractForm.renewDays"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="15"
+          />
+          日内同意按照本合同的条款续约的,由甲方、乙方、丙方续签合同。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;4.4&nbsp;如果甲方、乙方、丙方未能在合同期满前
+          <FieldItem
+            v-model="contractForm.renewDays"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="15"
+          />
+          日内就续约事项达成一致、且乙方在合同期满后亦不再继续入住甲方的,则乙方应当在合同到期日搬离甲方,办理离院手续并结清所有费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;4.5&nbsp;本合同约定的期限届满后,如各方未续订书面合同,且乙方未按期搬离甲方的,甲方有权通知乙方、丙方要求乙方于
+          <FieldItem
+            v-model="contractForm.moveOutDays"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="15"
+          />
+          日内搬离甲方,乙方、丙方办理离院手续并结清所有费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;4.6&nbsp;当出现4.4款或4.5款的情形时,为保障乙方安全,乙方监护人及丙方以外的人接走乙方时,应当征得乙方监护人或丙方的同意。未经乙方监护人或丙方同意,其他任何人均无权将乙方接离甲方。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第8页:第五条 甲方的权利义务 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">8</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第五条&nbsp;&nbsp;甲方的权利义务</h2>
+        <h3 class="subsection-title">5.1&nbsp;甲方权利</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.1.1&nbsp;按照本合同约定收取相关费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.1.2&nbsp;制订、修改、完善养老机构的各项管理制度,并按照公开的管理制度对乙方进行管理。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.1.3&nbsp;为了乙方的健康和安全,乙方出现紧急情况时,在通知乙方或丙方的同时,有权采取必要的处置措施,包括但不限于转送医疗机构,由此产生的费用由乙方或丙方承担。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">5.2&nbsp;甲方义务</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.1&nbsp;应是依法登记、备案的养老机构,履行养老机构备案承诺,按照建筑、消防、食品安全、医疗卫生、特种设备等法律法规、文件规定和《养老机构服务安全基本规范》强制性标准、《养老机构服务质量基本规范》等规范以及本合同的约定,为乙方提供养老服务。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.2&nbsp;应保证从事医疗、康复、安全等服务人员符合资质要求,保证养老护理人员接受专业技能培训,并定期组织工作人员进行职业道德教育和业务培训,能够满足岗位职责要求。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.3&nbsp;制订、修改的涉及乙方权利义务的各项管理服务制度和各类服务项目收费标准,应当在养老机构内的公共区域显著位置进行公示,并按照公示的制度为乙方提供管理服务。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.4&nbsp;在提供服务过程中,甲方及其工作人员应尊重乙方,依法维护乙方的人格尊严和保障人身、财产安全,并接受乙方、丙方的合理建议和监督。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.5&nbsp;应当根据需要为乙方提供情绪疏导、心理咨询、危机干预等精神慰藉服务;应当开展适合老年人的文化、教育、体育、娱乐活动,丰富老年人的精神文化生活。甲方开展文化、教育、体育、娱乐活动时,应当为乙方提供必要的安全防护措施。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.6&nbsp;定期组织所在地的医疗机构为乙方开展体检并建立个人健康档案。妥善保存乙方的入住登记表、体检报告等健康资料以及日常经费开支情况等个人信息。应当建立各类信息资料档案的保管、归档和保密制度,除向乙方、丙方和其他有权部门提供查阅、允许复制外,不得对外透露。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.7&nbsp;发现乙方为疑似传染病病人或者精神障碍患者时,应依照传染病防治、精神卫生等相关法律法规的规定处理,及时向附近的疾病预防控制机构或者医疗机构报告,配合实施卫生处理、隔离等预防控制措施,并及时通知丙方。乙方需要外出就医的,甲方应及时告知丙方并由有相关义务的丙方亲自携带乙方就诊,丙方确实未能亲自携带乙方就诊的,应通过委托办理等方式作出妥善安排。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.8&nbsp;应当为乙方家庭成员看望或者问候老年人提供便利,为乙方联系家庭成员提供帮助;但不得影响甲方对于乙方正常服务运营或执行政府部门规定的公共安全管理要求,否则甲方有权拒绝。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.9&nbsp;在发生公共卫生事件等特殊情况下,甲方应贯彻执行政府部门的防控政策规定,积极落实防控措施,并向乙方或丙方做好解释工作。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.10&nbsp;甲方暂停、终止服务的,应当在暂停或终止服务60日前,书面通知乙方或丙方,并向社会公告;将入住老年人的安置方案书面告知甲方所在地县级民政部门。在暂停、终止服务或解散清算前,甲方应当依法妥善安置乙方。如乙方或丙方有意与其他养老机构签订服务协议的,甲方应当积极为乙方联系与甲方具有同等资质和服务水平的养老机构,努力促进双方建立合同;如乙方或丙方需解除或终止服务协议的,甲方应当退还乙方或丙方剩余未消费或合同约定的有关费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.11&nbsp;当乙方出现紧急情况时,应及时联系和通知丙方,并积极采取必要的紧急处理措施。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.12&nbsp;乙方入住养老机构、请假外出、终止合同、离院和去世,甲方应及时在金民工程信息系统等填报有关信息。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;5.2.13&nbsp;甲方应妥善处理与乙方、丙方之间的纠纷,对不能及时处理的,应及时向相关管理部门报告,防止事态扩大。积极配合相关管理部门解决纠纷,并服从纠纷调解决定。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第9页:第六条 乙方的权利义务 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">9</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第六条&nbsp;&nbsp;乙方的权利义务</h2>
+        <h3 class="subsection-title">6.1&nbsp;乙方的权利</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.1.1&nbsp;享有隐私权及人格尊严和人身、财产安全不受侵害的权利。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.1.2&nbsp;有权按照约定的服务项目获得甲方提供的符合服务标准的养老服务,对甲方的服务有批评建议的权利。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.1.3&nbsp;对自身的健康状况、费用支出、入院记录等有知情权,有权查阅、复印甲方为其建立的个人档案。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.1.4&nbsp;有权了解提供服务的人员是否经过专业培训,是否具备相应资质,有权要求甲方更换未经专业培训或不具备相应资质或提供服务不合格的人员。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.1.5&nbsp;在突发疾病或意外的情况下,有权获得及时、必要的医疗帮助。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">6.2&nbsp;乙方的义务</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.1&nbsp;应当按照约定自行或与丙方共同支付养老服务费及相关费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.2&nbsp;入住前,应当按照《体检报告项目说明》提供相关的体检报告,如实告知甲方本人的健康状况、药品使用情况及过往病史等信息,如实填写《入住登记表》。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.3&nbsp;应当配合甲方做好持续评估及确认照护等级;入住期间,及时如实向甲方报告身体、心理等方面出现的状况变化,配合甲方定期参加体检。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.4&nbsp;应当服从甲方管理,遵守甲方的规章制度,维护养老机构正常服务秩序。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.5&nbsp;应当与其他入住人员和谐相处。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.6&nbsp;在接受甲方提供的养老服务期间,因疾病进行诊疗时,应在治疗期间遵守医嘱,配合治疗。如外出就医需在医院观察治疗的,应当及时通知甲方。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.7&nbsp;具备独自外出能力,外出应当做好请假、销假登记,在外留宿应当及时通知甲方。不具备独自外出能力,需限制外出服务管理,因特殊情况确需外出必须由乙方或丙方指定人员(如家属等)陪同,且每次外出须由乙方或丙方提前通知甲方,并做好登记。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.8&nbsp;爱护甲方提供的各项服务设施,入住期间损坏甲方设施设备的,乙方或丙方应当予以赔偿。损坏有关服务设施的赔偿价格可与甲方协商约定。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;6.2.9&nbsp;对于甲方提供的其他符合合同约定、法律规定的养老服务,乙方应予配合。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第10页:第七条 丙方的权利义务 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">10</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第七条&nbsp;&nbsp;丙方的权利义务</h2>
+        <h3 class="subsection-title">7.1&nbsp;丙方的权利</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.1.1&nbsp;对乙方有探视权,但不得影响甲方对乙方的正常服务管理和执行政府规定的公共安全管理要求。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.1.2&nbsp;对乙方的健康状况、享受服务情况、费用支出、入院记录等有知情权,有权查阅、复印甲方为乙方建立的个人档案。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.1.3&nbsp;有权了解提供服务的人员是否经过专业培训,是否具备相应资质,有权要求甲方更换未经专业培训或不具备相应资质或提供服务不合格的人员;有权对甲方的服务提出批评建议。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.1.4&nbsp;遇紧急情况,包括但不限于乙方走失、身体健康状况出现紧急情况时,有权及时从甲方得到相关信息。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">7.2&nbsp;丙方的义务</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.1&nbsp;按时足额向甲方支付约定的养老服务费及相关费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.2&nbsp;乙方入住前,丙方要如实向甲方反映乙方的情况,如脾气秉性、家庭成员、既往病史、健康状况和药品使用情况等,协助乙方如实填写《入住登记表》。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.3&nbsp;劝导乙方入住后自觉遵守养老机构的规章制度,接受管理,爱护甲方提供的各项服务设施,乙方违反本合同约定或甲方制定的规章制度,造成甲方损失的,乙方需承担赔偿责任,负有付款义务或连带责任的丙方应当承担有关赔偿责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.4&nbsp;劝导乙方与其他入住人员和谐相处,并经常与乙方沟通,保持联络,满足乙方的精神需求,及时将乙方的身体、心理等方面出现的状况变化如实向甲方报告。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.5&nbsp;劝导乙方在接收甲方提供的养老服务期间,因疾病进行诊疗时,应在治疗期间遵守医嘱,配合治疗。乙方需外出就医的,负有相关义务的丙方应当及时携乙方到医院就诊,并向甲方如实告知乙方的就医情况。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.6&nbsp;丙方的家庭及单位地址、联系方式变更时,应及时通知甲方。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.7&nbsp;及时协助甲方处理乙方出现的紧急情况。当甲方按照合同约定处理时,负有付款义务或连带责任的丙方应当及时足额支付由此产生的费用。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;7.2.8&nbsp;乙方在养老机构去世的,丙方应及时进行善后处理并承担由此产生的费用。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第11页:第八条 合同的变更、解除和终止 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">11</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第八条&nbsp;&nbsp;合同的变更、解除和终止</h2>
+        <h3 class="subsection-title">8.1&nbsp;合同的变更</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;在本合同履行过程中,根据乙方健康状况的变化,甲方可以提出变更服务方案或对合同相关内容进行变更,并以书面形式通知乙方或乙方监护人及丙方,经甲、乙、丙三方协商一致,可以签订补充协议。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">8.2&nbsp;合同的解除</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;8.2.1&nbsp;出现下列情形之一的,乙方、丙方有权解除本合同:
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (1)甲方提供的服务不符合合同约定,经乙方、丙方书面提出后15日内不改正的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (2)因甲方或甲方工作人员的故意或者重大过失造成乙方人身或重大财产损害的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (3)乙方在试住期内不适应居住环境或管理方式的。
+        </p>
+        <p class="content-text" style="padding-left: 2em"> (4)乙方在冷静期内要求解除合同的。 </p>
+        <p class="content-text" style="padding-left: 2em">
+          (5)本合同履行过程中,乙方或丙方提前15日书面通知甲方解除合同并结清服务费用的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (6)乙方因疾病离开甲方的,但乙方、丙方要求保留房间或者床位的除外。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;8.2.2&nbsp;出现下列情形之一的,甲方有权解除本合同:
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (1)乙方无故拖欠各项费用超过15日,经甲方催告后7日内仍不支付的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (2)乙方严重违反甲方的规章制度、拒不配合甲方管理造成甲方难以履行对乙方的养老服务,或者影响甲方其他入住人员的正常生活、造成其他入住老人伤害或现实性伤害危险的,导致甲方的运营秩序无法正常维持的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (3)乙方出现精神障碍、患有必须隔离治疗的疾病或者具有暴力倾向等人身危险的。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;8.2.3&nbsp;出现下列情形之一的,甲方、乙方、丙方均有权解除本合同:
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (1)因食品、材料、劳动力等与甲方日常管理服务直接相关的市场价格发生重大变化,调整相应费用时,乙方、丙方对收费标准有异议的,要求按原收费标准继续履行合同的,或拒绝根据调整后的收费标准支付相应费用的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (2)乙方、丙方对老年人能力评估结果如有异议,可向有资质的第三方机构申请复评,根据复评结果,仍需要调整乙方相应的照料护理等级、服务内容和服务费用,但乙方、丙方拒绝签署《变更事项确认表》的,或者乙方或丙方对评估结果有异议,但又不与甲方协商进行复评的。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          (3)如果根据乙方健康状况的变化,不调整服务项目将导致乙方的健康安全无法保障的,在甲方提出变更的服务方案后,乙方、丙方拒绝变更服务方案的。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">8.3&nbsp;合同的终止</h3>
+        <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;乙方在入住甲方期间去世,合同自动终止。 </p>
+      </div>
+    </div>
+
+    <!-- ========== 第12页:第九条 重大事项的特别约定 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">12</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第九条&nbsp;&nbsp;重大事项的特别约定</h2>
+        <h3 class="subsection-title">9.1&nbsp;乙方外出管理</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;甲方不对乙方是否具备独自外出能力进行判断,由丙方在《入住登记表》中"外出管理"栏确认是否需要实行限制外出服务管理,亦不对此承担责任。外出期间发生事故(包括但不限于摔伤、走失、财物丢失)、突发疾病、遭受人身损害、死亡或者造成他人损害的,因此造成的后果由乙方或丙方承担,甲方不承担责任。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px"
+          >9.2&nbsp;乙方突发重大情况,甲方与丙方的责任承担</h3
+        >
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;在乙方入住甲方期间,乙方因自身原因出现突发危重疾病、身体伤害等紧急情况,或精神状况恶化、去世等重大情况时,如甲方在所提供服务和自身能力的范围内,采取必要措施,及时联系120等医疗急救机构,并及时联系丙方,但甲方无法联系到丙方的,应尽早与本合同附件确定的其他联系人取得联系,通报情况。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;因乙方发生紧急情况产生的急救费用、治疗费用、住院押金等均由乙方或乙方监护人负担。甲方因此垫付费用的,乙方或乙方监护人应及时清偿。甲方在按照本合同的约定履行相应义务后,不承担责任。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px"
+          >9.3&nbsp;乙方去世善后事宜及相关费用</h3
+        >
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;乙方在入住甲方期间去世的,甲方应当及时通知丙方负责善后事宜并承担相关费用。甲方如与丙方无法取得联系的,应当及时联系殡仪馆妥善保存乙方遗体,因此发生的费用由丙方承担。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">9.4&nbsp;紧急联系人</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;由乙方、丙方指定
+          <FieldItem
+            v-model="contractForm.emergencyContactName"
+            :is-text-mode="isTextMode"
+            width="160px"
+            placeholder="紧急联系人姓名"
+          />
+          为紧急联系人,紧急联系人电话为:
+          <FieldItem
+            v-model="contractForm.emergencyContactPhone"
+            :is-text-mode="isTextMode"
+            width="180px"
+            placeholder="联系电话"
+          />
+          。联系地址为:
+          <FieldItem
+            v-model="contractForm.emergencyContactAddr"
+            :is-text-mode="isTextMode"
+            width="280px"
+            placeholder="联系地址"
+          />
+          。紧急联系人信息发生变更时,乙方、丙方应当及时通知甲方。因乙方或丙方提供的紧急联系人的联系地址、方式不准确或不详细或变更后未及时通知甲方,或其他客观原因致使甲方无法与乙方或丙方及时联系,连续达一个月则视为联系中断。甲方与乙方或乙方监护人协商后,可以重新确定联系人。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;在乙方发生突发疾病(含危重疾病)、身体伤害等紧急情况,而甲方无法联系丙方时,紧急联系人应当配合甲方联系丙方。紧急联系人不配合的,由乙方、丙方承担责任。
+        </p>
+        <h3 class="subsection-title" style="margin-top: 14px">9.5&nbsp;其他情形下责任的承担</h3>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;9.5.1&nbsp;在乙方入住甲方期间,甲方应当告知乙方不能食用与其身体状况不适宜的食品,乙方因食用他人带入的食品等原因造成损害的,甲方应当及时通知丙方自行承担责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;9.5.2&nbsp;在乙方属于完全民事行为能力人时,因乙方原因造成其自身或者他人损害的,由乙方承担相应责任,丙方承担民事连带责任;在乙方属于限制民事行为能力人或无民事行为能力人时,因乙方原因造成其自身或者他人损害的,应由丙方承担相应责任。甲方未充分履行安全保障义务的,需依法承担相应责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;9.5.3&nbsp;甲方在询问乙方健康状况、用药情况及过往病史时,乙方、丙方应当如实告知,若因未如实告知造成甲方在提供服务过程中导致乙方发生人身损害的,由乙方、丙方承担相应责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;9.5.4&nbsp;乙方需外出就医,丙方拒绝及时携乙方到医院就诊的,由此造成的一切后果由丙方承担;丙方拒绝及时携乙方就诊的,为了乙方的健康和安全,甲方可以派人陪同送医,就诊结果由乙方、丙方承担责任并支付就医费用。甲方充分履行安全保障义务的,无需承担其他责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;9.6&nbsp;本条约定的上述各款内容,甲方应当提示乙方、丙方重点注意,按照乙方或丙方的要求,逐条向乙方、丙方讲解。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第13页:第十条 违约责任 + 第十一条 不可抗力 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">13</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第十条&nbsp;&nbsp;违约责任</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;10.1&nbsp;因甲方及其工作人员的过错,造成乙方受到损失的,甲方应当赔偿损失。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;10.2&nbsp;甲方提供的服务不符合本合同约定的标准,乙方、丙方有权要求甲方改正,造成乙方人身或财产损失的,甲方应当赔偿相应损失。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;10.3&nbsp;甲方或其工作人员侵犯乙方、丙方知情权的,乙方、丙方有权要求甲方改正,造成乙方经济损失的,甲方应当赔偿损失。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;10.4&nbsp;因乙方原因造成甲方或第三人损害的,乙方、丙方应当承担赔偿责任。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;10.5&nbsp;乙方逾期支付入住费用,每逾期一日应按应补足金额的万分之
+          <FieldItem
+            v-model="contractForm.penaltyRate"
+            :is-text-mode="isTextMode"
+            width="60px"
+            placeholder="0"
+          />
+          向甲方支付违约金(违约金比例不得超过万分之三,违约金总额不超过应补足金额的30%)。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;10.6&nbsp;本合同期满终止或解除后,乙方既未搬离甲方又不支付服务费用的,应当自合同期满终止或解除后的次日起,按照养老服务费用每日金额的
+          <FieldItem
+            v-model="contractForm.penaltyPercent"
+            :is-text-mode="isTextMode"
+            width="60px"
+            placeholder="0"
+          />
+          %向甲方支付违约金。造成甲方经济损失的,乙方、丙方还应当赔偿损失。
+        </p>
+
+        <h2 class="section-title">第十一条&nbsp;&nbsp;不可抗力</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;11.1&nbsp;甲、乙、丙三方中的任何一方因遭受不可抗力导致未能履行其在本合同下的全部或部分义务的,不承担违约责任。遭受不可抗力的一方应及时将发生不可抗力的情形通知对方,并在15个工作日内将不可抗力发生的有效证明提供给对方。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;11.2&nbsp;不可抗力指在签订合同时不可预见,在合同履行过程中不可避免且不能克服的自然灾害和社会性突发事件,包括但不限于疫情、地震、海啸、骚乱、戒严、暴动、战争、政府行为等其他情形。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;11.3&nbsp;因不可抗力导致本合同无法履行的,遭受不可抗力的一方应在不可抗力发生后及时通知合同其他当事人,本合同可依法解除,各方互不承担违约责任。甲方、乙方、丙方应积极协调,妥善安置乙方。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第14页:第十二条 纠纷的解决方式 + 第十三条 通知与送达 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">14</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第十二条&nbsp;&nbsp;纠纷的解决方式</h2>
+        <template v-if="!isTextMode">
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;因本合同产生的纠纷,各方应当首先协商解决。协商不成的,按下列第
+            <el-radio-group
+              v-model="contractForm.disputeMode"
+              size="small"
+              style="display: inline-flex; vertical-align: middle; margin: 0 4px"
+            >
+              <el-radio label="1">1</el-radio>
+              <el-radio label="2">2</el-radio>
+            </el-radio-group>
+            种方式处理:
+          </p>
+          <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;(1)依法向 </p>
+          <p class="content-text" style="padding-left: 2em">
+            <FieldItem
+              v-model="contractForm.disputeCourt"
+              :is-text-mode="isTextMode"
+              width="280px"
+              placeholder="有管辖权的人民法院名称"
+            />
+            人民法院起诉。
+          </p>
+          <p class="content-text"> &nbsp;&nbsp;&nbsp;&nbsp;(2)提交 </p>
+          <p class="content-text" style="padding-left: 2em">
+            <FieldItem
+              v-model="contractForm.disputeArbitration"
+              :is-text-mode="isTextMode"
+              width="280px"
+              placeholder="仲裁委员会名称"
+            />
+            仲裁委员会仲裁。
+          </p>
+        </template>
+        <template v-else>
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;因本合同产生的纠纷,各方应当首先协商解决。协商不成的,按下列第
+            <span
+              style="
+                display: inline-block;
+                min-width: 30px;
+                text-align: center;
+                border-bottom: 1px solid #333;
+              "
+            >
+              {{ contractForm.disputeMode || '1' }}
+            </span>
+            种方式处理:
+          </p>
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;{{ contractForm.disputeMode === '1' ? '☑' : '□' }}
+            (1)依法向有管辖权的人民法院起诉。
+          </p>
+          <p class="content-text">
+            &nbsp;&nbsp;&nbsp;&nbsp;{{ contractForm.disputeMode === '2' ? '☑' : '□' }}
+            (2)提交仲裁委员会仲裁。
+          </p>
+        </template>
+
+        <h2 class="section-title">第十三条&nbsp;&nbsp;通知与送达</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;13.1&nbsp;本合同所载明的甲方、乙方、丙方的通信地址和联系电话为各方各自有效的通讯地址和联系方式,一方要求变更的,应于变更之日书面告知对方。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;13.2&nbsp;一方给对方发出的通知,按照下列情形确定通知送达对方的时间:
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          13.2.1&nbsp;采用邮寄、特快专递送达的,回执上注明的收件日期为送达日。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          13.2.2&nbsp;采用手机短信、微信等即时通信方式发送通知的,通知发送到收件方的手机时即为送达。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          13.2.3&nbsp;采用电子邮件发送通知的,通知进入收件方邮箱时即为送达。
+        </p>
+        <p class="content-text" style="padding-left: 2em">
+          13.2.4&nbsp;采用传真发送通知的,收件方的传真机接收通知时即为送达。
+        </p>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;13.3&nbsp;因收件方的通信地址、手机号码、传真、微信号、电子邮箱等信息错误、不详或发生变更未及时通知其他各方造成通知无法送达的,由收件方自行承担由此引起的一切后果和责任。
+        </p>
+      </div>
+    </div>
+
+    <!-- ========== 第15页:第十四条 合同生效及附件 + 签署页 ========== -->
+    <div class="a4-page">
+      <div class="page-header">
+        <div class="logo-line">
+          <span class="logo-circle">颐</span>
+          <span class="logo-text">颐年医养</span>
+        </div>
+        <span class="page-no-right">15</span>
+      </div>
+      <div class="page-content">
+        <h2 class="section-title">第十四条&nbsp;&nbsp;合同生效及附件</h2>
+        <p class="content-text">
+          &nbsp;&nbsp;&nbsp;&nbsp;14.1&nbsp;本合同一式
+          <FieldItem
+            v-model="contractForm.copyCount"
+            :is-text-mode="isTextMode"
+            width="60px"
+            placeholder="叁"
+          />
+          份,甲方执
+          <FieldItem
+            v-model="contractForm.partyACopy"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="壹"
+          />
+          份,乙方执
+          <FieldItem
+            v-model="contractForm.partyBCopy"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="壹"
+          />
+          份,乙方监护人执
+          <FieldItem
+            v-model="contractForm.partyCCopy"
+            :is-text-mode="isTextMode"
+            width="40px"
+            placeholder="壹"
+          />
+          份,自甲方签字盖章和乙方、丙方签字之日起生效。
+        </p>
+        <p class="content-text" style="margin-top: 12px">
+          &nbsp;&nbsp;&nbsp;&nbsp;14.2&nbsp;本合同的附件如下:
+        </p>
+        <template v-for="(item, index) in page24AttachmentItems" :key="item.no">
+          <p class="content-text" style="padding-left: 2em">
+            附件{{ toChineseNumber(index + 1) }}:{{ item.title }}
+          </p>
+          <p v-if="item.subTitle" class="content-text" style="padding-left: 3em; font-size: 13px">
+            {{ item.subTitle }}
+          </p>
+        </template>
+        <template v-for="(item, index) in page25AttachmentItems" :key="item.no">
+          <p class="content-text" style="padding-left: 2em">
+            附件{{ toChineseNumber(page24AttachmentItems.length + index + 1) }}:{{ item.title }}
+          </p>
+          <p v-if="item.subTitle" class="content-text" style="padding-left: 3em; font-size: 13px">
+            {{ item.subTitle }}
+          </p>
+        </template>
+        <p class="content-text" style="margin-top: 8px">
+          &nbsp;&nbsp;&nbsp;&nbsp;14.3&nbsp;本合同附件系本合同不可分割的组成部分,与本合同具有同等法律效力。
+        </p>
+
+        <p class="content-text" style="text-align: center; margin-top: 30px; font-size: 14px">
+          (本文以下无正文,为合同签署页)
+        </p>
+
+        <div style="margin-top: 30px; font-size: 14px; line-height: 2.2">
+          <p>
+            甲方(盖章):
+            <SignatureItem
+              :elder-id="props.recordRow.elderId"
+              v-model="contractForm.partyASign"
+              :is-text-mode="isTextMode"
+              :width="260"
+              :height="80"
+              placeholder="(签字/盖章处)"
+              title="甲方签字"
+            />
+          </p>
+          <div>
+            法定代表人或授权代表(签名并按指印):
+            <SignatureItem
+              :elder-id="props.recordRow.elderId"
+              v-model="contractForm.partyALegalSign"
+              :is-text-mode="isTextMode"
+              :width="260"
+              :height="80"
+              placeholder="(签字处)"
+              title="法定代表人签字"
+            />
+          </div>
+          <div style="margin-top: 6px">
+            日期:
+            <FieldItem
+              v-model="contractForm.partyADate"
+              :is-text-mode="isTextMode"
+              type="date"
+              width="200px"
+              placeholder="请选择日期"
+            />
+          </div>
+          <div style="margin-top: 20px">
+            乙方(签名并按指印):
+            <SignatureItem
+              :elder-id="props.recordRow.elderId"
+              v-model="contractForm.partyBSign"
+              :is-text-mode="isTextMode"
+              :width="260"
+              :height="80"
+              placeholder="(签字处)"
+              title="乙方签字"
+            />
+          </div>
+          <div style="margin-top: 6px">
+            日期:
+            <FieldItem
+              v-model="contractForm.partyBDate"
+              :is-text-mode="isTextMode"
+              type="date"
+              width="200px"
+              placeholder="请选择日期"
+            />
+          </div>
+          <div style="margin-top: 20px">
+            丙方(签名并按指,印系单位的盖章):
+            <SignatureItem
+              :elder-id="props.recordRow.elderId"
+              v-model="contractForm.partyCSign"
+              :is-text-mode="isTextMode"
+              :width="260"
+              :height="80"
+              placeholder="(签字/盖章处)"
+              title="丙方签字"
+            />
+          </div>
+          <div style="margin-top: 6px">
+            日期:
+            <FieldItem
+              v-model="contractForm.partyCDate"
+              :is-text-mode="isTextMode"
+              type="date"
+              width="200px"
+              placeholder="请选择日期"
+            />
+          </div>
+          <div style="margin-top: 20px">
+            签订地点:
+            <FieldItem
+              v-model="contractForm.servicePlace"
+              :is-text-mode="isTextMode"
+              width="260px"
+              placeholder="请输入"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.contract-body {
+  width: 100%;
+  padding: 0;
+  background: #f5f5f5;
+}
+
+.a4-page {
+  width: 210mm;
+  min-height: auto;
+  margin: 4mm auto;
+  padding: 10mm 14mm;
+  background: #fff;
+  box-shadow: 0 0 8px rgba(0, 0, 0, 0.12);
+  position: relative;
+  box-sizing: border-box;
+}
+
+/* 封面页保留完整 A4 高度 */
+.a4-page.cover-a4 {
+  min-height: 297mm;
+}
+
+/* 非封面页隐藏页眉(logo+页码) */
+.a4-page:not(.cover-a4) .page-header {
+  display: none;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 6px;
+  border-bottom: 1.5px solid #333;
+  margin-bottom: 16px;
+}
+
+.logo-line {
+  display: flex;
+  align-items: center;
+}
+
+.logo-circle {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  background: #2b7c4e;
+  color: #fff;
+  font-size: 14px;
+  font-weight: bold;
+  margin-right: 8px;
+}
+
+.logo-text {
+  font-size: 16px;
+  font-weight: bold;
+  color: #333;
+}
+
+.page-no-right {
+  font-size: 12px;
+  color: #666;
+}
+
+.page-content {
+  font-size: 14px;
+  line-height: 1.9;
+  color: #333;
+}
+
+.cover-page {
+  text-align: center;
+  padding-top: 30mm;
+}
+
+.cover-fields {
+  text-align: left;
+  margin: 0 auto;
+  width: 100%;
+}
+
+.cover-fields .label {
+  display: inline-block;
+  min-width: 80px;
+  font-size: 14px;
+}
+
+.doc-title {
+  margin-top: 50mm;
+  font-size: 32px;
+  font-weight: bold;
+  letter-spacing: 4px;
+  color: #222;
+}
+
+.cover-issuer {
+  margin-top: 60mm;
+  font-size: 16px;
+  line-height: 2;
+  text-align: center;
+}
+
+.section-title {
+  font-size: 18px;
+  font-weight: bold;
+  text-align: center;
+  margin: 20px 0 14px 0;
+  color: #222;
+}
+
+.subsection-title {
+  font-size: 15px;
+  font-weight: bold;
+  margin: 12px 0 6px 0;
+  color: #222;
+}
+
+.content-text {
+  font-size: 14px;
+  line-height: 1.9;
+  margin: 4px 0;
+  text-indent: 0;
+  text-align: left;
+  color: #333;
+}
+
+.fill-text {
+  display: inline-block;
+  min-width: 60px;
+  border-bottom: 1px solid #333;
+  padding: 0 4px;
+}
+
+@media print {
+  .contract-body {
+    background: #fff;
+    margin: 0;
+    padding: 0;
+    width: 210mm;
+  }
+
+  /* 封面页:填满一整页(@page margin 16mm,可打印区域高度 = 297-32=265mm) */
+  .a4-page.cover-a4 {
+    width: 210mm !important;
+    min-height: 265mm !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    background: #fff !important;
+    box-shadow: none !important;
+    box-sizing: border-box !important;
+    page-break-after: always;
+    break-after: page;
+  }
+
+  /* 其他页:保留屏幕内边距,由 @page margin 控制物理边距 */
+  .a4-page:not(.cover-a4) {
+    width: 210mm !important;
+    min-height: auto !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    background: #fff !important;
+    box-shadow: none !important;
+    box-sizing: border-box !important;
+    page-break-after: auto;
+    break-after: auto;
+  }
+
+  .a4-page:last-child {
+    page-break-after: auto;
+    break-after: auto;
+  }
+
+  /* 注意:@page 规则统一在 ContractManageForm.vue 中声明,避免多组件冲突 */
+}
+</style>

+ 37 - 5
src/views/elderly/fee/comprehensive-setting/index.vue

@@ -168,7 +168,7 @@ const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const formRef = ref()
 const configList:any = ref([]) // 配置列表
-const list = ref([
+const list = ref<any[]>([
   {
     title: '机构配置',
     children: [
@@ -419,6 +419,24 @@ const list = ref([
         prop: 'contract',
         tgType: true,
         title: '合同模块配置'
+      },
+      {
+        configName: '合同区域设置',
+        desc: '设置电子合同的默认合同区域,创建新合同时默认使用该区域',
+        type: 'select',
+        placeholder: '请选择',
+        list: [
+          { label: '广州地区合同', value: '广州地区合同' },
+          { label: '湖北地区合同', value: '湖北地区合同' },
+
+        ],
+        value: '广州地区合同',
+        id: '',
+        label: '请选择合同区域',
+        prop: 'contract-area',
+        tgType: true,
+        title: '合同模块配置',
+        value2Field: true
       }
     ]
   },
@@ -441,7 +459,7 @@ const list = ref([
     ]
   },
 ])
-const currItem = ref({}) // 当前处理的项
+const currItem = ref<any>({}) // 当前处理的项
 const handleEdit = async (c) => {
   currItem.value = c
   if(!c.tgType){
@@ -451,7 +469,21 @@ const handleEdit = async (c) => {
       formRef.value.validateField(c.prop)
       return
     }
-    const res = await updateBusinessParamsConfig(c)
+    // 构造干净的更新参数,合同区域使用 value2 字段
+    const payload: any = {
+      id: c.id,
+      title: c.title,
+      configName: c.configName,
+      description: c.desc,
+      label: c.label,
+      placeholder: c.placeholder,
+      prop: c.prop,
+      type: c.type,
+      appendText: c.appendText,
+      value: c.value2Field ? '' : c.value,
+      value2: c.value2Field ? c.value : undefined
+    }
+    const res = await updateBusinessParamsConfig(payload)
     if(res){
       c.tgType = true
       message.success(t('common.updateSuccess'))
@@ -500,7 +532,7 @@ const getList = async () => {
           }else{
             if(child.configName == r.configName){
               child.id = r.id
-              child.value = r.value
+              child.value = child.value2Field ? r.value2 : r.value
               child.title = r.title
               child.desc = r.description
             }
@@ -663,4 +695,4 @@ onMounted(()=>{
     }
   }
 }
-</style>
+</style>