chenjun 2 тижнів тому
батько
коміт
33783ad1ab

+ 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",

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

@@ -0,0 +1,434 @@
+/**
+ * 九防安全看板 - 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 elders = generateElders(params?.month, params?.buildId, params?.floorId)
+  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 })
+}
+
+/** 14. 月度评估次数(日历热力图) */
+export const getMonthlyAssessCalendar = (params?: { buildId?: string | number }) => {
+  const data: [string, number][] = []
+  const now = new Date(2026, 6, 24)
+  const buildSeed = params?.buildId ? Number(params.buildId) : 0
+  for (let i = 180; i >= 0; i--) {
+    const d = new Date(now)
+    d.setDate(d.getDate() - i)
+    const dateStr = d.toISOString().slice(0, 10)
+    const rnd = seededRandom(dateStr.charCodeAt(0) + dateStr.charCodeAt(2) + dateStr.charCodeAt(5) + i + buildSeed)
+    const count = rnd() > 0.3 ? randInt(2, 15, rnd) : 0
+    if (count > 0) data.push([dateStr, count])
+  }
+  return delay(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)
+}

+ 12 - 9
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">
@@ -166,6 +171,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 +393,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 +438,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 +461,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)

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

@@ -0,0 +1,815 @@
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import echarts from '@/plugins/echarts'
+import 'echarts-gl'
+
+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: { trigger: 'axis', textStyle: { color: '#fff' } },
+    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: {
+      trigger: 'item',
+      formatter: '{b}<br/>人数:{c}人 ({d}%)',
+      textStyle: { color: '#fff' }
+    },
+    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: {
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      textStyle: { color: '#fff' }
+    },
+    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: {
+      trigger: 'axis',
+      axisPointer: { type: 'cross' },
+      textStyle: { color: '#fff' }
+    },
+    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: {
+      trigger: 'axis',
+      axisPointer: { type: 'shadow' },
+      textStyle: { color: '#fff' }
+    },
+    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: {},
+    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>

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

@@ -0,0 +1,932 @@
+<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 { CalendarComponent, 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,
+  CalendarComponent,
+  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 calendarData = ref<[string, number][]>([])
+
+// ========================= 图表实例管理 =========================
+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)
+  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'
+}
+
+const levelNames: Record<string, string> = {
+  none: '无风险',
+  low: '低危',
+  medium: '中危',
+  high: '高危'
+}
+
+// ========================= 图表配置生成 =========================
+
+/** 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: 'vertical', right: 10, top: 'center', textStyle: { color: COLORS.text, fontSize: 12 } },
+    series: [{
+      name: '风险等级', type: 'pie', radius: ['35%', '70%'], center: ['38%', '50%'],
+      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 }
+    }]
+  })
+}
+
+/** 13. 月度评估日历热力图 */
+const renderCalendar = () => {
+  const data = calendarData.value
+  if (!data.length) return
+  createChart('calendar', {
+    backgroundColor: 'transparent',
+    tooltip: { textStyle: { color: '#fff' }, formatter: (p: any) => `${p.data[0]}<br/>评估次数:${p.data[1]} 次` },
+    visualMap: {
+      min: 0, max: 15, calculable: true, orient: 'horizontal', left: 'center', top: 0,
+      textStyle: { color: COLORS.text }, inRange: { color: ['#0a2f5e', '#00aaff', '#00ffff', '#ffd85a', '#ff5a3c'] }
+    },
+    calendar: {
+      top: 60, left: 40, right: 40, bottom: 20,
+      range: ['2026-01-01', '2026-07-24'],
+      cellSize: ['auto', 14],
+      itemStyle: { color: 'rgba(10, 47, 94, 0.3)', borderColor: 'rgba(26, 58, 107, 0.5)', borderWidth: 1 },
+      splitLine: { lineStyle: { color: 'rgba(0, 255, 255, 0.15)' } },
+      yearLabel: { color: COLORS.text, fontSize: 14 },
+      monthLabel: { color: COLORS.text },
+      dayLabel: { color: COLORS.textDim }
+    },
+    series: [{ type: 'heatmap', coordinateSystem: 'calendar', data }]
+  })
+}
+
+/** 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: 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, formatter: '{value}%' } },
+    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()
+  renderCalendar()
+  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, cal
+    ] = 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 }),
+      echartsApi.getMonthlyAssessCalendar({ buildId: params.buildId })
+    ])
+    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
+    calendarData.value = cal
+    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.month" placeholder="全部月份" clearable class="filter-select" @change="handleFilterChange">
+            <el-option v-for="m in monthOptions" :key="m" :label="m" :value="m" />
+          </el-select>
+        </div>
+        <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">
+        <div class="card-title"><span class="dot dot-high"></span>3D 楼栋 × 九防高危人数(可旋转)</div>
+        <div :ref="setRef('bar3D')" class="chart-tall"></div>
+      </div>
+
+      <!-- 日历热力图(全宽) -->
+      <div class="card span-full">
+        <div class="card-title"><span class="dot dot-low"></span>半年度评估频次日历</div>
+        <div :ref="setRef('calendar')" class="chart-calendar"></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; }
+.grid-auto .span-full { grid-column: 1 / -1; }
+@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);
+  &::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%; height: 320px; }
+.chart-tall { width: 100%; height: 420px; }
+.chart-calendar { width: 100%; height: 200px; }
+
+.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>

+ 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;

+ 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')