Browse Source

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

xiongxing 2 weeks ago
parent
commit
2b80a8e6cb
47 changed files with 4643 additions and 866 deletions
  1. 29 0
      .workbuddy/memory/2026-08-04.md
  2. 27 539
      package-lock.json
  3. 2 1
      package.json
  4. 417 0
      src/api/data-board/nine-safety.ts
  5. 15 10
      src/components/BedCardComponent/index.vue
  6. 4 2
      src/components/BedCardModal/index.vue
  7. 5 2
      src/utils/outBedCard.ts
  8. 823 0
      src/views/data-board/comprehensive-overview/index.vue
  9. 1580 0
      src/views/data-board/nine-safety-measures/index.vue
  10. 8 0
      src/views/elderly/apply/nine-precautions/MMSE/index.vue
  11. 8 0
      src/views/elderly/apply/nine-precautions/anti-entertainment/index.vue
  12. 8 0
      src/views/elderly/apply/nine-precautions/asphyxiation-by-choking/index.vue
  13. 8 0
      src/views/elderly/apply/nine-precautions/attack/index.vue
  14. 8 0
      src/views/elderly/apply/nine-precautions/commit-suicide/index.vue
  15. 8 1
      src/views/elderly/apply/nine-precautions/communication/index.vue
  16. 29 16
      src/views/elderly/apply/nine-precautions/daily-life/index.vue
  17. 8 0
      src/views/elderly/apply/nine-precautions/empyrosis/index.vue
  18. 8 0
      src/views/elderly/apply/nine-precautions/equilibrium/index.vue
  19. 8 0
      src/views/elderly/apply/nine-precautions/fall-down/index.vue
  20. 8 0
      src/views/elderly/apply/nine-precautions/fall-prevention-measures/index.vue
  21. 29 16
      src/views/elderly/apply/nine-precautions/nutritional-risk/index.vue
  22. 8 1
      src/views/elderly/apply/nine-precautions/pressure-sores/index.vue
  23. 33 25
      src/views/elderly/apply/nine-precautions/wander-away/index.vue
  24. 75 2
      src/views/elderly/contracts/change-confirmation-form/AddForm.vue
  25. 76 2
      src/views/elderly/contracts/check-in-tab/AddForm.vue
  26. 82 2
      src/views/elderly/contracts/comprehensive-assessment-capabilities/AddForm.vue
  27. 77 4
      src/views/elderly/contracts/delegation-agent-confirmation-form/AddForm.vue
  28. 80 3
      src/views/elderly/contracts/elder-safety-commitment/AddForm.vue
  29. 80 2
      src/views/elderly/contracts/explanation-examination-report-items/AddForm.vue
  30. 75 2
      src/views/elderly/contracts/first-service-project-confirmation/AddForm.vue
  31. 80 2
      src/views/elderly/contracts/guardian-party-b/AddForm.vue
  32. 83 2
      src/views/elderly/contracts/identification-document-partyb/AddForm.vue
  33. 80 3
      src/views/elderly/contracts/outing-confirmation-form/AddForm.vue
  34. 76 3
      src/views/elderly/contracts/power-of-attorney/AddForm.vue
  35. 79 2
      src/views/elderly/contracts/registration-certificate-civil-affairs-department/AddForm.vue
  36. 75 2
      src/views/elderly/contracts/risk-disclosure-statement/AddForm.vue
  37. 77 4
      src/views/elderly/contracts/room-facilities-equipment/AddForm.vue
  38. 80 3
      src/views/elderly/contracts/service-charging-standards-institution/AddForm.vue
  39. 80 3
      src/views/elderly/contracts/signature-proxy-application/AddForm.vue
  40. 100 69
      src/views/elderly/elder/elder-file/Third.vue
  41. 60 47
      src/views/elderly/elder/family-appointment/time-setting/index.vue
  42. 70 60
      src/views/social-worker/assessment/PerceptionAndCommunication/index.vue
  43. 8 0
      src/views/social-worker/assessment/SAS/index.vue
  44. 8 2
      src/views/social-worker/assessment/SDS/index.vue
  45. 8 0
      src/views/social-worker/assessment/psychological-social-situation/index.vue
  46. 5 5
      src/views/system/little-tools/index.vue
  47. 48 29
      src/views/system/supplierManage/index.vue

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

@@ -0,0 +1,29 @@
+# 2026-08-04
+
+## nine-safety-measures/index.vue — 卡片底部留白 / 饼图标签截断
+
+**最终方案:`.grid-auto` 加 `align-items: start`,让卡片按内容高度对齐,不再被同行高卡片拉伸。**
+
+走过的弯路(记下来避免再犯):
+1. 第一次把"九防高危占比"底部留白归因到 ECharts `grid.bottom: 55` 太大——错。
+2. 改成 `bottom: 0` 后没效果,才意识到留白在 ECharts 画布**外**(卡片被 Grid `align-items: stretch` 强行拉高,而 `.chart` 写死 320px)。
+3. 第二次改成 `.card { display:flex; flex-direction:column }` + `.chart { flex:1 0 320px }`,让图表撑满卡片——这解决了柱状图,但又引出饼图标签被裁成 "…" 的新问题。
+4. 根因:ECharts 饼图 `radius: '70%'` 是按**容器短边**算的。图表被拉高后短边变成卡片宽度,饼图直径超出卡片,左侧引导线/标签被裁断。
+5. **正确做法**:别让图表撑满。给 `.grid-auto` 加 `align-items: start`,卡片高度=内容高度,既无内部留白,饼图也回到合理尺寸。
+
+副带改动(清理):`renderAssessCompletion` 的 `grid.bottom` 从 55 → 30,配合 `axisLabel.margin: 10`,给旋转 35° 的 X 轴标签留呼吸空间。
+
+**教训**:dashboard 类页面遇到"卡片底部空白",先检查外层 Grid/容器是否在拉伸子元素,别急着改图表内部的 ECharts 配置。
+
+## 移除「半年度评估频次日历」图表(约 10:50)
+
+用户要求删除该日历热力图及其相关方法/数据。已清理的内容:
+- `index.vue`:`CalendarComponent` 的 import 与 `echarts.use()` 注册;`calendarData` ref;`renderCalendar()` 函数及其在 `renderAllCharts` 的调用;`loadAllData` 中 `getMonthlyAssessCalendar` 请求、解构变量 `cal`、`calendarData.value = cal` 赋值;模板中 `半年度评估频次日历` 卡片(含 `span-full`);CSS `.chart-calendar` 与 `.grid-auto .span-full`(`span-full` 仅此卡片使用)。
+- `src/api/data-board/nine-safety.ts`:删除 `getMonthlyAssessCalendar` 函数(已无其它引用)。
+
+注意:`.grid-auto` 此前为 `align-items: start`(上一轮结论),移除全宽卡片后不影响布局。
+
+补充(约 11:15):vue-tsc 类型检查报两处 TS6133 死代码,已清理——
+- `index.vue` 中未使用的 `levelNames` 常量(疑似日历遗留);
+- `nine-safety.ts` 的 `getNineRiskCorrelation` 中未使用的 `elders` 局部变量(`generateElders` 在 `getRiskRadar` 仍使用,不受影响)。
+重新检查 nine-safety 相关文件:NO_NINE_SAFETY_ERRORS,删除干净无编译错误。

+ 27 - 539
package-lock.json

@@ -33,7 +33,8 @@
         "decimal.js": "^3.0.1",
         "diagram-js": "^12.8.0",
         "driver.js": "^1.3.1",
-        "echarts": "^5.5.0",
+        "echarts": "^6.1.0",
+        "echarts-gl": "^2.1.0",
         "echarts-wordcloud": "^2.1.0",
         "element-plus": "2.9.1",
         "exceljs": "^4.4.0",
@@ -1855,25 +1856,6 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@bpmn-io/cm-theme": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/cm-theme/-/cm-theme-0.2.2.tgz",
-      "integrity": "sha512-9mD7fzzNDNBgseOUC6iRX4mi17LIO2es4zkcHt4e4owQC2Q3YPvqJTJaXqc7kkItloSFsqG2j/1iMJkpvjLv2A==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/language": "^6.12.3",
-        "@lezer/highlight": "^1.2.3"
-      },
-      "engines": {
-        "node": ">= 20.12.0"
-      },
-      "peerDependencies": {
-        "@codemirror/state": "^6.0.0",
-        "@codemirror/view": "^6.0.0"
-      }
-    },
     "node_modules/@bpmn-io/diagram-js-ui": {
       "version": "0.2.4",
       "resolved": "https://registry.npmjs.org/@bpmn-io/diagram-js-ui/-/diagram-js-ui-0.2.4.tgz",
@@ -1894,238 +1876,6 @@
         "min-dash": "^4.0.0"
       }
     },
-    "node_modules/@bpmn-io/feel-editor": {
-      "version": "2.6.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/feel-editor/-/feel-editor-2.6.0.tgz",
-      "integrity": "sha512-rI7vFBATeKJVi/KpkfxnclTObAvQGYda0l5eKhNwGIJ8D3+aPQlgdNtxJATX1keXVkqsA1Q/+Yn8Ih+dnC0NMw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/feel-lint": "^3.1.0",
-        "@bpmn-io/lang-feel": "^4.0.2",
-        "@camunda/feel-builtins": "^1.2.0",
-        "@codemirror/autocomplete": "^6.20.0",
-        "@codemirror/commands": "^6.10.3",
-        "@codemirror/language": "^6.11.3",
-        "@codemirror/lint": "^6.9.7",
-        "@codemirror/state": "^6.6.0",
-        "@codemirror/view": "^6.43.1",
-        "@lezer/highlight": "^1.2.3",
-        "min-dom": "^5.3.0",
-        "mitt": "^3.0.1"
-      },
-      "engines": {
-        "node": ">= 20"
-      },
-      "peerDependencies": {
-        "@bpmn-io/cm-theme": "^0.2.0"
-      }
-    },
-    "node_modules/@bpmn-io/feel-lint": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/feel-lint/-/feel-lint-3.1.0.tgz",
-      "integrity": "sha512-nOCYWMXgwR0joU4XKhu4f3P5f4/AsmuyiV1e+fzcXTCh7iMLU8VYdHXSIzZuJ0zxz3Gsnp7A8S8lt+HJ43H6IA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/lezer-feel": "^2.1.0",
-        "@codemirror/language": "^6.12.1"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/@bpmn-io/feelers-editor": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/feelers-editor/-/feelers-editor-1.1.1.tgz",
-      "integrity": "sha512-HNvssf1nb/OohOgFxxR0hWIpIb8JBvmr2CQ28GD/5WLPKe3uvu9mpItIjNTCpLxLuVHlsJLNDtLkf2B6tvmt9Q==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/feelers-lint": "^1.0.0",
-        "@bpmn-io/lang-feelers": "^1.0.0",
-        "@codemirror/autocomplete": "^6.20.0",
-        "@codemirror/commands": "^6.10.1",
-        "@codemirror/language": "^6.12.1",
-        "@codemirror/lint": "^6.9.3",
-        "@codemirror/state": "^6.5.4",
-        "@codemirror/view": "^6.39.12",
-        "@lezer/common": "^1.5.1",
-        "@lezer/html": "^1.3.13",
-        "@lezer/markdown": "^1.6.3",
-        "min-dom": "^5.2.0",
-        "mitt": "^3.0.1"
-      },
-      "peerDependencies": {
-        "@bpmn-io/cm-theme": "^0.2.1"
-      }
-    },
-    "node_modules/@bpmn-io/feelers-lint": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/feelers-lint/-/feelers-lint-1.0.0.tgz",
-      "integrity": "sha512-9McPCub8NBSD3gbAM9fu6laGVvb6U/D8Y5Ar/GGvOaAuJ6W34yP5jaV/on4MALTwmmXTDadZjO2RJJNub3JmwQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/feel-lint": "^3.1.0",
-        "@codemirror/language": "^6.12.1",
-        "@codemirror/lint": "^6.9.3"
-      }
-    },
-    "node_modules/@bpmn-io/lang-feel": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/lang-feel/-/lang-feel-4.0.2.tgz",
-      "integrity": "sha512-rTPeJ77/2nYKbJXpvDmrNXy+opoIpD3Rsch0grCUZAO6xF68r41QeV8SqOdr2yZxD/4gI6YXlQhqh4rqFRDJXA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/lezer-feel": "^3.0.1",
-        "@codemirror/autocomplete": "^6.20.3",
-        "@codemirror/language": "^6.12.3",
-        "@lezer/common": "^1.5.2"
-      },
-      "engines": {
-        "node": ">= 20.12.0"
-      }
-    },
-    "node_modules/@bpmn-io/lang-feel/node_modules/@bpmn-io/lezer-feel": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feel/-/lezer-feel-3.0.1.tgz",
-      "integrity": "sha512-+wWg4DP2h8WOEQx2Bz51blsw9dCqKj3kIDKYccABuoAEhzZeDhmm0A2k4ANmm9lf0w5pJmVSYz2djtrbzOvzzA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/highlight": "^1.2.3",
-        "@lezer/lr": "^1.4.10",
-        "min-dash": "^5.0.0"
-      },
-      "engines": {
-        "node": ">= 20.12.0"
-      }
-    },
-    "node_modules/@bpmn-io/lang-feel/node_modules/min-dash": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz",
-      "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/@bpmn-io/lang-feelers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/lang-feelers/-/lang-feelers-1.0.0.tgz",
-      "integrity": "sha512-VgBDw9cXr62e6Qs6LixQyLiBxxv7Kh8fxKFkYCmHPf/cO8beLQGjnF789wRlhK05yh/CuHi7oN7EYChrFQOHwg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/lezer-feel": "^3.0.0",
-        "@bpmn-io/lezer-feelers": "^1.0.0",
-        "@codemirror/language": "^6.12.1",
-        "@lezer/common": "^1.5.1"
-      }
-    },
-    "node_modules/@bpmn-io/lang-feelers/node_modules/@bpmn-io/lezer-feel": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feel/-/lezer-feel-3.0.1.tgz",
-      "integrity": "sha512-+wWg4DP2h8WOEQx2Bz51blsw9dCqKj3kIDKYccABuoAEhzZeDhmm0A2k4ANmm9lf0w5pJmVSYz2djtrbzOvzzA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/highlight": "^1.2.3",
-        "@lezer/lr": "^1.4.10",
-        "min-dash": "^5.0.0"
-      },
-      "engines": {
-        "node": ">= 20.12.0"
-      }
-    },
-    "node_modules/@bpmn-io/lang-feelers/node_modules/min-dash": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz",
-      "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/@bpmn-io/lezer-feel": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feel/-/lezer-feel-2.5.0.tgz",
-      "integrity": "sha512-09JlwNYeD5YYeFrl0dIFfgQKEL+NNiTyUwEtdcPhfftz1mYa7z+ylOPCv/8Z0GqG7X81uyMFvRZDFCRBFTiUPg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/highlight": "^1.2.3",
-        "@lezer/lr": "^1.4.10",
-        "min-dash": "^5.0.0"
-      },
-      "engines": {
-        "node": ">= 20.12.0"
-      }
-    },
-    "node_modules/@bpmn-io/lezer-feel/node_modules/min-dash": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz",
-      "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/@bpmn-io/lezer-feelers": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feelers/-/lezer-feelers-1.0.0.tgz",
-      "integrity": "sha512-P0znH2SY85O7RmOr/FgveFiTREet9DSCf3nH3J/StkI5i7Q384o+GEeTmv58n91FlUknS20KE3T8Yk5Qmab+hg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/common": "^1.5.1",
-        "@lezer/highlight": "^1.1.6",
-        "@lezer/lr": "^1.4.8"
-      }
-    },
-    "node_modules/@bpmn-io/properties-panel": {
-      "version": "3.47.0",
-      "resolved": "https://registry.npmjs.org/@bpmn-io/properties-panel/-/properties-panel-3.47.0.tgz",
-      "integrity": "sha512-tZQRM0x9KLUEFU2FyHCwnYjSq1lbxpY8oCwABFA00IcuyeEDQFDV5DFg7ytTv2qOyWgncZ2xNXPRQ9pydkN2gg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@bpmn-io/feel-editor": "^2.6.0",
-        "@bpmn-io/feelers-editor": "^1.0.0",
-        "@codemirror/autocomplete": "^6.18.6",
-        "@codemirror/commands": "^6.10.3",
-        "@codemirror/lang-json": "^6.0.2",
-        "@codemirror/language": "^6.11.0",
-        "@codemirror/lint": "^6.9.5",
-        "@codemirror/state": "^6.6.0",
-        "@codemirror/view": "^6.28.1",
-        "classnames": "^2.5.1",
-        "focus-trap": "^8.0.0",
-        "min-dash": "^5.0.0",
-        "min-dom": "^5.3.0"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/@bpmn-io/properties-panel/node_modules/min-dash": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz",
-      "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/@cacheable/memory": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
@@ -2187,108 +1937,6 @@
         "@keyv/serialize": "^1.1.1"
       }
     },
-    "node_modules/@camunda/feel-builtins": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/@camunda/feel-builtins/-/feel-builtins-1.3.0.tgz",
-      "integrity": "sha512-ZahpLTeBUjnZEMI6Hu/n9e/oTPcJ8DpWmIyJ00tgVNWM1kqAEcMhARK0GthSrD5NXiSVICNHZ7VRTERMq2FmnA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/@codemirror/autocomplete": {
-      "version": "6.20.3",
-      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
-      "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/language": "^6.0.0",
-        "@codemirror/state": "^6.0.0",
-        "@codemirror/view": "^6.17.0",
-        "@lezer/common": "^1.0.0"
-      }
-    },
-    "node_modules/@codemirror/commands": {
-      "version": "6.10.4",
-      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
-      "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/language": "^6.0.0",
-        "@codemirror/state": "^6.7.0",
-        "@codemirror/view": "^6.27.0",
-        "@lezer/common": "^1.1.0"
-      }
-    },
-    "node_modules/@codemirror/lang-json": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
-      "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/language": "^6.0.0",
-        "@lezer/json": "^1.0.0"
-      }
-    },
-    "node_modules/@codemirror/language": {
-      "version": "6.12.4",
-      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
-      "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/state": "^6.0.0",
-        "@codemirror/view": "^6.23.0",
-        "@lezer/common": "^1.5.0",
-        "@lezer/highlight": "^1.0.0",
-        "@lezer/lr": "^1.0.0",
-        "style-mod": "^4.0.0"
-      }
-    },
-    "node_modules/@codemirror/lint": {
-      "version": "6.9.7",
-      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
-      "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/state": "^6.0.0",
-        "@codemirror/view": "^6.42.0",
-        "crelt": "^1.0.5"
-      }
-    },
-    "node_modules/@codemirror/state": {
-      "version": "6.7.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.0.tgz",
-      "integrity": "sha512-Zbl9NyscLMZkfXPQnNAIIAFftidrA1UbcJEIMp24C0Bukc2I5T8wJS0wsXYsnDOqCFJUeJ1BITGNs5CqPDSmSg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@marijn/find-cluster-break": "^1.0.0"
-      }
-    },
-    "node_modules/@codemirror/view": {
-      "version": "6.43.4",
-      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.4.tgz",
-      "integrity": "sha512-YImu23iyKfncJzT7sRy+rEqEhSc8RhOHqDxwy4WzXRKJwYm6iwf/9OJk5ctCAdZ6yi2ZqaGEvmf55fSVqMDrgg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@codemirror/state": "^6.7.0",
-        "crelt": "^1.0.6",
-        "style-mod": "^4.1.0",
-        "w3c-keyname": "^2.2.4"
-      }
-    },
     "node_modules/@commitlint/cli": {
       "version": "19.8.1",
       "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz",
@@ -3953,82 +3601,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@lezer/common": {
-      "version": "1.5.2",
-      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
-      "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
-    "node_modules/@lezer/highlight": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
-      "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/common": "^1.3.0"
-      }
-    },
-    "node_modules/@lezer/html": {
-      "version": "1.3.13",
-      "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
-      "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/common": "^1.2.0",
-        "@lezer/highlight": "^1.0.0",
-        "@lezer/lr": "^1.0.0"
-      }
-    },
-    "node_modules/@lezer/json": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
-      "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/common": "^1.2.0",
-        "@lezer/highlight": "^1.0.0",
-        "@lezer/lr": "^1.0.0"
-      }
-    },
-    "node_modules/@lezer/lr": {
-      "version": "1.4.10",
-      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
-      "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/common": "^1.0.0"
-      }
-    },
-    "node_modules/@lezer/markdown": {
-      "version": "1.6.4",
-      "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.4.tgz",
-      "integrity": "sha512-N0SxazMj4k65DBfaf1azqtMZd6u7MqluP84/NZnB/io8Td9aleFmAhz9hcbvSfsxT5tdYlJ5qgv5aMJGY4zEtA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "@lezer/common": "^1.5.0",
-        "@lezer/highlight": "^1.0.0"
-      }
-    },
-    "node_modules/@marijn/find-cluster-break": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
-      "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/@microsoft/fetch-event-source": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
@@ -5830,13 +5402,6 @@
       "license": "MIT",
       "optional": true
     },
-    "node_modules/@types/video.js": {
-      "version": "7.3.58",
-      "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz",
-      "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==",
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/@types/web-bluetooth": {
       "version": "0.0.20",
       "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -8913,42 +8478,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/camunda-bpmn-js-behaviors": {
-      "version": "1.18.0",
-      "resolved": "https://registry.npmjs.org/camunda-bpmn-js-behaviors/-/camunda-bpmn-js-behaviors-1.18.0.tgz",
-      "integrity": "sha512-ozmC9Btz7l6x0edBZaRxCUxF28mVbnTHO+v8ve8xGfhVRI0y8fFN/enAAILGk/eT2e+87BMx7uIUHGaxQBzbjQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "ids": "^3.0.1",
-        "min-dash": "^5.0.0"
-      },
-      "peerDependencies": {
-        "bpmn-js": ">= 9",
-        "camunda-bpmn-moddle": ">= 7",
-        "zeebe-bpmn-moddle": ">= 0.18"
-      }
-    },
-    "node_modules/camunda-bpmn-js-behaviors/node_modules/ids": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/ids/-/ids-3.0.2.tgz",
-      "integrity": "sha512-t6YJP4mdC+GHF96Nbis/4FEANhP/8VWmYMvUuYpXvSdrhg5hpIVbq2XZlOA3UWTbtdwPCi0q7jEXOdHkAnqOnw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "engines": {
-        "node": ">= 20.12"
-      }
-    },
-    "node_modules/camunda-bpmn-js-behaviors/node_modules/min-dash": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz",
-      "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/camunda-bpmn-moddle": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/camunda-bpmn-moddle/-/camunda-bpmn-moddle-7.0.1.tgz",
@@ -9148,13 +8677,10 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/classnames": {
-      "version": "2.5.1",
-      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
-      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
+    "node_modules/claygl": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz",
+      "integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
     },
     "node_modules/cli-cursor": {
       "version": "5.0.0",
@@ -9682,14 +9208,6 @@
         "node": ">= 10"
       }
     },
-    "node_modules/crelt": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
-      "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/cropperjs": {
       "version": "1.6.2",
       "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
@@ -10817,13 +10335,26 @@
       "license": "MIT"
     },
     "node_modules/echarts": {
-      "version": "5.6.0",
-      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
-      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
+      "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
       "license": "Apache-2.0",
       "dependencies": {
         "tslib": "2.3.0",
-        "zrender": "5.6.1"
+        "zrender": "6.1.0"
+      }
+    },
+    "node_modules/echarts-gl": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.1.0.tgz",
+      "integrity": "sha512-GxzAPTYJyOANbu7InkdGV7QLLpVyuQWNvN1yyEEIiqgM11ilAO2OkvPNgRdf854R9ZPt/C1HAgjO8Udrj61lOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "claygl": "^1.2.1",
+        "zrender": "^5.1.1 || ^6.0.0"
+      },
+      "peerDependencies": {
+        "echarts": "^5.1.2 || ^6.0.0"
       }
     },
     "node_modules/echarts-wordcloud": {
@@ -12200,17 +11731,6 @@
       "dev": true,
       "license": "ISC"
     },
-    "node_modules/focus-trap": {
-      "version": "8.2.2",
-      "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.2.tgz",
-      "integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true,
-      "dependencies": {
-        "tabbable": "^6.5.0"
-      }
-    },
     "node_modules/follow-redirects": {
       "version": "1.16.0",
       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
@@ -19191,14 +18711,6 @@
       ],
       "license": "MIT"
     },
-    "node_modules/style-mod": {
-      "version": "4.1.3",
-      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
-      "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/stylelint": {
       "version": "16.26.1",
       "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz",
@@ -19982,14 +19494,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/tabbable": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
-      "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/table": {
       "version": "6.9.0",
       "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
@@ -20483,7 +19987,7 @@
       "version": "5.3.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
       "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
-      "devOptional": true,
+      "dev": true,
       "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
@@ -21815,14 +21319,6 @@
       "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
       "license": "MIT"
     },
-    "node_modules/w3c-keyname": {
-      "version": "2.2.8",
-      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
-      "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/wangeditor": {
       "version": "4.7.15",
       "resolved": "https://registry.npmjs.org/wangeditor/-/wangeditor-4.7.15.tgz",
@@ -22405,14 +21901,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/zeebe-bpmn-moddle": {
-      "version": "1.16.0",
-      "resolved": "https://registry.npmjs.org/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.16.0.tgz",
-      "integrity": "sha512-dsblK7DYgu4e3E4Vp9pSWjWpVrw+WtzLEMb82ak4FHL3FI7arWcTfIE/CviAS7iWD/UOl5YniQgee1LEJDdvaA==",
-      "dev": true,
-      "license": "MIT",
-      "peer": true
-    },
     "node_modules/zip-stream": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
@@ -22449,9 +21937,9 @@
       }
     },
     "node_modules/zrender": {
-      "version": "5.6.1",
-      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
-      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
+      "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
       "license": "BSD-3-Clause",
       "dependencies": {
         "tslib": "2.3.0"

+ 2 - 1
package.json

@@ -51,7 +51,8 @@
     "decimal.js": "^3.0.1",
     "diagram-js": "^12.8.0",
     "driver.js": "^1.3.1",
-    "echarts": "^5.5.0",
+    "echarts": "^6.1.0",
+    "echarts-gl": "^2.1.0",
     "echarts-wordcloud": "^2.1.0",
     "element-plus": "2.9.1",
     "exceljs": "^4.4.0",

+ 417 - 0
src/api/data-board/nine-safety.ts

@@ -0,0 +1,417 @@
+/**
+ * 九防安全看板 - Mock API
+ * 基于风险知情书 + 九防项目列表的数据结构,模拟后端返回的聚合统计数据。
+ * 所有接口支持 { month, floor } 筛选参数,返回 Promise。
+ */
+
+// ========================= 基础配置 =========================
+const NINE_RISKS = [
+  { key: 'asphyxiation', name: '防噎食' },
+  { key: 'pressureUlcer', name: '防压疮' },
+  { key: 'fall', name: '防跌倒' },
+  { key: 'bedFall', name: '防坠床' },
+  { key: 'scald', name: '防烫伤' },
+  { 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 = [
+  '张大爷', '王奶奶', '李爷爷', '赵奶奶', '刘大爷', '陈奶奶', '杨爷爷', '黄奶奶',
+  '吴大爷', '周奶奶', '徐爷爷', '孙奶奶', '马大爷', '朱奶奶', '胡爷爷', '郭奶奶',
+  '何大爷', '高奶奶', '林爷爷', '罗奶奶', '郑大爷', '梁奶奶', '宋爷爷', '谢奶奶'
+]
+
+// ========================= 简易随机数(带 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)
+}

+ 15 - 10
src/components/BedCardComponent/index.vue

@@ -4,9 +4,14 @@
     <div class="bed-card">
       <!-- 头部信息 -->
       <div class="card-header">
-        <h2 class="header-title">长者床头卡</h2>
-        <div class="header-room">{{ patientData.roomName || '' }}</div>
         <img src="../../assets/imgs/logo_card.png" alt="颐年医养" class="header-logo" />
+        <div style="display: flex; align-items: center">
+          <h2 class="header-title">长者床头卡</h2>
+        </div>
+        <div style="font-size: 16px; color: #fff; margin-right: 10px"
+          >更新时间:{{ formatToDate(patientData.updateTime) || '' }}</div
+        >
+        <!--        <div class="header-room">{{ patientData.roomName || '' }}</div>-->
       </div>
 
       <div class="card-main">
@@ -15,7 +20,9 @@
           <div class="bed-info">
             <div class="bed-letter" v-for="(item, index) in bedList" :key="index">
               <span>{{ item || '' }}</span>
-              <span v-if="index == (bedList || []).length - 1" class="bed-text">床</span>
+              <span v-if="index == (bedList || []).length - 1 && !item.includes('床')" class="bed-text"
+                >床</span
+              >
             </div>
           </div>
 
@@ -166,6 +173,7 @@ import html2canvas from 'html2canvas'
 import { Qrcode } from '@/components/Qrcode'
 import JSZip from 'jszip'
 import { Check } from '@element-plus/icons-vue'
+import { formatToDate } from '@/utils/dateUtil'
 // Props定义
 const props = defineProps<{
   roomNumber?: string
@@ -387,7 +395,7 @@ const updatePatientData = (data: any) => {
   qrCodeContent.value =
     'https://callback.ynims.com/qrcode/elderlyId?=' + data.qr?.eId + '&tenantId=' + data.qr?.tId
   Object.assign(patientData, data)
-  console.log("收到数据:",data)
+  console.log('收到数据:', data)
   try {
     bedList.value = patientData.bedName.toString().split('-')
     //console.log(bedList.value)
@@ -432,20 +440,18 @@ defineExpose({
 .card-header {
   background-color: #e78d35;
   color: white;
-  padding: 8px 20px;
+  padding: 8px 10px;
   display: flex;
-  justify-content: space-between;
   align-items: center;
+  justify-content: space-between;
   height: 76px;
   box-sizing: border-box;
-  flex-shrink: 0;
 }
 
 .header-title {
   margin: 0;
-  font-size: 20px;
+  font-size: 22px;
   font-weight: bold;
-  flex: 1;
 }
 
 .header-room {
@@ -457,7 +463,6 @@ defineExpose({
 
 .header-logo {
   height: 60px;
-  flex: 1;
   text-align: right;
   object-fit: contain;
 }

+ 4 - 2
src/components/BedCardModal/index.vue

@@ -108,7 +108,7 @@ const updatePatientData = async (data) => {
     // } catch (e) {
     //   console.error('处理床位信息失败:', e)
     // }
-    console.log("数据:",sourceData)
+    console.log('数据:', sourceData)
 
     const patientData = {
       roomName: sourceData.roomName,
@@ -140,8 +140,10 @@ const updatePatientData = async (data) => {
       qr: {
         eId: sourceData.id || sourceData.qr?.eId || '',
         tId: sourceData.tenantId || sourceData.qr?.tId || ''
-      }
+      },
+      updateTime: sourceData.updateTime||''
     }
+    console.log('DATA', data)
 
     bedCardRef.value.updatePatientData(patientData)
   }

+ 5 - 2
src/utils/outBedCard.ts

@@ -176,7 +176,9 @@ export const getPatientData = (data,dataElder) => {
       bedName: dataForm.value.bedName,
       elderName: dataForm.value.elderName,
       elderSex: dataForm.value.idCard
-        ? (getInfo(dataForm.value.idCard).sex==1?'男':'女')
+        ? getInfo(dataForm.value.idCard).sex == 1
+          ? '男'
+          : '女'
         : dataForm.value.elderSex?.toString() == '1'
           ? '男'
           : '女',
@@ -218,7 +220,8 @@ export const getPatientData = (data,dataElder) => {
         tId: dataForm.value.tenantId || parsedFlagData.tenantId
       },
       // 添加解析后的Flag字段中文文本,方便床头卡组件使用
-      flagTexts: parsedFlagData
+      flagTexts: parsedFlagData,
+      updateTime: dataElder?.updateTime || ''
     }
 
     console.log('床头卡组装后数据:::', result)

+ 823 - 0
src/views/data-board/comprehensive-overview/index.vue

@@ -0,0 +1,823 @@
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import echarts from '@/plugins/echarts'
+import 'echarts-gl'
+
+// 公共 tooltip 深色样式,避免白字白底看不见
+const TOOLTIP_DARK = {
+  backgroundColor: 'rgba(2, 11, 30, 0.92)',
+  borderColor: 'rgba(0, 255, 255, 0.3)',
+  borderWidth: 1,
+  padding: [8, 12],
+  textStyle: { color: '#fff' }
+}
+const overview = ref({
+  totalElderly: 328,
+  bedOccupancy: 89,
+  monthlyRevenue: 865400,
+  nursingStaff: 62,
+  todayAppointments: 24,
+  satisfaction: 96
+})
+
+const admissionRef = ref<HTMLDivElement>()
+const ageRef = ref<HTMLDivElement>()
+const diseaseRef = ref<HTMLDivElement>()
+const revenueRef = ref<HTMLDivElement>()
+const activityRef = ref<HTMLDivElement>()
+const campusRef = ref<HTMLDivElement>()
+
+let chartAdmission: any = null
+let chartAge: any = null
+let chartDisease: any = null
+let chartRevenue: any = null
+let chartActivity: any = null
+let chartCampus: any = null
+
+const handleResize = () => {
+  chartAdmission?.resize()
+  chartAge?.resize()
+  chartDisease?.resize()
+  chartRevenue?.resize()
+  chartActivity?.resize()
+  chartCampus?.resize()
+}
+
+const disposeCharts = () => {
+  chartAdmission?.dispose()
+  chartAge?.dispose()
+  chartDisease?.dispose()
+  chartRevenue?.dispose()
+  chartActivity?.dispose()
+  chartCampus?.dispose()
+  chartAdmission = null
+  chartAge = null
+  chartDisease = null
+  chartRevenue = null
+  chartActivity = null
+  chartCampus = null
+}
+
+// 月度入住 / 退住趋势
+const initAdmissionChart = () => {
+  if (!admissionRef.value) return
+  chartAdmission = echarts.init(admissionRef.value)
+  chartAdmission.setOption({
+    backgroundColor: 'transparent',
+    tooltip: { ...TOOLTIP_DARK, trigger: 'axis' },
+    legend: {
+      data: ['新入住', '退住', '累计在院'],
+      textStyle: { color: '#c7e0ff' },
+      top: 0
+    },
+    grid: { left: 50, right: 30, top: 45, bottom: 30 },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff' }
+    },
+    yAxis: [
+      {
+        type: 'value',
+        name: '人数',
+        nameTextStyle: { color: '#8fb0c7' },
+        splitLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.5)' } },
+        axisLine: { lineStyle: { color: '#1a3a6b' } },
+        axisLabel: { color: '#c7e0ff' }
+      },
+      {
+        type: 'value',
+        name: '累计(人)',
+        nameTextStyle: { color: '#8fb0c7' },
+        splitLine: { show: false },
+        axisLine: { lineStyle: { color: '#1a3a6b' } },
+        axisLabel: { color: '#c7e0ff' }
+      }
+    ],
+    series: [
+      {
+        name: '新入住',
+        type: 'bar',
+        barWidth: 14,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#00ffff' },
+            { offset: 1, color: '#0055aa' }
+          ])
+        },
+        data: [18, 22, 26, 19, 24, 28, 32, 27, 30, 25, 29, 31]
+      },
+      {
+        name: '退住',
+        type: 'bar',
+        barWidth: 14,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#ff9966' },
+            { offset: 1, color: '#aa3300' }
+          ])
+        },
+        data: [8, 10, 7, 11, 9, 12, 8, 13, 10, 9, 11, 14]
+      },
+      {
+        name: '累计在院',
+        type: 'line',
+        yAxisIndex: 1,
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 8,
+        lineStyle: { color: '#ffd85a', width: 3 },
+        itemStyle: { color: '#ffd85a' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(255, 216, 90, 0.3)' },
+            { offset: 1, color: 'rgba(255, 216, 90, 0)' }
+          ])
+        },
+        data: [260, 272, 291, 299, 314, 330, 354, 368, 388, 404, 422, 439]
+      }
+    ]
+  })
+}
+
+// 长者年龄分布
+const initAgeChart = () => {
+  if (!ageRef.value) return
+  chartAge = echarts.init(ageRef.value)
+  chartAge.setOption({
+    backgroundColor: 'transparent',
+    tooltip: {
+      ...TOOLTIP_DARK,
+      trigger: 'item',
+      formatter: '{b}<br/>人数:{c}人 ({d}%)'
+    },
+    legend: {
+      bottom: 0,
+      textStyle: { color: '#c7e0ff' }
+    },
+    series: [
+      {
+        name: '年龄分布',
+        type: 'pie',
+        radius: ['42%', '68%'],
+        center: ['50%', '42%'],
+        avoidLabelOverlap: true,
+        itemStyle: {
+          borderRadius: 6,
+          borderColor: '#05132b',
+          borderWidth: 2
+        },
+        label: {
+          show: true,
+          color: '#fff',
+          formatter: '{b}\n{d}%'
+        },
+        labelLine: {
+          length: 8,
+          length2: 10,
+          lineStyle: { color: '#557a9c' }
+        },
+        data: [
+          { value: 46, name: '60-69岁', itemStyle: { color: '#00ffff' } },
+          { value: 98, name: '70-79岁', itemStyle: { color: '#00aaff' } },
+          { value: 126, name: '80-89岁', itemStyle: { color: '#5578ff' } },
+          { value: 48, name: '90岁以上', itemStyle: { color: '#b380ff' } },
+          { value: 10, name: '60岁以下', itemStyle: { color: '#ff7a99' } }
+        ]
+      }
+    ]
+  })
+}
+
+// 长者慢病TOP统计
+const initDiseaseChart = () => {
+  if (!diseaseRef.value) return
+  chartDisease = echarts.init(diseaseRef.value)
+  chartDisease.setOption({
+    backgroundColor: 'transparent',
+    tooltip: {
+      ...TOOLTIP_DARK,
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' }
+    },
+    grid: { left: 90, right: 30, top: 15, bottom: 20 },
+    xAxis: {
+      type: 'value',
+      splitLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.5)' } },
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff' }
+    },
+    yAxis: {
+      type: 'category',
+      data: ['冠心病', '糖尿病', '骨质疏松', '高血压', '阿尔茨海默症', '高血脂'],
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff' }
+    },
+    series: [
+      {
+        name: '患病人数',
+        type: 'bar',
+        barWidth: 18,
+        label: {
+          show: true,
+          position: 'right',
+          color: '#c7e0ff',
+          formatter: '{c}人'
+        },
+        itemStyle: {
+          borderRadius: [0, 9, 9, 0],
+          color: (params: any) => {
+            const colors = [
+              new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: '#0055aa' },
+                { offset: 1, color: '#00ffff' }
+              ]),
+              new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: '#0055aa' },
+                { offset: 1, color: '#00aaff' }
+              ]),
+              new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: '#0055aa' },
+                { offset: 1, color: '#5578ff' }
+              ]),
+              new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: '#0055aa' },
+                { offset: 1, color: '#b380ff' }
+              ]),
+              new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: '#0055aa' },
+                { offset: 1, color: '#ff9966' }
+              ]),
+              new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: '#0055aa' },
+                { offset: 1, color: '#ff7a99' }
+              ])
+            ]
+            return colors[params.dataIndex % colors.length]
+          }
+        },
+        data: [86, 112, 74, 198, 52, 108]
+      }
+    ]
+  })
+}
+
+// 近6个月费用营收趋势
+const initRevenueChart = () => {
+  if (!revenueRef.value) return
+  chartRevenue = echarts.init(revenueRef.value)
+  chartRevenue.setOption({
+    backgroundColor: 'transparent',
+    tooltip: {
+      ...TOOLTIP_DARK,
+      trigger: 'axis',
+      axisPointer: { type: 'cross' }
+    },
+    legend: {
+      data: ['床位费', '护理费', '餐饮费', '服务费', '其他'],
+      textStyle: { color: '#c7e0ff' },
+      top: 0
+    },
+    grid: { left: 60, right: 30, top: 45, bottom: 30 },
+    xAxis: {
+      type: 'category',
+      data: ['2月', '3月', '4月', '5月', '6月', '7月'],
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff' }
+    },
+    yAxis: {
+      type: 'value',
+      name: '金额(元)',
+      nameTextStyle: { color: '#8fb0c7' },
+      splitLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.5)' } },
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff' }
+    },
+    series: [
+      {
+        name: '床位费',
+        type: 'line',
+        smooth: true,
+        stack: 'total',
+        lineStyle: { color: '#00ffff', width: 2 },
+        itemStyle: { color: '#00ffff' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(0, 255, 255, 0.4)' },
+            { offset: 1, color: 'rgba(0, 255, 255, 0)' }
+          ])
+        },
+        data: [320000, 338000, 352000, 368000, 378000, 395000]
+      },
+      {
+        name: '护理费',
+        type: 'line',
+        smooth: true,
+        stack: 'total',
+        lineStyle: { color: '#5578ff', width: 2 },
+        itemStyle: { color: '#5578ff' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(85, 120, 255, 0.4)' },
+            { offset: 1, color: 'rgba(85, 120, 255, 0)' }
+          ])
+        },
+        data: [186000, 196000, 208000, 222000, 238000, 252000]
+      },
+      {
+        name: '餐饮费',
+        type: 'line',
+        smooth: true,
+        stack: 'total',
+        lineStyle: { color: '#b380ff', width: 2 },
+        itemStyle: { color: '#b380ff' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(179, 128, 255, 0.4)' },
+            { offset: 1, color: 'rgba(179, 128, 255, 0)' }
+          ])
+        },
+        data: [96000, 102000, 108000, 114000, 120000, 126000]
+      },
+      {
+        name: '服务费',
+        type: 'line',
+        smooth: true,
+        stack: 'total',
+        lineStyle: { color: '#ff9966', width: 2 },
+        itemStyle: { color: '#ff9966' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(255, 153, 102, 0.4)' },
+            { offset: 1, color: 'rgba(255, 153, 102, 0)' }
+          ])
+        },
+        data: [58000, 64000, 72000, 78000, 84000, 88000]
+      },
+      {
+        name: '其他',
+        type: 'line',
+        smooth: true,
+        stack: 'total',
+        lineStyle: { color: '#ffd85a', width: 2 },
+        itemStyle: { color: '#ffd85a' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(255, 216, 90, 0.4)' },
+            { offset: 1, color: 'rgba(255, 216, 90, 0)' }
+          ])
+        },
+        data: [12000, 16000, 18000, 22000, 26000, 32000]
+      }
+    ]
+  })
+}
+
+// 本月活动参与情况
+const initActivityChart = () => {
+  if (!activityRef.value) return
+  chartActivity = echarts.init(activityRef.value)
+  chartActivity.setOption({
+    backgroundColor: 'transparent',
+    tooltip: {
+      ...TOOLTIP_DARK,
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' }
+    },
+    grid: { left: 60, right: 30, top: 15, bottom: 25 },
+    xAxis: {
+      type: 'category',
+      data: ['书法', '舞蹈', '棋牌', '合唱', '太极', '手工', '电影', '健康讲座'],
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff', rotate: 20 }
+    },
+    yAxis: {
+      type: 'value',
+      name: '参与人次',
+      nameTextStyle: { color: '#8fb0c7' },
+      splitLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.5)' } },
+      axisLine: { lineStyle: { color: '#1a3a6b' } },
+      axisLabel: { color: '#c7e0ff' }
+    },
+    series: [
+      {
+        name: '参与人次',
+        type: 'bar',
+        barWidth: 24,
+        label: {
+          show: true,
+          position: 'top',
+          color: '#c7e0ff'
+        },
+        itemStyle: {
+          borderRadius: [6, 6, 0, 0],
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#00ffff' },
+            { offset: 1, color: '#0055aa' }
+          ])
+        },
+        data: [146, 188, 212, 168, 134, 102, 156, 228]
+      }
+    ]
+  })
+}
+
+// 各院区入住对比 3D 柱状图
+const initCampusChart = () => {
+  if (!campusRef.value) return
+  chartCampus = echarts.init(campusRef.value)
+
+  const campuses = ['阳光院区', '颐养院区', '康乐院区', '仁爱院区', '怡福院区']
+  const indicators = ['床位总数', '已入住', '护理等级-特护']
+  const data: (number | string)[][] = []
+  const values = [
+    [220, 196, 48],
+    [180, 158, 36],
+    [260, 242, 62],
+    [160, 136, 28],
+    [200, 182, 52]
+  ]
+  campuses.forEach((c, i) => {
+    indicators.forEach((iname, j) => {
+      data.push([j, i, values[i][j]])
+    })
+  })
+
+  chartCampus.setOption({
+    backgroundColor: 'transparent',
+    tooltip: { ...TOOLTIP_DARK },
+    visualMap: {
+      show: false,
+      min: 20,
+      max: 270,
+      inRange: {
+        color: ['#0a2f5e', '#0055aa', '#00aaff', '#00ffff', '#b380ff']
+      }
+    },
+    xAxis3D: {
+      type: 'category',
+      data: indicators,
+      name: '',
+      axisLabel: { color: '#c7e0ff', textStyle: { color: '#c7e0ff' } }
+    },
+    yAxis3D: {
+      type: 'category',
+      data: campuses,
+      name: '',
+      axisLabel: { color: '#c7e0ff', textStyle: { color: '#c7e0ff' } }
+    },
+    zAxis3D: {
+      type: 'value',
+      name: '',
+      axisLabel: { color: '#c7e0ff', textStyle: { color: '#c7e0ff' } }
+    },
+    grid3D: {
+      boxWidth: 160,
+      boxDepth: 140,
+      boxHeight: 100,
+      viewControl: { alpha: 25, beta: 40, distance: 260, autoRotate: true, autoRotateSpeed: 4 },
+      light: {
+        main: { intensity: 1.2, shadow: true, shadowQuality: 'high' },
+        ambient: { intensity: 0.4 }
+      }
+    },
+    series: [
+      {
+        type: 'bar3D',
+        data: data.map((item) => ({
+          value: item,
+          name: `${campuses[item[1] as number]} - ${indicators[item[0] as number]}`
+        })),
+        shading: 'lambert',
+        label: { show: false },
+        itemStyle: { opacity: 0.9 }
+      }
+    ]
+  })
+}
+
+onMounted(() => {
+  initAdmissionChart()
+  initAgeChart()
+  initDiseaseChart()
+  initRevenueChart()
+  initActivityChart()
+  initCampusChart()
+  window.addEventListener('resize', handleResize)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize)
+  disposeCharts()
+})
+</script>
+
+<template>
+  <div class="overview-page">
+    <div class="page-header">
+      <div class="header-left">
+        <div class="header-line" ></div>
+        <h1 class="page-title">颐年智慧养老综合看板</h1>
+        <div class="header-line" ></div>
+      </div>
+      <p class="page-date">数据更新时间:{{ new Date().toLocaleString('zh-CN') }}</p>
+    </div>
+
+    <!-- 关键指标卡 -->
+    <div class="kpi-row">
+      <div class="kpi-card kpi-cyan">
+        <div class="kpi-label">在院长者</div>
+        <div class="kpi-value">{{ overview.totalElderly }}<span class="kpi-unit">人</span></div>
+        <div class="kpi-sub">较上月 <em class="up">+12</em></div>
+      </div>
+      <div class="kpi-card kpi-blue">
+        <div class="kpi-label">床位使用率</div>
+        <div class="kpi-value">{{ overview.bedOccupancy }}<span class="kpi-unit">%</span></div>
+        <div class="kpi-sub">可用床位 <em>42</em> 张</div>
+      </div>
+      <div class="kpi-card kpi-purple">
+        <div class="kpi-label">本月营收</div>
+        <div class="kpi-value">{{ (overview.monthlyRevenue / 10000).toFixed(1) }}<span class="kpi-unit">万</span></div>
+        <div class="kpi-sub">同比 <em class="up">+8.6%</em></div>
+      </div>
+      <div class="kpi-card kpi-orange">
+        <div class="kpi-label">护理人员</div>
+        <div class="kpi-value">{{ overview.nursingStaff }}<span class="kpi-unit">人</span></div>
+        <div class="kpi-sub">配比 <em>1:5.3</em></div>
+      </div>
+      <div class="kpi-card kpi-pink">
+        <div class="kpi-label">今日预约到访</div>
+        <div class="kpi-value">{{ overview.todayAppointments }}<span class="kpi-unit">组</span></div>
+        <div class="kpi-sub">已完成 <em class="up">18</em></div>
+      </div>
+      <div class="kpi-card kpi-yellow">
+        <div class="kpi-label">家属满意度</div>
+        <div class="kpi-value">{{ overview.satisfaction }}<span class="kpi-unit">分</span></div>
+        <div class="kpi-sub">近30天评价 <em class="up">168</em></div>
+      </div>
+    </div>
+
+    <!-- 图表区域:2行3列 -->
+    <div class="chart-grid">
+      <div class="chart-card col-2">
+        <div class="card-title">月度入住 / 退住趋势</div>
+        <div ref="admissionRef" class="chart-container" ></div>
+      </div>
+
+      <div class="chart-card">
+        <div class="card-title">长者年龄分布</div>
+        <div ref="ageRef" class="chart-container" ></div>
+      </div>
+
+      <div class="chart-card">
+        <div class="card-title">长者慢病 TOP</div>
+        <div ref="diseaseRef" class="chart-container" ></div>
+      </div>
+
+      <div class="chart-card col-2">
+        <div class="card-title">近 6 个月营收构成趋势</div>
+        <div ref="revenueRef" class="chart-container" ></div>
+      </div>
+
+      <div class="chart-card">
+        <div class="card-title">本月活动参与情况</div>
+        <div ref="activityRef" class="chart-container" ></div>
+      </div>
+
+      <div class="chart-card col-3">
+        <div class="card-title">各院区床位与入住对比(3D)</div>
+        <div ref="campusRef" class="chart-container chart-tall" ></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.overview-page {
+  min-height: 100%;
+  padding: 20px 24px 28px;
+  background:
+    radial-gradient(ellipse at top, rgba(0, 120, 200, 0.14) 0%, rgba(2, 11, 30, 0) 50%),
+    linear-gradient(180deg, #020b1e 0%, #05132b 100%);
+  color: #fff;
+  box-sizing: border-box;
+}
+
+.page-header {
+  margin-bottom: 18px;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 8px;
+
+    .header-line {
+      flex: 1;
+      height: 1px;
+      background: linear-gradient(90deg, rgba(0, 255, 255, 0) 0%, #00aaff 50%, rgba(0, 255, 255, 0) 100%);
+      max-width: 240px;
+    }
+  }
+
+  .page-title {
+    font-size: 28px;
+    font-weight: 700;
+    margin: 0 20px;
+    background: linear-gradient(90deg, #e4f9ff 0%, #00ffff 60%, #5578ff 100%);
+    -webkit-background-clip: text;
+    background-clip: text;
+    color: transparent;
+    letter-spacing: 2px;
+    text-shadow: 0 0 20px rgba(0, 255, 255, 0.25);
+  }
+
+  .page-date {
+    margin: 0;
+    text-align: center;
+    font-size: 13px;
+    color: #6c8db0;
+  }
+}
+
+.kpi-row {
+  display: grid;
+  grid-template-columns: repeat(6, 1fr);
+  gap: 16px;
+  margin-bottom: 22px;
+}
+
+.kpi-card {
+  position: relative;
+  padding: 16px 18px 14px;
+  border-radius: 10px;
+  overflow: hidden;
+  background: rgba(10, 47, 94, 0.3);
+  border: 1px solid rgba(0, 170, 255, 0.2);
+  box-shadow: 0 0 18px rgba(0, 170, 255, 0.06) inset;
+
+  .kpi-label {
+    font-size: 13px;
+    color: #a9c8e0;
+    margin-bottom: 8px;
+  }
+
+  .kpi-value {
+    font-size: 34px;
+    font-weight: 700;
+    line-height: 1.15;
+    margin-bottom: 6px;
+    font-family: 'DIN Alternate', -apple-system, 'Arial Black', sans-serif;
+
+    .kpi-unit {
+      font-size: 14px;
+      font-weight: 500;
+      color: #a9c8e0;
+      margin-left: 4px;
+    }
+  }
+
+  .kpi-sub {
+    font-size: 12px;
+    color: #6c8db0;
+
+    em {
+      font-style: normal;
+      color: #00ffff;
+      font-weight: 600;
+      &.up { color: #41e6a7; }
+    }
+  }
+}
+
+.kpi-cyan {
+  border-color: rgba(0, 255, 255, 0.35);
+  .kpi-value { color: #00ffff; text-shadow: 0 0 12px rgba(0, 255, 255, 0.35); }
+  &::before {
+    content: ''; position: absolute; right: -20px; top: -20px;
+    width: 100px; height: 100px;
+    background: radial-gradient(circle, rgba(0, 255, 255, 0.18) 0%, transparent 65%);
+  }
+}
+.kpi-blue {
+  border-color: rgba(85, 120, 255, 0.35);
+  .kpi-value { color: #84a0ff; text-shadow: 0 0 12px rgba(85, 120, 255, 0.35); }
+  &::before {
+    content: ''; position: absolute; right: -20px; top: -20px;
+    width: 100px; height: 100px;
+    background: radial-gradient(circle, rgba(85, 120, 255, 0.18) 0%, transparent 65%);
+  }
+}
+.kpi-purple {
+  border-color: rgba(179, 128, 255, 0.35);
+  .kpi-value { color: #d2acff; text-shadow: 0 0 12px rgba(179, 128, 255, 0.35); }
+  &::before {
+    content: ''; position: absolute; right: -20px; top: -20px;
+    width: 100px; height: 100px;
+    background: radial-gradient(circle, rgba(179, 128, 255, 0.18) 0%, transparent 65%);
+  }
+}
+.kpi-orange {
+  border-color: rgba(255, 153, 102, 0.35);
+  .kpi-value { color: #ffbe9a; text-shadow: 0 0 12px rgba(255, 153, 102, 0.35); }
+  &::before {
+    content: ''; position: absolute; right: -20px; top: -20px;
+    width: 100px; height: 100px;
+    background: radial-gradient(circle, rgba(255, 153, 102, 0.18) 0%, transparent 65%);
+  }
+}
+.kpi-pink {
+  border-color: rgba(255, 122, 153, 0.35);
+  .kpi-value { color: #ff9fb6; text-shadow: 0 0 12px rgba(255, 122, 153, 0.35); }
+  &::before {
+    content: ''; position: absolute; right: -20px; top: -20px;
+    width: 100px; height: 100px;
+    background: radial-gradient(circle, rgba(255, 122, 153, 0.18) 0%, transparent 65%);
+  }
+}
+.kpi-yellow {
+  border-color: rgba(255, 216, 90, 0.35);
+  .kpi-value { color: #ffe59a; text-shadow: 0 0 12px rgba(255, 216, 90, 0.35); }
+  &::before {
+    content: ''; position: absolute; right: -20px; top: -20px;
+    width: 100px; height: 100px;
+    background: radial-gradient(circle, rgba(255, 216, 90, 0.18) 0%, transparent 65%);
+  }
+}
+
+.chart-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 18px;
+}
+
+.chart-card {
+  position: relative;
+  background: rgba(10, 47, 94, 0.28);
+  border: 1px solid rgba(0, 255, 255, 0.2);
+  border-radius: 12px;
+  padding: 14px 16px 12px;
+  box-shadow: 0 0 20px rgba(0, 170, 255, 0.08);
+  backdrop-filter: blur(4px);
+
+  &.col-2 { grid-column: span 2; }
+  &.col-3 { grid-column: span 3; }
+
+  &::before {
+    content: '';
+    position: absolute;
+    left: -1px; top: -1px;
+    width: 14px; height: 14px;
+    border-top: 2px solid #00ffff;
+    border-left: 2px solid #00ffff;
+    border-top-left-radius: 4px;
+    pointer-events: none;
+  }
+  &::after {
+    content: '';
+    position: absolute;
+    right: -1px; bottom: -1px;
+    width: 14px; height: 14px;
+    border-bottom: 2px solid #00ffff;
+    border-right: 2px solid #00ffff;
+    border-bottom-right-radius: 4px;
+    pointer-events: none;
+  }
+
+  .card-title {
+    font-size: 15px;
+    font-weight: 600;
+    margin-bottom: 8px;
+    color: #e4f9ff;
+    border-left: 4px solid #00aaff;
+    padding-left: 10px;
+    letter-spacing: 0.5px;
+  }
+}
+
+.chart-container {
+  width: 100%;
+  height: 300px;
+}
+
+.chart-tall {
+  height: 360px;
+}
+
+@media (max-width: 1500px) {
+  .kpi-row { grid-template-columns: repeat(3, 1fr); }
+}
+
+@media (max-width: 1200px) {
+  .chart-grid { grid-template-columns: repeat(2, 1fr); }
+  .chart-card.col-2, .chart-card.col-3 { grid-column: span 2; }
+}
+
+@media (max-width: 768px) {
+  .kpi-row { grid-template-columns: repeat(2, 1fr); }
+  .chart-grid { grid-template-columns: 1fr; }
+  .chart-card.col-2, .chart-card.col-3 { grid-column: span 1; }
+}
+</style>

+ 1580 - 0
src/views/data-board/nine-safety-measures/index.vue

@@ -0,0 +1,1580 @@
+<script setup lang="ts">
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+import echarts from '@/plugins/echarts'
+import 'echarts-gl'
+import {
+  ScatterChart,
+  HeatmapChart,
+  BoxplotChart,
+  GraphChart,
+  EffectScatterChart,
+  SunburstChart,
+  SankeyChart,
+  ThemeRiverChart
+} from 'echarts/charts'
+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'
+
+const userStore = useUserStore()
+
+// 补充注册本页面需要的图表类型(不影响其他页面)
+echarts.use([
+  ScatterChart,
+  HeatmapChart,
+  BoxplotChart,
+  GraphChart,
+  EffectScatterChart,
+  SunburstChart,
+  SankeyChart,
+  ThemeRiverChart,
+  DataZoomComponent,
+  BrushComponent,
+  RadarComponent
+])
+
+// ========================= 筛选条件 =========================
+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[]>([])
+const handleBuildChange = (buildId: any) => {
+  const selectedBuild = buildList.value.find((item: any) => item.id === buildId)
+  floorList.value = selectedBuild?.floorList || []
+  filter.floorId = ''
+}
+
+// ========================= 数据 =========================
+const loading = ref(false)
+const overview = ref<any>({})
+const nineRiskDist = 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 assessorWorkload = ref<any[]>([])
+const nurseLevelRisk = ref<any[]>([])
+const riskCorrelation = ref<any>({})
+const riskRadar = ref<any>({})
+const bar3D = ref<any>({})
+
+// ========================= 图表实例管理 =========================
+const chartInstances = new Map<string, any>()
+const chartRefs = ref<Record<string, HTMLDivElement | undefined>>({})
+
+const setRef = (key: string) => (el: any) => {
+  chartRefs.value[key] = el
+}
+
+const createChart = (key: string, option: any) => {
+  const el = chartRefs.value[key]
+  if (!el) return null
+  let inst = chartInstances.get(key)
+  if (inst) {
+    inst.dispose()
+  }
+  inst = echarts.init(el)
+  // 统一注入 tooltip 深色背景,避免白字白底看不见
+  if (option.tooltip) {
+    option.tooltip = {
+      backgroundColor: 'rgba(2, 11, 30, 0.92)',
+      borderColor: 'rgba(0, 255, 255, 0.3)',
+      borderWidth: 1,
+      padding: [8, 12],
+      ...option.tooltip
+    }
+  }
+  inst.setOption(option)
+  chartInstances.set(key, inst)
+  return inst
+}
+
+const resizeAll = () => {
+  chartInstances.forEach((inst) => inst?.resize())
+}
+const disposeAll = () => {
+  chartInstances.forEach((inst) => inst?.dispose())
+  chartInstances.clear()
+}
+
+// ========================= 图表配色 =========================
+const COLORS = {
+  none: '#41e6a7',
+  low: '#00e0a7',
+  medium: '#ffd85a',
+  high: '#ff5a3c',
+  primary: '#00aaff',
+  cyan: '#00ffff',
+  bg: 'rgba(10, 47, 94, 0.28)',
+  border: 'rgba(0, 255, 255, 0.2)',
+  text: '#c7e0ff',
+  textDim: '#8fb0c7',
+  splitLine: 'rgba(26, 58, 107, 0.5)',
+  axisLine: '#1a3a6b'
+}
+
+// ========================= 图表配置生成 =========================
+
+/** 1. 九防风险等级分布 - 堆叠条形图 */
+const renderNineRiskDist = () => {
+  const data = nineRiskDist.value
+  if (!data.length) return
+  createChart('nineRisk', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      textStyle: { color: '#fff' },
+      formatter: (params: any) => {
+        const row = data[params[0].dataIndex]
+        return `${row.name}<br/>无风险:${row.none} 人<br/>低危:${row.low} 人<br/>中危:${row.medium} 人<br/>高危:${row.high} 人<br/>合计:${row.total} 人`
+      }
+    },
+    legend: {
+      data: ['无风险', '低危', '中危', '高危'],
+      textStyle: { color: COLORS.text },
+      top: 0,
+      right: 0
+    },
+    grid: { left: 70, right: 30, top: 36, bottom: 20 },
+    xAxis: {
+      type: 'value',
+      splitLine: { lineStyle: { color: COLORS.splitLine } },
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    yAxis: {
+      type: 'category',
+      data: data.map((r) => r.name),
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: '#e4f9ff', fontWeight: 600 }
+    },
+    series: [
+      {
+        name: '无风险',
+        type: 'bar',
+        stack: 't',
+        barWidth: 18,
+        itemStyle: { color: COLORS.none },
+        data: data.map((r) => r.none)
+      },
+      {
+        name: '低危',
+        type: 'bar',
+        stack: 't',
+        itemStyle: { color: COLORS.low },
+        data: data.map((r) => r.low)
+      },
+      {
+        name: '中危',
+        type: 'bar',
+        stack: 't',
+        itemStyle: { color: COLORS.medium },
+        data: data.map((r) => r.medium)
+      },
+      {
+        name: '高危',
+        type: 'bar',
+        stack: 't',
+        label: {
+          show: true,
+          color: '#fff',
+          fontWeight: 700,
+          formatter: (p: any) => (p.value ? `高危${p.value}` : '')
+        },
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: '#c7301f' },
+            { offset: 1, color: '#ff5a3c' }
+          ])
+        },
+        data: data.map((r) => r.high)
+      }
+    ]
+  })
+}
+
+/** 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)
+  createChart('overallPie', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}<br/>{c} 人次 ({d}%)',
+      textStyle: { color: '#fff' }
+    },
+    legend: {
+      orient: 'horizontal',
+      right: 0,
+      top: 0,
+      textStyle: { color: COLORS.text, fontSize: 12 }
+    },
+    series: [
+      {
+        name: '风险等级',
+        type: 'pie',
+        radius: ['35%', '70%'],
+        center: ['50%', '58%'],
+        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 } }))
+      }
+    ]
+  })
+}
+
+/** 3. 风险等级月度趋势 - 堆叠面积折线图 */
+const renderRiskTrend = () => {
+  const data = riskTrend.value
+  if (!data.length) return
+  createChart('riskTrend', {
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'axis', textStyle: { color: '#fff' } },
+    legend: { data: ['无风险', '低危', '中危', '高危'], textStyle: { color: COLORS.text }, top: 0 },
+    grid: { left: 50, right: 30, top: 40, bottom: 30 },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: data.map((d) => d.month),
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: { lineStyle: { color: COLORS.splitLine } },
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    series: [
+      {
+        name: '无风险',
+        type: 'line',
+        stack: 't',
+        areaStyle: { opacity: 0.3 },
+        itemStyle: { color: COLORS.none },
+        data: data.map((d) => d.none)
+      },
+      {
+        name: '低危',
+        type: 'line',
+        stack: 't',
+        areaStyle: { opacity: 0.3 },
+        itemStyle: { color: COLORS.low },
+        data: data.map((d) => d.low)
+      },
+      {
+        name: '中危',
+        type: 'line',
+        stack: 't',
+        areaStyle: { opacity: 0.3 },
+        itemStyle: { color: COLORS.medium },
+        data: data.map((d) => d.medium)
+      },
+      {
+        name: '高危',
+        type: 'line',
+        stack: 't',
+        areaStyle: { opacity: 0.4 },
+        itemStyle: { color: COLORS.high },
+        data: data.map((d) => d.high)
+      }
+    ]
+  })
+}
+
+/** 4. 楼栋×九防风险热力图 */
+const renderFloorHeatmap = () => {
+  const { floors, risks, data } = floorHeatmap.value
+  if (!floors || !data) return
+  const maxVal = Math.max(...data.map((d: any) => d[2]), 1)
+  createChart('floorHeatmap', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'item',
+      textStyle: { color: '#fff' },
+      formatter: (p: any) =>
+        `${floors[p.data[0]]} - ${risks[p.data[1]]}<br/>高危人数:${p.data[2]} 人`
+    },
+    grid: { left: 60, right: 50, top: 30, bottom: 80 },
+    xAxis: {
+      type: 'category',
+      data: floors,
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text },
+      splitArea: {
+        show: true,
+        areaStyle: { color: ['rgba(0,255,255,0.02)', 'rgba(0,255,255,0.04)'] }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: risks,
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text },
+      splitArea: {
+        show: true,
+        areaStyle: { color: ['rgba(0,255,255,0.02)', 'rgba(0,255,255,0.04)'] }
+      }
+    },
+    visualMap: {
+      min: 0,
+      max: maxVal,
+      calculable: true,
+      orient: 'horizontal',
+      left: 'center',
+      bottom: 10,
+      textStyle: { color: COLORS.text },
+      inRange: { color: ['#0a2f5e', '#00aaff', '#ffd85a', '#ff5a3c'] }
+    },
+    series: [
+      {
+        type: 'heatmap',
+        data,
+        label: { show: true, color: '#fff', fontSize: 11 },
+        emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,255,255,0.5)' } }
+      }
+    ]
+  })
+}
+
+/** 5. 年龄-高风险散点图 */
+const renderAgeScatter = () => {
+  const data = ageScatter.value
+  if (!data.length) return
+  const male = data.filter((d) => d.sex === 1).map((d) => [d.age, d.highCount, d.name])
+  const female = data.filter((d) => d.sex === 2).map((d) => [d.age, d.highCount, d.name])
+  createChart('ageScatter', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'item',
+      textStyle: { color: '#fff' },
+      formatter: (p: any) => `${p.data[2]}<br/>年龄:${p.data[0]} 岁<br/>高风险项:${p.data[1]} 个`
+    },
+    legend: { data: ['男性', '女性'], textStyle: { color: COLORS.text }, top: 0 },
+    grid: { left: 50, right: 30, top: 40, bottom: 40 },
+    xAxis: {
+      type: 'value',
+      name: '年龄',
+      nameTextStyle: { color: COLORS.textDim },
+      min: 60,
+      max: 100,
+      splitLine: { lineStyle: { color: COLORS.splitLine } },
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    yAxis: {
+      type: 'value',
+      name: '高风险项数',
+      nameTextStyle: { color: COLORS.textDim },
+      splitLine: { lineStyle: { color: COLORS.splitLine } },
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    series: [
+      {
+        name: '男性',
+        type: 'scatter',
+        symbolSize: 12,
+        itemStyle: { color: '#5578ff', opacity: 0.8 },
+        data: male
+      },
+      {
+        name: '女性',
+        type: 'scatter',
+        symbolSize: 12,
+        itemStyle: { color: '#ff7ab0', opacity: 0.8 },
+        data: female
+      }
+    ]
+  })
+}
+
+/** 6. 评估得分分布 - 箱线图 */
+const renderScoreDist = () => {
+  const data = scoreDist.value
+  if (!data.length) return
+  createChart('scoreDist', {
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'item', textStyle: { color: '#fff' } },
+    grid: { left: 50, right: 30, top: 30, bottom: 60 },
+    xAxis: {
+      type: 'category',
+      data: data.map((d) => d.name),
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text, rotate: 25, fontSize: 11 }
+    },
+    yAxis: {
+      type: 'value',
+      name: '得分',
+      nameTextStyle: { color: COLORS.textDim },
+      splitLine: { lineStyle: { color: COLORS.splitLine } },
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    series: [
+      {
+        type: 'boxplot',
+        data: data.map((d) => [d.min, d.q1, d.median, d.q3, d.max]),
+        itemStyle: { color: 'rgba(0, 170, 255, 0.3)', borderColor: '#00aaff' }
+      }
+    ]
+  })
+}
+
+/** 7. 签署状态 - 环形图 */
+const renderSignature = () => {
+  const d = signature.value
+  if (!d.total) return
+  const rate = (d.signedRate || 0).toFixed(1)
+  createChart('signature', {
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'item', formatter: '{b}: {c}人 ({d}%)', textStyle: { color: '#fff' } },
+    title: {
+      text: `${rate}%`,
+      subtext: '签署率',
+      left: 'center',
+      top: '40%',
+      textStyle: {
+        color: COLORS.cyan,
+        fontSize: 26,
+        fontWeight: 700,
+        textShadowColor: 'rgba(0,255,255,0.4)',
+        textShadowBlur: 10
+      },
+      subtextStyle: { color: COLORS.textDim, fontSize: 12 }
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['58%', '78%'],
+        center: ['50%', '50%'],
+        silent: true,
+        label: { show: false },
+        labelLine: { show: false },
+        data: [
+          {
+            value: d.signed,
+            name: '已签署',
+            itemStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
+                { offset: 0, color: '#00ffff' },
+                { offset: 1, color: '#00aaff' }
+              ])
+            }
+          },
+          { value: d.firstSignedOnly, name: '仅首次', itemStyle: { color: '#ffd85a' } },
+          {
+            value: d.unsigned,
+            name: '未签署',
+            itemStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
+                { offset: 0, color: '#ff9966' },
+                { offset: 1, color: '#ff5a3c' }
+              ])
+            }
+          }
+        ]
+      }
+    ]
+  })
+}
+
+/** 8. 按护理级别统计风险 - 漏斗图 */
+const renderNurseLevelFunnel = () => {
+  const data = nurseLevelRisk.value
+  if (!data.length) return
+  const totalHigh = data.map((d) => ({ name: d.name, value: d.high })).filter((d) => d.value > 0)
+  createChart('nurseFunnel', {
+    backgroundColor: 'transparent',
+    tooltip: { trigger: 'item', formatter: '{b}: {c} 个高危项', textStyle: { color: '#fff' } },
+    legend: { textStyle: { color: COLORS.text }, top: 0 },
+    series: [
+      {
+        type: 'funnel',
+        left: '10%',
+        top: 40,
+        bottom: 20,
+        width: '80%',
+        label: { color: '#fff', fontSize: 12, formatter: '{b}: {c}' },
+        itemStyle: { borderColor: '#05132b', borderWidth: 1 },
+        data: totalHigh.map((d, i) => ({
+          name: d.name,
+          value: d.value,
+          itemStyle: { color: ['#ff5a3c', '#ff9966', '#ffd85a', '#00e0a7'][i % 4] }
+        }))
+      }
+    ]
+  })
+}
+
+/** 9. 九防风险雷达图 */
+const renderRiskRadar = () => {
+  const { indicators, series } = riskRadar.value
+  if (!indicators || !series) return
+  const colors = ['#ff5a3c', '#ff9966', '#ffd85a', '#00e0a7']
+  createChart('riskRadar', {
+    backgroundColor: 'transparent',
+    tooltip: { textStyle: { color: '#fff' } },
+    legend: { data: series.map((s: any) => s.name), textStyle: { color: COLORS.text }, top: 0 },
+    radar: {
+      indicator: indicators,
+      shape: 'polygon',
+      splitNumber: 5,
+      axisName: { color: '#e4f9ff', fontSize: 11 },
+      splitLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.6)' } },
+      splitArea: { areaStyle: { color: ['rgba(0,255,255,0.03)', 'rgba(0,255,255,0.06)'] } },
+      axisLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.6)' } }
+    },
+    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 }
+        }))
+      }
+    ]
+  })
+}
+
+/** 10. 评估人工作量 - 柱状图 */
+const renderAssessorWorkload = () => {
+  const data = assessorWorkload.value
+  if (!data.length) return
+  createChart('assessorWorkload', {
+    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 },
+    xAxis: {
+      type: 'category',
+      data: data.map((d) => d.name),
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: { lineStyle: { color: COLORS.splitLine } },
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text }
+    },
+    series: [
+      {
+        name: '评估人数',
+        type: 'bar',
+        barWidth: 18,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#00ffff' },
+            { offset: 1, color: '#0055aa' }
+          ]),
+          borderRadius: [4, 4, 0, 0]
+        },
+        data: data.map((d) => d.count)
+      },
+      {
+        name: '发现高危',
+        type: 'bar',
+        barWidth: 18,
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#ff7a6b' },
+            { offset: 1, color: '#c7301f' }
+          ]),
+          borderRadius: [4, 4, 0, 0]
+        },
+        data: data.map((d) => d.highCount)
+      }
+    ]
+  })
+}
+
+/** 11. 九防风险相关性矩阵 - 热力图 */
+const renderRiskCorrelation = () => {
+  const { risks, matrix } = riskCorrelation.value
+  if (!risks || !matrix) return
+  const data: [number, number, number][] = []
+  for (let i = 0; i < matrix.length; i++) {
+    for (let j = 0; j < matrix[i].length; j++) {
+      data.push([j, i, matrix[i][j]])
+    }
+  }
+  createChart('riskCorrelation', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'item',
+      textStyle: { color: '#fff' },
+      formatter: (p: any) => `${risks[p.data[1]]} → ${risks[p.data[0]]}<br/>相关性:${p.data[2]}`
+    },
+    grid: { left: 80, right: 50, top: 30, bottom: 80 },
+    xAxis: {
+      type: 'category',
+      data: risks,
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text, rotate: 35, fontSize: 11 },
+      splitArea: { show: true }
+    },
+    yAxis: {
+      type: 'category',
+      data: risks,
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text, fontSize: 11 },
+      splitArea: { show: true }
+    },
+    visualMap: {
+      min: 0,
+      max: 1,
+      calculable: true,
+      orient: 'horizontal',
+      left: 'center',
+      bottom: 10,
+      textStyle: { color: COLORS.text },
+      inRange: { color: ['#0a2f5e', '#5578ff', '#00ffff', '#ffd85a', '#ff5a3c'] }
+    },
+    series: [
+      {
+        type: 'heatmap',
+        data,
+        label: {
+          show: true,
+          color: '#fff',
+          fontSize: 10,
+          formatter: (p: any) => p.data[2].toFixed(2)
+        }
+      }
+    ]
+  })
+}
+
+/** 12. 3D 楼栋×九防风险 - 3D 柱状图 */
+const render3DBar = () => {
+  const { floors, risks, data } = bar3D.value
+  if (!floors || !data) return
+  const maxVal = Math.max(...data.map((d: any) => d[2]), 1)
+  createChart('bar3D', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      formatter: (p: any) =>
+        `${floors[p.data[0]]} - ${risks[p.data[1]]}<br/>高危人数:${p.data[2]} 人`
+    },
+    visualMap: {
+      max: maxVal,
+      min: 0,
+      inRange: { color: ['#0a2f5e', '#00aaff', '#ffd85a', '#ff5a3c'] },
+      textStyle: { color: COLORS.text }
+    },
+    xAxis3D: {
+      type: 'category',
+      data: floors,
+      name: '楼栋',
+      nameTextStyle: { color: COLORS.text },
+      axisLabel: { color: COLORS.text }
+    },
+    yAxis3D: {
+      type: 'category',
+      data: risks,
+      name: '九防',
+      nameTextStyle: { color: COLORS.text },
+      axisLabel: { color: COLORS.text }
+    },
+    zAxis3D: {
+      type: 'value',
+      name: '高危人数',
+      nameTextStyle: { color: COLORS.text },
+      axisLabel: { color: COLORS.text }
+    },
+    grid3D: {
+      viewControl: { autoRotate: true, autoRotateSpeed: 8, distance: 180 },
+      boxWidth: 180,
+      boxDepth: 180,
+      light: {
+        main: { intensity: 1.2, shadow: true },
+        ambient: { intensity: 0.3 }
+      },
+      environment: 'auto',
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      splitLine: { lineStyle: { color: 'rgba(26, 58, 107, 0.3)' } }
+    },
+    series: [
+      {
+        type: 'bar3D',
+        data: data.map((d: any) => ({ value: [d[0], d[1], d[2]] })),
+        shading: 'color',
+        label: { show: false },
+        itemStyle: { opacity: 0.9 }
+      }
+    ]
+  })
+}
+
+
+/** 15. 评估完成情况 - 柱状图 */
+const renderAssessCompletion = () => {
+  const data = nineRiskDist.value
+  if (!data.length) return
+  createChart('assessCompletion', {
+    backgroundColor: 'transparent',
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      textStyle: { color: '#fff' },
+      formatter: (params: any) => {
+        const row = data[params[0].dataIndex]
+        const rate = row.total > 0 ? ((row.high / row.total) * 100).toFixed(1) : '0.0'
+        return `${row.name}<br/>高危占比:${rate}%<br/>高危:${row.high}人 / 合计:${row.total}人`
+      }
+    },
+    grid: { left: 40, right: 20, top: 40, bottom: 30 },
+    xAxis: {
+      type: 'category',
+      data: data.map((d) => d.name),
+      axisLine: { lineStyle: { color: COLORS.axisLine } },
+      axisLabel: { color: COLORS.text, rotate: 35, fontSize: 11, margin: 10 }
+    },
+    yAxis: {
+      type: 'value',
+      name: '高危占比',
+      nameTextStyle: { color: COLORS.textDim, padding: [0, 0, 20, 0] },
+      splitLine: { lineStyle: { color: COLORS.splitLine, paddingBottom: 20 } },
+      axisLine: { lineStyle: { color: COLORS.axisLine, paddingBottom: 20 } },
+      axisLabel: { color: COLORS.text, formatter: '{value}%', paddingBottom: 20 }
+    },
+    series: [
+      {
+        type: 'bar',
+        barWidth: 20,
+        itemStyle: {
+          color: (p: any) => {
+            const row = data[p.dataIndex]
+            const ratio = row.total > 0 ? row.high / row.total : 0
+            if (ratio > 0.2)
+              return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: '#ff5a3c' },
+                { offset: 1, color: '#c7301f' }
+              ])
+            if (ratio > 0.1)
+              return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: '#ffd85a' },
+                { offset: 1, color: '#cc9a00' }
+              ])
+            return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: '#00ffff' },
+              { offset: 1, color: '#0055aa' }
+            ])
+          },
+          borderRadius: [4, 4, 0, 0]
+        },
+        label: {
+          show: true,
+          position: 'top',
+          color: COLORS.cyan,
+          fontSize: 11,
+          formatter: (p: any) => {
+            const row = data[p.dataIndex]
+            const rate = row.total > 0 ? ((row.high / row.total) * 100).toFixed(0) : '0'
+            return `${rate}%`
+          }
+        },
+        data: data.map((d) => {
+          const rate = d.total > 0 ? +((d.high / d.total) * 100).toFixed(1) : 0
+          return rate
+        })
+      }
+    ]
+  })
+}
+
+// ========================= 渲染所有图表 =========================
+const renderAllCharts = () => {
+  renderNineRiskDist()
+  renderOverallPie()
+  renderRiskTrend()
+  renderFloorHeatmap()
+  renderAgeScatter()
+  renderScoreDist()
+  renderSignature()
+  renderNurseLevelFunnel()
+  renderRiskRadar()
+  renderAssessorWorkload()
+  renderRiskCorrelation()
+  render3DBar()
+  renderAssessCompletion()
+}
+
+// ========================= 数据加载 =========================
+const loadAllData = async () => {
+  loading.value = true
+  disposeAll()
+  const params = {
+    month: filter.month || undefined,
+    buildId: filter.buildId || undefined,
+    floorId: filter.floorId || undefined
+  }
+  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
+    await nextTick()
+    renderAllCharts()
+  } catch (e) {
+    console.error('九防看板数据加载失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+// ========================= 筛选交互 =========================
+const handleFilterChange = () => {
+  loadAllData()
+}
+const handleReset = () => {
+  filter.month = ''
+  filter.buildId = ''
+  filter.floorId = ''
+  floorList.value = []
+  loadAllData()
+}
+
+// ========================= 红名单风险标签颜色 =========================
+const riskTagColor = (name: string): string => {
+  const map: Record<string, string> = {
+    噎食: '#ff9966',
+    压疮: '#ffd85a',
+    跌倒: '#ff5a3c',
+    坠床: '#ff5a3c',
+    烫伤: '#ffd85a',
+    走失: '#ff9966',
+    自杀: '#b380ff',
+    误食: '#5578ff',
+    文娱意外: '#5578ff'
+  }
+  return map[name] || '#00aaff'
+}
+
+// ========================= 生命周期 =========================
+onMounted(async () => {
+  const months = await echartsApi.getMonthList()
+  monthOptions.value = months
+  // 楼栋使用真实接口
+  try {
+    const tenantIds = userStore.orgTenantId
+    const tid = Array.isArray(tenantIds) ? tenantIds[0] : tenantIds
+    buildList.value = tid ? await getBuildList({ tenantIds: tid }) : await getBuildList({})
+  } catch (e) {
+    console.error('楼栋列表加载失败', e)
+  }
+  await loadAllData()
+  window.addEventListener('resize', resizeAll)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeAll)
+  disposeAll()
+})
+</script>
+
+<template>
+  <div class="page" v-loading="loading">
+    <!-- 标题 + 筛选 -->
+    <div class="header">
+      <div class="title-wrap">
+        <div class="title-line"></div>
+        <h1 class="title">九防安全风险综合看板</h1>
+        <div class="title-line"></div>
+      </div>
+      <div class="filter-bar">
+        <div class="filter-item">
+          <span class="filter-label">楼栋</span>
+          <el-select
+            v-model="filter.buildId"
+            placeholder="全部楼栋"
+            clearable
+            class="filter-select"
+            @change="handleBuildChange"
+          >
+            <el-option v-for="b in buildList" :key="b.id" :label="b.buildName" :value="b.id" />
+          </el-select>
+        </div>
+        <div class="filter-item">
+          <span class="filter-label">楼层</span>
+          <el-select
+            v-model="filter.floorId"
+            placeholder="全部楼层"
+            clearable
+            class="filter-select"
+            :disabled="!filter.buildId"
+            @change="handleFilterChange"
+          >
+            <el-option v-for="f in floorList" :key="f.id" :label="f.floorName" :value="f.id" />
+          </el-select>
+        </div>
+        <el-button type="primary" @click="handleFilterChange">查询</el-button>
+        <el-button color="#666666" @click="handleReset">重置</el-button>
+        <span class="update-time">数据更新:{{ new Date().toLocaleString('zh-CN') }}</span>
+      </div>
+    </div>
+
+    <!-- KPI -->
+    <div class="kpi-row" v-if="overview.totalElders">
+      <div class="kpi kpi-blue">
+        <div class="kpi-icon">
+          <svg
+            viewBox="0 0 24 24"
+            width="28"
+            height="28"
+            fill="none"
+            stroke="#84a0ff"
+            stroke-width="2"
+          >
+            <path d="M12 2l3 6 7 1-5 5 1 7-6-3-6 3 1-7-5-5 7-1z" />
+          </svg>
+        </div>
+        <div class="kpi-body">
+          <div class="kpi-label">知情书总数</div>
+          <div class="kpi-value">{{ overview.totalElders }}<span class="u">人</span></div>
+          <div class="kpi-sub">已覆盖九防风险评估档案</div>
+        </div>
+      </div>
+      <div class="kpi kpi-red">
+        <div class="kpi-icon">
+          <svg
+            viewBox="0 0 24 24"
+            width="28"
+            height="28"
+            fill="none"
+            stroke="#ff9fb6"
+            stroke-width="2"
+          >
+            <path
+              d="M12 21s-7-4.5-9.5-9.2C.8 8.3 2.9 4.5 6.5 4.5c2 0 3.5 1.1 4.5 2.6C12 5.6 13.5 4.5 15.5 4.5 19.1 4.5 21.2 8.3 19.5 11.8 17 16.5 12 21 12 21z"
+            />
+          </svg>
+        </div>
+        <div class="kpi-body">
+          <div class="kpi-label">高风险总人次</div>
+          <div class="kpi-value">{{ overview.highRiskCount }}<span class="u">人次</span></div>
+          <div class="kpi-sub"
+            >占比 <em class="up">{{ overview.highRiskRatio }}%</em></div
+          >
+        </div>
+      </div>
+      <div class="kpi kpi-orange">
+        <div class="kpi-icon">
+          <svg
+            viewBox="0 0 24 24"
+            width="28"
+            height="28"
+            fill="none"
+            stroke="#ffbe9a"
+            stroke-width="2"
+          >
+            <rect x="3" y="5" width="18" height="16" rx="2" />
+            <path d="M7 9h10M7 13h10M7 17h6" />
+          </svg>
+        </div>
+        <div class="kpi-body">
+          <div class="kpi-label">待签署知情书</div>
+          <div class="kpi-value">{{ overview.pendingSignatures }}<span class="u">份</span></div>
+          <div class="kpi-sub">签署率 {{ overview.signedRate }}%</div>
+        </div>
+      </div>
+      <div class="kpi kpi-cyan">
+        <div class="kpi-icon">
+          <svg
+            viewBox="0 0 24 24"
+            width="28"
+            height="28"
+            fill="none"
+            stroke="#5df3ea"
+            stroke-width="2"
+          >
+            <path d="M4 12l4 4 12-12" />
+          </svg>
+        </div>
+        <div class="kpi-body">
+          <div class="kpi-label">评估完成率</div>
+          <div class="kpi-value">{{ overview.evalCompletionRate }}<span class="u">%</span></div>
+          <div class="kpi-sub">9 项防险综合覆盖率</div>
+        </div>
+      </div>
+      <div class="kpi kpi-purple">
+        <div class="kpi-icon">
+          <svg
+            viewBox="0 0 24 24"
+            width="28"
+            height="28"
+            fill="none"
+            stroke="#c9a0ff"
+            stroke-width="2"
+          >
+            <circle cx="12" cy="8" r="4" />
+            <path d="M4 20c0-4 4-6 8-6s8 2 8 6" />
+          </svg>
+        </div>
+        <div class="kpi-body">
+          <div class="kpi-label">评估人员数</div>
+          <div class="kpi-value">{{ overview.assessorCount }}<span class="u">人</span></div>
+          <div class="kpi-sub">参与九防评估护士</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 图表区:自适应布局,大屏多列、小屏单列 -->
+    <div class="grid-auto">
+      <!-- 九防分布(大图) -->
+      <div class="card span-2">
+        <div class="card-title"
+          ><span class="dot dot-high"></span>风险全景 · 九防风险等级分布(按高危排序)</div
+        >
+        <div :ref="setRef('nineRisk')" class="chart"></div>
+      </div>
+      <!-- 整体占比 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-mid"></span>风险等级整体占比</div>
+        <div :ref="setRef('overallPie')" class="chart"></div>
+      </div>
+
+      <!-- 月度趋势 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-low"></span>风险等级月度趋势(近 6 个月)</div>
+        <div :ref="setRef('riskTrend')" class="chart"></div>
+      </div>
+      <!-- 楼栋热力图 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-high"></span>九防 × 高危人数热力图</div>
+        <div :ref="setRef('floorHeatmap')" class="chart"></div>
+      </div>
+
+      <!-- 年龄散点 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-mid"></span>年龄 × 高风险项 散点分布</div>
+        <div :ref="setRef('ageScatter')" class="chart"></div>
+      </div>
+      <!-- 得分箱线图 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-low"></span>评估得分分布(箱线图)</div>
+        <div :ref="setRef('scoreDist')" class="chart"></div>
+      </div>
+      <!-- 签署状态 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-mid"></span>知情书签署状态</div>
+        <div :ref="setRef('signature')" class="chart"></div>
+      </div>
+
+      <!-- 护理级别漏斗 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-high"></span>按护理级别 · 高危项漏斗</div>
+        <div :ref="setRef('nurseFunnel')" class="chart"></div>
+      </div>
+      <!-- 风险雷达 -->
+      <div class="card">
+        <div class="card-title"><span class="dot dot-mid"></span>各护理级别 · 九防高危占比雷达</div>
+        <div :ref="setRef('riskRadar')" class="chart"></div>
+      </div>
+      <!-- 评估人工作量 -->
+      <div class="card">
+        <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('assessCompletion')" class="chart"></div>
+      </div>
+      <!-- 3D柱状图 -->
+      <div class="card span-2">
+        <div class="card-title"
+          ><span class="dot dot-high"></span>3D 楼栋 × 九防高危人数(可旋转)</div
+        >
+        <div :ref="setRef('bar3D')" class="chart-tall"></div>
+      </div>
+    </div>
+
+    <!-- 高风险红名单 -->
+    <div class="card">
+      <div class="card-title danger-title">
+        <svg
+          viewBox="0 0 24 24"
+          width="18"
+          height="18"
+          fill="none"
+          stroke="#ff5a3c"
+          stroke-width="2"
+        >
+          <path
+            d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
+          />
+          <path d="M12 9v4M12 17h.01" />
+        </svg>
+        重点人群红名单 · 2 项及以上高风险(共 {{ redList.length }} 人)
+        <span class="tip danger">建议:纳入每日护士长巡床必查清单</span>
+      </div>
+      <div class="table-wrap" v-if="redList.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>
+            </tr>
+          </thead>
+          <tbody>
+            <tr v-for="r in redList" :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>
+                <span
+                  v-for="rk in r.risks"
+                  :key="rk"
+                  class="risk-tag"
+                  :style="{
+                    background: riskTagColor(rk) + '33',
+                    color: riskTagColor(rk),
+                    borderColor: riskTagColor(rk) + '66'
+                  }"
+                  >{{ rk }}</span
+                >
+              </td>
+              <td
+                ><span class="high-count">{{ r.highCount }}</span></td
+              >
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      <div v-else class="empty-red"><span>暂无重点人群数据</span></div>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.page {
+  min-height: 100%;
+  padding: 20px 24px 28px;
+  background:
+    radial-gradient(ellipse at top, rgba(255, 90, 60, 0.08) 0%, rgba(2, 11, 30, 0) 45%),
+    radial-gradient(ellipse at 20% 40%, rgba(0, 255, 255, 0.06) 0%, rgba(2, 11, 30, 0) 50%),
+    linear-gradient(180deg, #020b1e 0%, #05132b 100%);
+  color: #fff;
+  box-sizing: border-box;
+}
+
+.header {
+  margin-bottom: 18px;
+  .title-wrap {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 14px;
+  }
+  .title-line {
+    flex: 1;
+    height: 1px;
+    max-width: 220px;
+    background: linear-gradient(
+      90deg,
+      rgba(0, 255, 255, 0) 0%,
+      #00aaff 50%,
+      rgba(0, 255, 255, 0) 100%
+    );
+  }
+  .title {
+    margin: 0 22px;
+    font-size: 28px;
+    font-weight: 700;
+    letter-spacing: 2px;
+    background: linear-gradient(90deg, #fff 0%, #00ffff 50%, #ff7a6b 100%);
+    -webkit-background-clip: text;
+    background-clip: text;
+    color: transparent;
+    text-shadow: 0 0 20px rgba(0, 255, 255, 0.2);
+  }
+}
+
+.filter-bar {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  justify-content: center;
+  flex-wrap: wrap;
+  .filter-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+  }
+  .filter-label {
+    font-size: 13px;
+    color: #8fb0c7;
+  }
+  .filter-select {
+    width: 160px;
+  }
+  .update-time {
+    font-size: 12px;
+    color: #6c8db0;
+  }
+}
+
+/* el-select 暗色主题适配 */
+:deep(.el-select__wrapper) {
+  background-color: rgba(10, 47, 94, 0.5) !important;
+  box-shadow: 0 0 0 1px rgba(0, 255, 255, 0.3) inset !important;
+  .el-select__placeholder,
+  .el-select__selected-item {
+    color: #e4f9ff !important;
+  }
+}
+:deep(.el-select__wrapper):hover {
+  box-shadow: 0 0 0 1px #00ffff inset !important;
+}
+:deep(.el-select__wrapper.is-focused) {
+  box-shadow:
+    0 0 0 1px #00ffff inset,
+    0 0 8px rgba(0, 255, 255, 0.3) !important;
+}
+
+/* KPI 自适应:大屏 5 列,中屏 3 列,小屏 2 列 */
+.kpi-row {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+  gap: 16px;
+  margin-bottom: 22px;
+}
+.kpi {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 14px 18px;
+  background: rgba(10, 47, 94, 0.3);
+  border-radius: 10px;
+  overflow: hidden;
+  box-shadow: 0 0 20px rgba(0, 170, 255, 0.05) inset;
+  .kpi-icon {
+    width: 50px;
+    height: 50px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+  }
+  .kpi-label {
+    font-size: 12px;
+    color: #a9c8e0;
+    margin-bottom: 4px;
+  }
+  .kpi-value {
+    font-size: 28px;
+    font-weight: 700;
+    line-height: 1.15;
+    margin-bottom: 4px;
+    font-family: 'DIN Alternate', 'Arial Black', sans-serif;
+    .u {
+      font-size: 13px;
+      font-weight: 500;
+      color: #a9c8e0;
+      margin-left: 4px;
+    }
+  }
+  .kpi-sub {
+    font-size: 11px;
+    color: #6c8db0;
+    em {
+      font-style: normal;
+      color: #00ffff;
+      font-weight: 600;
+    }
+    .up {
+      color: #ff9c8c;
+    }
+  }
+}
+.kpi-blue {
+  border: 1px solid rgba(85, 120, 255, 0.35);
+  .kpi-icon {
+    background: rgba(85, 120, 255, 0.12);
+  }
+  .kpi-value {
+    color: #84a0ff;
+    text-shadow: 0 0 12px rgba(85, 120, 255, 0.3);
+  }
+}
+.kpi-red {
+  border: 1px solid rgba(255, 122, 107, 0.35);
+  .kpi-icon {
+    background: rgba(255, 122, 107, 0.12);
+  }
+  .kpi-value {
+    color: #ff9fb6;
+    text-shadow: 0 0 12px rgba(255, 122, 153, 0.3);
+  }
+}
+.kpi-orange {
+  border: 1px solid rgba(255, 153, 102, 0.35);
+  .kpi-icon {
+    background: rgba(255, 153, 102, 0.12);
+  }
+  .kpi-value {
+    color: #ffbe9a;
+    text-shadow: 0 0 12px rgba(255, 153, 102, 0.3);
+  }
+}
+.kpi-cyan {
+  border: 1px solid rgba(0, 255, 255, 0.35);
+  .kpi-icon {
+    background: rgba(0, 255, 255, 0.12);
+  }
+  .kpi-value {
+    color: #5df3ea;
+    text-shadow: 0 0 12px rgba(0, 255, 255, 0.3);
+  }
+}
+.kpi-purple {
+  border: 1px solid rgba(179, 128, 255, 0.35);
+  .kpi-icon {
+    background: rgba(179, 128, 255, 0.12);
+  }
+  .kpi-value {
+    color: #c9a0ff;
+    text-shadow: 0 0 12px rgba(179, 128, 255, 0.3);
+  }
+}
+
+/* 图表区自适应布局:每列最小 440px,自动填充 */
+.grid-auto {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
+  gap: 18px;
+  margin-bottom: 18px;
+}
+/* 大图跨两列;小屏(<960px)下取消跨列,全部单列 */
+.grid-auto .span-2 {
+  grid-column: span 2;
+}
+@media (max-width: 960px) {
+  .grid-auto {
+    grid-template-columns: 1fr;
+  }
+  .grid-auto .span-2 {
+    grid-column: span 1;
+  }
+}
+
+.card {
+  position: relative;
+  background: rgba(10, 47, 94, 0.28);
+  border: 1px solid rgba(0, 255, 255, 0.2);
+  border-radius: 12px;
+  padding: 14px 16px 12px;
+  box-shadow: 0 0 20px rgba(0, 170, 255, 0.06);
+  backdrop-filter: blur(4px);
+  /* 让子图表能撑满卡片剩余高度,避免被 Grid 同行高卡片拉伸后底部留白 */
+  display: flex;
+  flex-direction: column;
+  &::before,
+  &::after {
+    content: '';
+    position: absolute;
+    width: 14px;
+    height: 14px;
+    pointer-events: none;
+  }
+  &::before {
+    left: -1px;
+    top: -1px;
+    border-top: 2px solid #00ffff;
+    border-left: 2px solid #00ffff;
+    border-top-left-radius: 4px;
+  }
+  &::after {
+    right: -1px;
+    bottom: -1px;
+    border-bottom: 2px solid #00ffff;
+    border-right: 2px solid #00ffff;
+    border-bottom-right-radius: 4px;
+  }
+}
+.card-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 14px;
+  font-weight: 600;
+  margin-bottom: 10px;
+  color: #e4f9ff;
+  letter-spacing: 0.5px;
+  border-left: 4px solid #00aaff;
+  padding-left: 10px;
+  .dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    display: inline-block;
+  }
+  .dot-high {
+    background: #ff5a3c;
+    box-shadow: 0 0 8px #ff5a3c;
+  }
+  .dot-mid {
+    background: #ffd85a;
+    box-shadow: 0 0 8px #ffd85a;
+  }
+  .dot-low {
+    background: #41e6a7;
+    box-shadow: 0 0 8px #41e6a7;
+  }
+  .tip {
+    margin-left: auto;
+    font-size: 12px;
+    color: #6c8db0;
+    font-weight: 400;
+    &.danger {
+      color: #ff9c8c;
+    }
+  }
+}
+.danger-title {
+  border-left-color: #ff5a3c;
+  color: #ffd9d1;
+}
+
+.chart {
+  width: 100%;
+  /* 最小 320px,自动撑满卡片剩余空间,修复同行高卡片造成的底部空白 */
+  flex: 1 0 320px;
+}
+.chart-tall {
+  width: 100%;
+  flex: 1 0 420px;
+}
+
+.table-wrap {
+  overflow-x: auto;
+}
+.red-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 13px;
+  th,
+  td {
+    padding: 10px 12px;
+    text-align: left;
+    border-bottom: 1px solid rgba(26, 58, 107, 0.4);
+  }
+  th {
+    background: rgba(255, 90, 60, 0.1);
+    color: #ffd9d1;
+    font-weight: 600;
+    border-bottom-color: rgba(255, 90, 60, 0.35);
+  }
+  tbody tr:hover {
+    background: rgba(0, 170, 255, 0.05);
+  }
+  td.name {
+    font-weight: 600;
+    color: #e4f9ff;
+  }
+  .mono {
+    font-family: 'Consolas', monospace;
+    color: #8fb0c7;
+  }
+  .risk-tag {
+    display: inline-block;
+    padding: 2px 8px;
+    border-radius: 4px;
+    border: 1px solid;
+    margin: 2px 4px 2px 0;
+    font-size: 12px;
+    font-weight: 600;
+  }
+  .high-count {
+    display: inline-block;
+    padding: 2px 8px;
+    border-radius: 999px;
+    background: rgba(255, 90, 60, 0.2);
+    color: #ff9c8c;
+    font-weight: 700;
+  }
+}
+.empty-red {
+  text-align: center;
+  padding: 40px 0;
+  color: #6c8db0;
+  font-size: 14px;
+}
+
+/* 小屏下 KPI 双列 */
+@media (max-width: 640px) {
+  .kpi-row {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+</style>

+ 8 - 0
src/views/elderly/apply/nine-precautions/MMSE/index.vue

@@ -130,6 +130,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/anti-entertainment/index.vue

@@ -123,6 +123,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/asphyxiation-by-choking/index.vue

@@ -97,6 +97,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/attack/index.vue

@@ -130,6 +130,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/commit-suicide/index.vue

@@ -130,6 +130,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 1
src/views/elderly/apply/nine-precautions/communication/index.vue

@@ -130,6 +130,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"
@@ -319,7 +327,6 @@ const handleImportCard = async () => {
   }
 }
 
-
 /** 查询列表 */
 const getList = async () => {
   loading.value = true

+ 29 - 16
src/views/elderly/apply/nine-precautions/daily-life/index.vue

@@ -80,6 +80,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"
@@ -214,36 +222,41 @@ const handleImportCard = async () => {
 
     const headers = [
       {
-            'key': 'orgName',
-            'title': '机构名称'
+        key: 'orgName',
+        title: '机构名称'
       },
       {
-            'key': 'elderName',
-            'title': '长者姓名'
+        key: 'elderName',
+        title: '长者姓名'
       },
       {
-            'key': 'assessor',
-            'title': '评估人'
+        key: 'assessor',
+        title: '评估人'
       },
       {
-            'key': 'assessDate',
-            'title': '记录日期'
+        key: 'assessDate',
+        title: '记录日期'
       },
       {
-            'key': 'assessScore',
-            'title': '评估总得分'
+        key: 'assessScore',
+        title: '评估总得分'
       },
       {
-            'key': 'riskLevelText',
-            'title': '结果等级'
+        key: 'riskLevelText',
+        title: '结果等级'
       },
       {
-            'key': 'creator',
-            'title': '记录人'
+        key: 'creator',
+        title: '记录人'
       }
-]
+    ]
 
-    exportWithExpandedObjectArrays(processedData, headers, `日常生活活动能力评估_${new Date().getTime()}.xlsx`, '日常生活活动能力评估')
+    exportWithExpandedObjectArrays(
+      processedData,
+      headers,
+      `日常生活活动能力评估_${new Date().getTime()}.xlsx`,
+      '日常生活活动能力评估'
+    )
     message.success('导出成功')
   } catch (error) {
     console.error('导出失败:', error)

+ 8 - 0
src/views/elderly/apply/nine-precautions/empyrosis/index.vue

@@ -97,6 +97,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/equilibrium/index.vue

@@ -97,6 +97,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/fall-down/index.vue

@@ -97,6 +97,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 0
src/views/elderly/apply/nine-precautions/fall-prevention-measures/index.vue

@@ -97,6 +97,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 29 - 16
src/views/elderly/apply/nine-precautions/nutritional-risk/index.vue

@@ -67,36 +67,41 @@ const handleImportCard = async () => {
 
     const headers = [
       {
-            'key': 'orgName',
-            'title': '机构名称'
+        key: 'orgName',
+        title: '机构名称'
       },
       {
-            'key': 'elderName',
-            'title': '长者姓名'
+        key: 'elderName',
+        title: '长者姓名'
       },
       {
-            'key': 'assessor',
-            'title': '评估人'
+        key: 'assessor',
+        title: '评估人'
       },
       {
-            'key': 'assessDate',
-            'title': '记录日期'
+        key: 'assessDate',
+        title: '记录日期'
       },
       {
-            'key': 'assessScore',
-            'title': '量表总分'
+        key: 'assessScore',
+        title: '量表总分'
       },
       {
-            'key': 'riskLevelText',
-            'title': '评估结果'
+        key: 'riskLevelText',
+        title: '评估结果'
       },
       {
-            'key': 'creator',
-            'title': '记录人'
+        key: 'creator',
+        title: '记录人'
       }
-]
+    ]
 
-    exportWithExpandedObjectArrays(processedData, headers, `营养风险评估_${new Date().getTime()}.xlsx`, '营养风险评估')
+    exportWithExpandedObjectArrays(
+      processedData,
+      headers,
+      `营养风险评估_${new Date().getTime()}.xlsx`,
+      '营养风险评估'
+    )
     message.success('导出成功')
   } catch (error) {
     console.error('导出失败:', error)
@@ -304,6 +309,14 @@ onMounted(async () => {
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 1
src/views/elderly/apply/nine-precautions/pressure-sores/index.vue

@@ -97,6 +97,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"
@@ -173,7 +181,6 @@ import AddForm from './AddForm.vue'
 import ButtonAdd from '@/components/ButtonAdd/src/ButtonAdd.vue'
 import ButtonImport from '@/components/ButtonImport/src/ButtonImport.vue'
 import { useUserStore } from '@/store/modules/user'
-import { getCurrentMonthRange } from '@/utils/dateUtil'
 import { pressureSoresDelete, pressureSoresPage } from '@/api/social-work'
 import { exportWithExpandedObjectArrays } from '@/utils/excel-export'
 import { findLabelById } from '@/utils/dict'

+ 33 - 25
src/views/elderly/apply/nine-precautions/wander-away/index.vue

@@ -35,31 +35,31 @@
         </el-select>
       </el-form-item>
 
-<!--      <el-form-item label="楼栋">-->
-<!--        <el-select-->
-<!--          v-model="queryParams.buildId"-->
-<!--          placeholder="选择楼栋"-->
-<!--          style="width: 180px"-->
-<!--          @change="handleBuildChange"-->
-<!--        >-->
-<!--          <el-option-->
-<!--            v-for="(item, index) in buildList"-->
-<!--            :key="index"-->
-<!--            :label="item.buildName"-->
-<!--            :value="item.id"-->
-<!--          />-->
-<!--        </el-select>-->
-<!--      </el-form-item>-->
-<!--      <el-form-item label="楼层">-->
-<!--        <el-select v-model="queryParams.floorId" placeholder="选择楼层" style="width: 180px">-->
-<!--          <el-option-->
-<!--            v-for="(item, index) in floorList"-->
-<!--            :key="index"-->
-<!--            :label="item.floorName"-->
-<!--            :value="item.id"-->
-<!--          />-->
-<!--        </el-select>-->
-<!--      </el-form-item>-->
+      <!--      <el-form-item label="楼栋">-->
+      <!--        <el-select-->
+      <!--          v-model="queryParams.buildId"-->
+      <!--          placeholder="选择楼栋"-->
+      <!--          style="width: 180px"-->
+      <!--          @change="handleBuildChange"-->
+      <!--        >-->
+      <!--          <el-option-->
+      <!--            v-for="(item, index) in buildList"-->
+      <!--            :key="index"-->
+      <!--            :label="item.buildName"-->
+      <!--            :value="item.id"-->
+      <!--          />-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
+      <!--      <el-form-item label="楼层">-->
+      <!--        <el-select v-model="queryParams.floorId" placeholder="选择楼层" style="width: 180px">-->
+      <!--          <el-option-->
+      <!--            v-for="(item, index) in floorList"-->
+      <!--            :key="index"-->
+      <!--            :label="item.floorName"-->
+      <!--            :value="item.id"-->
+      <!--          />-->
+      <!--        </el-select>-->
+      <!--      </el-form-item>-->
 
       <el-form-item label="评估人">
         <el-input v-model="queryParams.assessor" placeholder="评估人" class="!w-240px" clearable />
@@ -123,6 +123,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 75 - 2
src/views/elderly/contracts/change-confirmation-form/AddForm.vue

@@ -76,6 +76,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +89,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <ChangeConfirmationFormBody v-model="changeForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import ChangeConfirmationFormBody from '../components/ChangeConfirmationFormBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   changeConfirmationForm_create,
@@ -101,12 +113,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -235,6 +255,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -267,3 +297,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 76 - 2
src/views/elderly/contracts/check-in-tab/AddForm.vue

@@ -74,8 +74,15 @@
       </div>
     </div>
 
-    <template #footer>
+        <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button
+        type="success"
+        size="small"
+        v-show="isDetail"
+        @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <CheckInRegistrationBody v-model="checkInRegistrationForm" :is-text-mode="true" :elder-id="dataForm.elderId" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import CheckInRegistrationBody from '../components/CheckInRegistrationBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   checkInRegistration_create,
@@ -101,12 +117,17 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
 const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(() =>
+  findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) || userStore.getTenantName || ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -444,6 +465,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -476,3 +507,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 82 - 2
src/views/elderly/contracts/comprehensive-assessment-capabilities/AddForm.vue

@@ -84,6 +84,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -94,13 +97,29 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <DocumentAttachmentBody
+        v-model="attachmentForm"
+        :is-text-mode="true"
+        title="老年人能力综合评估结果"
+        :elder-id="dataForm.elderId"
+        :elder-name="dataForm.elderName"
+        fun-name="老年人能力综合评估"
+      />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import DocumentAttachmentBody from '../components/DocumentAttachmentBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   comprehensiveAssessmentCapabilities_create,
@@ -109,12 +128,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -247,6 +274,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -279,3 +316,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 77 - 4
src/views/elderly/contracts/delegation-agent-confirmation-form/AddForm.vue

@@ -80,6 +80,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -90,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <DelegationAgentConfirmationBody v-model="delegationAgentForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import DelegationAgentConfirmationBody from '../components/DelegationAgentConfirmationBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   delegationAgentConfirmation_create,
@@ -105,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -129,8 +149,8 @@ const elderUp = (e) => {
   dataForm.value.contractNumber = e.contractNumber || e.fileNumber
   delegationAgentForm.value.elderName = e.elderName
   delegationAgentForm.value.elderGender = e.elderSex == 1 ? '男' : e.elderSex == 2 ? '女' : ''
-  delegationAgentForm.value.elderAge = e.elderAge||''
-  delegationAgentForm.value.elderIdCardNo = e.idCard||''
+  delegationAgentForm.value.elderAge = e.elderAge || ''
+  delegationAgentForm.value.elderIdCardNo = e.idCard || ''
 }
 
 const delegationAgentForm = ref({
@@ -272,6 +292,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -304,3 +334,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 80 - 3
src/views/elderly/contracts/elder-safety-commitment/AddForm.vue

@@ -70,12 +70,19 @@
 
       <!-- 内容区域 -->
       <div class="form-body">
-        <ElderSafetyCommitmentBody ref="bodyRef" v-model="safetyCommitmentForm" :is-text-mode="isDetail" />
+        <ElderSafetyCommitmentBody
+          ref="bodyRef"
+          v-model="safetyCommitmentForm"
+          :is-text-mode="isDetail"
+        />
       </div>
     </div>
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <ElderSafetyCommitmentBody v-model="safetyCommitmentForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import ElderSafetyCommitmentBody from '../components/ElderSafetyCommitmentBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   elderSafetyCommitment_create,
@@ -101,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -240,6 +264,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -272,3 +306,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 80 - 2
src/views/elderly/contracts/explanation-examination-report-items/AddForm.vue

@@ -82,6 +82,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -92,13 +95,27 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <ExplanationExaminationReportBody
+        v-model="examinationReportForm"
+        :is-text-mode="true"
+        :elder-id="dataForm.elderId"
+        :elder-name="dataForm.elderName"
+      />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import ExplanationExaminationReportBody from '../components/ExplanationExaminationReportBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   explanationExaminationReport_create,
@@ -107,12 +124,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -245,6 +270,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -277,3 +312,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 75 - 2
src/views/elderly/contracts/first-service-project-confirmation/AddForm.vue

@@ -80,6 +80,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -90,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <FirstServiceProjectConfirmationBody v-model="firstServiceForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import FirstServiceProjectConfirmationBody from '../components/FirstServiceProjectConfirmationBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   firstServiceProjectConfirmation_create,
@@ -105,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -274,6 +294,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -306,3 +336,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 80 - 2
src/views/elderly/contracts/guardian-party-b/AddForm.vue

@@ -82,6 +82,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -92,13 +95,27 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <DocumentAttachmentBody
+        v-model="attachmentForm"
+        :is-text-mode="true"
+        title="乙方监护人(丙方)身份证、户口本复印件"
+        description="丙方:(系自然人的)有效身份证件复印件(正反面复印)及户口本复印件(户主页和本人页);(系单位的)的有效注册登登记材料复印件(加盖公章)、法定代表人/负责人及联系人的身份证件复印件(身份证件需正反面复印)"
+      />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import DocumentAttachmentBody from '../components/DocumentAttachmentBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   guardianPartyB_create,
@@ -107,12 +124,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -245,6 +270,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -277,3 +312,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 83 - 2
src/views/elderly/contracts/identification-document-partyb/AddForm.vue

@@ -85,6 +85,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -95,13 +98,30 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <DocumentAttachmentBody
+        v-model="attachmentForm"
+        :is-text-mode="true"
+        title="乙方有效证件"
+        description="乙方:有效身份证件复印件(正反面复印)及户口本复印件(户主页和本人页)"
+        :elder-id="dataForm.elderId"
+        :elder-name="dataForm.elderName"
+        fun-name="乙方有效证件"
+      />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import DocumentAttachmentBody from '../components/DocumentAttachmentBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   partyBIdentificationDocument_create,
@@ -110,12 +130,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -248,6 +276,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -280,3 +318,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 80 - 3
src/views/elderly/contracts/outing-confirmation-form/AddForm.vue

@@ -70,12 +70,19 @@
 
       <!-- 内容区域 -->
       <div class="form-body">
-        <OutingConfirmationBody ref="bodyRef" v-model="outingConfirmationForm" :is-text-mode="isDetail" />
+        <OutingConfirmationBody
+          ref="bodyRef"
+          v-model="outingConfirmationForm"
+          :is-text-mode="isDetail"
+        />
       </div>
     </div>
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <OutingConfirmationBody v-model="outingConfirmationForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import OutingConfirmationBody from '../components/OutingConfirmationBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   outingConfirmation_create,
@@ -101,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -245,6 +269,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -277,3 +311,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 76 - 3
src/views/elderly/contracts/power-of-attorney/AddForm.vue

@@ -76,6 +76,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +89,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <PowerOfAttorneyBody v-model="powerOfAttorneyForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import PowerOfAttorneyBody from '../components/PowerOfAttorneyBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   powerOfAttorney_create,
@@ -101,12 +113,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -124,7 +144,7 @@ const elderUp = (e) => {
   dataForm.value.checkInTime = e.checkInTime
   dataForm.value.contractNumber = e.contractNumber || e.fileNumber
   powerOfAttorneyForm.value.elderName = e.elderName
-  powerOfAttorneyForm.value.principalGender = (e.elderSex == 1 ? '男' :e.elderSex == 2? '女':'')
+  powerOfAttorneyForm.value.principalGender = e.elderSex == 1 ? '男' : e.elderSex == 2 ? '女' : ''
   powerOfAttorneyForm.value.principalIdCardNo = e.idCard
 }
 
@@ -263,6 +283,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -295,3 +325,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 79 - 2
src/views/elderly/contracts/registration-certificate-civil-affairs-department/AddForm.vue

@@ -81,6 +81,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -91,13 +94,26 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <DocumentAttachmentBody
+        v-model="attachmentForm"
+        :is-text-mode="true"
+        description="甲方登记证书及民政部门备案回执"
+      />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import DocumentAttachmentBody from '../components/DocumentAttachmentBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   registrationCertificateCivilAffairs_create,
@@ -106,12 +122,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -244,6 +268,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -276,3 +310,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 75 - 2
src/views/elderly/contracts/risk-disclosure-statement/AddForm.vue

@@ -76,6 +76,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +89,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <RiskDisclosureBody v-model="riskDisclosureForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import RiskDisclosureBody from '../components/RiskDisclosureBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   extra_riskDisclosure_create,
@@ -101,12 +113,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -251,6 +271,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -283,3 +313,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 77 - 4
src/views/elderly/contracts/room-facilities-equipment/AddForm.vue

@@ -80,6 +80,9 @@
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -90,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <RoomFacilitiesEquipmentBody v-model="roomFacilitiesForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import RoomFacilitiesEquipmentBody from '../components/RoomFacilitiesEquipmentBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   roomFacilitiesEquipment_create,
@@ -105,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -127,8 +147,8 @@ const elderUp = (e) => {
   dataForm.value.bedNumber = e.bedName || ''
   dataForm.value.checkInTime = e.checkInTime
   dataForm.value.contractNumber = e.contractNumber || e.fileNumber
-  roomFacilitiesForm.value.elderName = e.elderName||''
-  roomFacilitiesForm.value.roomNo = e.bedName||''
+  roomFacilitiesForm.value.elderName = e.elderName || ''
+  roomFacilitiesForm.value.roomNo = e.bedName || ''
 }
 
 const roomFacilitiesForm = ref({
@@ -256,6 +276,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -288,3 +318,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 80 - 3
src/views/elderly/contracts/service-charging-standards-institution/AddForm.vue

@@ -70,12 +70,19 @@
 
       <!-- 内容区域 -->
       <div class="form-body">
-        <ServiceChargingStandardsBody ref="bodyRef" v-model="serviceChargingForm" :is-text-mode="isDetail" />
+        <ServiceChargingStandardsBody
+          ref="bodyRef"
+          v-model="serviceChargingForm"
+          :is-text-mode="isDetail"
+        />
       </div>
     </div>
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <ServiceChargingStandardsBody v-model="serviceChargingForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import ServiceChargingStandardsBody from '../components/ServiceChargingStandardsBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   serviceChargingStandards_create,
@@ -101,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -241,6 +265,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -273,3 +307,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 80 - 3
src/views/elderly/contracts/signature-proxy-application/AddForm.vue

@@ -70,12 +70,19 @@
 
       <!-- 内容区域 -->
       <div class="form-body">
-        <SignatureProxyApplicationBody ref="bodyRef" v-model="signatureProxyForm" :is-text-mode="isDetail" />
+        <SignatureProxyApplicationBody
+          ref="bodyRef"
+          v-model="signatureProxyForm"
+          :is-text-mode="isDetail"
+        />
       </div>
     </div>
 
     <template #footer>
       <el-button @click="handleClosed">关闭</el-button>
+      <el-button type="success" size="small" v-show="isDetail" @click="handlePrint"
+        >打印/导出</el-button
+      >
       <el-button
         style="margin-left: 22px; margin-right: 30px"
         v-loading="formLoading"
@@ -86,13 +93,22 @@
       >
     </template>
   </el-drawer>
+
+  <Teleport to="body">
+    <div v-if="isPrint" class="print-container">
+      <div class="print-org-name">{{ orgName }}</div>
+      <SignatureProxyApplicationBody v-model="signatureProxyForm" :is-text-mode="true" />
+    </div>
+  </Teleport>
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive } from 'vue'
+import { ref, reactive, computed, nextTick } from 'vue'
 
 import dayjs from 'dayjs'
 import SignatureProxyApplicationBody from '../components/SignatureProxyApplicationBody.vue'
+import { useUserStore } from '@/store/modules/user'
+import { findLabelById } from '@/utils/dict'
 
 import {
   signatureProxyApplication_create,
@@ -101,12 +117,20 @@ import {
 } from '@/api/elderly/contracts'
 const message = useMessage()
 const { t } = useI18n()
+const userStore = useUserStore()
 const title = ref('')
 const dialogVisible = ref(false)
+const formLoading = ref(false)
 const selectElderRef = ref()
 const bodyRef = ref()
 const isDetail = ref(false)
-const formLoading = ref(false)
+const isPrint = ref(false)
+const orgName = computed(
+  () =>
+    findLabelById(userStore.getOrgTenant?.options || [], dataForm.value.tenantId) ||
+    userStore.getTenantName ||
+    ''
+)
 let dataForm = ref({
   id: undefined,
   elderName: '',
@@ -252,6 +276,16 @@ const handleClosed = () => {
   dialogVisible.value = false
   resetForm()
 }
+
+const handlePrint = async () => {
+  isPrint.value = true
+  await nextTick()
+  await new Promise((resolve) => setTimeout(resolve, 1500))
+  window.print()
+  setTimeout(() => {
+    isPrint.value = false
+  }, 500)
+}
 </script>
 
 <style scoped lang="scss">
@@ -284,3 +318,46 @@ const handleClosed = () => {
   }
 }
 </style>
+
+<style lang="scss">
+.print-container {
+  display: none;
+}
+
+@media print {
+  html,
+  body {
+    background: #fff !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    overflow: visible !important;
+  }
+
+  body > *:not(.print-container) {
+    display: none !important;
+  }
+
+  body > .print-container {
+    display: block !important;
+  }
+
+  .print-container .print-org-name {
+    text-align: start;
+    font-size: 14px;
+    margin-bottom: 12px;
+  }
+
+  .print-container > *:not(.print-org-name) {
+    width: 210mm !important;
+    max-width: none !important;
+    margin: 0 !important;
+    padding: 0 !important;
+    box-sizing: border-box !important;
+  }
+
+  @page {
+    size: 210mm 297mm;
+    margin: 16mm;
+  }
+}
+</style>

+ 100 - 69
src/views/elderly/elder/elder-file/Third.vue

@@ -1,32 +1,35 @@
 <template>
   <div class="third">
-  <!-- 修改合同日期对话框 -->
-  <Dialog
-    style="margin-top: 30vh"
-    v-model="dialogVisible"
-    title="修改合同结束日期"
-    width="30vw"
-    @confirm="confirmChange"
-  >
-    <el-form :model="formData" label-width="120px">
-      <el-form-item label="合同结束日期">
-        <el-date-picker
-          v-model="formData.contractEndDate"
-          type="date"
-          :clearable="false"
-          :editable="false"
-          placeholder="选择合同结束日期"
-          style="width: 100%"
-          format="YYYY-MM-DD"
-          value-format="YYYY-MM-DD"
-        />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-      <el-button  type="primary" @click="confirmChange" >确 定</el-button>
-    </template>
-  </Dialog>
+    <!-- 修改合同日期对话框 -->
+    <Dialog
+      style="margin-top: 30vh"
+      v-model="dialogVisible"
+      title="修改合同结束日期"
+      width="30vw"
+      @confirm="confirmChange"
+    >
+      <el-form :model="formData" label-width="120px">
+        <el-form-item label="合同编号">
+          <el-input placeholder="请输入合同编号" v-model="formData.contractNumber" />
+        </el-form-item>
+        <el-form-item label="合同结束日期">
+          <el-date-picker
+            v-model="formData.contractEndDate"
+            type="date"
+            :clearable="false"
+            :editable="false"
+            placeholder="选择合同结束日期"
+            style="width: 100%"
+            format="YYYY-MM-DD"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmChange">确 定</el-button>
+      </template>
+    </Dialog>
     <div class="title">在住信息</div>
     <el-row class="content">
       <el-col :span="4" :xs="24">
@@ -62,13 +65,29 @@
       <div class="scrollbar-flex-content">
         <div class="item" v-for="(item, index) in info.expenseItems" :key="index">
           <span>{{ item.itemName }}</span>
-          <p class="description green">{{ item.isFreeGift == 1 ? '赠送' : (item.isHirePurchase == 1 ? '分期' : '') }}</p>
+          <p class="description green">{{
+            item.isFreeGift == 1 ? '赠送' : item.isHirePurchase == 1 ? '分期' : ''
+          }}</p>
           <div v-if="item.isFreeGift == 1 || item.isHirePurchase == 1">
             <template v-if="item.isFreeGift == 1">
-              <span>{{ formatTime(item.freeStartTime, 'yyyy-MM-dd HH:mm:ss')  }} 至 {{ formatTime(item.freeEndTime, 'yyyy-MM-dd HH:mm:ss')  }} 共{{ dayjs(item.freeEndTime).diff(item.freeStartTime, 'month') + 1 }}月</span>
+              <span
+                >{{ formatTime(item.freeStartTime, 'yyyy-MM-dd HH:mm:ss') }} 至
+                {{ formatTime(item.freeEndTime, 'yyyy-MM-dd HH:mm:ss') }} 共{{
+                  dayjs(item.freeEndTime).diff(item.freeStartTime, 'month') + 1
+                }}月</span
+              >
             </template>
             <template v-if="item.isHirePurchase == 1">
-              <span>{{ formatTime(info.checkInTime, 'yyyy-MM-dd HH:mm:ss')  }} 至 {{ dayjs(info.checkInTime).add(item.hirePurchaseNumber, 'month').subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss')  }} 共{{ item.hirePurchaseNumber }}月</span>
+              <span
+                >{{ formatTime(info.checkInTime, 'yyyy-MM-dd HH:mm:ss') }} 至
+                {{
+                  dayjs(info.checkInTime)
+                    .add(item.hirePurchaseNumber, 'month')
+                    .subtract(1, 'day')
+                    .format('YYYY-MM-DD HH:mm:ss')
+                }}
+                共{{ item.hirePurchaseNumber }}月</span
+              >
             </template>
           </div>
           <div v-else>
@@ -81,48 +100,60 @@
     </ElScrollbar>
 
     <div class="title">合同信息</div>
-    <el-table :header-cell-style="tableHeaderColor" :data="info.contracts" >
-<!--      <el-table-column prop="date" label="序号">-->
-<!--        <template #default="scope">-->
-<!--          {{ scope.$index + 1 }}-->
-<!--        </template>-->
-<!--      </el-table-column>-->
-      <el-table-column prop="contractNumber" label="合同编号" />
-      <el-table-column prop="beginTime" label="合同开始日期" :formatter="dateFormatter2" />
-      <el-table-column prop="expireTime" label="合同结束日期">
+    <el-table :header-cell-style="tableHeaderColor" :data="info.contracts">
+      <!--      <el-table-column prop="date" label="序号">-->
+      <!--        <template #default="scope">-->
+      <!--          {{ scope.$index + 1 }}-->
+      <!--        </template>-->
+      <!--      </el-table-column>-->
+      <el-table-column prop="contractNumber" label="合同编号" align="center"/>
+      <el-table-column prop="beginTime" label="合同开始日期" :formatter="dateFormatter2" align="center"/>
+      <el-table-column prop="expireTime" label="合同结束日期" align="center">
         <template #default="scope">
-          <view style="display:flex;align-items: center;flex-direction: row">
+          <div >
             {{ formatTime(scope.row.expireTime, 'yyyy-MM-dd') }}
-            <el-icon size="20" @click="changeContract(formatTime(scope.row.expireTime, 'yyyy-MM-dd'),scope.row)" color="#888888" style="margin-left: 2px;cursor: pointer"><Edit /></el-icon>
-          </view>
+          </div>
         </template>
       </el-table-column>
-      <el-table-column prop="contractTerm" label="合同年限">
+      <el-table-column prop="contractTerm" label="合同年限" align="center" >
         <template #default="scope">
-          {{ getDictLabel(DICT_TYPE.CONTRACT_ARR, scope.row.contractTerm) }}
+          {{ getDictLabel(DICT_TYPE.CONTRACT_ARR, scope.row.contractTerm)||'--' }}
         </template>
       </el-table-column>
-      <el-table-column prop="amount" label="合同金额(元)">
+      <el-table-column prop="amount" label="合同金额(元)" align="center">
         <template #default="scope">{{ formatNum(scope.row.amount) }}</template>
       </el-table-column>
-      <el-table-column prop="contractStatus" label="合同状态">
-        <template #default="scope">{{ scope.row.status==1?'进行中':scope.row.status==2?'未开始':scope.row.status==0?'已过期':'未知'}}</template>
+      <el-table-column prop="contractStatus" label="合同状态" align="center">
+        <template #default="scope">{{
+          scope.row.status == 1
+            ? '进行中'
+            : scope.row.status == 2
+              ? '未开始'
+              : scope.row.status == 0
+                ? '已过期'
+                : '未知'
+        }}</template>
       </el-table-column>
-      <el-table-column prop="remarks" label="合同附件">
+      <el-table-column prop="remarks" label="合同附件" align="center">
         <template #default="scope">
-          <el-link v-for="item in scope.row.files" :key="item.fileUrl" @click="handleDownLoad(item)">{{ item.fileName }}</el-link>
+          <el-link
+            v-for="item in scope.row.files"
+            :key="item.fileUrl"
+            @click="handleDownLoad(item)"
+            >{{ item.fileName }}</el-link
+          >
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="right" width="70" fixed="right">
-        <template #default="scope" >
+      <el-table-column label="操作" align="center" width="150" fixed="right">
+        <template #default="scope">
           <el-button
             link
             type="primary"
-            @click="openFormC(scope.row)"
+            @click="changeContract(formatTime(scope.row.expireTime, 'yyyy-MM-dd'), scope.row)"
           >
-            电子合同
+            编辑
           </el-button>
-           
+          <el-button link type="warning" @click="openFormC(scope.row)"> 电子合同 </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -182,8 +213,6 @@
     </el-tabs>
 
     <ContractManageForm ref="contractManageFormRef" />
-
-
   </div>
 </template>
 <script lang="ts" setup>
@@ -196,9 +225,9 @@ import dayjs from 'dayjs'
 import { formatNum } from '@/utils/formatter'
 import { ElScrollbar } from 'element-plus'
 import { formatTime, handleDownLoad } from '@/utils'
-import {Edit} from "@element-plus/icons-vue"
-import {updateContractDate} from "@/api/elderly/fee/leave-settle";
-import ContractManageForm from "@/views/elderly/contracts/ContractManageForm.vue";
+import { Edit } from '@element-plus/icons-vue'
+import { updateContractDate } from '@/api/elderly/fee/leave-settle'
+import ContractManageForm from '@/views/elderly/contracts/ContractManageForm.vue'
 const message = useMessage()
 
 const props = defineProps({
@@ -246,7 +275,8 @@ const getInfo = async () => {
 const dialogVisible = ref(false)
 // 修改后的合同日期
 const formData = ref({
-  contractEndDate: ''
+  contractEndDate: '',
+  contractNumber: ''
 })
 
 // 格式化日期字符串
@@ -256,12 +286,12 @@ const openFormC = (row) => {
 
 let tempContractData = {}
 // 打开修改合同日期对话框
-const changeContract = (data,item) => {
-    console.log("AAAAA",item)
-    tempContractData = item
-    formData.value.contractEndDate = data
-    dialogVisible.value = true
-
+const changeContract = (data, item) => {
+  //console.log('AAAAA', item)
+  tempContractData = item
+  formData.value.contractEndDate = data
+  formData.value.contractNumber = item.contractNumber
+  dialogVisible.value = true
 }
 
 // 确认修改合同日期
@@ -269,8 +299,9 @@ const confirmChange = async () => {
   try {
     // 这里应该调用修改合同日期的API,需要根据实际情况修改
     tempContractData.expireTime = new Date(formData.value.contractEndDate).getTime()
-     const res = await updateContractDate(tempContractData)
-    if(res){
+    tempContractData.contractNumber = formData.value.contractNumber
+    const res = await updateContractDate(tempContractData)
+    if (res) {
       message.success('合同日期修改成功')
       getInfo()
     }
@@ -339,7 +370,7 @@ defineExpose({ init })
       padding: 15px;
       margin: 0 10px;
       background: var(--el-color-primary-light-9);
-      .description{
+      .description {
         height: 30px;
         line-height: 40px;
         margin-bottom: 0;

+ 60 - 47
src/views/elderly/elder/family-appointment/time-setting/index.vue

@@ -299,41 +299,33 @@
         </el-col>
 
         <el-col :span="12" class="col-right">
-<!--          <div class="card card-mid-right">-->
-<!--            <div class="card-header">-->
-<!--              <el-icon class="card-icon icon-green"><Tickets /></el-icon>-->
-<!--              <span class="card-title">总名额限制(按自然周和月)</span>-->
-<!--              <el-button-->
-<!--                type="primary"-->
-<!--                size="small"-->
-<!--                :disabled="isDisabled || totalLimits.length >= 3"-->
-<!--                @click="openTotalLimitDialog"-->
-<!--              >-->
-<!--                <el-icon><Plus /></el-icon>-->
-<!--                添加-->
-<!--              </el-button>-->
-<!--            </div>-->
-<!--            <div class="card-body card-body-limit">-->
-<!--              <div v-if="totalLimits.length === 0" class="empty-limits-inline">-->
-<!--                <el-empty description="暂无限制,点击右上角添加" :image-size="40" />-->
-<!--              </div>-->
-<!--              <div v-else class="limit-list-inline">-->
-<!--                <div v-for="(limit, index) in totalLimits" :key="index" class="limit-item-inline">-->
-<!--                  <span class="limit-type-badge">{{ getLimitTypeLabel(limit.type) }}</span>-->
-<!--                  <span class="limit-count-num">{{ limit.count }} <em>次</em></span>-->
-<!--                  <el-button-->
-<!--                    type="danger"-->
-<!--                    link-->
-<!--                    size="small"-->
-<!--                    :disabled="isDisabled"-->
-<!--                    @click="removeTotalLimit(index)"-->
-<!--                  >-->
-<!--                    <el-icon><Delete /></el-icon>-->
-<!--                  </el-button>-->
-<!--                </div>-->
-<!--              </div>-->
-<!--            </div>-->
-<!--          </div>-->
+          <div class="card card-mid-right">
+            <div class="card-header">
+              <el-icon class="card-icon icon-green"><Tickets /></el-icon>
+              <span class="card-title">放号时间段(每天可开始预约时间)</span>
+            </div>
+            <div class="card-body card-body-limit" style="padding: 40px 30px">
+              <span class="field-label">放号开始时间:</span>
+              <el-time-picker
+                v-model="formData.startTime"
+                format="HH:mm"
+                value-format="HH:mm"
+                placeholder="开始时间"
+                style="width: 150px"
+                :disabled="isDisabled"
+              />
+
+              <span class="field-label" style="margin-left: 3vw">放号结束时间:</span>
+              <el-time-picker
+                v-model="formData.endTime"
+                format="HH:mm"
+                value-format="HH:mm"
+                placeholder="结束时间"
+                style="width: 150px"
+                :disabled="isDisabled"
+              />
+            </div>
+          </div>
 
           <div class="card card-bottom-right">
             <div class="card-header">
@@ -667,6 +659,8 @@ const defaultFormData = () => ({
   ruleType: 1,
   startDay: 0,
   advanceDays: 6,
+  startTime: '08:00',
+  endTime: '17:30',
   appointmentEnabled: true
 })
 
@@ -1005,11 +999,22 @@ const getSetting = async () => {
     if (res && res.config) {
       const config = res.config
       configId.value = config.id
+      let startTime = defaultFormData().startTime
+      let endTime = defaultFormData().endTime
+      if (config.openTimeSlot && typeof config.openTimeSlot === 'string') {
+        const [s, e] = config.openTimeSlot.split('-')
+        if (s && e) {
+          startTime = s
+          endTime = e
+        }
+      }
       Object.assign(formData, {
         ruleType: 1,
         startDay: config.beginBookDays ?? 0,
         advanceDays: config.advanceDays ?? 0,
-        appointmentEnabled: config.enabled === 1
+        appointmentEnabled: config.enabled === 1,
+        startTime,
+        endTime
       })
       totalLimits.value = []
       personLimits.value = []
@@ -1164,6 +1169,14 @@ const handleSubmit = async () => {
     await ElMessageBox.alert('提前预约天数最少为1', '警告', { type: 'warning' })
     return
   }
+  if (
+    formData.startTime &&
+    formData.endTime &&
+    timeToMinutes(formData.startTime) >= timeToMinutes(formData.endTime)
+  ) {
+    await ElMessageBox.alert('放号开始时间不能晚于或等于结束时间', '警告', { type: 'warning' })
+    return
+  }
   const groupError = validateGroups()
   if (groupError) {
     await ElMessageBox.alert(groupError, '警告', { type: 'warning' })
@@ -1186,20 +1199,16 @@ const handleSubmit = async () => {
         item.endTime &&
         timeToMinutes(item.startTime) >= timeToMinutes(item.endTime)
       ) {
-        await ElMessageBox.alert(
-          '存在开始时间晚于或等于结束时间的预约项,请检查',
-          '警告',
-          { type: 'warning' }
-        )
+        await ElMessageBox.alert('存在开始时间晚于或等于结束时间的预约项,请检查', '警告', {
+          type: 'warning'
+        })
         return
       }
       for (let j = i + 1; j < group.items.length; j++) {
         if (isOverlap(item, group.items[j])) {
-          await ElMessageBox.alert(
-            '同一预约组内存在时间段重叠的预约项,请检查',
-            '警告',
-            { type: 'warning' }
-          )
+          await ElMessageBox.alert('同一预约组内存在时间段重叠的预约项,请检查', '警告', {
+            type: 'warning'
+          })
           return
         }
       }
@@ -1213,7 +1222,11 @@ const handleSubmit = async () => {
       tenantId: organizationId,
       advanceDays: formData.advanceDays,
       beginBookDays: formData.startDay,
-      enabled: formData.appointmentEnabled ? 1 : 0
+      enabled: formData.appointmentEnabled ? 1 : 0,
+      openTimeSlot:
+        formData.startTime && formData.endTime
+          ? `${formData.startTime}-${formData.endTime}`
+          : undefined
     }
     totalLimits.value.forEach((limit) => {
       const field = limitTypeToFieldMap[limit.type]

+ 70 - 60
src/views/social-worker/assessment/PerceptionAndCommunication/index.vue

@@ -43,7 +43,9 @@
       </el-form-item>
 
       <el-form-item>
-        <el-button @click="handleQuery" style="margin-left: 2vw"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="handleQuery" style="margin-left: 2vw"
+          ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
+        >
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
       </el-form-item>
     </el-form>
@@ -52,12 +54,9 @@
   <!-- 列表 -->
   <ContentWrap>
     <div class="mb-10px">
-
-      <ButtonAdd @click="openForm(undefined)"  />
+      <ButtonAdd @click="openForm(undefined)" />
       <!--      <el-button @click="inputH" type="success"><Icon icon="fa:cloud-upload" :size="16" class="mr-1"/>导入</el-button>-->
-<!--      <ButtonImport @click="handleImportCard"  />-->
-
-
+      <!--      <ButtonImport @click="handleImportCard"  />-->
     </div>
     <el-table v-loading="loading" :data="list" :header-cell-style="tableHeaderColor">
       <el-table-column header-align="center" align="center" label="序号" width="60">
@@ -68,37 +67,57 @@
         </template>
       </el-table-column>
 
-
-      <el-table-column prop="elderName" header-align="center" align="center" label="长者姓名" min-width="150" show-overflow-tooltip/>
-      <el-table-column prop="assessor" header-align="center" align="center" label="评估人" min-width="200" show-overflow-tooltip/>
-      <el-table-column prop="assessDate" header-align="center" align="center" label="记录日期" min-width="200" show-overflow-tooltip/>
-
-      <el-table-column prop="creator" header-align="center" align="center" label="记录人" min-width="200" show-overflow-tooltip/>
-
-
-      <el-table-column label="操作" align="center" width="200" >
+      <el-table-column
+        prop="elderName"
+        header-align="center"
+        align="center"
+        label="长者姓名"
+        min-width="150"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="assessor"
+        header-align="center"
+        align="center"
+        label="评估人"
+        min-width="200"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="assessDate"
+        header-align="center"
+        align="center"
+        label="记录日期"
+        min-width="200"
+        show-overflow-tooltip
+      />
+
+      <el-table-column
+        prop="creator"
+        header-align="center"
+        align="center"
+        label="记录人"
+        min-width="200"
+        show-overflow-tooltip
+      />
+
+      <el-table-column label="操作" align="center" width="200">
         <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openFormEdit(scope.row, scope.row.id)"
-          >
+          <el-button link type="primary" @click="openFormEdit(scope.row, scope.row.id)">
             编辑
           </el-button>
-          <el-button
-            link
-            type="warning"
-            @click="openFormDetail(scope.row,scope.row.id)"
-          >
+          <el-button link type="warning" @click="openFormDetail(scope.row, scope.row.id)">
             详情
           </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="openClose(scope.row)"
-          >
-            删除
-          </el-button>
+          <el-button link type="danger" @click="openClose(scope.row)"> 删除 </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -130,7 +149,6 @@
     @cancel="showBatchExport = false"
   />
 
-
   <Import
     ref="importRef"
     @success="getList"
@@ -139,21 +157,20 @@
       downloadUrl: 'elderly-service-order/get-import-template',
       excelTempName: '居家工单-【导入】模板',
       importUrl: '/elderly-service-order/import',
-      failExportUrl: '',
+      failExportUrl: ''
     }"
   />
-
 </template>
 
 <script setup lang="ts">
-import AddForm from "./AddForm.vue";
-import ButtonAdd from "@/components/ButtonAdd/src/ButtonAdd.vue";
-import ButtonImport from "@/components/ButtonImport/src/ButtonImport.vue";
+import AddForm from './AddForm.vue'
+import ButtonAdd from '@/components/ButtonAdd/src/ButtonAdd.vue'
+import ButtonImport from '@/components/ButtonImport/src/ButtonImport.vue'
 import { useUserStore } from '@/store/modules/user'
-import {formatTimestamp, getCurrentMonthRange} from "@/utils/dateUtil";
-import Import from "@/components/ImportFile/index.vue";
-import {restraintCreatePage, restraintDelete} from "@/api/elderly/nursing/keep-fit";
-import {perceptionDelete, perceptionPage} from "@/api/social-work";
+import { formatTimestamp, getCurrentMonthRange } from '@/utils/dateUtil'
+import Import from '@/components/ImportFile/index.vue'
+import { restraintCreatePage, restraintDelete } from '@/api/elderly/nursing/keep-fit'
+import { perceptionDelete, perceptionPage } from '@/api/social-work'
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const userStore = useUserStore()
@@ -192,14 +209,13 @@ const inputH = () => {
 }
 // 打开导出弹窗
 const handleImportCard = async () => {
-
   // showBatchExport.value = true
 }
 
 // 处理批量导出
 const handleBatchExport = async (batch: number, count: number) => {
   exportLoading.value = true
-  console.log(batch,count)
+  console.log(batch, count)
   // try {
   //   let queryParams = {
   //     pageNo: batch,
@@ -221,7 +237,6 @@ const handleBatchExport = async (batch: number, count: number) => {
   //   ]
   //   exportWithExpandedObjectArrays(list, headers, `医疗护理记录${formatToDateTime()}.xlsx`, '医疗护理记录')
   // } catch (_) {}
-
 }
 
 /** 查询列表 */
@@ -251,7 +266,7 @@ const resetQuery = () => {
   queryParams.elderName = ''
   queryParams.assessor = ''
   queryParams.tenantIds = userStore.orgTenantId
-  queryParams.assessDate= getCurrentMonthRange()
+  queryParams.assessDate = getCurrentMonthRange()
   queryFormRef.value.resetFields()
   handleQuery()
 }
@@ -259,39 +274,34 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (id?: number) => {
-  if(queryParams.tenantIds.length == 0 || queryParams.tenantIds.length > 1){
+  if (queryParams.tenantIds.length == 0 || queryParams.tenantIds.length > 1) {
     message.error('新增只能选择一个机构')
     return
   }
-  formRef.value.open(queryParams.tenantIds[0], id,false)
+  formRef.value.open(queryParams.tenantIds[0], id, false)
 }
 
 const editRef = ref()
 const openFormEdit = (row: any = {}, id?: number) => {
-  formRef.value.open(row.tenantId, id,false)
+  formRef.value.open(row.tenantId, id, false)
 }
 
-
-const openFormDetail = (row: any = {},id?: number) => {
-  formRef.value.open(row.tenantId,id,true)
+const openFormDetail = (row: any = {}, id?: number) => {
+  formRef.value.open(row.tenantId, id, true)
 }
 
-
-
-
-
 const openClose = async (item) => {
   try {
-    console.log("任务ID",item)
+    console.log('任务ID', item)
     const res = await message.confirm('确定要删除吗?', '提示')
     if (res == 'confirm') {
       // 发起
       try {
         const res = await perceptionDelete(item.id)
-        if (res){
+        if (res) {
           message.success(t('common.updateSuccess'))
         }
-      }catch(err) {}
+      } catch (err) {}
     }
     // 刷新列表
     await getList()
@@ -314,7 +324,7 @@ const cancelDelete = async (id: number) => {
 const route = useRoute()
 /** 初始化 **/
 onMounted(() => {
-  if(route.query && route.query.elderName){
+  if (route.query && route.query.elderName) {
     queryParams.elderName = route.query.elderName as string
   }
   getList()

+ 8 - 0
src/views/social-worker/assessment/SAS/index.vue

@@ -101,6 +101,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 8 - 2
src/views/social-worker/assessment/SDS/index.vue

@@ -99,6 +99,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"
@@ -355,8 +363,6 @@ onMounted(async () => {
   try {
     buildList.value = await getBuildList({ tenantIds: queryParams.tenantIds[0] })
   } catch (e) {}
-
-
 })
 
 // 表头格式

+ 8 - 0
src/views/social-worker/assessment/psychological-social-situation/index.vue

@@ -94,6 +94,14 @@
         min-width="150"
         show-overflow-tooltip
       />
+      <el-table-column
+        prop="bedName"
+        header-align="center"
+        align="center"
+        label="床位号"
+        min-width="150"
+        show-overflow-tooltip
+      />
       <el-table-column
         prop="assessor"
         header-align="center"

+ 5 - 5
src/views/system/little-tools/index.vue

@@ -151,8 +151,8 @@ const handleExportProcess = async () => {
 
     if (dataList.length > 0) {
       const headers = [
-        { key: 'orgName', title: '机构名称', width: 40 },
-        { key: 'num', title: '院区审批流程超过3天未及时审核数量(条)', width: 40 }
+        { key: 'orgName', title: '机构名称', width: 30 },
+        { key: 'num', title: '院区审批流程超过3天未及时审核数量', width: 30 }
       ]
       console.log('准备导出的数据:', dataList)
       await exportWithCustomHeaders(dataList, headers, '各院审批数据及时性统计.xlsx')
@@ -207,9 +207,9 @@ const handleExportBill = async () => {
 
     if (dataList.length > 0) {
       const headers = [
-        { key: 'orgName', title: '机构名称', width: 40 },
-        { key: 'num1', title: '月度账单未及时缴费(条)', width: 30 },
-        { key: 'num2', title: '退住账单未及时缴费(条)', width: 30 }
+        { key: 'orgName', title: '机构名称', width: 30 },
+        { key: 'num1', title: '月度账单未及时缴费', width: 30 },
+        { key: 'num2', title: '退住账单未及时缴费', width: 30 }
       ]
       console.log('准备导出的数据:', dataList)
       await exportWithCustomHeaders(dataList, headers, '账单缴费及时性统计.xlsx')

+ 48 - 29
src/views/system/supplierManage/index.vue

@@ -8,7 +8,7 @@
       :inline="true"
       label-width="90px"
     >
-        <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
+      <TenantSelect v-model="queryParams.tenantIds" placeholder="请选择机构名称" prop="tenantIds" />
       <el-form-item label="供应商名称">
         <el-input
           v-model="queryParams.supplierName"
@@ -43,15 +43,19 @@
         <Icon icon="ep:plus" class="mr-5px" />
         新增
       </el-button>
-      <!-- <el-button type="success" plain @click="importRefCK" v-hasPermi="['system:supplier:input']">
+      <el-button type="success" plain @click="importRefCK" v-hasPermi="['system:supplier:input']">
         <Icon icon="ep:upload" class="mr-5px" />
         导入
-      </el-button> -->
+      </el-button>
     </div>
 
-
-    <el-table v-loading="loading" :data="list" @selection-change="handleSelectable" :header-cell-style="tableHeaderColor">
-      <el-table-column width="55"  align="center" type="selection" />
+    <el-table
+      v-loading="loading"
+      :data="list"
+      @selection-change="handleSelectable"
+      :header-cell-style="tableHeaderColor"
+    >
+      <el-table-column width="55" align="center" type="selection" />
 
       <el-table-column header-align="center" align="center" label="序号" width="100">
         <template #default="scope">
@@ -60,16 +64,33 @@
           }}
         </template>
       </el-table-column>
-      <el-table-column prop="supplierNumber" header-align="center" align="center" label="供应商编号" />
-      <el-table-column prop="typeName" header-align="center" align="center" label="供应商类型"/>
-      <el-table-column prop="supplierNickName" header-align="center" align="center" label="供应商简称" />
-      <el-table-column prop="supplierName" header-align="center" align="center" label="供应商全称" />
-      <el-table-column prop="phone" header-align="center" align="center" label="电话"/>
+      <el-table-column
+        prop="supplierNumber"
+        header-align="center"
+        align="center"
+        label="供应商编号"
+      />
+      <el-table-column prop="typeName" header-align="center" align="center" label="供应商类型" />
+      <el-table-column
+        prop="supplierNickName"
+        header-align="center"
+        align="center"
+        label="供应商简称"
+      />
+      <el-table-column
+        prop="supplierName"
+        header-align="center"
+        align="center"
+        label="供应商全称"
+      />
+      <el-table-column prop="phone" header-align="center" align="center" label="电话" />
       <el-table-column prop="contacts" header-align="center" align="center" label="联系人" />
       <el-table-column prop="operator" header-align="center" align="center" label="业务员" />
       <el-table-column prop="status" header-align="center" align="center" label="状态">
         <template #default="scope">
-          <el-tag :type="scope.row.status==0?'primary':'danger'">{{scope.row.status==0?'正常':'禁用'}}</el-tag>
+          <el-tag :type="scope.row.status == 0 ? 'primary' : 'danger'">{{
+            scope.row.status == 0 ? '正常' : '禁用'
+          }}</el-tag>
         </template>
       </el-table-column>
 
@@ -126,16 +147,14 @@
       desc: '供应商设置-导入模板'
     }"
   />
-
-
 </template>
 
 <script setup lang="ts">
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import AddForm from '@/views/system/supplierManage/AddForm.vue'
 import DetailForm from '@/views/system/supplierManage/DetailsForm.vue'
-import {getSupplierPage, supplierDelete} from "@/api/system/supplierManage";
-import Import from "@/components/ImportFile/index.vue";
+import { getSupplierPage, supplierDelete } from '@/api/system/supplierManage'
+import Import from '@/components/ImportFile/index.vue'
 import { useUserStore } from '@/store/modules/user'
 
 const message = useMessage() // 消息弹窗
@@ -151,7 +170,7 @@ let queryParams = reactive({
   pageSize: 10,
   supplierName: '',
   typeName: '',
-  tenantIds:userStore.orgTenantId
+  tenantIds: userStore.orgTenantId
 })
 const queryFormRef = ref() // 搜索的表单
 
@@ -171,32 +190,32 @@ const getList = async () => {
 
 /** 删除列表 */
 const deleteList = async (id) => {
-
   try {
     // 取消的二次确认
     await message.delConfirm('确定要删除这条数据吗?')
     // 发起取消
     loading.value = true
-    await  supplierDelete(id)
+    await supplierDelete(id)
     loading.value = false
     await getList()
     message.success('删除成功!')
-  } catch {loading.value = false}
-
+  } catch {
+    loading.value = false
+  }
 }
 
 const selectableList = ref([] as any[])
-const handleSelectable = (row:any) => {
+const handleSelectable = (row: any) => {
   selectableList.value = row
-   console.log(row)
+  console.log(row)
 }
 
-const deleteCK = (id)=>{
-    if(id){
-      deleteList(id)
-    }else {
-      deleteList(selectableList.value[0].id)
-    }
+const deleteCK = (id) => {
+  if (id) {
+    deleteList(id)
+  } else {
+    deleteList(selectableList.value[0].id)
+  }
 }
 
 const importRefCK = () => {