Selaa lähdekoodia

新增长者档案,去处理,长者位置,所有设备接口对接

xiongxing 2 viikkoa sitten
vanhempi
commit
4e84d8667d

+ 2 - 1
.env.local

@@ -10,7 +10,8 @@ VITE_DEV=true
 VITE_BASE_URL='http://47.107.245.0:4080' #//测试
 VITE_API_WSS_URL='wss://home.ynims.com:7060/ws/homecare-web/'
 # VITE_API_WSS_URL='ws://192.168.101.174:3443/ws/homecare-web/'
-VITE_BASE_URL_APP_LITE='http://47.107.245.0:4080'  # 家属端小程序用到的
+# VITE_BASE_URL_APP_LITE='http://47.107.245.0:4080'  # 家属端小程序用到的
+VITE_BASE_URL_APP_LITE='http://home.ynims.com:4080'  # 家属端小程序用到的
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 319 - 225
package-lock.json

@@ -1,26 +1,28 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "2.5.0-snapshot",
+  "version": "2025.10-snapshot",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "yudao-ui-admin-vue3",
-      "version": "2.5.0-snapshot",
+      "version": "2025.10-snapshot",
       "license": "MIT",
       "dependencies": {
-        "@element-plus/icons-vue": "^2.1.0",
+        "@element-plus/icons-vue": "2.3.2",
         "@form-create/designer": "^3.2.6",
         "@form-create/element-ui": "^3.2.11",
         "@iconify/iconify": "^3.1.1",
         "@microsoft/fetch-event-source": "^2.0.1",
         "@videojs-player/vue": "^1.0.0",
         "@vueuse/core": "^10.9.0",
-        "@wangeditor/editor": "^5.1.23",
-        "@wangeditor/editor-for-vue": "^5.1.10",
+        "@wangeditor-next/editor": "^5.6.46",
+        "@wangeditor-next/editor-for-vue": "^5.1.14",
+        "@wangeditor-next/plugin-mention": "^1.0.16",
         "@zxcvbn-ts/core": "^3.0.4",
+        "amfe-flexible": "^2.2.1",
         "animate.css": "^4.1.1",
-        "axios": "^1.6.8",
+        "axios": "1.9.0",
         "benz-amr-recorder": "^1.1.5",
         "bpmn-js-token-simulation": "^0.36.0",
         "camunda-bpmn-moddle": "^7.0.1",
@@ -31,10 +33,11 @@
         "driver.js": "^1.3.1",
         "echarts": "^5.5.0",
         "echarts-wordcloud": "^2.1.0",
-        "element-plus": "2.9.1",
+        "element-plus": "2.11.1",
         "fast-xml-parser": "^4.3.2",
         "highlight.js": "^11.9.0",
         "jsencrypt": "^3.3.2",
+        "jsoneditor": "^10.1.3",
         "lodash-es": "^4.17.21",
         "markdown-it": "^14.1.0",
         "markmap-common": "^0.16.0",
@@ -46,18 +49,21 @@
         "nprogress": "^0.2.0",
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",
+        "postcss-pxtorem": "^6.1.0",
         "qrcode": "^1.5.3",
         "qs": "^6.12.0",
+        "snabbdom": "^3.6.2",
         "sortablejs": "^1.15.3",
         "steady-xml": "^0.1.0",
         "url": "^0.11.3",
-        "v3-jsoneditor": "^0.0.6",
         "video.js": "^7.21.5",
         "vue": "3.5.12",
         "vue-dompurify-html": "^4.1.4",
         "vue-i18n": "9.10.2",
         "vue-router": "4.4.5",
         "vue-types": "^5.1.1",
+        "vue3-print-nb": "^0.1.4",
+        "vue3-seamless-scroll": "^3.0.2",
         "vue3-signature": "^0.2.4",
         "vuedraggable": "^4.1.0",
         "web-storage-cache": "^1.1.1",
@@ -69,6 +75,7 @@
         "@iconify/json": "^2.2.187",
         "@intlify/unplugin-vue-i18n": "^2.0.0",
         "@purge-icons/generated": "^0.9.0",
+        "@types/jsoneditor": "^9.9.5",
         "@types/lodash-es": "^4.17.12",
         "@types/node": "^20.11.21",
         "@types/nprogress": "^0.2.3",
@@ -2310,9 +2317,9 @@
       }
     },
     "node_modules/@element-plus/icons-vue": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
-      "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
       "license": "MIT",
       "peerDependencies": {
         "vue": "^3.2.0"
@@ -4492,6 +4499,13 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/@types/ace": {
+      "version": "0.0.52",
+      "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.52.tgz",
+      "integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/conventional-commits-parser": {
       "version": "5.0.1",
       "resolved": "https://registry.npmmirror.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz",
@@ -4792,6 +4806,41 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/jsoneditor": {
+      "version": "9.9.6",
+      "resolved": "https://registry.npmjs.org/@types/jsoneditor/-/jsoneditor-9.9.6.tgz",
+      "integrity": "sha512-SJ29nWBIhnhtU5n72wxhPiuUVd8cnDHd7ZYMqVkzWtdRxTUdS8+oy1pg66yhmM1kcuanX3xmAAKfcyhhBnHEjQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/ace": "*",
+        "ajv": "^6.12.0"
+      }
+    },
+    "node_modules/@types/jsoneditor/node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/@types/jsoneditor/node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/lodash": {
       "version": "4.17.16",
       "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz",
@@ -6991,163 +7040,275 @@
         }
       }
     },
-    "node_modules/@wangeditor/basic-modules": {
-      "version": "1.1.7",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
-      "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
+    "node_modules/@wangeditor-next/editor": {
+      "version": "5.6.49",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/editor/-/editor-5.6.49.tgz",
+      "integrity": "sha512-gDh7CLzsuPvUp1n4rO//V1NTHlpGzEibL71oRcRcxpz76oNaW12u+GWWvRde4cWivaCTLzHwz7EfEVdyDkt/Ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@uppy/core": "^2.1.1",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor-next/basic-modules": "~1.5.47",
+        "@wangeditor-next/code-highlight": "~1.3.43",
+        "@wangeditor-next/core": "~1.7.45",
+        "@wangeditor-next/list-module": "~1.1.52",
+        "@wangeditor-next/table-module": "~1.6.60",
+        "@wangeditor-next/upload-image-module": "~1.1.50",
+        "@wangeditor-next/video-module": "~1.3.51",
+        "dom7": "^4.0.0",
+        "is-hotkey": "^0.2.0",
+        "lodash.camelcase": "^4.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "lodash.foreach": "^4.5.0",
+        "lodash.throttle": "^4.1.1",
+        "lodash.toarray": "^4.4.0",
+        "nanoid": "^5.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
+      }
+    },
+    "node_modules/@wangeditor-next/editor-for-vue": {
+      "version": "5.1.14",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/editor-for-vue/-/editor-for-vue-5.1.14.tgz",
+      "integrity": "sha512-Xkrdo590AhLHvzyR+U246t6T89nIWHz1weAgMuo8jEA2HS5RiUnsA4U6+iUGaQ2E5c8mYQaeNqzHQXUp9Okbiw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@wangeditor-next/editor": ">=5.1.0",
+        "vue": "^3.0.5"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/basic-modules": {
+      "version": "1.5.47",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/basic-modules/-/basic-modules-1.5.47.tgz",
+      "integrity": "sha512-FHydtBbfpsi4R4JTo5MvwWhzButwq6x36o+GoxsALdItwDW2qVgJkrlhw25aWYpg6ff1xqjivHfLBaAPWC4J+w==",
       "license": "MIT",
       "dependencies": {
         "is-url": "^1.2.4"
       },
       "peerDependencies": {
-        "@wangeditor/core": "1.x",
-        "dom7": "^3.0.0",
+        "@wangeditor-next/core": "1.7.45",
+        "dom7": "^3.0.0 || ^4.0.0",
         "lodash.throttle": "^4.1.1",
-        "nanoid": "^3.2.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "nanoid": "^5.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
       }
     },
-    "node_modules/@wangeditor/code-highlight": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
-      "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/code-highlight": {
+      "version": "1.3.43",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/code-highlight/-/code-highlight-1.3.43.tgz",
+      "integrity": "sha512-22eHjYDmtTxZqZOma2ls9zWA6gsgSkWq3XtmLylA15kegVBKAy7YxYbRrdS7D4Y/igqOerSbc5oMsOdeYjRfnQ==",
       "license": "MIT",
       "dependencies": {
         "prismjs": "^1.23.0"
       },
       "peerDependencies": {
-        "@wangeditor/core": "1.x",
-        "dom7": "^3.0.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "@wangeditor-next/core": "1.7.45",
+        "dom7": "^3.0.0 || ^4.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
       }
     },
-    "node_modules/@wangeditor/core": {
-      "version": "1.1.19",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
-      "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/core": {
+      "version": "1.7.45",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/core/-/core-1.7.45.tgz",
+      "integrity": "sha512-5Pt8JCmdzJWk4q18zUZse+zM+mBW6jYt3npXVkLswYysx01krC3bBQq1J9JeZe4Ci+rQAs0tQj3t1imjpsmRgg==",
       "license": "MIT",
       "dependencies": {
         "@types/event-emitter": "^0.3.3",
         "event-emitter": "^0.3.5",
-        "html-void-elements": "^2.0.0",
-        "i18next": "^20.4.0",
-        "scroll-into-view-if-needed": "^2.2.28",
-        "slate-history": "^0.66.0"
+        "html-void-elements": "^3.0.0",
+        "i18next": "^23.0.0",
+        "scroll-into-view-if-needed": "^3.0.0",
+        "slate-history": "^0.109.0"
       },
       "peerDependencies": {
         "@uppy/core": "^2.1.1",
         "@uppy/xhr-upload": "^2.0.3",
-        "dom7": "^3.0.0",
+        "dom7": "^3.0.0 || ^4.0.0",
         "is-hotkey": "^0.2.0",
         "lodash.camelcase": "^4.3.0",
         "lodash.clonedeep": "^4.5.0",
         "lodash.debounce": "^4.0.8",
         "lodash.foreach": "^4.5.0",
-        "lodash.isequal": "^4.5.0",
         "lodash.throttle": "^4.1.1",
         "lodash.toarray": "^4.4.0",
-        "nanoid": "^3.2.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "nanoid": "^5.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
       }
     },
-    "node_modules/@wangeditor/editor": {
-      "version": "5.1.23",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
-      "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/list-module": {
+      "version": "1.1.52",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/list-module/-/list-module-1.1.52.tgz",
+      "integrity": "sha512-FMzvx+iXXkatFFRZZ+rbiPjZpEcPa3UtNBFs40VpZG0w7O3gQWM7B/oPec3SKvAmre/US4CC5DJEqeEY3QX4hw==",
       "license": "MIT",
-      "dependencies": {
-        "@uppy/core": "^2.1.1",
-        "@uppy/xhr-upload": "^2.0.3",
-        "@wangeditor/basic-modules": "^1.1.7",
-        "@wangeditor/code-highlight": "^1.0.3",
-        "@wangeditor/core": "^1.1.19",
-        "@wangeditor/list-module": "^1.0.5",
-        "@wangeditor/table-module": "^1.1.4",
-        "@wangeditor/upload-image-module": "^1.0.2",
-        "@wangeditor/video-module": "^1.1.4",
-        "dom7": "^3.0.0",
-        "is-hotkey": "^0.2.0",
-        "lodash.camelcase": "^4.3.0",
-        "lodash.clonedeep": "^4.5.0",
+      "peerDependencies": {
+        "@wangeditor-next/core": "1.7.45",
+        "dom7": "^3.0.0 || ^4.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/table-module": {
+      "version": "1.6.60",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/table-module/-/table-module-1.6.60.tgz",
+      "integrity": "sha512-BGTG1YzPSIC4XJRafllCcynT9CkElWDSFxYBJ2svS36AvJc3ivQuj5Fhv+rCS4RqGggsN1hdeA4iP+xrtwWI4w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@wangeditor-next/core": "1.7.45",
+        "dom7": "^3.0.0 || ^4.0.0",
         "lodash.debounce": "^4.0.8",
-        "lodash.foreach": "^4.5.0",
-        "lodash.isequal": "^4.5.0",
         "lodash.throttle": "^4.1.1",
-        "lodash.toarray": "^4.4.0",
-        "nanoid": "^3.2.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "nanoid": "^5.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
       }
     },
-    "node_modules/@wangeditor/editor-for-vue": {
-      "version": "5.1.12",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
-      "integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/upload-image-module": {
+      "version": "1.1.50",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/upload-image-module/-/upload-image-module-1.1.50.tgz",
+      "integrity": "sha512-KIzI1IIQA6J5Hg3/UJF/AlEsrxJ62LZZUt61tenkO8cxks2UQMvH4CEsgEU5NNfQ0PUnOeR4ErjOgyhtbZKyaQ==",
       "license": "MIT",
       "peerDependencies": {
-        "@wangeditor/editor": ">=5.1.0",
-        "vue": "^3.0.5"
+        "@uppy/core": "^2.0.3",
+        "@uppy/xhr-upload": "^2.0.3",
+        "@wangeditor-next/basic-modules": "1.5.47",
+        "@wangeditor-next/core": "1.7.45",
+        "dom7": "^3.0.0 || ^4.0.0",
+        "lodash.foreach": "^4.5.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
       }
     },
-    "node_modules/@wangeditor/list-module": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
-      "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
+    "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/video-module": {
+      "version": "1.3.51",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/video-module/-/video-module-1.3.51.tgz",
+      "integrity": "sha512-67ecZCGIY+MUsqFtmwR9QKWlzGeIXVyXHmzPuevYwEqRwg50oR2xCSuoQLhfs5CKjXDZKsZhOnD/CGgt82TU+A==",
       "license": "MIT",
       "peerDependencies": {
-        "@wangeditor/core": "1.x",
-        "dom7": "^3.0.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "@uppy/core": "^2.1.4",
+        "@uppy/xhr-upload": "^2.0.7",
+        "@wangeditor-next/core": "1.7.45",
+        "dom7": "^3.0.0 || ^4.0.0",
+        "nanoid": "^5.0.0",
+        "slate": "^0.82.0",
+        "snabbdom": "^3.6.0"
       }
     },
-    "node_modules/@wangeditor/table-module": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
-      "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
+    "node_modules/@wangeditor-next/editor/node_modules/compute-scroll-into-view": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+      "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+      "license": "MIT"
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/dom7": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz",
+      "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==",
       "license": "MIT",
-      "peerDependencies": {
-        "@wangeditor/core": "1.x",
-        "dom7": "^3.0.0",
-        "lodash.isequal": "^4.5.0",
-        "lodash.throttle": "^4.1.1",
-        "nanoid": "^3.2.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+      "dependencies": {
+        "ssr-window": "^4.0.0"
       }
     },
-    "node_modules/@wangeditor/upload-image-module": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
-      "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
+    "node_modules/@wangeditor-next/editor/node_modules/html-void-elements": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
+      "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/i18next": {
+      "version": "23.16.8",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
+      "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://locize.com"
+        },
+        {
+          "type": "individual",
+          "url": "https://locize.com/i18next.html"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.2"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/nanoid": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
+      "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.js"
+      },
+      "engines": {
+        "node": "^18 || >=20"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/scroll-into-view-if-needed": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^3.0.2"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/slate": {
+      "version": "0.82.1",
+      "resolved": "https://registry.npmjs.org/slate/-/slate-0.82.1.tgz",
+      "integrity": "sha512-3mdRdq7U3jSEoyFrGvbeb28hgrvrr4NdFCtJX+IjaNvSFozY0VZd/CGHF0zf/JDx7aEov864xd5uj0HQxxEWTQ==",
       "license": "MIT",
+      "dependencies": {
+        "immer": "^9.0.6",
+        "is-plain-object": "^5.0.0",
+        "tiny-warning": "^1.0.3"
+      }
+    },
+    "node_modules/@wangeditor-next/editor/node_modules/slate-history": {
+      "version": "0.109.0",
+      "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.109.0.tgz",
+      "integrity": "sha512-DHavPwrTTAEAV66eAocB3iQHEj65N6IVtbRK98ZuqGT0S44T3zXlhzY+5SZ7EPxRcoOYVt1dioRxXYM/+PmCiQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "^5.0.0"
+      },
       "peerDependencies": {
-        "@uppy/core": "^2.0.3",
-        "@uppy/xhr-upload": "^2.0.3",
-        "@wangeditor/basic-modules": "1.x",
-        "@wangeditor/core": "1.x",
-        "dom7": "^3.0.0",
-        "lodash.foreach": "^4.5.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "slate": ">=0.65.3"
       }
     },
-    "node_modules/@wangeditor/video-module": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
-      "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
+    "node_modules/@wangeditor-next/editor/node_modules/ssr-window": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
+      "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==",
+      "license": "MIT"
+    },
+    "node_modules/@wangeditor-next/plugin-mention": {
+      "version": "1.0.19",
+      "resolved": "https://registry.npmjs.org/@wangeditor-next/plugin-mention/-/plugin-mention-1.0.19.tgz",
+      "integrity": "sha512-aH81xDT4hZ+PdEFPsptJ/Gn4KDyIOhQdrNewLi2BKadmVBiYXlLneseodeFyv9MLhtNg2ekt+KNGJNK3kKzCsw==",
       "license": "MIT",
       "peerDependencies": {
-        "@uppy/core": "^2.1.4",
-        "@uppy/xhr-upload": "^2.0.7",
-        "@wangeditor/core": "1.x",
-        "dom7": "^3.0.0",
-        "nanoid": "^3.2.0",
-        "slate": "^0.72.0",
-        "snabbdom": "^3.1.0"
+        "@wangeditor-next/editor": "5.6.49",
+        "snabbdom": "^3.6.0"
       }
     },
     "node_modules/@xmldom/xmldom": {
@@ -7235,6 +7396,12 @@
         "url": "https://github.com/sponsors/epoberezkin"
       }
     },
+    "node_modules/amfe-flexible": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/amfe-flexible/-/amfe-flexible-2.2.1.tgz",
+      "integrity": "sha512-L2VfvDzoETBjhRptg5u/IUuzHSuxm22JpSRb404p/TBGeRfwWmmNEbB+TFPIP/sS/+pbM18bCFH9QnMojLuPNw==",
+      "license": "MIT"
+    },
     "node_modules/animate.css": {
       "version": "4.1.1",
       "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
@@ -8231,12 +8398,6 @@
       "integrity": "sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw==",
       "license": "MIT"
     },
-    "node_modules/compute-scroll-into-view": {
-      "version": "1.0.20",
-      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
-      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
-      "license": "MIT"
-    },
     "node_modules/computeds": {
       "version": "0.0.1",
       "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
@@ -9234,15 +9395,6 @@
       "resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
       "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
     },
-    "node_modules/dom7": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
-      "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
-      "license": "MIT",
-      "dependencies": {
-        "ssr-window": "^3.0.0-alpha.1"
-      }
-    },
     "node_modules/domelementtype": {
       "version": "2.3.0",
       "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
@@ -9424,9 +9576,9 @@
       "license": "ISC"
     },
     "node_modules/element-plus": {
-      "version": "2.9.1",
-      "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.1.tgz",
-      "integrity": "sha512-9Agqf/jt4Ugk7EZ6C5LME71sgkvauPCsnvJN12Xid2XVobjufxMGpRE4L7pS4luJMOmFAH3J0NgYEGZT5r+NDg==",
+      "version": "2.11.1",
+      "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.1.tgz",
+      "integrity": "sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==",
       "license": "MIT",
       "dependencies": {
         "@ctrl/tinycolor": "^3.4.1",
@@ -11232,16 +11384,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/html-void-elements": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
-      "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
-      "license": "MIT",
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/wooorm"
-      }
-    },
     "node_modules/htmlparser2": {
       "version": "8.0.2",
       "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -11271,15 +11413,6 @@
         "node": ">=16.17.0"
       }
     },
-    "node_modules/i18next": {
-      "version": "20.6.1",
-      "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
-      "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/runtime": "^7.12.0"
-      }
-    },
     "node_modules/iconv-lite": {
       "version": "0.6.3",
       "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -11866,20 +11999,19 @@
       }
     },
     "node_modules/jsoneditor": {
-      "version": "9.10.5",
-      "resolved": "https://registry.npmmirror.com/jsoneditor/-/jsoneditor-9.10.5.tgz",
-      "integrity": "sha512-fVZ0NMt+zm4rqTKBv2x7zPdLeaRyKo1EjJkaR1QjK4gEM1rMwICILYSW1OPxSc1qqyAoDaA/eeNrluKoxOocCA==",
+      "version": "10.4.2",
+      "resolved": "https://registry.npmjs.org/jsoneditor/-/jsoneditor-10.4.2.tgz",
+      "integrity": "sha512-SQPCXlanU4PqdVsYuj2X7yfbLiiJYjklbksGfMKPsuwLhAIPxDlG43jYfXieGXvxpuq1fkw08YoRbkKXKabcLA==",
       "license": "Apache-2.0",
       "dependencies": {
-        "ace-builds": "^1.31.1",
+        "ace-builds": "^1.36.2",
         "ajv": "^6.12.6",
         "javascript-natural-sort": "^0.7.1",
         "jmespath": "^0.16.0",
         "json-source-map": "^0.6.1",
-        "jsonrepair": "3.1.0",
-        "mobius1-selectr": "^2.4.13",
+        "jsonrepair": "^3.8.1",
         "picomodal": "^3.0.0",
-        "vanilla-picker": "^2.12.2"
+        "vanilla-picker": "^2.12.3"
       }
     },
     "node_modules/jsoneditor/node_modules/ajv": {
@@ -11928,9 +12060,9 @@
       "license": "MIT"
     },
     "node_modules/jsonrepair": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmmirror.com/jsonrepair/-/jsonrepair-3.1.0.tgz",
-      "integrity": "sha512-idqReg23J0PVRAADmZMc5xQM3xeOX5bTB6OTyMnzq33IXJXmn9iJuWIEvGmrN80rQf4d7uLTMEDwpzujNcI0Rg==",
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.13.1.tgz",
+      "integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==",
       "license": "ISC",
       "bin": {
         "jsonrepair": "bin/cli.js"
@@ -12342,13 +12474,6 @@
       "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
       "license": "MIT"
     },
-    "node_modules/lodash.isequal": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
-      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
-      "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
-      "license": "MIT"
-    },
     "node_modules/lodash.isplainobject": {
       "version": "4.0.6",
       "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -13025,12 +13150,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/mobius1-selectr": {
-      "version": "2.4.13",
-      "resolved": "https://registry.npmmirror.com/mobius1-selectr/-/mobius1-selectr-2.4.13.tgz",
-      "integrity": "sha512-Mk9qDrvU44UUL0EBhbAA1phfQZ7aMZPjwtL7wkpiBzGh8dETGqfsh50mWoX9EkjDlkONlErWXArHCKfoxVg0Bw==",
-      "license": "MIT"
-    },
     "node_modules/moddle": {
       "version": "6.2.3",
       "resolved": "https://registry.npmmirror.com/moddle/-/moddle-6.2.3.tgz",
@@ -13772,6 +13891,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/postcss-pxtorem": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-pxtorem/-/postcss-pxtorem-6.1.0.tgz",
+      "integrity": "sha512-ROODSNci9ADal3zUcPHOF/K83TiCgNSPXQFSbwyPHNV8ioHIE4SaC+FPOufd8jsr5jV2uIz29v1Uqy1c4ov42g==",
+      "license": "MIT",
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
     "node_modules/postcss-resolve-nested-selector": {
       "version": "0.1.6",
       "resolved": "https://registry.npmmirror.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz",
@@ -14824,15 +14952,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/scroll-into-view-if-needed": {
-      "version": "2.2.31",
-      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
-      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
-      "license": "MIT",
-      "dependencies": {
-        "compute-scroll-into-view": "^1.0.20"
-      }
-    },
     "node_modules/scule": {
       "version": "1.3.0",
       "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
@@ -14994,29 +15113,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/slate": {
-      "version": "0.72.8",
-      "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
-      "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
-      "license": "MIT",
-      "dependencies": {
-        "immer": "^9.0.6",
-        "is-plain-object": "^5.0.0",
-        "tiny-warning": "^1.0.3"
-      }
-    },
-    "node_modules/slate-history": {
-      "version": "0.66.0",
-      "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
-      "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
-      "license": "MIT",
-      "dependencies": {
-        "is-plain-object": "^5.0.0"
-      },
-      "peerDependencies": {
-        "slate": ">=0.65.3"
-      }
-    },
     "node_modules/slice-ansi": {
       "version": "5.0.0",
       "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-5.0.0.tgz",
@@ -15108,12 +15204,6 @@
       "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
       "license": "BSD-3-Clause"
     },
-    "node_modules/ssr-window": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
-      "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
-      "license": "MIT"
-    },
     "node_modules/steady-xml": {
       "version": "0.1.0",
       "resolved": "https://registry.npmmirror.com/steady-xml/-/steady-xml-0.1.0.tgz",
@@ -16489,19 +16579,6 @@
         "uuid": "dist/bin/uuid"
       }
     },
-    "node_modules/v3-jsoneditor": {
-      "version": "0.0.6",
-      "resolved": "https://registry.npmmirror.com/v3-jsoneditor/-/v3-jsoneditor-0.0.6.tgz",
-      "integrity": "sha512-9G0sXWXUn67SBkn46ycWfwPwjuJu/lcsQaNzMtXAR2/95hMV21WfcRNsqJ+vVVrSHQehohB/9fVLwYEXz0u/KA==",
-      "license": "MIT",
-      "dependencies": {
-        "jsoneditor": "^9.10.0"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/pratik227"
-      }
-    },
     "node_modules/vanilla-picker": {
       "version": "2.12.3",
       "resolved": "https://registry.npmmirror.com/vanilla-picker/-/vanilla-picker-2.12.3.tgz",
@@ -17064,6 +17141,23 @@
       "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
       "license": "MIT"
     },
+    "node_modules/vue3-print-nb": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/vue3-print-nb/-/vue3-print-nb-0.1.4.tgz",
+      "integrity": "sha512-LExI7viEzplR6ZKQ2b+V4U0cwGYbVD4fut/XHvk3UPGlT5CcvIGs6VlwGp107aKgk6P8Pgx4rco3Rehv2lti3A==",
+      "dependencies": {
+        "vue": "^3.0.5"
+      }
+    },
+    "node_modules/vue3-seamless-scroll": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/vue3-seamless-scroll/-/vue3-seamless-scroll-3.0.2.tgz",
+      "integrity": "sha512-LpKoL1ht71MASabUBsoSqbhLqcuKSrD+u01dgHac+/cthPAShKvcdM7dSGtaxjVntSFqdeViqJssjcwy/KqRSA==",
+      "license": "MIT",
+      "dependencies": {
+        "element-plus": "^2.9.3"
+      }
+    },
     "node_modules/vue3-signature": {
       "version": "0.2.4",
       "resolved": "https://registry.npmmirror.com/vue3-signature/-/vue3-signature-0.2.4.tgz",

+ 1 - 0
package.json

@@ -79,6 +79,7 @@
     "vue-router": "4.4.5",
     "vue-types": "^5.1.1",
     "vue3-print-nb": "^0.1.4",
+    "vue3-seamless-scroll": "^3.0.2",
     "vue3-signature": "^0.2.4",
     "vuedraggable": "^4.1.0",
     "web-storage-cache": "^1.1.1",

+ 3 - 2
src/config/amapConfig.ts

@@ -6,9 +6,10 @@ export const AMAP_CONFIG = {
   // 高德地图 API Key
   key: '65bddc1c5df7d381507bc3ea3128b242',
   // 若账号开启了安全密钥校验,请配置 securityJsCode
+  // 需要在高德地图开发者平台获取安全密钥
   securityJsCode: '',
-  // 地图版本
-  version: '2.0',
+  // 地图版本 - 使用 1.4.15 版本以避免 v2.0 的兼容性问题
+  version: '1.4.15',
   // 地图插件
   plugins: ['AMap.Scale', 'AMap.ToolBar']
 }

+ 4 - 0
src/main.ts

@@ -52,6 +52,8 @@ import 'amfe-flexible/index.js'
 
 import fetchPlugin from './config/axios/fetch'
 
+import vue3SeamlessScroll from 'vue3-seamless-scroll'
+
 // 创建实例
 const setupAll = async () => {
   const app = createApp(App)
@@ -84,6 +86,8 @@ const setupAll = async () => {
   // 打印
   app.use(print)
 
+  app.use(vue3SeamlessScroll)
+
   app.mount('#app')
 }
 

+ 2 - 0
src/views/Home/COMPLETION_REPORT.md

@@ -382,3 +382,5 @@ npm run dev
 **感谢使用本重构项目!祝你开发愉快!** 🚀
 
 
+
+

+ 2 - 0
src/views/Home/FILES_CREATED.md

@@ -351,3 +351,5 @@
 **创建时间:** 2024年12月9日 **总耗时:** 约 2 小时 **状态:** ✅ 完成并可用
 
 
+
+

+ 2 - 0
src/views/Home/IMPLEMENTATION_CHECKLIST.md

@@ -255,3 +255,5 @@
 **检查日期**: 2024-01-15
 **检查人员**: [Your Name]
 **最终状态**: ✅ 通过
+
+

+ 2 - 0
src/views/Home/QUICK_REFERENCE.md

@@ -263,3 +263,5 @@ npm run dev
 **最后更新:** 2024年12月9日 **版本:** 1.0
 
 
+
+

+ 2 - 0
src/views/Home/README.md

@@ -344,3 +344,5 @@ A: 点击右上角关闭按钮或按 ESC 键。
 **最后更新**: 2024-01-15
 **版本**: 1.0.0
 **状态**: 生产就绪 ✅
+
+

+ 2 - 0
src/views/Home/REFACTORING_GUIDE.md

@@ -495,3 +495,5 @@ A: 检查以下几点:
 继续保持这种模块化的开发方式,会让项目更加健壮和易于维护。
 
 
+
+

+ 2 - 0
src/views/Home/REFACTORING_SUMMARY.md

@@ -397,3 +397,5 @@ A: 参考现有的对话框组件(如 `AddDeviceDialog.vue`),创建新的
 **重构完成时间:** 2024年12月9日 **重构者:** AI Assistant **状态:** ✅ 完成并可用
 
 
+
+

+ 2 - 0
src/views/Home/START_HERE.md

@@ -287,3 +287,5 @@ npm run dev
 **状态:** ✅ 生产就绪
 
 
+
+

+ 56 - 13
src/views/Home/components/AddElderDialog.vue

@@ -7,10 +7,42 @@
     center
     class="large-screen-dialog"
   >
-    <el-form ref="elderFormRef" :model="form" :rules="formRules" label-width="80px">
+    <el-form ref="elderFormRef" :model="form" :rules="formRules" label-width="90px">
       <el-form-item label="姓名" prop="name">
         <el-input v-model="form.name" placeholder="请输入长者姓名" size="large" maxlength="20" />
       </el-form-item>
+      <el-form-item label="年龄" prop="age">
+        <el-input v-model="form.age" placeholder="请输入长者年龄" size="large" />
+      </el-form-item>
+      <el-form-item label="性别" prop="gender">
+        <el-select v-model="form.gender" size="large" placeholder="请选择性别">
+          <el-option label="男" :value="1" />
+          <el-option label="女" :value="0" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="长者手机号" prop="elderPhone">
+        <el-input
+          v-model="form.elderPhone"
+          placeholder="请输入长者手机号"
+          size="large"
+          maxlength="11"
+          show-word-limit
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="家属手机号" prop="relativePhone">
+        <el-input
+          v-model="form.relativePhone"
+          placeholder="请输入家属手机号"
+          size="large"
+          maxlength="11"
+          show-word-limit
+          clearable
+        />
+      </el-form-item>
+
       <el-form-item label="地址" prop="address">
         <el-input
           v-model="form.address"
@@ -22,12 +54,6 @@
           show-word-limit
         />
       </el-form-item>
-      <el-form-item label="性别" prop="gender">
-        <el-select v-model="form.gender" size="large" placeholder="请选择性别">
-          <el-option label="男" :value="1" />
-          <el-option label="女" :value="0" />
-        </el-select>
-      </el-form-item>
     </el-form>
     <template #footer>
       <el-button @click="closeDialog" size="large">取消</el-button>
@@ -55,19 +81,33 @@ const elderFormRef = ref()
 const form = reactive({
   name: '',
   address: '',
-  gender: 1
+  gender: 1,
+  elderPhone: '',
+  relativePhone: '',
+  age: ''
 })
 
+const mobilePattern = /^1\d{10}$/
+
 const formRules = {
   name: [
     { required: true, message: '请输入长者姓名', trigger: 'blur' },
     { min: 2, max: 20, message: '姓名长度在 2 到 20 个字符', trigger: 'blur' }
   ],
+  age: [{ required: true, message: '请输入长者年龄', trigger: 'blur' }],
+  gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
+  elderPhone: [
+    { required: true, message: '请输入长者手机号', trigger: 'blur' },
+    { pattern: mobilePattern, message: '请输入有效的11位手机号', trigger: ['blur', 'change'] }
+  ],
+  relativePhone: [
+    { required: true, message: '请输入家属手机号', trigger: 'blur' },
+    { pattern: mobilePattern, message: '请输入有效的11位手机号', trigger: ['blur', 'change'] }
+  ],
   address: [
     { required: true, message: '请输入长者地址', trigger: 'blur' },
     { min: 5, max: 100, message: '地址长度在 5 到 100 个字符', trigger: 'blur' }
-  ],
-  gender: [{ required: true, message: '请选择性别', trigger: 'change' }]
+  ]
 }
 
 const visible = computed({
@@ -88,7 +128,10 @@ const submit = async () => {
     emit('submit', {
       name: form.name,
       address: form.address,
-      gender: form.gender
+      gender: form.gender,
+      elderPhone: form.elderPhone,
+      relativePhone: form.relativePhone,
+      age: Number(form.age || 0)
     })
 
     closeDialog()
@@ -101,6 +144,8 @@ const initForm = () => {
   form.name = ''
   form.address = ''
   form.gender = 1
+  form.elderPhone = ''
+  form.relativePhone = ''
 
   nextTick(() => {
     if (elderFormRef.value) {
@@ -113,5 +158,3 @@ defineExpose({
   initForm
 })
 </script>
-
-

+ 98 - 4
src/views/Home/components/AllDevicesView.vue

@@ -10,8 +10,8 @@
     <div class="device-grid">
       <div v-for="dev in devices" :key="dev.deviceCode" class="device-card">
         <div class="device-card-header">
-          <span class="device-type">{{ dev.deviceType || '未知类型' }}</span>
-          <span class="device-code">#{{ dev.deviceCode }}</span>
+          <span class="device-type">{{ getDeviceInfo(dev).name }}</span>
+          <el-tag :type="getStatusTagType(dev.status)" size="small">{{ dev.status }}</el-tag>
         </div>
         <div class="device-meta">
           <div>安装位置:{{ dev.installPosition || '未知' }}</div>
@@ -37,6 +37,7 @@ interface AllDeviceItem {
   deviceCode: string
   installPosition: string
   elderName: string
+  status: string
 }
 
 const props = defineProps<{
@@ -48,6 +49,100 @@ defineEmits<{
   back: []
   'show-device-detail': [deviceCode: string]
 }>()
+
+const deviceTypeMap: Record<
+  string,
+  {
+    name: string
+    icon: string
+    color: string
+  }
+> = {
+  ['health_band']: { name: '健康监测手环', icon: 'mdi:watch-variant', color: '#ff6b6b' },
+  ['smart_mattress']: { name: '智能床垫', icon: 'mdi:bed-queen', color: '#4ecdc4' },
+  ['security_camera']: { name: '安防摄像头', icon: 'mdi:cctv', color: '#45aaf2' },
+  ['blood_pressure_monitor']: {
+    name: '血压监测仪',
+    icon: 'mdi:heart-pulse',
+    color: '#a55eea'
+  },
+  ['emergency_button']: {
+    name: '紧急呼叫按钮',
+    icon: 'mdi:alarm-light',
+    color: '#fd9644'
+  },
+  ['smoke_sensor']: { name: '烟雾传感器', icon: 'mdi:smoke-detector', color: '#26de81' },
+  ['water_sensor']: { name: '水浸传感器', icon: 'mdi:water-alert', color: '#26de81' },
+  ['infrared_sensor']: {
+    name: '人体红外传感器',
+    icon: 'mdi:motion-sensor',
+    color: '#26de81'
+  },
+  ['door_sensor']: { name: '门磁传感器', icon: 'mdi:door-closed', color: '#26de81' },
+  ['gas_sensor']: { name: '燃气传感器', icon: 'mdi:gas-cylinder', color: '#26de81' },
+  ['temperature_sensor']: {
+    name: '温度传感器',
+    icon: 'mdi:thermometer',
+    color: '#ff6b6b'
+  },
+  ['humidity_sensor']: {
+    name: '湿度传感器',
+    icon: 'mdi:water-percent',
+    color: '#48dbfb'
+  },
+  ['fall_detection_sensor']: {
+    name: '跌倒检测传感器',
+    icon: 'mdi:human-falling',
+    color: '#ff9ff3'
+  },
+  ['pill_box']: {
+    name: '智能药盒',
+    icon: 'mdi:pill',
+    color: '#1dd1a1'
+  },
+  ['oxygen_saturation_monitor']: {
+    name: '血氧监测仪',
+    icon: 'mdi:heart-pulse',
+    color: '#a55eea'
+  },
+  ['glucose_meter']: {
+    name: '血糖仪',
+    icon: 'mdi:needle',
+    color: '#fed330'
+  },
+  ['sleep_radar']: {
+    name: '睡眠雷达',
+    icon: 'mdi:radar',
+    color: '#26de81'
+  },
+  ['alarm_controller']: {
+    name: '报警主机',
+    icon: 'mdi:shield-home',
+    color: '#fd9644'
+  }
+}
+
+const getDeviceInfo = (device: AllDeviceItem) => {
+  return (
+    deviceTypeMap[device.deviceType] || {
+      name: '未知设备',
+      icon: 'mdi:help-circle-outline',
+      color: '#a5b1c2'
+    }
+  )
+}
+
+const statusTagTypeMap: Record<string, string> = {
+  在线: 'success',
+  离线: 'danger',
+  故障: 'warning',
+  维护: 'info',
+  未激活: 'info'
+}
+
+const getStatusTagType = (status: string): string => {
+  return statusTagTypeMap[status] || 'info'
+}
 </script>
 
 <style lang="scss" scoped>
@@ -96,6 +191,7 @@ $text-gray: #8a8f98;
       .device-type {
         font-weight: 600;
         color: #fff;
+        font-size: 20px;
       }
       .device-code {
         color: $text-gray;
@@ -118,5 +214,3 @@ $text-gray: #8a8f98;
   }
 }
 </style>
-
-

+ 17 - 4
src/views/Home/components/DetailSection.vue

@@ -29,7 +29,7 @@
 
     <!-- 地图模式:替换下方全部内容 -->
     <div v-if="showLocationMap" class="map-full-wrapper">
-      <ElderLocationMap :elder-id="selectedElderly.id" />
+      <ElderLocationMap ref="locationMapRef" :elder-id="selectedElderly.id" />
     </div>
 
     <!-- 详情模式:健康指标 + 设备监控 -->
@@ -50,7 +50,7 @@
               <Icon :icon="getHealthIcon(metric)" />
             </div>
             <div class="metric-info">
-              <div class="metric-value">{{ metric.value }}{{ metric.unit }}</div>
+              <div class="metric-value">{{ metric.value || 0 }}{{ metric.unit }}</div>
               <div class="metric-name">{{ metric.name }}</div>
             </div>
             <div class="metric-trend" :class="metric.status.includes('警') ? 'warning' : 'normal'">
@@ -106,6 +106,7 @@ const DeviceCard = defineAsyncComponent(() => import('./DeviceCard.vue'))
 const ElderLocationMap = defineAsyncComponent(() => import('./ElderLocationMap.vue'))
 
 const showLocationMap = ref(false)
+const locationMapRef = ref<any>(null)
 
 const toggleLocationMap = () => {
   showLocationMap.value = !showLocationMap.value
@@ -115,6 +116,18 @@ const reBackToDetail = () => {
   showLocationMap.value = false
 }
 
+// 供父组件调用:是否处于地图模式
+const isMapShown = () => showLocationMap.value
+// 供父组件调用:向地图追加实时点
+const addMapPoint = (pt: {
+  longitude: number | string
+  latitude: number | string
+  locationTime?: string
+}) => {
+  if (!showLocationMap.value) return
+  locationMapRef.value?.addRealtimePoint?.(pt)
+}
+
 interface HealthVO {
   name: string
   value: string
@@ -257,7 +270,7 @@ const viewWarningService = () => {
   emit('viewWarningService', props.selectedElderly)
 }
 
-defineExpose({ reBackToDetail })
+defineExpose({ reBackToDetail, isMapShown, addMapPoint })
 </script>
 
 <style lang="scss" scoped>
@@ -309,7 +322,7 @@ $warning-color: #fd9644;
 
 .map-full-wrapper {
   width: 100%;
-  height: 600px;
+  height: 720px;
   border-radius: 12px;
   overflow: hidden;
 }

+ 10 - 0
src/views/Home/components/DeviceCard.vue

@@ -121,6 +121,16 @@ const deviceTypeMap: Record<
     name: '血糖仪',
     icon: 'mdi:needle',
     color: '#fed330'
+  },
+  ['sleep_radar']: {
+    name: '睡眠雷达',
+    icon: 'mdi:radar',
+    color: '#26de81'
+  },
+  ['alarm_controller']: {
+    name: '报警主机',
+    icon: 'mdi:shield-home',
+    color: '#fd9644'
   }
 }
 

+ 10 - 2
src/views/Home/components/DeviceDetailDialog.vue

@@ -167,6 +167,16 @@ const deviceTypeMap: Record<
     name: '血糖仪',
     icon: 'mdi:needle',
     color: '#fed330'
+  },
+  ['sleep_radar']: {
+    name: '睡眠雷达',
+    icon: 'mdi:radar',
+    color: '#26de81'
+  },
+  ['alarm_controller']: {
+    name: '报警主机',
+    icon: 'mdi:shield-home',
+    color: '#fd9644'
   }
 }
 
@@ -292,5 +302,3 @@ $primary-color: #1a73e8;
   }
 }
 </style>
-
-

+ 447 - 99
src/views/Home/components/ElderLocationMap.vue

@@ -1,6 +1,31 @@
 <template>
   <div class="elder-location-map">
-    <div class="map-container" ref="mapContainer"></div>
+    <div class="map-and-list">
+      <div class="map-container" ref="mapContainer"></div>
+      <div class="side-list">
+        <vue3-seamless-scroll
+          class="list-body"
+          :list="list"
+          :step="2"
+          :visibleCount="10"
+          :hover="true"
+          direction="up"
+          :singleHeight="0"
+          :singleWaitTime="0"
+          v-if="list.length"
+        >
+          <div class="list-item" v-for="(p, idx) in list" :key="idx" @click="focusPoint(p)">
+            <div class="list-item-index">#{{ idx + 1 }}</div>
+            <div class="list-item-main">
+              <div class="time">{{ p.locationTime || '-' }}</div>
+              <div class="coord">
+                {{ p.address }}
+              </div>
+            </div>
+          </div>
+        </vue3-seamless-scroll>
+      </div>
+    </div>
     <div v-if="loading" class="loading-overlay">
       <el-icon class="is-loading">
         <Loading />
@@ -15,7 +40,7 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from 'vue'
 import { AMAP_CONFIG } from '@/config/amapConfig'
 import { Loading } from '@element-plus/icons-vue'
 import fetchHttp from '@/config/axios/fetchHttp'
@@ -32,10 +57,53 @@ const props = defineProps<Props>()
 
 const mapContainer = ref<HTMLElement>()
 let amap: any = null
-let marker: any = null
+let markers: any[] = []
+let polyline: any = null
+let historyPoints: HistoryPoint[] = []
+let list = ref<HistoryPoint[]>([])
 const loading = ref(false)
 const error = ref('')
 
+// 默认中心点:广州市中心(越秀区)
+const DEFAULT_CENTER = { lng: 113.264385, lat: 23.129112 }
+
+// 右侧轨迹列表
+const listBody = ref<HTMLElement | null>(null)
+const displayPoints = ref<HistoryPoint[]>([])
+// 地址缓存,避免重复逆地理
+const addressCache: Record<string, string> = {}
+
+const syncDisplayPoints = () => {
+  // 展示前100条(与接口原始顺序一致)
+  displayPoints.value = historyPoints.slice(0, 100).map((p) => {
+    return {
+      ...p,
+      address: addressCache[`${Number(p.longitude).toFixed(6)},${Number(p.latitude).toFixed(6)}`]
+    }
+  })
+  list.value = displayPoints.value.splice(0, 15)
+}
+
+const scrollListToBottom = () => {
+  nextTick(() => {
+    if (!listBody.value) return
+    try {
+      listBody.value.scrollTop = listBody.value.scrollHeight
+    } catch {}
+  })
+}
+
+const focusPoint = (p: HistoryPoint) => {
+  const lng = Number(p.longitude)
+  const lat = Number(p.latitude)
+  if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
+  ensureMap(lng, lat)
+  try {
+    amap?.setZoom?.(16)
+    amap?.setCenter?.([lng, lat])
+  } catch {}
+}
+
 // 加载高德地图脚本
 const loadAmapScript = (): Promise<void> => {
   return new Promise((resolve, reject) => {
@@ -52,37 +120,66 @@ const loadAmapScript = (): Promise<void> => {
     }
 
     const script = document.createElement('script')
-    script.src = `https://webapi.amap.com/maps?v=${AMAP_CONFIG.version}&key=${AMAP_CONFIG.key}`
+    // 添加 plugin 参数以加载必要的插件
+    const plugins = 'AMap.Scale,AMap.ToolBar'
+    script.src = `https://webapi.amap.com/maps?v=${AMAP_CONFIG.version}&key=${AMAP_CONFIG.key}&plugin=${plugins}`
     script.async = true
+    script.type = 'text/javascript'
+
+    let loadTimeout: any
     script.onload = () => {
-      const AMap = (window as any).AMap
-      if (AMap && typeof AMap.Map === 'function') {
-        resolve()
-      } else {
-        reject(new Error('AMap 未注入,请检查 Key 或网络'))
-      }
+      clearTimeout(loadTimeout)
+      // 给 AMap 一些时间来初始化
+      setTimeout(() => {
+        const AMap = (window as any).AMap
+        if (AMap && typeof AMap.Map === 'function') {
+          resolve()
+        } else {
+          reject(new Error('AMap 未正确初始化,请检查 Key 或网络'))
+        }
+      }, 100)
     }
     script.onerror = () => {
-      reject(new Error('高德地图脚本加载失败'))
+      clearTimeout(loadTimeout)
+      reject(new Error('高德地图脚本加载失败,请检查网络连接'))
     }
+
+    // 设置加载超时
+    loadTimeout = setTimeout(() => {
+      reject(new Error('高德地图脚本加载超时'))
+    }, 10000)
+
     document.head.appendChild(script)
   })
 }
 
-// 初始化地图
-const initMap = (longitude: number, latitude: number) => {
+// 初始化地图(若未初始化则以给定中心创建)
+const ensureMap = (longitude: number, latitude: number) => {
   if (!mapContainer.value) return
-
+  if (amap) {
+    try {
+      if (typeof amap.setZoomAndCenter === 'function') {
+        amap.setZoomAndCenter(15, [longitude, latitude])
+      } else {
+        if (typeof amap.setZoom === 'function') amap.setZoom(15)
+        amap.setCenter && amap.setCenter([longitude, latitude])
+      }
+    } catch {}
+    return
+  }
   try {
     const AMap = (window as any).AMap
+    if (!AMap || !AMap.Map) {
+      throw new Error('AMap 库未正确加载')
+    }
 
-    // 创建地图实例(先用默认样式,避免样式 id 导致报错)
     amap = new AMap.Map(mapContainer.value, {
       zoom: 15,
-      center: [longitude, latitude]
+      center: [longitude, latitude],
+      resizeEnable: true
     })
 
-    // 尝试加载控件(失败忽略,不影响核心功能)
+    // 添加控件
     if (AMap && typeof AMap.plugin === 'function') {
       AMap.plugin(['AMap.ToolBar', 'AMap.Scale'], () => {
         try {
@@ -99,15 +196,6 @@ const initMap = (longitude: number, latitude: number) => {
         }
       })
     }
-
-    // 添加标记
-    addMarker(longitude, latitude)
-
-    // 监听地图缩放变化
-    amap.on &&
-      amap.on('zoomchange', () => {
-        console.log('地图缩放级别:', amap.getZoom && amap.getZoom())
-      })
   } catch (err) {
     console.error('地图初始化失败:', err)
     const msg = err instanceof Error ? err.message : String(err)
@@ -115,101 +203,229 @@ const initMap = (longitude: number, latitude: number) => {
   }
 }
 
-// 添加标记
-const addMarker = async (longitude: number, latitude: number) => {
-  if (!amap) return
+interface HistoryPoint {
+  longitude: number | string
+  latitude: number | string
+  locationTime?: string
+  address?: string
+}
 
+const clearOverlays = () => {
+  if (!amap) return
   try {
-    const AMap = (window as any).AMap
+    if (markers.length) {
+      if (typeof amap.remove === 'function') amap.remove(markers)
+      else if (typeof amap.removeOverlays === 'function') amap.removeOverlays(markers)
+    }
+    if (polyline) {
+      if (typeof amap.remove === 'function') amap.remove(polyline)
+      else if (typeof amap.removeOverlays === 'function') amap.removeOverlays([polyline])
+      polyline = null
+    }
+    markers = []
+  } catch (e) {
+    console.warn('清理覆盖物失败(忽略):', e)
+  }
+}
 
-    // 移除旧标记
-    if (marker) {
-      if (amap && typeof amap.remove === 'function') amap.remove(marker)
-      marker = null
+const updatePolyline = () => {
+  if (!amap) return
+  const AMap = (window as any).AMap
+  if (!AMap || !AMap.Polyline) return
+  const path = historyPoints
+    .map((p) => [Number(p.longitude), Number(p.latitude)])
+    .filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))
+  try {
+    if (!polyline) {
+      polyline = new AMap.Polyline({
+        path,
+        strokeColor: '#409EFF',
+        strokeOpacity: 0.9,
+        strokeWeight: 4,
+        lineJoin: 'round',
+        lineCap: 'round'
+      })
+      if (typeof amap.add === 'function') amap.add(polyline)
+      else if (typeof amap.addOverlays === 'function') amap.addOverlays([polyline])
+    } else {
+      polyline.setPath(path)
     }
+  } catch (e) {
+    console.warn('轨迹绘制失败(忽略):', e)
+  }
+}
 
-    // 创建新标记(使用默认图标,避免 Icon 构造器异常)
-    marker = new AMap.Marker({
-      position: [longitude, latitude],
-      title: '长者位置'
-    })
+const renderHistoryPoints = async (list: HistoryPoint[]) => {
+  if (!list || list.length === 0) {
+    // 无数据也要显示默认地图
+    error.value = ''
+    historyPoints = []
+    syncDisplayPoints()
+    ensureMap(DEFAULT_CENTER.lng, DEFAULT_CENTER.lat)
+    clearOverlays()
+    return
+  }
+  // 直接截取原始数据前100条,并过滤非法坐标(不做去重)
+  const limitedList = list.slice(0, 100).filter((item) => {
+    const lng = Number(item.longitude)
+    const lat = Number(item.latitude)
+    return Number.isFinite(lng) && Number.isFinite(lat)
+  })
 
-    if (amap && typeof amap.add === 'function') {
-      amap.add(marker)
-    } else if (amap && typeof amap.addOverlays === 'function') {
-      amap.addOverlays([marker])
+  // 逆地理:仅对这100条中不同经纬度去解析,缓存结果
+  const seenKeys = new Set<string>()
+  const toFetch: { key: string; lng: number; lat: number }[] = []
+  for (const it of limitedList) {
+    const lng = Number(it.longitude)
+    const lat = Number(it.latitude)
+    const key = `${lng.toFixed(6)},${lat.toFixed(6)}`
+    if (!seenKeys.has(key)) {
+      seenKeys.add(key)
+      if (!addressCache[key]) {
+        toFetch.push({ key, lng, lat })
+      }
     }
+  }
+  if (toFetch.length) {
+    await Promise.all(
+      toFetch.map(async (t) => {
+        try {
+          addressCache[t.key] = await amapReverseGeocode(t.lng, t.lat)
+        } catch (e) {
+          addressCache[t.key] = ''
+        }
+      })
+    )
+  }
+
+  // 构建带地址的点集合(保留顺序、不去重)
+  historyPoints = limitedList.map((it) => {
+    const lng = Number(it.longitude)
+    const lat = Number(it.latitude)
+    const key = `${lng.toFixed(6)},${lat.toFixed(6)}`
+    return { ...it, address: addressCache[key] || '' }
+  })
+
+  if (limitedList.length === 0) {
+    // 全部无效坐标,展示默认地图
+    error.value = ''
+    ensureMap(DEFAULT_CENTER.lng, DEFAULT_CENTER.lat)
+    clearOverlays()
+    return
+  }
+
+  // 确保地图可用(使用前100条中的第一个点作为中心)
+  const first = limitedList[0]
+  const firstLng = Number(first.longitude)
+  const firstLat = Number(first.latitude)
+  ensureMap(firstLng, firstLat)
+  clearOverlays()
+
+  const AMap = (window as any).AMap
+  if (!AMap || !AMap.Marker) {
+    error.value = '地图库加载不完整'
+    return
+  }
+
+  // 同步添加标记,异步加载地址信息
+  limitedList.forEach((item, idx) => {
+    const lng = Number(item.longitude)
+    const lat = Number(item.latitude)
+    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
 
-    // 添加信息窗口(失败不影响)
     try {
-      let addressInfo = await amapReverseGeocode(longitude, latitude)
-      const infoWindow = new AMap.InfoWindow({
-        content: `<div style="padding: 10px; font-size: 12px;color:#000;width:300px">
-          <div style="margin-bottom: 5px;"><strong>长者位置</strong></div>
-          <div>地点: ${addressInfo}</div>   
-          <div>经度: ${longitude.toFixed(6)}</div>
-          <div>纬度: ${latitude.toFixed(6)}</div>
-        </div>`,
-        offset: new AMap.Pixel(0, -30)
+      const marker = new AMap.Marker({
+        position: [lng, lat],
+        title: item.locationTime ? `时间:${item.locationTime}` : `点位${idx + 1}`,
+        zIndex: limitedList.length - idx
       })
-      marker.on &&
-        marker.on('click', () => {
-          infoWindow.open(amap, marker.getPosition())
-        })
-      infoWindow.open(amap, marker.getPosition())
-    } catch (e) {
-      console.warn('信息窗口创建失败(忽略):', e)
+
+      if (amap && typeof amap.add === 'function') {
+        amap.add(marker)
+      } else if (amap && typeof amap.addOverlays === 'function') {
+        amap.addOverlays([marker])
+      }
+
+      // 异步加载地址信息和设置信息窗
+      try {
+        const key = `${lng.toFixed(6)},${lat.toFixed(6)}`
+        const addressInfo = addressCache[key] || item.address || ''
+        const infoContent = `<div style="padding:8px 10px;font-size:12px;color:#000;">
+          <div><strong>历史定位</strong></div>
+          ${item.locationTime ? `<div>时间:${item.locationTime}</div>` : ''}
+          ${addressInfo ? `<div>地点:${addressInfo}</div>` : ''}
+          <div>经度:${lng.toFixed(6)},纬度:${lat.toFixed(6)}</div>
+        </div>`
+
+        if (AMap.InfoWindow) {
+          const info = new AMap.InfoWindow({
+            content: infoContent,
+            offset: new AMap.Pixel(0, -30)
+          })
+          marker.on &&
+            marker.on('click', () => {
+              if (amap) info.open(amap, marker.getPosition())
+            })
+        }
+      } catch (err) {
+        console.warn('信息窗创建失败:', err)
+      }
+
+      markers.push(marker)
+    } catch (err) {
+      console.error('标记创建失败:', err)
+    }
+  })
+
+  // 绘制轨迹并同步右侧列表
+  historyPoints = limitedList.slice()
+  updatePolyline()
+  syncDisplayPoints()
+  scrollListToBottom()
+
+  // 适配视野
+  try {
+    if (amap && typeof amap.setFitView === 'function') {
+      setTimeout(() => {
+        if (markers.length > 0) {
+          const fitOverlays = polyline ? [polyline, ...markers] : markers
+          amap.setFitView(fitOverlays)
+        }
+      }, 100)
     }
   } catch (err) {
-    console.error('添加标记失败:', err)
+    console.warn('视野适配失败:', err)
   }
 }
 
-// 获取长者位置
-const fetchElderLocation = async () => {
+// 获取历史定位
+const fetchElderHistoryLocations = async () => {
   loading.value = true
   error.value = ''
-
   try {
-    // 若直接传入了经纬度(测试/外部定位),优先使用
-    if (props.longitude != null && props.latitude != null) {
-      const lng = Number(props.longitude)
-      const lat = Number(props.latitude)
-      if (Number.isFinite(lng) && Number.isFinite(lat)) {
-        initMap(lng, lat)
-        return
-      } else {
-        throw new Error('经纬度格式错误')
-      }
-    }
-
     const res = await fetchHttp.get(
-      '/pc/admin/getElderLocation',
+      '/api/pc/admin/getElderHistoryLocations',
       { elderId: props.elderId },
       {
-        headers: {
-          Authorization: `Bearer ${getAccessToken()}`
-        }
+        headers: { Authorization: `Bearer ${getAccessToken()}` }
       }
     )
-
-    // 兼容返回结构:res 或 { code, data }
     const payload = res?.data ?? res
-
-    if (payload && payload.longitude != null && payload.latitude != null) {
-      const lng = Number(payload.longitude)
-      const lat = Number(payload.latitude)
-      if (Number.isFinite(lng) && Number.isFinite(lat)) {
-        initMap(lng, lat)
-      } else {
-        error.value = '经纬度格式错误'
-      }
+    const list = payload?.historyList ?? payload
+    if (Array.isArray(list)) {
+      await renderHistoryPoints(list as HistoryPoint[])
     } else {
-      error.value = '位置信息不完整'
+      // 返回格式不符合预期时也展示默认地图
+      error.value = ''
+      ensureMap(DEFAULT_CENTER.lng, DEFAULT_CENTER.lat)
+      clearOverlays()
     }
   } catch (err) {
-    console.error('获取位置信息异常:', err)
-    error.value = err instanceof Error ? err.message : '获取位置信息失败'
+    console.error('获取历史定位异常:', err)
+    // 请求失败时也渲染默认地图,不显示遮挡错误
+    error.value = ''
+    ensureMap(DEFAULT_CENTER.lng, DEFAULT_CENTER.lat)
+    clearOverlays()
   } finally {
     loading.value = false
   }
@@ -219,8 +435,7 @@ const fetchElderLocation = async () => {
 onMounted(async () => {
   try {
     await loadAmapScript()
-    // await fetchElderLocation()
-    initMap(113.424018, 23.160882)
+    await fetchElderHistoryLocations()
   } catch (err) {
     console.error('初始化失败:', err)
     error.value = err instanceof Error ? err.message : '初始化失败'
@@ -244,9 +459,96 @@ watch(
       amap.destroy()
       amap = null
     }
-    await fetchElderLocation()
+    await fetchElderHistoryLocations()
   }
 )
+
+// 实时追加点位(用于 WebSocket 推送)
+const addRealtimePoint = async (pt: {
+  longitude: number | string
+  latitude: number | string
+  locationTime?: string
+}) => {
+  try {
+    const lng = Number(pt.longitude)
+    const lat = Number(pt.latitude)
+    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
+
+    // 去重:与最后一点相同则忽略
+    const last = historyPoints[historyPoints.length - 1]
+    const lastKey = last
+      ? `${Number(last.longitude).toFixed(6)},${Number(last.latitude).toFixed(6)}`
+      : ''
+    const curKey = `${lng.toFixed(6)},${lat.toFixed(6)}`
+    if (lastKey && lastKey === curKey) return
+
+    // 确保地图存在
+    ensureMap(lng, lat)
+
+    // 逆地理(缓存优先)
+    if (!addressCache[curKey]) {
+      try {
+        addressCache[curKey] = await amapReverseGeocode(lng, lat)
+      } catch {
+        addressCache[curKey] = ''
+      }
+    }
+
+    // 保存到历史(带地址)
+    const rec: HistoryPoint = {
+      longitude: lng,
+      latitude: lat,
+      locationTime: pt.locationTime,
+      address: addressCache[curKey] || ''
+    }
+    historyPoints.push(rec)
+
+    const AMap = (window as any).AMap
+    if (!AMap || !AMap.Marker) return
+
+    // 添加标记
+    const marker = new AMap.Marker({
+      position: [lng, lat],
+      title: rec.locationTime ? `时间:${rec.locationTime}` : `实时点位`
+    })
+    if (amap && typeof amap.add === 'function') {
+      amap.add(marker)
+    } else if (amap && typeof amap.addOverlays === 'function') {
+      amap.addOverlays([marker])
+    }
+    markers.push(marker)
+
+    // 限制最多100条,同时删除最早的覆盖物
+    if (historyPoints.length > 100) {
+      historyPoints.shift()
+      const removed = markers.shift()
+      try {
+        if (removed) {
+          if (typeof amap.remove === 'function') amap.remove(removed)
+          else if (typeof amap.removeOverlays === 'function') amap.removeOverlays([removed])
+        }
+      } catch {}
+    }
+
+    // 更新轨迹与右侧列表
+    updatePolyline()
+    syncDisplayPoints()
+    scrollListToBottom()
+
+    // 适配视野或居中到最新点
+    try {
+      if (amap?.setFitView && polyline) {
+        amap.setFitView([polyline, ...markers])
+      } else if (amap?.setCenter) {
+        amap.setCenter([lng, lat])
+      }
+    } catch {}
+  } catch (e) {
+    console.warn('追加实时点失败(忽略):', e)
+  }
+}
+
+defineExpose({ addRealtimePoint })
 </script>
 
 <style lang="scss" scoped>
@@ -256,12 +558,58 @@ watch(
   height: 100%;
   min-height: 500px;
   border-radius: 12px;
-  overflow: hidden;
+}
+
+.map-and-list {
+  display: flex;
+  height: 100%;
 }
 
 .map-container {
-  width: 100%;
+  flex: 1;
+  height: 100%;
+}
+
+.side-list {
+  width: 320px;
   height: 100%;
+  background: rgb(255 255 255 / 6%);
+  border-left: 1px solid rgb(255 255 255 / 12%);
+  display: flex;
+  flex-direction: column;
+}
+
+.side-list .list-item {
+  display: flex;
+  gap: 8px;
+  padding: 10px 12px;
+  border-bottom: 1px solid rgb(255 255 255 / 6%);
+  cursor: pointer;
+}
+
+.side-list .list-item:hover {
+  background: rgb(255 255 255 / 6%);
+}
+
+.side-list .list-item-index {
+  width: 34px;
+  flex-shrink: 0;
+  color: #a5b1c2;
+}
+
+.side-list .list-item-main {
+  display: flex;
+  flex-direction: column;
+}
+
+.side-list .time {
+  color: #ffffff;
+  font-size: 13px;
+}
+
+.side-list .coord {
+  color: #8a8f98;
+  font-size: 12px;
 }
 
 .loading-overlay {

+ 13 - 64
src/views/Home/components/ElderProfileDialog.vue

@@ -34,11 +34,11 @@
           </div>
           <div class="info-row">
             <span class="label">年龄:</span>
-            <span class="value">{{ elderData.age }}岁</span>
+            <span class="value">{{ elderData.age || 0 }}岁</span>
           </div>
           <div class="info-row">
             <span class="label">性别:</span>
-            <span class="value">{{ elderData.gender }}</span>
+            <span class="value">{{ elderData.gender == 1 ? '男' : '女' }}</span>
           </div>
         </div>
       </div>
@@ -66,14 +66,14 @@
       <div class="warning-section">
         <h4 class="section-title">
           预警历史
-          <span v-if="elderData.warningData?.length" class="warning-count">
-            ({{ elderData.warningData.length }})
+          <span v-if="elderData.eventList?.length" class="warning-count">
+            ({{ elderData.eventList.length }})
           </span>
         </h4>
-        <div v-if="elderData.warningData && elderData.warningData.length > 0" class="warning-list">
-          <div v-for="(warning, index) in elderData.warningData" :key="index" class="warning-item">
+        <div v-if="elderData.eventList && elderData.eventList.length > 0" class="warning-list">
+          <div v-for="(warning, index) in elderData.eventList" :key="index" class="warning-item">
             <div class="warning-header">
-              <span class="event-type">{{ warning.eventType }}</span>
+              <span class="event-type">{{ eventText[warning.eventType] }}</span>
               <span class="time">{{ formatTime(warning.happensAt) }}</span>
             </div>
             <div class="warning-message">{{ warning.message }}</div>
@@ -116,7 +116,7 @@ interface ElderProfileData {
   relativePhone?: string
   elderPhone?: string
   address?: string
-  warningData?: WarningRecord[]
+  eventList?: WarningRecord[]
 }
 
 // Props 和 Emits
@@ -137,6 +137,11 @@ const emit = defineEmits<{
 const loading = ref(false)
 const elderData = ref<ElderProfileData | null>(null)
 
+const eventText = {
+  HEALTH_ALERT: '健康数据异常',
+  SOS: 'SOS报警'
+}
+
 const visible = computed({
   get: () => props.modelValue,
   set: (value: boolean) => {
@@ -170,56 +175,6 @@ const formatTime = (time: string | number) => {
   return formatToDateTime(time)
 }
 
-// 生成假数据
-const generateMockData = (elderId: number): ElderProfileData => {
-  const names = ['王奶奶', '李爷爷', '张奶奶', '刘爷爷', '陈奶奶', '杨爷爷']
-  const genders = ['女', '男']
-  const addresses = [
-    '北京市朝阳区建国路1号',
-    '上海市浦东新区世纪大道100号',
-    '广州市天河区珠江新城',
-    '深圳市南山区科技园路1号',
-    '杭州市西湖区文三路477号'
-  ]
-  const eventTypes = ['跌倒预警', '心率异常', '血压偏高', '离床预警', '体温异常']
-  const messages = [
-    '检测到长者跌倒,请立即查看',
-    '心率过高,建议就医检查',
-    '血压偏高,请注意休息',
-    '长者离床时间过长,请关注',
-    '体温异常,建议测量体温'
-  ]
-
-  const warningData: WarningRecord[] = [
-    {
-      eventType: eventTypes[Math.floor(Math.random() * eventTypes.length)],
-      message: messages[Math.floor(Math.random() * messages.length)],
-      happensAt: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString()
-    },
-    {
-      eventType: eventTypes[Math.floor(Math.random() * eventTypes.length)],
-      message: messages[Math.floor(Math.random() * messages.length)],
-      happensAt: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString()
-    },
-    {
-      eventType: eventTypes[Math.floor(Math.random() * eventTypes.length)],
-      message: messages[Math.floor(Math.random() * messages.length)],
-      happensAt: new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000).toISOString()
-    }
-  ]
-
-  return {
-    id: elderId,
-    name: names[elderId % names.length],
-    age: 65 + (elderId % 20),
-    gender: genders[elderId % 2],
-    elderPhone: `1${Math.floor(Math.random() * 9) + 3}${String(Math.floor(Math.random() * 1000000000)).padStart(9, '0')}`,
-    relativePhone: `1${Math.floor(Math.random() * 9) + 3}${String(Math.floor(Math.random() * 1000000000)).padStart(9, '0')}`,
-    address: addresses[elderId % addresses.length],
-    warningData: warningData
-  }
-}
-
 const fetchElderDetail = async (elderId: number) => {
   if (!elderId) return
 
@@ -237,15 +192,9 @@ const fetchElderDetail = async (elderId: number) => {
 
     if (res) {
       elderData.value = res
-    } else {
-      // 接口暂无数据,使用假数据
-      console.log('接口暂无数据,使用假数据')
-      elderData.value = generateMockData(elderId)
     }
   } catch (error) {
     console.error('获取长者档案失败:', error)
-    // 接口出错,使用假数据
-    elderData.value = generateMockData(elderId)
   } finally {
     loading.value = false
   }

+ 6 - 6
src/views/Home/components/HandleWarningDialog.vue

@@ -54,7 +54,7 @@
           </div>
         </el-form-item>
 
-        <el-form-item
+        <!-- <el-form-item
           v-if="form.handleType === 'report' || isReportOnly"
           label="上报类型"
           prop="message"
@@ -64,7 +64,7 @@
             <el-option label="SOS_报警" value="SOS_报警" />
             <el-option label="健康指标异常" value="健康指标异常" />
           </el-select>
-        </el-form-item>
+        </el-form-item> -->
         <el-form-item
           v-if="form.handleType === 'report' || isReportOnly"
           label="上报信息"
@@ -118,7 +118,7 @@ const handleWarningFormRef = ref()
 
 const form = reactive({
   handleType: 'phone' as 'phone' | 'report',
-  eventType: '',
+  // eventType: '',
   message: ''
 })
 
@@ -134,7 +134,7 @@ watch(
   () => props.visible,
   (val) => {
     if (val) {
-      form.eventType = ''
+      // form.eventType = ''
       form.message = ''
       form.handleType = isReportOnly.value ? 'report' : 'phone'
       nextTick(() => {
@@ -163,7 +163,7 @@ const submit = async () => {
     emit('submit', {
       elderId: props.currentElderly?.id,
       handleType: isReportOnly.value ? 'report' : form.handleType,
-      eventType: form.eventType,
+      // eventType: form.eventType,
       message: form.message
     })
 
@@ -175,7 +175,7 @@ const submit = async () => {
 
 const initForm = () => {
   form.handleType = 'phone'
-  form.eventType = ''
+  // form.eventType = ''
   form.message = ''
 
   nextTick(() => {

+ 2 - 0
src/views/Home/components/StatusBar.vue

@@ -83,3 +83,5 @@ $warning-color: #fd9644;
 </style>
 
 
+
+

+ 2 - 0
src/views/Home/components/TopInfoBar.vue

@@ -157,3 +157,5 @@ $text-gray: #8a8f98;
 </style>
 
 
+
+

+ 2 - 2
src/views/Home/components/WarningDrawer.vue

@@ -15,11 +15,11 @@
             <div class="history-header">
               <span class="history-time">{{ record.happensAt }}</span>
             </div>
-            <div class="history-actions">
+            <!-- <div class="history-actions">
               <el-button type="warning" size="small" @click="onHandleWarning(record)">
                 去处理
               </el-button>
-            </div>
+            </div> -->
             <div class="history-event">{{ record.content }}</div>
           </div>
         </div>

+ 8 - 0
src/views/Home/composables/useWebSocket.ts

@@ -5,6 +5,8 @@ interface WebSocketConfig {
   wsUrl: string
   onSOSAlert?: (data: any) => void
   onHealthAlert?: (data: any) => void
+  onLocationAlert?: (data: any) => void
+  onHealthUpdateAlert?: (data: any) => void
   onDeviceDataUpdate?: (data: any) => void
   onStatsUpdate?: (data: any) => void
 }
@@ -217,6 +219,12 @@ export const useWebSocket = (config: WebSocketConfig) => {
       case 'HEALTH_ALERT':
         config.onHealthAlert?.(data)
         break
+      case 'LOCATION_UPDATE':
+        config.onLocationAlert?.(data)
+        break
+      case 'HEALTH_UPDATE':
+        config.onHealthUpdateAlert?.(data)
+        break
       case 'DEVICE_DATA_UPDATE':
         config.onDeviceDataUpdate?.(data)
         break

+ 47 - 14
src/views/Home/home-refactored.vue

@@ -40,7 +40,7 @@
       <AllDevicesView
         v-else
         :devices="allDevices"
-        @refresh="refreshAllDevices"
+        @refresh="handelDeviceRefresh"
         @back="backToOverview"
         @show-device-detail="showDeviceDetailByCode"
       />
@@ -459,13 +459,12 @@ const openHandleWarningDialog = (elderly: Elderly) => {
 
 const submitHandleWarning = async (data: any) => {
   if (data.handleType === 'report') {
-    data = {
-      ...data,
-      organizationId: organizationId,
-      organizationName: getTenantName()
+    let params = {
+      elderId: data.elderId,
+      remarks: data.message
     }
     try {
-      const res = await fetchHttp.post('/api/pc/admin/dealWith', data, {
+      const res = await fetchHttp.post('/api/pc/admin/processAlert', params, {
         headers: {
           Authorization: `Bearer ${getAccessToken()}`
         }
@@ -666,11 +665,16 @@ const addFlashingEffect = (elder: Elderly) => {
   }, 10000)
 }
 
+const handelDeviceRefresh = async () => {
+  await refreshAllDevices()
+  ElMessage.success('设备刷新成功!')
+}
+
 // 全部设备视图相关方法
 const refreshAllDevices = async () => {
   try {
     const res = await fetchHttp.get(
-      '/api/pc/admin/getAllDevice',
+      '/api/pc/admin/getOrganizationDevices',
       {},
       {
         headers: {
@@ -678,12 +682,9 @@ const refreshAllDevices = async () => {
         }
       }
     )
-    if (Array.isArray(res)) {
-      allDevices.value = res
-    } else if (res?.list && Array.isArray(res.list)) {
-      allDevices.value = res.list
-    } else {
-      allDevices.value = []
+    console.log('res', res)
+    if (res?.deviceList && Array.isArray(res.deviceList)) {
+      allDevices.value = res.deviceList
     }
   } catch (e) {
     console.error('获取全部设备失败:', e)
@@ -826,12 +827,44 @@ const handleHealthAlert = async (healthAlert: any) => {
   }, 10000)
 }
 
+const handleLocationAlert = async (locationAlert: any) => {
+  try {
+    const payload = locationAlert?.data || locationAlert || {}
+    const elderId = Number(payload.elderId || payload.elderID || payload.elder_id || 0)
+    const lng = Number(payload.longitude)
+    const lat = Number(payload.latitude)
+    const ts = payload.timestamp || payload.time || Date.now()
+
+    // 只处理当前选中长者
+    if (!elderId || elderId !== selectedElderly.value.id) return
+    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
+
+    // 仅当右侧详情处于“长者位置”地图模式时,实时追加
+    const isMapShown = detailSectionRef.value?.isMapShown?.()
+    if (!isMapShown) return
+
+    detailSectionRef.value?.addMapPoint?.({
+      longitude: lng,
+      latitude: lat,
+      locationTime: new Date(ts).toLocaleString()
+    })
+  } catch (e) {
+    console.warn('处理 LOCATION_UPDATE 失败(忽略):', e)
+  }
+}
+
+const handleHealthUpdateAlert = async (healthUpdateAlert: any) => {
+  console.log('healthUpdateAlert', healthUpdateAlert)
+}
+
 // WebSocket 连接
 const wsUrl = import.meta.env.VITE_API_WSS_URL
 const { connect, disconnect, sendMessage } = useWebSocket({
   wsUrl,
   onSOSAlert: handleSOSAlert,
-  onHealthAlert: handleHealthAlert
+  onHealthAlert: handleHealthAlert,
+  onLocationAlert: handleLocationAlert,
+  onHealthUpdateAlert: handleHealthUpdateAlert
 })
 
 // 生命周期