feat:设备地图页面

master
黄伟杰 2 months ago
parent 4b13db5faf
commit 1999b8e5d9

@ -62,6 +62,7 @@
"min-dash": "^4.1.1",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"ol": "^10.8.0",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",

@ -119,6 +119,9 @@ importers:
nprogress:
specifier: ^0.2.0
version: 0.2.0
ol:
specifier: ^10.8.0
version: 10.8.0
pinia:
specifier: ^2.1.7
version: 2.2.8(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3))
@ -1468,6 +1471,9 @@ packages:
resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==}
engines: {node: '>= 10.0.0'}
'@petamoriken/float16@3.9.3':
resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@ -1846,6 +1852,9 @@ packages:
'@types/qs@6.9.17':
resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
'@types/rbush@4.0.0':
resolution: {integrity: sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==}
'@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@ -2352,6 +2361,9 @@ packages:
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'}
'@zarrita/storage@0.1.4':
resolution: {integrity: sha512-qURfJAQcQGRfDQ4J9HaCjGaj3jlJKc66bnRk6G/IeLUsM7WKyG7Bzsuf1EZurSXyc0I4LVcu6HaeQQ4d3kZ16g==}
'@zxcvbn-ts/core@3.0.4':
resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==}
@ -3035,6 +3047,9 @@ packages:
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
earcut@3.0.2:
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -3278,6 +3293,9 @@ packages:
feelin@3.2.0:
resolution: {integrity: sha512-GFDbHsTYk7YXO1tyw1dOjb7IODeAZvNIosdGZThUwPx5XcD/XhO0hnPZXsIbAzSsIdrgGlTEEdby9fZ2gixysA==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -3362,6 +3380,10 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
geotiff@3.0.5:
resolution: {integrity: sha512-OWcL9S9+yDZ6iAlXMt32T1iwUApJM8UiD47xbm6ZP1h33d10fqkPs14EG/ttT5EnefpZSx3G15iDFC5FxUNUwA==}
engines: {node: '>=10.19'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@ -3729,6 +3751,9 @@ packages:
lang-feel@2.2.0:
resolution: {integrity: sha512-Ebo5nftYsMfJzB3Ny8Oy4oaDXZXb5x61qtVVmKv6aImvAZUbT76mD60ZbEilizjZQzsR2CcU1iMK5sacIa1NVA==}
lerc@3.0.0:
resolution: {integrity: sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==}
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@ -4075,6 +4100,9 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
numcodecs@0.3.2:
resolution: {integrity: sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==}
object-inspect@1.13.3:
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
engines: {node: '>= 0.4'}
@ -4088,6 +4116,9 @@ packages:
ofetch@1.4.1:
resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
ol@10.8.0:
resolution: {integrity: sha512-kLk7jIlJvKyhVMAjORTXKjzlM6YIByZ1H/d0DBx3oq8nSPCG6/gbLr5RxukzPgwbhnAqh+xHNCmrvmFKhVMvoQ==}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@ -4137,10 +4168,16 @@ packages:
package-manager-detector@0.2.5:
resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-headers@2.0.6:
resolution: {integrity: sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==}
parse-json@5.2.0:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
@ -4198,6 +4235,10 @@ packages:
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pbf@4.0.1:
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
hasBin: true
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
@ -4337,6 +4378,9 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
@ -4366,9 +4410,19 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
quick-lru@6.1.2:
resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==}
engines: {node: '>=12'}
quickselect@3.0.0:
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
randomcolor@0.6.2:
resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==}
rbush@4.0.1:
resolution: {integrity: sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==}
rd@2.0.1:
resolution: {integrity: sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw==}
@ -4383,6 +4437,9 @@ packages:
resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
engines: {node: '>= 14.16.0'}
reference-spec-reader@0.2.0:
resolution: {integrity: sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==}
regenerate-unicode-properties@10.2.0:
resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==}
engines: {node: '>=4'}
@ -4437,6 +4494,9 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
resolve-protobuf-schema@2.1.0:
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
@ -4884,6 +4944,10 @@ packages:
resolution: {integrity: sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==}
engines: {node: '>=14.0.0'}
unzipit@1.4.3:
resolution: {integrity: sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==}
engines: {node: '>=12'}
update-browserslist-db@1.1.1:
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
hasBin: true
@ -4907,6 +4971,9 @@ packages:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uzip-module@1.0.3:
resolution: {integrity: sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==}
vanilla-picker@2.12.3:
resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==}
@ -5070,6 +5137,9 @@ packages:
web-storage-cache@1.1.1:
resolution: {integrity: sha512-D0MieGooOs8RpsrK+vnejXnvh4OOv/+lTFB35JRkJJQt+uOjPE08XpaE0QBLMTRu47B1KGT/Nq3Gbag3Orinzw==}
web-worker@1.5.0:
resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -5129,6 +5199,9 @@ packages:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
xml-utils@1.10.2:
resolution: {integrity: sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
@ -5177,12 +5250,18 @@ packages:
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
engines: {node: '>=12.20'}
zarrita@0.6.2:
resolution: {integrity: sha512-8IV+2bWt5yiHNVK9GVEVK1tscpqDcJj8iz5cIKFOiWiWYUsK4V5njgMtnpkvKu6L7K+Og6zUShd8f+dwb6LvTA==}
zeebe-bpmn-moddle@1.7.0:
resolution: {integrity: sha512-eZ6OXSt0c4n9V/oN/46gTlwDIS3GhWQLt9jbM5uS/YryB4yN8wdrrKrtw+TpyNy0SSKWXNDHyC83nCA2blPO3Q==}
zrender@5.6.0:
resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==}
zstddec@0.2.0:
resolution: {integrity: sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==}
snapshots:
'@ampproject/remapping@2.3.0':
@ -6502,6 +6581,8 @@ snapshots:
'@parcel/watcher-win32-x64': 2.5.0
optional: true
'@petamoriken/float16@3.9.3': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@ -6826,6 +6907,8 @@ snapshots:
'@types/qs@6.9.17': {}
'@types/rbush@4.0.0': {}
'@types/semver@7.5.8': {}
'@types/trusted-types@2.0.7':
@ -7600,6 +7683,11 @@ snapshots:
'@xmldom/xmldom@0.8.10': {}
'@zarrita/storage@0.1.4':
dependencies:
reference-spec-reader: 0.2.0
unzipit: 1.4.3
'@zxcvbn-ts/core@3.0.4':
dependencies:
fastest-levenshtein: 1.0.16
@ -8337,6 +8425,8 @@ snapshots:
duplexer@0.1.2: {}
earcut@3.0.2: {}
eastasianwidth@0.2.0: {}
echarts-wordcloud@2.1.0(echarts@5.5.1):
@ -8663,6 +8753,8 @@ snapshots:
lezer-feel: 1.4.0
luxon: 3.5.0
fflate@0.8.2: {}
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@ -8750,6 +8842,17 @@ snapshots:
gensync@1.0.0-beta.2: {}
geotiff@3.0.5:
dependencies:
'@petamoriken/float16': 3.9.3
lerc: 3.0.0
pako: 2.1.0
parse-headers: 2.0.6
quick-lru: 6.1.2
web-worker: 1.5.0
xml-utils: 1.10.2
zstddec: 0.2.0
get-caller-file@2.0.5: {}
get-east-asian-width@1.3.0: {}
@ -9075,6 +9178,8 @@ snapshots:
'@lezer/common': 1.2.3
lezer-feel: 1.4.0
lerc@3.0.0: {}
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@ -9420,6 +9525,10 @@ snapshots:
dependencies:
boolbase: 1.0.0
numcodecs@0.3.2:
dependencies:
fflate: 0.8.2
object-inspect@1.13.3: {}
object-refs@0.3.0: {}
@ -9432,6 +9541,15 @@ snapshots:
node-fetch-native: 1.6.4
ufo: 1.5.4
ol@10.8.0:
dependencies:
'@types/rbush': 4.0.0
earcut: 3.0.2
geotiff: 3.0.5
pbf: 4.0.1
rbush: 4.0.1
zarrita: 0.6.2
once@1.4.0:
dependencies:
wrappy: 1.0.2
@ -9483,10 +9601,14 @@ snapshots:
package-manager-detector@0.2.5: {}
pako@2.1.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
parse-headers@2.0.6: {}
parse-json@5.2.0:
dependencies:
'@babel/code-frame': 7.26.2
@ -9532,6 +9654,10 @@ snapshots:
pathe@2.0.3: {}
pbf@4.0.1:
dependencies:
resolve-protobuf-schema: 2.1.0
perfect-debounce@1.0.0: {}
picocolors@1.1.1: {}
@ -9650,6 +9776,8 @@ snapshots:
progress@2.0.3: {}
protocol-buffers-schema@3.6.0: {}
proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {}
@ -9672,8 +9800,16 @@ snapshots:
queue-microtask@1.2.3: {}
quick-lru@6.1.2: {}
quickselect@3.0.0: {}
randomcolor@0.6.2: {}
rbush@4.0.1:
dependencies:
quickselect: 3.0.0
rd@2.0.1:
dependencies:
'@types/node': 10.17.60
@ -9686,6 +9822,8 @@ snapshots:
readdirp@4.0.2: {}
reference-spec-reader@0.2.0: {}
regenerate-unicode-properties@10.2.0:
dependencies:
regenerate: 1.4.2
@ -9732,6 +9870,10 @@ snapshots:
resolve-from@5.0.0: {}
resolve-protobuf-schema@2.1.0:
dependencies:
protocol-buffers-schema: 3.6.0
resolve@1.22.8:
dependencies:
is-core-module: 2.15.1
@ -10256,6 +10398,10 @@ snapshots:
acorn: 8.14.0
webpack-virtual-modules: 0.6.2
unzipit@1.4.3:
dependencies:
uzip-module: 1.0.3
update-browserslist-db@1.1.1(browserslist@4.24.2):
dependencies:
browserslist: 4.24.2
@ -10277,6 +10423,8 @@ snapshots:
uuid@10.0.0: {}
uzip-module@1.0.3: {}
vanilla-picker@2.12.3:
dependencies:
'@sphinxxxx/color-conversion': 2.2.2
@ -10471,6 +10619,8 @@ snapshots:
web-storage-cache@1.1.1: {}
web-worker@1.5.0: {}
webidl-conversions@3.0.1: {}
webpack-virtual-modules@0.6.2: {}
@ -10531,6 +10681,8 @@ snapshots:
xml-name-validator@4.0.0: {}
xml-utils@1.10.2: {}
y18n@4.0.3: {}
y18n@5.0.8: {}
@ -10582,8 +10734,15 @@ snapshots:
yocto-queue@1.1.1: {}
zarrita@0.6.2:
dependencies:
'@zarrita/storage': 0.1.4
numcodecs: 0.3.2
zeebe-bpmn-moddle@1.7.0: {}
zrender@5.6.0:
dependencies:
tslib: 2.3.0
zstddec@0.2.0: {}

@ -154,6 +154,10 @@ export const DeviceApi = {
})
},
getDeviceRunStatusStats: async () => {
return await request.get({ url: `/iot/device/device-run-status-stats` })
},
devicePointList: async () => {
return await request.get({ url: `/iot/device/devicePointList` })
},

@ -11,7 +11,23 @@
class="device-mgmt__search"
/>
</div>
<div class="left-panel__stats">
<div class="stat-item">
<el-icon><OfficeBuilding /></el-icon> : {{ customerNodeTotal }}
</div>
<div class="stat-item">
<el-icon><Monitor /></el-icon> : {{ deviceNodeTotal }}
</div>
<el-button
class="stat-refresh-btn"
text
:loading="treeRefreshing"
@click="refreshTreeData"
>
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
<el-tree
ref="treeRef"
node-key="nodeKey"
@ -165,24 +181,6 @@
selectedDeviceDetail?.deviceName || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">设备类型</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.deviceType || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">关联设备模型</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.deviceModelId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">状态</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.status || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">运行状态</div>
<div class="device-mgmt__baseValue">{{
@ -190,21 +188,21 @@
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">关联组织</div>
<div class="device-mgmt__baseLabel">客户ID</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.org || '-'
selectedDeviceDetail?.customerId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">客户ID</div>
<div class="device-mgmt__baseLabel">客户名称</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.customerId || '-'
selectedDeviceDetail?.customerName || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">组织节点ID</div>
<div class="device-mgmt__baseLabel">组织名称</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.orgNodeId || '-'
selectedDeviceDetail?.orgNodeName || '-'
}}</div>
</div>
</div>
@ -220,48 +218,6 @@
selectedDeviceDetail?.protocol || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">端点url</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.url || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">采集周期</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.sampleCycle || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">用户名</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.username || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">密码</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.password ? '******' : '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">网关id</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.gatewayId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">读主题</div>
<div class="device-mgmt__baseValue device-mgmt__baseValue--bg">{{
selectedDeviceDetail?.readTopic || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">写主题</div>
<div class="device-mgmt__baseValue device-mgmt__baseValue--bg">{{
selectedDeviceDetail?.writeTopic || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">mqtt订阅主题</div>
<div class="device-mgmt__baseValue device-mgmt__baseValue--bg">{{
@ -297,12 +253,6 @@
: '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">离线间隔</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.offLineDuration || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">最后上线时间</div>
<div class="device-mgmt__baseValue">{{
@ -311,12 +261,6 @@
: '-'
}}</div>
</div>
<div class="device-mgmt__baseItem">
<div class="device-mgmt__baseLabel">设备品牌id</div>
<div class="device-mgmt__baseValue">{{
selectedDeviceDetail?.deviceBrandId || '-'
}}</div>
</div>
<div class="device-mgmt__baseItem device-mgmt__baseItem--full">
<div class="device-mgmt__baseLabel">备注</div>
<div class="device-mgmt__baseValue">{{
@ -506,7 +450,7 @@ import { useRoute } from 'vue-router'
import NodeForm from './NodeForm.vue'
import DeviceForm from '../DeviceForm.vue'
import { formatDate } from '@/utils/formatTime'
import { OfficeBuilding, Monitor, Refresh } from '@element-plus/icons-vue'
defineOptions({ name: 'IoTDeviceManagement' })
type TreeNodeType = 'company' | 'line' | 'device'
@ -567,6 +511,7 @@ const deviceFormRef = ref()
const rightMode = ref<'list' | 'detail'>('list')
const nodeDeviceLoading = ref(false)
const nodeDeviceList = ref<DeviceListRow[]>([])
const treeRefreshing = ref(false)
const flattenDevices = (nodes: TreeNode[]) => {
const devices: TreeNode[] = []
@ -583,6 +528,28 @@ const flattenDevices = (nodes: TreeNode[]) => {
const deviceList = computed(() => flattenDevices(treeData.value))
const deviceTotal = computed(() => deviceList.value.length)
const deviceOnline = computed(() => deviceList.value.filter((d) => !!d.online).length)
const customerNodeTotal = computed(() => {
let total = 0
const walk = (nodes: TreeNode[]) => {
nodes.forEach((node) => {
if (Number(node.nodeType) === 1) total += 1
if (node.children?.length) walk(node.children)
})
}
walk(treeData.value)
return total
})
const deviceNodeTotal = computed(() => {
let total = 0
const walk = (nodes: TreeNode[]) => {
nodes.forEach((node) => {
if (Number(node.nodeType) === 4) total += 1
if (node.children?.length) walk(node.children)
})
}
walk(treeData.value)
return total
})
const filterTreeNode = (value: string, data: TreeNode) => {
if (!value) return true
@ -610,7 +577,7 @@ const getNodeTypeLabel = (nodeType?: number | string) => {
if (value === 2) return '车间'
if (value === 3) return '产线'
if (value === 4) return '设备'
return '组织'
return '客户'
}
const getNodeTypeClass = (nodeType?: number | string) => {
@ -745,6 +712,15 @@ const getTreeData = async () => {
}
}
const refreshTreeData = async () => {
treeRefreshing.value = true
try {
await getTreeData()
} finally {
treeRefreshing.value = false
}
}
const getDefaultParentNode = () => {
if (selectedNode.value && Number(selectedNode.value.nodeType) !== 4) return selectedNode.value
const device =
@ -1377,4 +1353,23 @@ watch([() => activeTab.value, () => selectedDeviceDetail.value.id], ([tab, devic
border-radius: 4px;
font-family: monospace;
}
.left-panel__stats {
display: flex;
gap: 12px;
margin-bottom: 16px;
align-items: center;
.stat-item {
background-color: var(--el-fill-color-light);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
display: flex;
align-items: center;
gap: 6px;
}
.stat-refresh-btn {
margin-left: auto;
}
}
</style>

@ -0,0 +1,254 @@
<template>
<div class="map-container">
<div id="device-map" class="map-instance"></div>
<!-- Custom HTML Markers for Map -->
<div style="display: none;">
<div
v-for="item in markersData"
:key="item.name"
:id="`marker-${item.name}`"
class="custom-marker"
:class="[getMarkerStatus(item), { 'is-active': selectedName === item.name }]"
@click="onMarkerClick(item)"
>
<div class="marker-core">{{ item.total }}</div>
<div class="marker-label">{{ item.name }}</div>
<div class="ripple"></div>
<div class="ripple ripple-2"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue'
import { Map, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'
import Overlay from 'ol/Overlay'
import { fromLonLat } from 'ol/proj'
import 'ol/ol.css'
defineOptions({ name: 'DeviceMap' })
const props = defineProps<{
viewType: 'global' | 'china'
markersData: any[]
selectedName?: string
}>()
const emit = defineEmits<{
(e: 'select', data: any): void
}>()
const map = ref<Map | null>(null)
const overlays = ref<Overlay[]>([])
const getMarkerStatus = (item: any) => {
if (item.alarm > 0) return 'status-alarm'
if (item.online === 0) return 'status-offline'
return 'status-normal'
}
const onMarkerClick = (item: any) => {
emit('select', item)
}
const initMap = () => {
// Use a light map tile to match design (e.g. CartoDB Positron or similar)
const baseLayer = new TileLayer({
source: new XYZ({
url: 'https://{a-c}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png',
attributions: '&copy; OpenStreetMap contributors &copy; CARTO'
})
})
map.value = new Map({
target: 'device-map',
layers: [baseLayer],
view: new View({
center: fromLonLat([0, 20]),
zoom: 2,
minZoom: 1,
maxZoom: 10
}),
controls: [] // Hide default controls like zoom buttons
})
renderMarkers()
}
const renderMarkers = () => {
if (!map.value) return
// Clear old overlays
overlays.value.forEach(overlay => map.value?.removeOverlay(overlay))
overlays.value = []
// Add new overlays
props.markersData.forEach(item => {
const el = document.getElementById(`marker-${item.name}`)
if (el) {
const overlay = new Overlay({
element: el,
position: fromLonLat([item.longitude, item.latitude]),
positioning: 'center-center',
stopEvent: true
})
map.value.addOverlay(overlay)
overlays.value.push(overlay)
}
})
}
const updateView = () => {
if (!map.value) return
const view = map.value.getView()
if (props.viewType === 'global') {
view.animate({
center: fromLonLat([0, 20]),
zoom: 2,
duration: 800
})
} else {
view.animate({
center: fromLonLat([104.1954, 35.8617]),
zoom: 4,
duration: 800
})
}
}
watch(() => props.viewType, () => {
updateView()
})
watch(() => props.markersData, () => {
nextTick(() => {
renderMarkers()
})
}, { deep: true })
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (map.value) {
map.value.setTarget(undefined)
map.value = null
}
})
</script>
<style lang="scss" scoped>
.map-container {
width: 100%;
height: 100%;
position: relative;
background-color: #f0f4f8; /* Soft background color for oceans */
}
.map-instance {
width: 100%;
height: 100%;
}
/* Custom Marker Styles */
.custom-marker {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
z-index: 10;
transition: transform 0.2s;
&:hover, &.is-active {
transform: scale(1.15);
z-index: 20;
}
.marker-core {
position: relative;
z-index: 2;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: bold;
color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.marker-label {
position: absolute;
right: -40px;
top: 50%;
transform: translateY(-50%);
background: rgba(255,255,255,0.9);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: #333;
white-space: nowrap;
pointer-events: none;
font-weight: bold;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.ripple {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
transform: translate(-50%, -50%);
border-radius: 50%;
animation: ripple-anim 2s infinite ease-out;
pointer-events: none;
z-index: 1;
border: 1px solid currentColor;
}
.ripple-2 {
animation-delay: 1s;
}
/* Status Colors */
&.status-normal {
color: #10b981;
.marker-core { background: #10b981; }
}
&.status-alarm {
color: #f56c6c;
.marker-core { background: #f56c6c; }
}
&.status-offline {
color: #909399;
.marker-core { background: #909399; }
.ripple { animation: none; border-color: rgba(144, 147, 153, 0.4); }
}
}
@keyframes ripple-anim {
0% {
width: 100%;
height: 100%;
opacity: 0.8;
}
100% {
width: 250%;
height: 250%;
opacity: 0;
}
}
</style>

@ -0,0 +1,615 @@
<template>
<div class="device-map-page">
<!-- Top Header & Stats -->
<div class="stats-container">
<el-row :gutter="16">
<el-col :span="3" v-for="stat in topStats" :key="stat.label">
<el-card shadow="never" class="stat-card">
<div class="stat-content">
<div class="stat-icon" :class="`icon-${stat.type}`">
<Icon :icon="stat.icon" :size="24" />
</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</el-card>
</el-col>
<!-- View Toggle Buttons -->
<el-col :span="6" class="toggle-col">
<div class="view-toggle">
<el-radio-group v-model="currentView" size="large">
<el-radio-button label="global">
<Icon icon="ep:map-location" class="mr-1" /> 全球视图
</el-radio-button>
<el-radio-button label="china">
<Icon icon="ep:location" class="mr-1" /> 中国视图
</el-radio-button>
</el-radio-group>
</div>
</el-col>
</el-row>
</div>
<!-- Main Map Area -->
<el-card
shadow="never"
class="map-section"
:body-style="{ padding: 0, height: '100%', display: 'flex', flexDirection: 'column' }"
>
<div class="map-header">
<div class="map-title">
<span class="dot"></span>
{{ currentView === 'global' ? '全球设备分布地图' : '中国设备分布地图' }}
</div>
</div>
<div class="map-body">
<DeviceMap
:view-type="currentView"
:markers-data="mapData"
:selected-name="selectedCountry?.name"
@select="handleMarkerSelect"
/>
<!-- Legend -->
<div class="map-legend">
<div class="legend-title">图例说明</div>
<div class="legend-item"> <span class="legend-dot dot-normal"></span> 运行正常 </div>
<div class="legend-item"> <span class="legend-dot dot-alarm"></span> 存在报警 </div>
<div class="legend-item"> <span class="legend-dot dot-offline"></span> 离线设备 </div>
<div class="legend-desc">
<div><Icon icon="ep:pointer" /> 点击设备标记查看详细统计</div>
<div><Icon icon="ep:switch" /> 切换视图查看不同区域</div>
</div>
</div>
<!-- Right Drawer Panel -->
<transition name="slide-fade">
<div class="detail-panel" v-if="selectedCountry">
<div class="panel-header">
<span>{{ selectedCountry.name }}</span>
<Icon icon="ep:close" class="close-icon" @click="selectedCountry = null" />
</div>
<div class="panel-content">
<div class="detail-row">
<span class="label">设备总数</span>
<span class="value">{{ selectedCountry.total }}</span>
</div>
<div class="detail-row">
<span class="label">联网设备</span>
<span class="value text-success">{{ selectedCountry.online }}</span>
</div>
<div class="detail-row">
<span class="label">运行中</span>
<span class="value text-primary">{{ selectedCountry.running }}</span>
</div>
<div class="detail-row">
<span class="label">离线设备</span>
<span class="value text-info">{{ selectedCountry.offline }}</span>
</div>
<div class="detail-row">
<span class="label">报警设备</span>
<span class="value text-danger">{{ selectedCountry.alarm }}</span>
</div>
<div class="progress-section">
<div class="progress-bar">
<el-progress
:percentage="
Math.round((selectedCountry.online / selectedCountry.total) * 100) || 0
"
:show-text="false"
color="#10b981"
/>
</div>
<div class="progress-label"
>联网率:
{{
Math.round((selectedCountry.online / selectedCountry.total) * 100) || 0
}}%</div
>
</div>
</div>
</div>
</transition>
</div>
</el-card>
<!-- Bottom Distribution List -->
<el-card shadow="never" class="dist-section">
<div class="dist-header">国家/地区分布</div>
<div class="dist-list">
<div
class="dist-item"
v-for="item in mapData"
:key="item.name"
@click="handleMarkerSelect(item)"
:class="{ 'is-active': selectedCountry?.name === item.name }"
>
<span class="dist-name">{{ item.name }}</span>
<el-tag size="small" type="primary" class="dist-tag">{{ item.total }}</el-tag>
<el-tag size="small" type="success" class="dist-tag" v-if="item.online > 0"
>{{ item.online }}在线</el-tag
>
<el-tag size="small" type="danger" class="dist-tag" v-if="item.alarm > 0"
>{{ item.alarm }}报警</el-tag
>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import DeviceMap from './components/DeviceMap.vue'
import { DeviceApi } from '@/api/iot/device'
defineOptions({ name: 'IoTDeviceMap' })
const currentView = ref<'global' | 'china'>('global')
const selectedCountry = ref<any>(null)
const runStatusStats = ref({
totalDeviceCount: 0,
offlineCount: 0,
runningCount: 0,
standbyCount: 0,
faultStandbyCount: 0,
alarmRunningCount: 0
})
// Mock Data
const mapData = ref([
{
name: '中国',
longitude: 104.1954,
latitude: 35.8617,
total: 89,
online: 82,
alarm: 3,
running: 79,
offline: 7
},
{
name: '美国',
longitude: -95.7129,
latitude: 37.0902,
total: 25,
online: 23,
alarm: 1,
running: 18,
offline: 2
},
{
name: '日本',
longitude: 138.2529,
latitude: 36.2048,
total: 12,
online: 11,
alarm: 0,
running: 11,
offline: 1
},
{
name: '德国',
longitude: 10.4515,
latitude: 51.1657,
total: 15,
online: 14,
alarm: 0,
running: 14,
offline: 1
},
{
name: '印度',
longitude: 78.9629,
latitude: 20.5937,
total: 4,
online: 3,
alarm: 0,
running: 3,
offline: 1
},
{
name: '巴西',
longitude: -51.9253,
latitude: -14.235,
total: 2,
online: 2,
alarm: 0,
running: 2,
offline: 0
},
{
name: '韩国',
longitude: 127.7669,
latitude: 35.9078,
total: 8,
online: 7,
alarm: 1,
running: 6,
offline: 1
},
{
name: '墨西哥',
longitude: -102.5528,
latitude: 23.6345,
total: 1,
online: 0,
alarm: 0,
running: 0,
offline: 1
}
])
const topStats = computed(() => [
{
label: '设备总数',
value: runStatusStats.value.totalDeviceCount,
icon: 'ep:data-line',
type: 'primary'
},
{
label: '离线设备',
value: runStatusStats.value.offlineCount,
icon: 'ep:turn-off',
type: 'offline'
},
{
label: '运行设备',
value: runStatusStats.value.runningCount,
icon: 'ep:setting',
type: 'running'
},
{
label: '待机设备',
value: runStatusStats.value.standbyCount,
icon: 'ep:clock',
type: 'success'
},
{
label: '故障待机',
value: runStatusStats.value.faultStandbyCount,
icon: 'ep:warning-filled',
type: 'alarm'
},
{
label: '报警运行',
value: runStatusStats.value.alarmRunningCount,
icon: 'ep:bell',
type: 'alarm'
}
])
const handleMarkerSelect = (item: any) => {
selectedCountry.value = item
}
const loadRunStatusStats = async () => {
const data = await DeviceApi.getDeviceRunStatusStats()
runStatusStats.value = {
totalDeviceCount: Number(data?.totalDeviceCount || 0),
offlineCount: Number(data?.offlineCount || 0),
runningCount: Number(data?.runningCount || 0),
standbyCount: Number(data?.standbyCount || 0),
faultStandbyCount: Number(data?.faultStandbyCount || 0),
alarmRunningCount: Number(data?.alarmRunningCount || 0)
}
}
onMounted(async () => {
await loadRunStatusStats()
})
</script>
<style lang="scss" scoped>
.device-map-page {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
gap: 16px;
min-height: 700px;
}
/* Stats Cards */
.stats-container {
flex: 0 0 auto;
}
.stat-card {
border-radius: 8px;
border: 1px solid var(--el-border-color-light);
.stat-content {
display: flex;
align-items: center;
gap: 16px;
padding: 4px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
&.icon-primary {
background: linear-gradient(135deg, #409eff, #53a8ff);
}
&.icon-success {
background: linear-gradient(135deg, #10b981, #34d399);
}
&.icon-running {
background: linear-gradient(135deg, #8b5cf6, #a78bfa);
}
&.icon-offline {
background: linear-gradient(135deg, #6b7280, #9ca3af);
}
&.icon-alarm {
background: linear-gradient(135deg, #ef4444, #f87171);
}
}
.stat-info {
flex: 1;
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1.2;
}
.stat-label {
font-size: 13px;
color: var(--el-text-color-regular);
margin-top: 4px;
}
}
}
.toggle-col {
display: flex;
align-items: center;
justify-content: flex-end;
}
.view-toggle {
display: flex;
align-items: center;
height: 100%;
}
/* Map Section */
.map-section {
flex: 1 1 auto;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
.map-header {
flex: 0 0 auto;
padding: 16px 20px;
background: linear-gradient(90deg, #6d28d9, #8b5cf6);
color: #fff;
display: flex;
align-items: center;
.map-title {
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.3);
}
}
}
.map-body {
flex: 1 1 auto;
position: relative;
overflow: hidden;
}
}
/* Legend */
.map-legend {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(4px);
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 220px;
z-index: 100;
border: 1px solid var(--el-border-color-light);
.legend-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 12px;
color: var(--el-text-color-primary);
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
font-size: 13px;
color: var(--el-text-color-regular);
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
&.dot-normal {
background-color: #10b981;
}
&.dot-alarm {
background-color: #ef4444;
}
&.dot-offline {
background-color: #9ca3af;
}
}
}
.legend-desc {
margin-top: 16px;
padding-top: 12px;
border-top: 1px dashed var(--el-border-color-lighter);
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.8;
display: flex;
flex-direction: column;
gap: 4px;
div {
display: flex;
align-items: center;
gap: 6px;
}
}
}
/* Right Detail Panel */
.detail-panel {
position: absolute;
top: 20px;
right: 260px;
width: 280px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 101;
overflow: hidden;
border: 1px solid var(--el-border-color-light);
.panel-header {
padding: 16px 20px;
font-size: 16px;
font-weight: bold;
border-bottom: 1px solid var(--el-border-color-lighter);
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8fafc;
.close-icon {
cursor: pointer;
color: var(--el-text-color-secondary);
&:hover {
color: var(--el-text-color-primary);
}
}
}
.panel-content {
padding: 20px;
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px dashed var(--el-border-color-lighter);
font-size: 14px;
.label {
color: var(--el-text-color-regular);
}
.value {
font-weight: bold;
font-size: 15px;
}
.text-success {
color: #10b981;
}
.text-primary {
color: #409eff;
}
.text-info {
color: #9ca3af;
}
.text-danger {
color: #ef4444;
}
}
.progress-section {
margin-top: 24px;
.progress-label {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
}
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
/* Bottom Distribution */
.dist-section {
flex: 0 0 auto;
border-radius: 8px;
.dist-header {
font-weight: bold;
font-size: 14px;
color: var(--el-text-color-primary);
margin-bottom: 16px;
}
.dist-list {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
gap: 12px;
padding-bottom: 8px;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #e5e7eb;
border-radius: 3px;
}
.dist-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #f8fafc;
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s;
&:hover,
&.is-active {
background: #f0fdf4;
border-color: #86efac;
}
.dist-name {
font-size: 14px;
font-weight: 500;
margin-right: 4px;
}
.dist-tag {
border-radius: 4px;
}
}
}
}
</style>
Loading…
Cancel
Save