初始化

This commit is contained in:
2026-07-01 16:27:02 +08:00
parent fe0d4304f8
commit accd62a2f3
31 changed files with 6448 additions and 1136 deletions

1
.env.development Normal file
View File

@@ -0,0 +1 @@
VITE_APP_BASE_API=/api

View File

@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />

678
package-lock.json generated
View File

@@ -9,11 +9,16 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@types/node": "^20.4.4", "@types/node": "^20.4.4",
"axios": "^1.18.1",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"echarts-liquidfill": "^3.1.0", "echarts-liquidfill": "^3.1.0",
"jsencrypt": "^3.5.4",
"naive-ui": "^2.44.1",
"ol": "^7.4.0", "ol": "^7.4.0",
"pinia": "^3.0.4",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"vue": "^3.2.47" "vue": "^3.2.47",
"vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
@@ -429,6 +434,30 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@css-render/plugin-bem": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
"integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
"license": "MIT",
"peerDependencies": {
"css-render": "~0.15.14"
}
},
"node_modules/@css-render/vue3-ssr": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
"integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
@@ -852,6 +881,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0"
},
"node_modules/@mapbox/jsonlint-lines-primitives": { "node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
@@ -914,6 +949,21 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/@types/lodash": {
"version": "4.17.24",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.43", "version": "20.19.43",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.43.tgz",
@@ -1089,6 +1139,39 @@
"@vue/shared": "3.5.38" "@vue/shared": "3.5.38"
} }
}, },
"node_modules/@vue/devtools-api": {
"version": "7.7.10",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.10.tgz",
"integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.10"
}
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.10",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz",
"integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.10",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.10",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz",
"integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "1.8.27", "version": "1.8.27",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz", "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz",
@@ -1165,6 +1248,42 @@
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.18.1",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1185,6 +1304,15 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/birpc": {
"version": "2.9.0",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz",
@@ -1229,6 +1357,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001799", "version": "1.0.30001799",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
@@ -1250,6 +1391,18 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/computeds": { "node_modules/computeds": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz", "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
@@ -1280,6 +1433,22 @@
"url": "https://github.com/sponsors/mesqueeb" "url": "https://github.com/sponsors/mesqueeb"
} }
}, },
"node_modules/css-render": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "~0.8.0",
"csstype": "~3.0.5"
}
},
"node_modules/css-render/node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"license": "MIT"
},
"node_modules/csscolorparser": { "node_modules/csscolorparser": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/csscolorparser/-/csscolorparser-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/csscolorparser/-/csscolorparser-1.0.3.tgz",
@@ -1292,6 +1461,25 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.4.0.tgz",
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
@@ -1303,7 +1491,6 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -1317,6 +1504,29 @@
} }
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/earcut": { "node_modules/earcut": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmmirror.com/earcut/-/earcut-2.2.4.tgz", "resolved": "https://registry.npmmirror.com/earcut/-/earcut-2.2.4.tgz",
@@ -1375,6 +1585,51 @@
"errno": "cli.js" "errno": "cli.js"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz",
@@ -1429,6 +1684,48 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/evtd": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
"license": "MIT"
},
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.6",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -1444,6 +1741,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -1473,6 +1779,55 @@
"node": ">=10.19" "node": ">=10.19"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1481,6 +1836,33 @@
"license": "ISC", "license": "ISC",
"optional": true "optional": true
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-sum": { "node_modules/hash-sum": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz",
@@ -1488,6 +1870,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": { "node_modules/he": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
@@ -1498,6 +1892,34 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -1566,6 +1988,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jsencrypt": {
"version": "3.5.4",
"resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.5.4.tgz",
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
"license": "MIT"
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
@@ -1630,6 +2058,18 @@
"source-map": "~0.6.0" "source-map": "~0.6.0"
} }
}, },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -1669,6 +2109,15 @@
"integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==", "integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime": { "node_modules/mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
@@ -1683,6 +2132,27 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.9", "version": "9.0.9",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
@@ -1708,11 +2178,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": { "node_modules/muggle-string": {
@@ -1722,6 +2197,38 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/naive-ui": {
"version": "2.44.1",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.44.1.tgz",
"integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==",
"license": "MIT",
"dependencies": {
"@css-render/plugin-bem": "^0.15.14",
"@css-render/vue3-ssr": "^0.15.14",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"async-validator": "^4.2.5",
"css-render": "^0.15.14",
"csstype": "^3.1.3",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"evtd": "^0.2.4",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"seemly": "^0.3.10",
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
"vueuc": "^0.4.65"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.15", "version": "3.3.15",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz",
@@ -1848,6 +2355,12 @@
"pbf": "bin/pbf" "pbf": "bin/pbf"
} }
}, },
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@@ -1867,6 +2380,27 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.5.0",
"vue": "^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.15", "version": "8.5.15",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz",
@@ -1916,6 +2450,15 @@
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/prr": { "node_modules/prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",
@@ -1960,6 +2503,12 @@
"protocol-buffers-schema": "^3.3.1" "protocol-buffers-schema": "^3.3.1"
} }
}, },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.30.0", "version": "3.30.0",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.30.0.tgz", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.30.0.tgz",
@@ -2002,6 +2551,12 @@
"node": ">=11.0.0" "node": ">=11.0.0"
} }
}, },
"node_modules/seemly": {
"version": "0.3.10",
"resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz",
"integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
"license": "MIT"
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
@@ -2060,6 +2615,60 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.6",
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
"license": "MIT",
"dependencies": {
"copy-anything": "^4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/superjson/node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
"license": "MIT",
"dependencies": {
"is-what": "^5.2.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/superjson/node_modules/is-what": {
"version": "5.5.0",
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/treemate": {
"version": "0.3.11",
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
"license": "MIT"
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
@@ -2117,6 +2726,18 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
"license": "MIT",
"dependencies": {
"evtd": "^0.2.2"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "4.5.14", "version": "4.5.14",
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz",
@@ -2173,6 +2794,18 @@
} }
} }
}, },
"node_modules/vooks": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
"license": "MIT",
"dependencies": {
"evtd": "^0.2.2"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.38", "version": "3.5.38",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.38.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.38.tgz",
@@ -2194,6 +2827,27 @@
} }
} }
}, },
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-template-compiler": { "node_modules/vue-template-compiler": {
"version": "2.7.16", "version": "2.7.16",
"resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
@@ -2236,6 +2890,24 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/vueuc": {
"version": "0.4.65",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz",
"integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==",
"license": "MIT",
"dependencies": {
"@css-render/vue3-ssr": "^0.15.10",
"@juggle/resize-observer": "^3.3.1",
"css-render": "^0.15.10",
"evtd": "^0.2.4",
"seemly": "^0.3.6",
"vdirs": "^0.1.4",
"vooks": "^0.2.4"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/web-worker": { "node_modules/web-worker": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmmirror.com/web-worker/-/web-worker-1.5.0.tgz", "resolved": "https://registry.npmmirror.com/web-worker/-/web-worker-1.5.0.tgz",

View File

@@ -11,18 +11,23 @@
}, },
"dependencies": { "dependencies": {
"@types/node": "^20.4.4", "@types/node": "^20.4.4",
"axios": "^1.18.1",
"echarts": "^5.4.3", "echarts": "^5.4.3",
"echarts-liquidfill": "^3.1.0", "echarts-liquidfill": "^3.1.0",
"jsencrypt": "^3.5.4",
"naive-ui": "^2.44.1",
"ol": "^7.4.0", "ol": "^7.4.0",
"pinia": "^3.0.4",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"vue": "^3.2.47" "vue": "^3.2.47",
"vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^1.3.3",
"less": "^4.1.3", "less": "^4.1.3",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"vite": "^4.2.0", "vite": "^4.2.0",
"vue-tsc": "^1.2.0", "vue-tsc": "^1.2.0"
"@vitejs/plugin-vue-jsx": "^1.3.3"
} }
} }

BIN
public/room.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

7
public/room.yaml Normal file
View File

@@ -0,0 +1,7 @@
image: room.pgm
mode: trinary
resolution: 0.05
origin: [-1.26, -1.77, 0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import dashboard1 from "@/views/dashboard1/index.vue";
const DESIGN_WIDTH = 1920; const DESIGN_WIDTH = 1920;
const DESIGN_HEIGHT = 1080; const DESIGN_HEIGHT = 1080;
@@ -44,7 +43,7 @@ onBeforeUnmount(() => {
<template> <template>
<div class="screen-adapter"> <div class="screen-adapter">
<div class="screen-adapter__inner" :style="adapterStyle"> <div class="screen-adapter__inner" :style="adapterStyle">
<Component :is="dashboard1" /> <RouterView />
</div> </div>
</div> </div>
</template> </template>

75
src/api/alarm.ts Normal file
View File

@@ -0,0 +1,75 @@
import request from "@/utils/request";
export interface RosInspectionRecordLineImageQueryDTO {
recognizeType?: number | string;
startTime?: string;
endTime?: string;
keyword?: string;
}
export interface OrderItem {
property: string;
tableIndex: number;
asc?: boolean;
}
export interface PageParam<T> {
page?: number;
limit?: number;
orderList?: OrderItem[];
query?: T;
}
export type RosInspectionRecordLineImagePageQuery =
Partial<RosInspectionRecordLineImage> & {
keyword?: string;
};
export interface RosInspectionRecordLineImageCar {
id?: string;
deviceKey?: string;
deviceNo?: string;
name?: string;
}
export interface RosInspectionRecordLineImage {
recordId: string;
carId?: string;
car?: RosInspectionRecordLineImageCar;
recordActionId: string;
taskId?: string;
recognizeType?: number;
imageUrl?: string;
captureTime?: string;
poseX?: number;
poseY?: number;
poseYaw?: number;
recognitionStatus?: number;
prompt?: string;
recognitionResult?: string;
warning?: boolean;
warningValue?: string;
failReason?: string;
startTime?: string;
endTime?: string;
createTime?: string;
updateTime?: string;
id?: string;
}
// 查询近期告警列表
export function getRecentAlarmList(data: RosInspectionRecordLineImageQueryDTO) {
return request({
url: "/admin/rosInspectionRecordLineImage/warnList",
data,
});
}
// 分页查询告警列表
export function getAlarmPage(data: PageParam<RosInspectionRecordLineImagePageQuery>) {
return request({
url: "/admin/rosInspectionRecordLineImage/list",
method: "POST",
data,
});
}

73
src/api/rosCar.ts Normal file
View File

@@ -0,0 +1,73 @@
import request from "@/utils/request";
// 小车分页列表
export function getRosCarList(data: unknown) {
return request({
url: "/admin/rosCar/list",
data,
});
}
// 保存巡检任务模板
export function saveRosInspectionTask(data: unknown) {
return request({
url: "/admin/rosInspectionTask/save",
data,
});
}
// 巡检任务模板详情
export function getRosInspectionTaskDetail(cardId: string) {
return request({
url: "/admin/rosInspectionTask/detail",
params: { cardId },
});
}
// 查询小车当前巡检状态
export function getCurrentRosInspection(carId: string) {
return request({
url: "/admin/rosCar/currentInspection",
params: { carId },
});
}
// 下发巡检任务
export function dispatchRosInspectionTask(taskId: string) {
return request({
url: "/admin/rosInspectionTask/dispatch",
params: { taskId },
});
}
// 停止已下发的巡检任务
export function stopDispatchRosInspectionTask(taskId: string) {
return request({
url: "/admin/rosInspectionTask/stopDispatch",
params: { taskId },
});
}
// 上传文件并返回后端保存后的访问地址
export function uploadFile(file: File) {
const data = new FormData();
data.append("file", file);
return request({
url: "/admin/file/upload",
data,
});
}
// 更新小车地图文件地址
export function updateRosCarMapUrls(data: {
id: string;
mapPgmUrl?: string;
mapYamlUrl?: string;
}) {
return request({
url: "/admin/rosCar/updateMapUrls",
data,
});
}

52
src/api/user.js Normal file
View File

@@ -0,0 +1,52 @@
import request, { BASE_URL } from '@/utils/request'
export function getPublicKey() {
return request({
url: 'admin/account/publicKey'
})
}
export function login(data) {
return request({
url: 'admin/account/login',
data
})
}
export function captcha() {
return BASE_URL + '/admin/account/captcha'
}
export function getInfo() {
return request({
url: 'admin/account/info',
})
}
export function updateInfo(data) {
return request({
url: 'admin/account/update',
data
})
}
export function updatePwd(data) {
return request({
url: 'admin/account/updatePwd',
data
})
}
// 发送重置密码验证码
export function getVerifyCode(data) {
return request({
url: '/admin/sysUser/getVerifyCode',
data
})
}
//忘记密码
export function resetUserPassword(data) {
return request({
url: '/admin/sysUser/resetUserPassword',
data
})
}

View File

@@ -4,11 +4,20 @@ import App from "./App.vue";
import Chart from "./components/chart/index.vue"; import Chart from "./components/chart/index.vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import echartsConfig from "./config/echarts.config"; import echartsConfig from "./config/echarts.config";
import { createDiscreteApi } from "naive-ui";
import { createPinia } from "pinia";
import router from "./router";
echarts.registerTheme("t-theme", echartsConfig); echarts.registerTheme("t-theme", echartsConfig);
const { message, dialog } = createDiscreteApi(["message", "dialog"]);
window.$message = message;
window.$modal = dialog;
const app = createApp(App); const app = createApp(App);
app.use(createPinia());
app.use(router);
app.component("TChart", Chart); app.component("TChart", Chart);
app.mount("#app"); app.mount("#app");

97
src/router/index.ts Normal file
View File

@@ -0,0 +1,97 @@
import { getToken } from "@/utils/auth";
import {
createRouter,
createWebHashHistory,
type RouteRecordRaw,
} from "vue-router";
const dashboardDefaultPath = "/dashboard/integrated-center";
const routes: RouteRecordRaw[] = [
{
path: "/",
redirect: () => (getToken() ? dashboardDefaultPath : "/login"),
},
{
path: "/dashboard",
component: () => import("@/views/dashboard1/index.vue"),
redirect: dashboardDefaultPath,
children: [
{
path: "integrated-center",
name: "IntegratedCenter",
component: () =>
import("@/views/dashboard1/pages/IntegratedCenterPage.vue"),
meta: { title: "监管中心", tabId: 1, },
},
{
path: "equipment-management",
name: "EquipmentManagement",
component: () =>
import("@/views/dashboard1/pages/EquipmentManagementPage.vue"),
meta: { title: "设备管理", tabId: 2, },
},
{
path: "video-center",
name: "VideoCenter",
component: () => import("@/views/dashboard1/pages/VideoCenterPage.vue"),
meta: { title: "视频中心", tabId: 3, },
},
{
path: "alarm-management",
name: "AlarmManagement",
component: () =>
import("@/views/dashboard1/pages/AlarmManagementPage.vue"),
meta: { title: "告警管理", tabId: 4, },
},
{
path: "patrol-plan",
name: "PatrolPlan",
component: () => import("@/views/dashboard1/pages/PatrolPlanPage.vue"),
meta: { title: "巡检记录", tabId: 5, },
},
{
path: "data-report",
name: "DataReport",
component: () => import("@/views/dashboard1/pages/DataReportPage.vue"),
meta: { title: "统计分析", tabId: 6, },
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/login/index.vue"),
meta: { title: "登录" },
},
{
path: "/:pathMatch(.*)*",
redirect: dashboardDefaultPath,
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
const whiteList = ["/login"];
router.beforeEach((to) => {
const hasToken = Boolean(getToken());
if (!hasToken && !whiteList.includes(to.path)) {
return {
path: "/login",
query: { redirect: to.fullPath },
};
}
return true;
});
export function resetRouter() {
// 当前项目暂无动态 addRoute 路由,保留该方法兼容登出流程。
}
export default router;

5
src/store/index.js Normal file
View File

@@ -0,0 +1,5 @@
import { createPinia } from 'pinia'
export function setupStore(app) {
app.use(createPinia())
}

47
src/store/modules/app.js Normal file
View File

@@ -0,0 +1,47 @@
import { defineStore } from 'pinia'
import { nextTick } from 'vue'
import { useDark } from '@vueuse/core'
const sidebarStatusKey = 'sidebar_status'
export const useAppStore = defineStore('app', {
state() {
return {
sidebarStatus: localStorage.getItem(sidebarStatusKey) ? localStorage.getItem(sidebarStatusKey) === '1' : true,
device: 'desktop',
config: {},
reloadFlag: true,
isDark: !useDark(),
}
},
getters: {
sidebar() {
return this.sidebarStatus
}
},
actions: {
async toggleSideBar() {
this.sidebarStatus = !this.sidebarStatus
if (this.sidebarStatus) {
localStorage.setItem(sidebarStatusKey, '1')
} else {
localStorage.setItem(sidebarStatusKey, '0')
}
},
closeSideBar(withoutAnimation) {
localStorage.setItem(sidebarStatusKey, '0')
this.sidebarStatus = false
},
toggleDevice(device) {
this.device = device
},
async reloadPage() {
this.reloadFlag = false
await nextTick()
this.reloadFlag = true
},
toggleDark() {
this.isDark = !this.isDark
},
}
})

112
src/store/modules/user.js Normal file
View File

@@ -0,0 +1,112 @@
import { getToken, removeToken, setToken } from "@/utils/auth.js";
import { getInfo, login as doLogin } from "@/api/user.js";
import { defineStore } from "pinia";
import { resetRouter } from "@/router";
export const useUserStore = defineStore("user", {
state() {
return {
userInfo: {
token: getToken() || "",
id: "",
avatar: "",
name: "",
profile: {},
permissions: [],
dictMap: {},
dictTreeMap: {},
dictIdMap: {},
dynamicAttrs: [],
companyLogStatus: null,
attrIds: [],
regionIds: [],
},
};
},
getters: {
userId() {
return this.userInfo?.id || "";
},
name() {
return this.userInfo?.name || "";
},
avatar() {
return this.userInfo?.profile?.avatar || "";
},
permissions() {
return this.userInfo?.permissions || [];
},
dictMap() {
return this.userInfo?.dictMap || {};
},
dictTreeMap() {
return this.userInfo?.dictTreeMap || {};
},
dictIdMap() {
return this.userInfo?.dictIdMap || {};
},
dynamicAttrs() {
return this.userInfo?.dynamicAttrs || [];
},
companyLogStatus() {
return this.userInfo?.companyLogStatus || 0;
},
attrIds() {
return this.userInfo?.attrIds || [];
},
regionIds() {
return this.userInfo?.regionIds || [];
},
},
actions: {
async login(userInfo) {
const res = await doLogin(userInfo);
const { token, tokenName } = res.data;
this.userInfo.token = token;
setToken(token, tokenName);
return true;
},
async getInfo() {
const res = await getInfo();
const data = res.data;
const {
name,
profile,
permissions,
dynamicAttrs,
attrIds,
companyLogStatus,
regionIds,
} = data;
// 保留当前的 token避免被覆盖
const currentToken = this.userInfo.token || getToken();
this.userInfo = {
token: currentToken,
id: profile.id,
name,
profile,
permissions,
dynamicAttrs,
attrIds,
companyLogStatus,
regionIds,
};
return data;
},
// user logout
async logout() {
await this.resetToken();
await resetRouter();
return true;
},
async resetToken() {
this.userInfo = {
profile: {},
permissions: [],
};
removeToken();
},
},
});

63
src/types/js-modules.d.ts vendored Normal file
View File

@@ -0,0 +1,63 @@
interface Window {
$message: {
error(message: string, options?: Record<string, unknown>): void;
success(message: string, options?: Record<string, unknown>): void;
warning(message: string, options?: Record<string, unknown>): void;
info(message: string, options?: Record<string, unknown>): void;
};
$modal: {
create(options: Record<string, unknown>): unknown;
warning(options: Record<string, unknown>): unknown;
};
}
declare module "@/utils/auth" {
export function getToken(): string | null;
export function getTokenName(): string | null;
export function setToken(tokenValue: string, tokenNameValue?: string): void;
export function removeToken(): void;
}
declare module "@/utils/auth.js" {
export function getToken(): string | null;
export function getTokenName(): string | null;
export function setToken(tokenValue: string, tokenNameValue?: string): void;
export function removeToken(): void;
}
declare module "@/store/modules/user" {
export function useUserStore(): {
login(userInfo: Record<string, unknown>): Promise<boolean>;
logout(): Promise<boolean>;
};
}
declare module "@/store/modules/user.js" {
export function useUserStore(): {
login(userInfo: Record<string, unknown>): Promise<boolean>;
logout(): Promise<boolean>;
};
}
declare module "@/utils/passwordEncrypt.js" {
export function encryptPasswordPayload<T extends Record<string, unknown>>(
payload: T,
fields: Array<keyof T>,
): Promise<T>;
}
declare module "@/utils/request" {
import type { AxiosRequestConfig } from "axios";
export const BASE_URL: string;
const request: <T = unknown>(config: AxiosRequestConfig) => Promise<T>;
export default request;
}
declare module "@/utils/request.js" {
import type { AxiosRequestConfig } from "axios";
export const BASE_URL: string;
const request: <T = unknown>(config: AxiosRequestConfig) => Promise<T>;
export default request;
}

35
src/utils/auth.js Normal file
View File

@@ -0,0 +1,35 @@
const TokenKey = "token";
const TOKEN_NAME_KEY = "tokenName";
let token = null;
let tokenName = null;
export function getToken() {
const storedToken = token || window.localStorage.getItem(TokenKey);
if (!storedToken || storedToken === "null" || storedToken === "undefined") {
token = null;
window.localStorage.removeItem(TokenKey);
return null;
}
token = storedToken;
return token;
}
export function setToken(tokenValue, tokenNameValue) {
token = tokenValue;
tokenName = tokenNameValue || "Authorization";
window.localStorage.setItem(TokenKey, token);
window.localStorage.setItem(TOKEN_NAME_KEY, tokenName);
}
export function removeToken() {
token = null;
tokenName = null;
window.localStorage.removeItem(TokenKey);
window.localStorage.removeItem(TOKEN_NAME_KEY);
}
export function getTokenName() {
tokenName = tokenName || window.localStorage.getItem(TOKEN_NAME_KEY);
return tokenName;
}

View File

@@ -0,0 +1,38 @@
import { JSEncrypt } from 'jsencrypt'
import { getPublicKey } from '@/api/user'
export async function getPasswordPublicKey() {
const res = await getPublicKey()
return res.data
}
export function encryptPassword(plainText, publicKey) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(formatPublicKey(publicKey))
const encrypted = encryptor.encrypt(plainText)
if (!encrypted) {
throw new Error('密码加密失败,请刷新页面后重试')
}
return encrypted
}
export async function encryptPasswordPayload(payload, fields) {
const publicKey = await getPasswordPublicKey()
const nextPayload = { ...payload }
for (const field of fields) {
nextPayload[field] = encryptPassword(payload[field] || '', publicKey)
}
return nextPayload
}
function formatPublicKey(publicKey) {
if (!publicKey) {
return ''
}
if (publicKey.includes('BEGIN PUBLIC KEY')) {
return publicKey
}
const normalized = publicKey.replace(/\s+/g, '')
const lines = normalized.match(/.{1,64}/g) || []
return ['-----BEGIN PUBLIC KEY-----', ...lines, '-----END PUBLIC KEY-----'].join('\n')
}

108
src/utils/request.js Normal file
View File

@@ -0,0 +1,108 @@
import axios from "axios";
import { useUserStore } from "@/store/modules/user";
import { getToken, getTokenName } from "@/utils/auth";
import router from "@/router";
export const BASE_URL = import.meta.env.VITE_APP_BASE_API || "/api";
// create an axios instance
const service = axios.create({
method: "POST",
baseURL: BASE_URL, // url = base url + request url
withCredentials: false, // dont send cookies when cross-domain requests
timeout: 20000, // request timeout
});
// request interceptor
service.interceptors.request.use(
(config) => {
// do something before request is sent
const token = getToken();
if (token) {
config.headers[getTokenName() || "Authorization"] = token; // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config;
},
(error) => {
// do something with request error
console.log(error); // for debug
return Promise.reject(error);
},
);
// response interceptor
service.interceptors.response.use(
(response) => {
if (response.data instanceof Blob) {
return response.data;
}
const res = response.data;
// 状态码不是0, 说明出错了
if (res.code !== 200) {
showApiMessage(res);
if (res.code === 401) {
// to re-login
handleNoLogin();
}
return Promise.reject(res);
} else {
return res;
}
},
(error) => {
console.log(error);
if (error.response && error.response.status === 401) {
if (error.response.data.msg) {
if (error.response.data.msg.includes("权限")) {
window.$message.error(error.response.data.msg, {
duration: 5 * 1000,
});
} else {
handleNoLogin();
}
} else {
handleNoLogin();
}
} else {
window.$message.error(error.message, { duration: 5 * 1000 });
}
return Promise.reject(error);
},
);
export default service;
function showApiMessage(res) {
window.$message.error(res.msg || "error", {
duration: 5 * 1000,
});
}
// 弹窗确认防抖
let noLoginAlert = null;
function handleNoLogin() {
if (noLoginAlert == null) {
noLoginAlert = window.$modal.create({
type: "warning",
preset: "dialog",
title: "登录失效",
content: "您的登录已过期,请重新登录",
positiveText: "确认退出",
maskClosable: false,
closable: false,
onPositiveClick: async () => {
await useUserStore().logout();
router.replace({
path: "/login",
query: { redirect: router.currentRoute.value.path },
});
noLoginAlert = null;
},
onClose: () => (noLoginAlert = null),
});
} else {
console.log("跳过确认退出弹窗");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,37 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, type Component } from "vue"; import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import SwitchPageBtn from "./components/btn"; import SwitchPageBtn from "./components/btn";
import backgroundVideo from "./assets/循环背景动画.mp4"; import backgroundVideo from "./assets/循环背景动画.mp4";
import AlarmManagementPage from "./pages/AlarmManagementPage.vue";
import DataReportPage from "./pages/DataReportPage.vue";
import EquipmentManagementPage from "./pages/EquipmentManagementPage.vue";
import IntegratedCenterPage from "./pages/IntegratedCenterPage.vue";
import PatrolPlanPage from "./pages/PatrolPlanPage.vue";
import VideoCenterPage from "./pages/VideoCenterPage.vue";
const currentTab = ref<number>(1); interface DashboardTab {
id: number;
text: string;
path: string;
}
const tabs = { const route = useRoute();
const router = useRouter();
const tabs: Record<"left" | "right", DashboardTab[]> = {
left: [ left: [
{ id: 1, text: "监管中心" }, { id: 1, text: "监管中心", path: "/dashboard/integrated-center" },
{ id: 2, text: "设备管理" }, { id: 2, text: "设备管理", path: "/dashboard/equipment-management" },
{ id: 3, text: "视频中心" }, { id: 3, text: "视频中心", path: "/dashboard/video-center" },
], ],
right: [ right: [
{ id: 4, text: "告警管理" }, { id: 4, text: "告警管理", path: "/dashboard/alarm-management" },
{ id: 5, text: "巡检记录" }, { id: 5, text: "巡检记录", path: "/dashboard/patrol-plan" },
{ id: 6, text: "统计分析" }, { id: 6, text: "统计分析", path: "/dashboard/data-report" },
], ],
}; };
const pageComponents: Record<number, Component> = { const activeTabId = computed(() => Number(route.meta.tabId) || 1);
1: IntegratedCenterPage,
2: EquipmentManagementPage,
3: VideoCenterPage,
4: AlarmManagementPage,
5: PatrolPlanPage,
6: DataReportPage,
};
const currentViewComponent = computed<Component>( function switchPage(tab: DashboardTab) {
() => pageComponents[currentTab.value] ?? IntegratedCenterPage, if (route.path !== tab.path) {
); router.push(tab.path);
}
function switchPage(tab: number) {
currentTab.value = tab;
} }
</script> </script>
@@ -58,27 +51,27 @@ function switchPage(tab: number) {
<SwitchPageBtn <SwitchPageBtn
v-for="tab in tabs.left" v-for="tab in tabs.left"
:key="tab.id" :key="tab.id"
:active="currentTab === tab.id" :active="activeTabId === tab.id"
:text="tab.text" :text="tab.text"
direction="left" direction="left"
@click="switchPage(tab.id)" @click="switchPage(tab)"
/> />
</div> </div>
<div class="right"> <div class="right">
<SwitchPageBtn <SwitchPageBtn
v-for="tab in tabs.right" v-for="tab in tabs.right"
:key="tab.id" :key="tab.id"
:active="currentTab === tab.id" :active="activeTabId === tab.id"
:text="tab.text" :text="tab.text"
direction="right" direction="right"
@click="switchPage(tab.id)" @click="switchPage(tab)"
/> />
</div> </div>
</div> </div>
<div class="content-stage"> <div class="content-stage">
<KeepAlive> <RouterView v-slot="{ Component, route }">
<component :is="currentViewComponent" /> <component :is="Component" :key="route.fullPath" />
</KeepAlive> </RouterView>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { NImage } from "naive-ui";
import { computed, onMounted, ref } from "vue";
import {
getAlarmPage,
type RosInspectionRecordLineImage,
} from "@/api/alarm";
import { BASE_URL } from "@/utils/request";
import { getRecentAlertPhoto } from "../shared/alertPhotos"; import { getRecentAlertPhoto } from "../shared/alertPhotos";
interface AlarmRecord { interface AlarmRecord {
@@ -7,80 +13,55 @@ interface AlarmRecord {
type: string; type: string;
robot: string; robot: string;
photo: string; photo: string;
status: "待处理"; status: RecognitionStatusText;
time: string; time: string;
position: string;
result: string;
warning: boolean;
} }
const alarms: AlarmRecord[] = [ type RecognitionStatusText = "待执行" | "执行中" | "执行成功" | "执行失败" | "未知状态";
{
id: "AL-001",
type: "线缆破损",
robot: "巡检机器人 R1",
photo: getRecentAlertPhoto("线缆破损"),
status: "待处理",
time: "2026-06-23 10:24:18",
},
{
id: "AL-002",
type: "局部高温",
robot: "巡检机器人 Q1",
photo: getRecentAlertPhoto("局部高温"),
status: "待处理",
time: "2026-06-23 10:18:42",
},
{
id: "AL-003",
type: "墙体裂缝",
robot: "巡检机器人 N3",
photo: getRecentAlertPhoto("墙体裂缝"),
status: "待处理",
time: "2026-06-23 09:56:11",
},
{
id: "AL-004",
type: "水渍渗漏",
robot: "巡检机器人 R2",
photo: getRecentAlertPhoto("局部高温"),
status: "待处理",
time: "2026-06-23 09:20:36",
},
{
id: "AL-005",
type: "应急门开启异常",
robot: "巡检机器人 N1",
photo: getRecentAlertPhoto("线缆破损"),
status: "待处理",
time: "2026-06-23 08:42:57",
},
];
const keyword = ref(""); interface AlarmPageResult {
const pageSize = 4; total?: number;
data?: RosInspectionRecordLineImage[];
}
interface AlarmPageResponse {
data?: AlarmPageResult;
}
const recognizeTypeMap: Record<number, string> = {
100: "局部高温",
1: "电缆表面破损",
2: "桥架断裂",
3: "隧道积水",
4: "墙体裂缝",
};
const recognitionStatusMap: Record<number, RecognitionStatusText> = {
0: "待执行",
1: "执行中",
2: "执行成功",
3: "执行失败",
};
const alarmNo = ref("");
const recognizeType = ref("");
const startTime = ref("");
const endTime = ref("");
const alarms = ref<AlarmRecord[]>([]);
const totalAlarms = ref(0);
const listLoading = ref(false);
const listError = ref("");
const pageSize = 10;
const currentPage = ref(1); const currentPage = ref(1);
const filteredAlarms = computed(() => {
const search = keyword.value.trim().toLowerCase();
if (!search) {
return alarms;
}
return alarms.filter((alarm) =>
[alarm.id, alarm.type, alarm.robot, alarm.status]
.join(" ")
.toLowerCase()
.includes(search),
);
});
const totalPages = computed(() => const totalPages = computed(() =>
Math.max(1, Math.ceil(filteredAlarms.value.length / pageSize)), Math.max(1, Math.ceil(totalAlarms.value / pageSize)),
); );
const pagedAlarms = computed(() => { const pagedAlarms = computed(() => alarms.value);
const start = (currentPage.value - 1) * pageSize;
return filteredAlarms.value.slice(start, start + pageSize);
});
const visiblePages = computed(() => { const visiblePages = computed(() => {
const pages: number[] = []; const pages: number[] = [];
@@ -95,20 +76,124 @@ const visiblePages = computed(() => {
}); });
function getAlarmStatusClass(status: AlarmRecord["status"]) { function getAlarmStatusClass(status: AlarmRecord["status"]) {
const classMap = { const classMap: Record<AlarmRecord["status"], string> = {
处理: "alarm-status-handling", 执行: "alarm-status-pending",
执行中: "alarm-status-handling",
执行成功: "alarm-status-finished",
执行失败: "alarm-status-false",
未知状态: "alarm-status-false",
}; };
return classMap[status]; return classMap[status];
} }
function setPage(page: number) { function setPage(page: number) {
currentPage.value = page; currentPage.value = Math.min(Math.max(page, 1), totalPages.value);
fetchAlarmList();
} }
function handleSearch() { function handleSearch() {
currentPage.value = 1; currentPage.value = 1;
fetchAlarmList();
} }
// 请求告警分页列表并更新表格数据
async function fetchAlarmList() {
listLoading.value = true;
listError.value = "";
try {
const response = (await getAlarmPage({
page: currentPage.value,
limit: pageSize,
query: {
recognizeType: normalizeRecognizeType(recognizeType.value),
startTime: normalizeDateTime(startTime.value),
endTime: normalizeDateTime(endTime.value),
id: alarmNo.value.trim() || undefined,
},
})) as AlarmPageResponse;
const result = response.data;
const rows = result?.data ?? [];
alarms.value = rows.map((record, index) => normalizeAlarmRecord(record, index));
totalAlarms.value = result?.total ?? rows.length;
} catch (error) {
listError.value = "近期告警加载失败";
alarms.value = [];
totalAlarms.value = 0;
console.error(error);
} finally {
listLoading.value = false;
}
}
// 将后端告警记录转换为页面表格展示结构
function normalizeAlarmRecord(record: RosInspectionRecordLineImage, index: number): AlarmRecord {
const type = getRecognizeTypeText(record.recognizeType);
return {
id: record.id || record.recordActionId || record.recordId || `AL-${index + 1}`,
type,
robot: getCarName(record),
photo: buildFileAccessUrl(record.imageUrl || "") || getRecentAlertPhoto(type),
status: getRecognitionStatusText(record.recognitionStatus),
time: record.captureTime || record.createTime || "--",
position: formatPosition(record),
result: record.warningValue || record.recognitionResult || record.failReason || "--",
warning: Boolean(record.warning),
};
}
function getCarName(record: RosInspectionRecordLineImage) {
return record.car?.name || record.car?.deviceNo || record.car?.deviceKey || record.carId || "未知设备";
}
function getRecognizeTypeText(type?: number) {
return type ? recognizeTypeMap[type] ?? "未知告警" : "未知告警";
}
function getRecognitionStatusText(status?: number) {
return typeof status === "number" ? recognitionStatusMap[status] ?? "未知状态" : "未知状态";
}
// 相对文件地址通过接口代理访问,避免开发环境直接请求后端静态路径失败
function buildFileAccessUrl(fileUrl: string) {
if (
!fileUrl ||
/^(https?:)?\/\//i.test(fileUrl) ||
fileUrl.startsWith("blob:") ||
fileUrl.startsWith("data:")
) {
return fileUrl;
}
if (fileUrl === BASE_URL || fileUrl.startsWith(`${BASE_URL}/`)) {
return fileUrl;
}
return `${BASE_URL}${fileUrl.startsWith("/") ? fileUrl : `/${fileUrl}`}`;
}
function normalizeDateTime(value: string) {
return value ? value.replace("T", " ") : undefined;
}
function normalizeRecognizeType(value: string) {
return value ? Number(value) : undefined;
}
function formatPosition(record: RosInspectionRecordLineImage) {
if (typeof record.poseX !== "number" || typeof record.poseY !== "number") {
return "--";
}
return `X:${record.poseX.toFixed(2)} Y:${record.poseY.toFixed(2)}`;
}
onMounted(() => {
fetchAlarmList();
});
</script> </script>
<template> <template>
@@ -117,16 +202,35 @@ function handleSearch() {
<section class="page-card management-table-card"> <section class="page-card management-table-card">
<div class="management-toolbar"> <div class="management-toolbar">
<div class="toolbar-left"> <div class="toolbar-left">
<button class="page-action-btn ghost" type="button">导出告警</button> <select v-model="recognizeType" class="toolbar-select">
<option value="">全部告警类型</option>
<option value="100">局部高温</option>
<option value="1">电缆表面破损</option>
<option value="2">桥架断裂</option>
<option value="3">隧道积水</option>
<option value="4">墙体裂缝</option>
</select>
<input <input
v-model="keyword" v-model="startTime"
class="toolbar-input" class="toolbar-input alarm-time-input"
placeholder="搜索告警编号 / 告警类型 / 报警机器人" type="datetime-local"
type="text"
@input="handleSearch"
/> />
<input
v-model="endTime"
class="toolbar-input alarm-time-input"
type="datetime-local"
/>
<input
v-model="alarmNo"
class="toolbar-input"
placeholder="搜索告警编号"
type="text"
@keyup.enter="handleSearch"
/>
<button class="page-action-btn primary" type="button" @click="handleSearch">搜索</button>
<button class="page-action-btn ghost" type="button" @click="fetchAlarmList">刷新</button>
</div> </div>
<div class="toolbar-right"> {{ filteredAlarms.length }} 条告警记录</div> <div class="toolbar-right"> {{ totalAlarms }} 条告警记录</div>
</div> </div>
<div class="table-shell"> <div class="table-shell">
@@ -139,16 +243,33 @@ function handleSearch() {
<th>取证照片</th> <th>取证照片</th>
<th>案件状态</th> <th>案件状态</th>
<th>告警时间</th> <th>告警时间</th>
<th>操作</th> <th>位置</th>
<th>识别结果</th>
<th>是否预警</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="alarm in pagedAlarms" :key="alarm.id"> <tr v-if="listLoading">
<td class="table-empty" colspan="9">近期告警加载中...</td>
</tr>
<tr v-else-if="listError">
<td class="table-empty" colspan="9">{{ listError }}</td>
</tr>
<tr v-else-if="!pagedAlarms.length">
<td class="table-empty" colspan="9">暂无告警记录</td>
</tr>
<tr v-for="alarm in pagedAlarms" v-else :key="alarm.id">
<td>{{ alarm.id }}</td> <td>{{ alarm.id }}</td>
<td>{{ alarm.type }}</td> <td>{{ alarm.type }}</td>
<td>{{ alarm.robot }}</td> <td>{{ alarm.robot }}</td>
<td> <td>
<img class="evidence-photo" :src="alarm.photo" :alt="`${alarm.type}取证照片`" /> <NImage
class="evidence-photo"
:src="alarm.photo"
:alt="`${alarm.type}取证照片`"
object-fit="contain"
:previewed-img-props="{ style: { maxWidth: '82vw', maxHeight: '82vh', objectFit: 'contain' } }"
/>
</td> </td>
<td> <td>
<span :class="['table-status', getAlarmStatusClass(alarm.status)]"> <span :class="['table-status', getAlarmStatusClass(alarm.status)]">
@@ -156,11 +277,9 @@ function handleSearch() {
</span> </span>
</td> </td>
<td>{{ alarm.time }}</td> <td>{{ alarm.time }}</td>
<td> <td>{{ alarm.position }}</td>
<div class="table-actions"> <td>{{ alarm.result }}</td>
<button class="danger" type="button">删除</button> <td>{{ alarm.warning ? "是" : "否" }}</td>
</div>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -169,7 +288,7 @@ function handleSearch() {
<div class="pagination-bar"> <div class="pagination-bar">
<button <button
class="pagination-btn" class="pagination-btn"
:disabled="currentPage === 1" :disabled="currentPage === 1 || listLoading"
type="button" type="button"
@click="setPage(currentPage - 1)" @click="setPage(currentPage - 1)"
> >
@@ -179,6 +298,7 @@ function handleSearch() {
v-for="page in visiblePages" v-for="page in visiblePages"
:key="page" :key="page"
:class="['pagination-btn', { active: currentPage === page }]" :class="['pagination-btn', { active: currentPage === page }]"
:disabled="listLoading"
type="button" type="button"
@click="setPage(page)" @click="setPage(page)"
> >
@@ -186,7 +306,7 @@ function handleSearch() {
</button> </button>
<button <button
class="pagination-btn" class="pagination-btn"
:disabled="currentPage === totalPages" :disabled="currentPage === totalPages || listLoading"
type="button" type="button"
@click="setPage(currentPage + 1)" @click="setPage(currentPage + 1)"
> >
@@ -197,3 +317,9 @@ function handleSearch() {
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.alarm-time-input {
width: 190px;
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +1,77 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, nextTick, ref } from "vue"; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import monitorVideo from "../assets/循环背景动画.mp4"; import { getRosCarList } from "@/api/rosCar";
type GridMode = 1 | 4 | 9; type GridMode = 1 | 4 | 9;
type CameraType = "可见光" | "红外";
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
interface RosCarRecord {
id?: string;
deviceNo?: string;
name?: string;
batteryLevel?: number;
status?: number;
projectName?: string;
cameraWebrtcUrl?: string;
cameraNo?: string;
cameraWebrtcUrl2?: string;
cameraNo2?: string;
}
interface RosCarListResult {
total?: number;
data?: RosCarRecord[];
}
interface RosCarListResponse {
data?: RosCarListResult;
}
interface CameraItem { interface CameraItem {
id: string; id: string;
name: string; name: string;
type: string; type: CameraType;
status: string; status: DeviceStatus;
url: string;
cameraNo?: string;
} }
interface DeviceNode { interface DeviceNode {
id: string; id: string;
deviceNo?: string;
name: string; name: string;
project: string;
status: DeviceStatus;
battery: string;
cameras: CameraItem[]; cameras: CameraItem[];
} }
interface ProjectNode {
id: string;
name: string;
devices: DeviceNode[];
}
interface ProjectCameraItem extends CameraItem { interface ProjectCameraItem extends CameraItem {
deviceId: string; deviceId: string;
deviceName: string; deviceName: string;
project: string;
} }
const projectData: ProjectNode = { interface SlotStreamState {
id: "project-1", loading: boolean;
name: "西江隧道", error: string;
devices: [ }
{
id: "ROBOT-001",
name: "巡检机器人 R1",
cameras: [
{ id: "CAM-001", name: "白光镜头", type: "白光", status: "在线" },
{ id: "CAM-002", name: "红外镜头", type: "红外", status: "在线" },
{ id: "CAM-003", name: "云台辅助镜头", type: "辅助", status: "在线" },
],
},
{
id: "ROBOT-002",
name: "巡检机器人 R2",
cameras: [
{ id: "CAM-004", name: "白光镜头", type: "白光", status: "在线" },
{ id: "CAM-005", name: "红外镜头", type: "红外", status: "在线" },
],
},
],
};
const defaultProject = "视频中心";
const gridMode = ref<GridMode>(4); const gridMode = ref<GridMode>(4);
const activeCameraId = ref(projectData.devices[0].cameras[0].id); const activeSlotIndex = ref(0);
const videoSlots = ref<Array<ProjectCameraItem | null>>([]);
const slotVideoRefs = ref<Array<HTMLVideoElement | null>>([]);
const slotStreamStates = ref<SlotStreamState[]>([]);
const videoGridBoardRef = ref<HTMLElement | null>(null);
const videoGridBoardStyle = ref<Record<string, string>>({});
const slotPeerConnections = new Map<number, RTCPeerConnection>();
const slotRequestIds = new Map<number, number>();
let videoGridResizeObserver: ResizeObserver | null = null;
const devices = ref<DeviceNode[]>([]);
const listLoading = ref(false);
const listError = ref("");
const playbackDialogVisible = ref(false); const playbackDialogVisible = ref(false);
const playbackStartAt = ref("2026-06-23T14:20"); const playbackStartAt = ref("2026-06-23T14:20");
const playbackEndAt = ref("2026-06-23T14:50"); const playbackEndAt = ref("2026-06-23T14:50");
@@ -61,61 +79,351 @@ const playbackProgress = ref(0);
const playbackPaused = ref(true); const playbackPaused = ref(true);
const playbackPlayerRef = ref<HTMLVideoElement | null>(null); const playbackPlayerRef = ref<HTMLVideoElement | null>(null);
const currentProject = computed(() => projectData); const slotCountMap: Record<GridMode, number> = {
const projectCameras = computed<ProjectCameraItem[]>(() =>
currentProject.value.devices.flatMap((device) =>
device.cameras.map((camera) => ({
...camera,
deviceId: device.id,
deviceName: device.name,
})),
),
);
const activeCamera = computed(
() =>
projectCameras.value.find((camera) => camera.id === activeCameraId.value) ??
projectCameras.value[0],
);
const orderedCameras = computed(() => {
const selected = activeCamera.value;
return projectCameras.value
.filter((camera) => camera.id !== selected.id)
.reduce<ProjectCameraItem[]>((list, camera) => {
list.push(camera);
return list;
}, [selected]);
});
const visibleCameras = computed(() => {
const countMap: Record<GridMode, number> = {
1: 1, 1: 1,
4: 4, 4: 4,
9: 9, 9: 9,
}; };
const targetCount = countMap[gridMode.value];
const cameras = orderedCameras.value;
return Array.from({ length: targetCount }, (_, index) => cameras[index] ?? null); const currentProjectName = computed(() => devices.value[0]?.project || defaultProject);
const playableCameras = computed<ProjectCameraItem[]>(() =>
devices.value.flatMap((device) =>
device.cameras.map((camera) => ({
...camera,
deviceId: device.id,
deviceName: device.name,
project: device.project,
})),
),
);
const cameraTotal = computed(() => playableCameras.value.length);
const visibleSlots = computed(() => {
const targetCount = slotCountMap[gridMode.value];
return Array.from(
{ length: targetCount },
(_, index) => videoSlots.value[index] ?? null,
);
}); });
const activeCamera = computed(() => visibleSlots.value[activeSlotIndex.value] ?? null);
const activeSlotLabel = computed(() => `画面 ${activeSlotIndex.value + 1}`);
const playbackRangeLabel = computed(() => { const playbackRangeLabel = computed(() => {
const startLabel = playbackStartAt.value.replace("T", " "); const startLabel = playbackStartAt.value.replace("T", " ");
const endLabel = playbackEndAt.value.replace("T", " "); const endLabel = playbackEndAt.value.replace("T", " ");
return `${startLabel} - ${endLabel}`; return `${startLabel} - ${endLabel}`;
}); });
function selectCamera(cameraId: string) { // 根据视频墙实际可用宽高计算宫格尺寸,保证每个画面都是 16:9 且不会溢出卡片
activeCameraId.value = cameraId; function updateVideoGridSize() {
const board = videoGridBoardRef.value;
if (!board) {
return;
} }
function setGridMode(mode: GridMode) { const columns = Math.sqrt(gridMode.value);
const rows = columns;
const gap = 8;
const availableWidth = board.clientWidth - gap * (columns - 1);
const availableHeight = board.clientHeight - gap * (rows - 1);
const cellWidthByContainerWidth = availableWidth / columns;
const cellWidthByContainerHeight = (availableHeight / rows) * (16 / 9);
const cellWidth = Math.max(0, Math.min(cellWidthByContainerWidth, cellWidthByContainerHeight));
const cellHeight = cellWidth * (9 / 16);
videoGridBoardStyle.value = {
gridTemplateColumns: `repeat(${columns}, ${cellWidth}px)`,
gridAutoRows: `${cellHeight}px`,
};
}
// 根据当前宫格数量补齐画面槽位,切换布局时保留已播放的视频
function ensureVideoSlotCount() {
const targetCount = slotCountMap[gridMode.value];
const previousCount = videoSlots.value.length;
for (let index = targetCount; index < previousCount; index += 1) {
stopSlotStream(index);
}
videoSlots.value = Array.from(
{ length: targetCount },
(_, index) => videoSlots.value[index] ?? null,
);
slotVideoRefs.value = Array.from(
{ length: targetCount },
(_, index) => slotVideoRefs.value[index] ?? null,
);
slotStreamStates.value = Array.from(
{ length: targetCount },
(_, index) => slotStreamStates.value[index] ?? { loading: false, error: "" },
);
if (activeSlotIndex.value >= targetCount) {
activeSlotIndex.value = targetCount - 1;
}
}
// 将接口的小车状态码转换为页面展示状态
function normalizeDeviceStatus(status?: number): DeviceStatus {
const statusMap: Record<number, DeviceStatus> = {
0: "空闲",
1: "巡检",
2: "离线",
3: "充电中",
};
return statusMap[status ?? 2] ?? "离线";
}
// 根据小车视频地址生成可播放的相机节点
function buildCameraItem(
car: RosCarRecord,
deviceId: string,
type: CameraType,
url?: string,
cameraNo?: string,
): CameraItem | null {
if (!url) {
return null;
}
return {
id: `${deviceId}-${type}`,
name: `${type}相机`,
type,
status: normalizeDeviceStatus(car.status),
url,
cameraNo,
};
}
// 将设备列表接口的小车记录转换为左侧机器人树节点
function normalizeDeviceNode(car: RosCarRecord, index: number): DeviceNode {
const deviceId = car.id ?? `ROBOT-${index + 1}`;
const cameras = [
buildCameraItem(car, deviceId, "可见光", car.cameraWebrtcUrl, car.cameraNo),
buildCameraItem(car, deviceId, "红外", car.cameraWebrtcUrl2, car.cameraNo2),
].filter((camera): camera is CameraItem => Boolean(camera));
return {
id: deviceId,
deviceNo: car.deviceNo,
name: car.name || car.deviceNo || `机器人 ${index + 1}`,
project: car.projectName || defaultProject,
status: normalizeDeviceStatus(car.status),
battery: typeof car.batteryLevel === "number" ? `${car.batteryLevel}%` : "--",
cameras,
};
}
// 请求设备列表接口,加载左侧机器人及其可见光/红外相机
async function fetchDeviceList() {
listLoading.value = true;
listError.value = "";
try {
const response = (await getRosCarList({
page: 1,
limit: 999,
query: {},
})) as RosCarListResponse;
const rows = response.data?.data ?? [];
devices.value = rows.map((car, index) => normalizeDeviceNode(car, index));
} catch (error) {
listError.value = "设备列表加载失败";
console.error(error);
} finally {
listLoading.value = false;
}
}
// 选中右侧视频墙中的目标画面
function selectVideoSlot(index: number) {
activeSlotIndex.value = index;
}
// 停止全部画面的 WebRTC 拉流,用于页面卸载时释放资源
function stopAllSlotStreams() {
videoSlots.value.forEach((_, index) => stopSlotStream(index));
}
// 停止指定画面的 WebRTC 拉流并释放播放器资源
function stopSlotStream(index: number, clearState = true) {
slotRequestIds.set(index, (slotRequestIds.get(index) ?? 0) + 1);
if (clearState) {
slotStreamStates.value[index] = { loading: false, error: "" };
}
const peerConnection = slotPeerConnections.get(index);
if (peerConnection) {
peerConnection.close();
slotPeerConnections.delete(index);
}
const video = slotVideoRefs.value[index];
if (video) {
video.srcObject = null;
}
}
// 将设备返回的 webrtc 播放地址转换为 ZLMediaKit WebRTC 播放接口地址
function buildWebrtcApiUrl(streamUrl: string) {
const normalizedUrl = streamUrl.replace(/^webrtc:\/\//, "https://");
const url = new URL(normalizedUrl);
return `${url.origin}${url.pathname}${url.search}`;
}
// 等待本地 ICE 候选收集完成,确保发给流媒体服务的 offer 信息完整
function waitForIceGatheringComplete(peerConnection: RTCPeerConnection) {
if (peerConnection.iceGatheringState === "complete") {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
const handleIceGatheringStateChange = () => {
if (peerConnection.iceGatheringState === "complete") {
peerConnection.removeEventListener("icegatheringstatechange", handleIceGatheringStateChange);
resolve();
}
};
peerConnection.addEventListener("icegatheringstatechange", handleIceGatheringStateChange);
});
}
// 收集视频墙每个画面对应的 video 元素,供 WebRTC ontrack 写入媒体流
function setSlotVideoRef(element: Element | { $el?: Element } | null, index: number) {
const target = element instanceof Element ? element : element?.$el;
slotVideoRefs.value[index] = target instanceof HTMLVideoElement ? target : null;
}
// 通过 WebRTC offer/answer 将指定相机拉流到选中的画面槽位
async function playSlotStream(index: number, camera: ProjectCameraItem) {
stopSlotStream(index);
if (!window.RTCPeerConnection) {
slotStreamStates.value[index] = { loading: false, error: "当前浏览器不支持 WebRTC 播放" };
return;
}
const video = slotVideoRefs.value[index];
if (!video) {
slotStreamStates.value[index] = { loading: false, error: "播放器未准备好" };
return;
}
const requestId = slotRequestIds.get(index) ?? 0;
const peerConnection = new RTCPeerConnection();
slotPeerConnections.set(index, peerConnection);
slotStreamStates.value[index] = { loading: true, error: "" };
peerConnection.addTransceiver("video", { direction: "recvonly" });
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.ontrack = (event) => {
if (requestId !== slotRequestIds.get(index)) {
return;
}
video.srcObject = event.streams[0];
};
try {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
await waitForIceGatheringComplete(peerConnection);
const response = await fetch(buildWebrtcApiUrl(camera.url), {
method: "POST",
headers: {
"Content-Type": "application/sdp",
},
body: peerConnection.localDescription?.sdp ?? offer.sdp,
});
if (!response.ok) {
throw new Error(`拉流失败:${response.status}`);
}
const responseText = await response.text();
let answer = responseText;
try {
const responseData = JSON.parse(responseText) as { code?: number; sdp?: string; msg?: string };
if (responseData.code && responseData.code !== 0) {
throw new Error(responseData.msg || "视频流拉取失败");
}
answer = responseData.sdp || responseText;
} catch (error) {
if (responseText.trim().startsWith("{")) {
throw error;
}
}
if (requestId !== slotRequestIds.get(index)) {
return;
}
await peerConnection.setRemoteDescription({
type: "answer",
sdp: answer,
});
} catch (error) {
if (requestId === slotRequestIds.get(index)) {
slotStreamStates.value[index] = {
loading: false,
error: error instanceof Error ? error.message : "视频流拉取失败",
};
stopSlotStream(index, false);
}
} finally {
if (requestId === slotRequestIds.get(index)) {
slotStreamStates.value[index] = {
...slotStreamStates.value[index],
loading: false,
};
}
}
}
// 关闭指定画面并释放对应 WebRTC 拉流资源
function closeVideoSlot(index: number) {
stopSlotStream(index);
videoSlots.value[index] = null;
if (activeSlotIndex.value === index) {
activeSlotIndex.value = index;
}
}
// 将左侧相机播放到当前选中的右侧画面
async function playCamera(camera: ProjectCameraItem) {
const targetIndex = activeSlotIndex.value;
videoSlots.value[targetIndex] = camera;
await nextTick();
await playSlotStream(targetIndex, camera);
}
// 切换视频墙宫格布局
async function setGridMode(mode: GridMode) {
gridMode.value = mode; gridMode.value = mode;
await nextTick();
updateVideoGridSize();
} }
// 同步录像回放播放器进度和暂停状态
function syncPlaybackProgress() { function syncPlaybackProgress() {
const video = playbackPlayerRef.value; const video = playbackPlayerRef.value;
@@ -130,6 +438,7 @@ function syncPlaybackProgress() {
playbackPaused.value = video.paused; playbackPaused.value = video.paused;
} }
// 从头开始播放当前回放视频
function startPlayback() { function startPlayback() {
const video = playbackPlayerRef.value; const video = playbackPlayerRef.value;
@@ -143,7 +452,12 @@ function startPlayback() {
syncPlaybackProgress(); syncPlaybackProgress();
} }
// 打开当前画面的录像回放弹窗
async function openPlaybackDialog() { async function openPlaybackDialog() {
if (!activeCamera.value) {
return;
}
playbackDialogVisible.value = true; playbackDialogVisible.value = true;
playbackProgress.value = 0; playbackProgress.value = 0;
playbackPaused.value = true; playbackPaused.value = true;
@@ -152,12 +466,14 @@ async function openPlaybackDialog() {
startPlayback(); startPlayback();
} }
// 关闭录像回放弹窗并暂停播放器
function closePlaybackDialog() { function closePlaybackDialog() {
playbackPlayerRef.value?.pause(); playbackPlayerRef.value?.pause();
playbackDialogVisible.value = false; playbackDialogVisible.value = false;
playbackPaused.value = true; playbackPaused.value = true;
} }
// 切换录像回放的播放和暂停状态
function togglePlaybackPause() { function togglePlaybackPause() {
const video = playbackPlayerRef.value; const video = playbackPlayerRef.value;
@@ -174,6 +490,7 @@ function togglePlaybackPause() {
syncPlaybackProgress(); syncPlaybackProgress();
} }
// 按秒数快进或快退录像回放
function seekPlayback(offsetSeconds: number) { function seekPlayback(offsetSeconds: number) {
const video = playbackPlayerRef.value; const video = playbackPlayerRef.value;
@@ -187,6 +504,7 @@ function seekPlayback(offsetSeconds: number) {
syncPlaybackProgress(); syncPlaybackProgress();
} }
// 根据拖动条位置更新录像回放播放进度
function updatePlaybackProgress() { function updatePlaybackProgress() {
const video = playbackPlayerRef.value; const video = playbackPlayerRef.value;
@@ -201,32 +519,83 @@ function updatePlaybackProgress() {
video.currentTime = (playbackProgress.value / 100) * duration; video.currentTime = (playbackProgress.value / 100) * duration;
} }
watch(gridMode, async () => {
ensureVideoSlotCount();
await nextTick();
updateVideoGridSize();
}, { immediate: true });
onMounted(() => {
void fetchDeviceList();
updateVideoGridSize();
if (videoGridBoardRef.value) {
videoGridResizeObserver = new ResizeObserver(updateVideoGridSize);
videoGridResizeObserver.observe(videoGridBoardRef.value);
}
});
onBeforeUnmount(() => {
videoGridResizeObserver?.disconnect();
stopAllSlotStreams();
});
</script> </script>
<template> <template>
<div class="page-shell video-center-page video-center-shell"> <div class="page-shell video-center-page video-center-shell">
<div class="video-center-layout"> <div class="video-center-layout">
<aside class="page-card video-tree-card"> <aside class="page-card video-tree-card">
<div class="page-card-title">摄像头列表</div> <div class="page-card-title">机器人列表</div>
<div class="tree-panel"> <div class="tree-panel">
<div class="tree-panel-project"> <!-- <div class="tree-panel-project">
<div class="tree-panel-project-name">{{ currentProject.name }}</div> <div class="tree-panel-project-name">{{ currentProjectName }}</div>
<div class="tree-panel-project-meta"> {{ projectCameras.length }} 路摄像头</div> <div class="tree-panel-project-meta">
{{ devices.length }} 台机器人 / {{ cameraTotal }} 路视频
</div> </div>
<div class="tree-device-list"> <div class="tree-panel-project-meta">当前目标{{ activeSlotLabel }}</div>
</div> -->
<div v-if="listLoading" class="tree-loading">设备列表加载中...</div>
<div v-else-if="listError" class="tree-error">{{ listError }}</div>
<div v-else class="tree-device-list">
<div v-if="!devices.length" class="tree-list-empty">暂无机器人</div>
<div v-for="device in devices" :key="device.id" class="tree-robot-node">
<div class="tree-robot-header">
<span class="tree-device-name">{{ device.name }}</span>
<span class="tree-status-line">{{ device.status }} / 电量 {{ device.battery }}</span>
</div>
<div class="tree-device-id">
{{ device.deviceNo || device.id }} / {{ device.project }}
</div>
<div v-if="device.cameras.length" class="tree-camera-list">
<button <button
v-for="camera in projectCameras" v-for="camera in device.cameras"
:key="camera.id" :key="camera.id"
:class="['tree-device-btn', { active: activeCameraId === camera.id }]" :class="[
'tree-device-btn',
{ active: activeCamera?.id === camera.id },
]"
type="button" type="button"
@click="selectCamera(camera.id)" @click="playCamera({ ...camera, deviceId: device.id, deviceName: device.name, project: device.project })"
> >
<span class="tree-device-name">{{ camera.name }}</span> <span class="tree-device-name">{{ camera.name }}</span>
<span class="tree-device-id">{{ camera.deviceName }} / {{ camera.id }}</span> <span class="tree-device-id">
{{ camera.cameraNo || camera.type }} / {{ camera.status }}
</span>
<span class="tree-device-action">播放到 {{ activeSlotLabel }}</span>
</button> </button>
</div> </div>
<div v-else class="tree-list-empty">暂无视频地址</div>
</div>
</div>
<div class="tree-panel-footer"> <div class="tree-panel-footer">
<button class="tree-playback-btn" type="button" @click="openPlaybackDialog"> <button
class="tree-playback-btn"
type="button"
:disabled="!activeCamera"
@click="openPlaybackDialog"
>
录像回放 录像回放
</button> </button>
</div> </div>
@@ -238,8 +607,10 @@ function updatePlaybackProgress() {
<div> <div>
<div class="page-card-title">视频播放</div> <div class="page-card-title">视频播放</div>
<div class="video-wall-meta"> <div class="video-wall-meta">
{{ currentProject.name }} / {{ activeCamera.name }} {{ activeSlotLabel }} /
{{ activeCamera ? `${activeCamera.deviceName} - ${activeCamera.name}` : "未播放" }}
</div> </div>
<div class="video-wall-meta">先选中右侧画面再从左侧点击相机播放</div>
</div> </div>
<div class="grid-switch"> <div class="grid-switch">
<button <button
@@ -254,26 +625,54 @@ function updatePlaybackProgress() {
</div> </div>
</div> </div>
<div :class="['video-grid-board', `grid-${gridMode}`]">
<div <div
v-for="(camera, index) in visibleCameras" ref="videoGridBoardRef"
:key="camera?.id ?? `empty-${index}`" :class="['video-grid-board', `grid-${gridMode}`]"
class="video-grid-cell" :style="videoGridBoardStyle"
>
<div
v-for="(camera, index) in visibleSlots"
:key="camera?.id ? `${camera.id}-${index}` : `empty-${index}`"
:class="[
'video-grid-cell',
{ active: activeSlotIndex === index, empty: !camera },
]"
@click="selectVideoSlot(index)"
> >
<template v-if="camera"> <template v-if="camera">
<video autoplay loop muted playsinline :src="monitorVideo"></video> <video
:ref="(element) => setSlotVideoRef(element, index)"
autoplay
muted
playsinline
></video>
<div class="video-cell-top"> <div class="video-cell-top">
<span class="cell-badge">LIVE</span> <button
<span class="cell-type">{{ camera.type }}</span> class="video-close-btn"
type="button"
title="关闭画面"
@click.stop="closeVideoSlot(index)"
>
×
</button>
</div> </div>
<div class="video-cell-bottom"> <div class="video-cell-bottom">
<div>
<div class="cell-name">{{ camera.name }}</div> <div class="cell-name">{{ camera.name }}</div>
<div class="cell-meta">{{ camera.deviceName }} / {{ camera.status }}</div> <div class="cell-meta">{{ camera.deviceName }} / {{ camera.status }}</div>
</div> </div>
<div class="cell-meta">{{ activeSlotIndex === index ? "已选中" : `画面 ${index + 1}` }}</div>
</div>
<div v-if="slotStreamStates[index]?.error" class="video-empty stream-message">
{{ slotStreamStates[index]?.error }}
</div>
<div v-else-if="slotStreamStates[index]?.loading" class="video-empty stream-message">
视频流连接中...
</div>
</template> </template>
<template v-else> <template v-else>
<div class="video-empty"> <div class="video-empty">
<span>未分配画面</span> <span>{{ activeSlotIndex === index ? "已选中,点击左侧相机播放" : "点击选中画面" }}</span>
</div> </div>
</template> </template>
</div> </div>
@@ -314,7 +713,6 @@ function updatePlaybackProgress() {
<div class="playback-preview"> <div class="playback-preview">
<video <video
ref="playbackPlayerRef" ref="playbackPlayerRef"
:src="monitorVideo"
playsinline playsinline
@ended="syncPlaybackProgress" @ended="syncPlaybackProgress"
@loadedmetadata="syncPlaybackProgress" @loadedmetadata="syncPlaybackProgress"
@@ -322,6 +720,7 @@ function updatePlaybackProgress() {
@play="syncPlaybackProgress" @play="syncPlaybackProgress"
@timeupdate="syncPlaybackProgress" @timeupdate="syncPlaybackProgress"
></video> ></video>
<div class="video-empty stream-message">录像回放需接入回放拉流地址</div>
</div> </div>
<div class="playback-dialog-controls"> <div class="playback-dialog-controls">

View File

@@ -0,0 +1,162 @@
# 小车状态 WS 推送协议
## 适用范围
本文档说明 ROS 小车状态通过 WebSocket 上报到后端,以及后端向浏览器推送 `car_status` 消息的对接方式。
- 浏览器订阅地址:`ws://{host}:{port}/ws/ros?token={token}`
- 小车设备连接地址:`ws://{host}:{port}/ws/ros?deviceKey={deviceKey}`
浏览器连接成功后无需额外发送订阅指令,后端会把收到的小车状态广播给当前所有在线浏览器会话。
消息统一使用 JSON心跳兼容字符串消息收到 `ping` 时回复 `pong`;主动发送 `ping` 时对端回复 `pong`
## 小车上报消息
消息类型:`telemetry`
```json
{
"type": "telemetry",
"data": {
"batteryLevel": 86,
"voltage": 24.6,
"poseX": 12.345,
"poseY": 6.789,
"poseYaw": 1.57,
"status": 1,
"signal": "-58dBm"
}
}
```
字段说明:
| 字段 | 必填 | 说明 |
| ------------------- | ---- | -------------------------------------------------- |
| `type` | 是 | 固定为 `telemetry` |
| `data.batteryLevel` | 否 | 电量百分比 |
| `data.voltage` | 否 | 电压 |
| `data.poseX` | 否 | 当前 X 坐标 |
| `data.poseY` | 否 | 当前 Y 坐标 |
| `data.poseYaw` | 否 | 当前朝向 |
| `data.status` | 否 | 小车状态:`0` 空闲,`1` 巡检,`2` 离线,`3` 充电中 |
| `data.signal` | 否 | 小车信号 |
说明:
- 支持增量上报,`data` 中只传有变化的字段即可。
- 后端只会更新本次上报里有值的字段,未传字段保持上一次状态不变。
- `deviceKey` 不需要放在消息体里,后端以连接参数 `deviceKey` 识别当前小车。
仅上报电量和状态示例:
```json
{
"type": "telemetry",
"data": {
"batteryLevel": 85,
"status": 3
}
}
```
## 浏览器收到的推送
后端在处理完小车上报后,会向所有在线浏览器广播 `car_status` 消息。
```json
{
"type": "car_status",
"data": {
"deviceKey": "ROBOT-001",
"liftHeight": 1200,
"batteryLevel": 86,
"voltage": 24.6,
"poseX": 12.345,
"poseY": 6.789,
"poseYaw": 1.57,
"status": 1,
"signal": "-58dBm"
},
"timestamp": 1782702000010
}
```
顶层字段说明:
| 字段 | 说明 |
| ---------------- | ---------------------------------------- |
| `type` | 固定为 `car_status` |
| `data.deviceKey` | 小车设备标识 |
| `data.liftHeight` | 升降柱高度,单位 mm |
| `timestamp` | 后端推送时间戳,毫秒 |
## 小车速度上报消息
如果 ROS 端还需要把当前线速度、角速度实时推给浏览器,可额外发送 `velocity` 消息。该消息只做转发,不落库。
消息类型:`velocity`
```json
{
"type": "velocity",
"data": {
"linearX": 0.12,
"angularZ": -0.03
}
}
```
字段说明:
| 字段 | 必填 | 说明 |
| --------------- | ---- | ---------------------------------------------------- |
| `type` | 是 | 固定为 `velocity` |
| `data.linearX` | 否 | 当前线速度,对应 ROS `geometry_msgs/Twist.linear.x` |
| `data.angularZ` | 否 | 当前角速度,对应 ROS `geometry_msgs/Twist.angular.z` |
说明:
- `velocity``telemetry` 分开处理,不写入 `ros_car`
- 支持高频上报,后端收到后直接转发给浏览器。
- `deviceKey` 仍然从连接参数获取,不需要放在消息体里。
## 浏览器收到的速度推送
后端收到 ROS 端 `velocity` 后,会向所有在线浏览器广播 `car_velocity` 消息。
```json
{
"type": "car_velocity",
"data": {
"deviceKey": "ROBOT-001",
"velocity": {
"linearX": 0.12,
"angularZ": -0.03
}
},
"timestamp": 1782702001010
}
```
字段说明:
| 字段 | 说明 |
| ------------------------ | --------------------- |
| `type` | 固定为 `car_velocity` |
| `data.deviceKey` | 小车设备标识 |
| `data.velocity.linearX` | 当前线速度 |
| `data.velocity.angularZ` | 当前角速度 |
| `timestamp` | 后端推送时间戳,毫秒 |
## 对接约束
- 浏览器侧只需要建立连接并监听 `car_status`,不需要发送订阅命令。
- 如果需要展示实时速度,浏览器额外监听 `car_velocity`
- 同一条 `car_status` 中,`telemetry` 表示本次上报内容,`car` 表示合并后的完整结果;前端展示建议优先使用 `car`
- 后端当前仅在收到小车 `telemetry` 上报后广播 `car_status`
- 后端当前在收到小车 `velocity` 上报后广播 `car_velocity`,不做缓存和存储。
- 小车连接建立时,后端会将小车状态置为空闲;小车连接断开时,后端会将小车状态置为离线。
- 仅连接断开导致的离线变化,当前不会单独触发一条 `car_status` 广播;如前端需要强一致离线提示,需要额外结合列表查询或后续补充离线广播事件。

View File

@@ -0,0 +1,190 @@
# 浏览器 WS 控制协议
## 连接
浏览器端使用登录 token 建立连接:
```text
ws://{host}:{port}/ws/ros?token={token}
```
消息统一使用 JSON
```json
{
"type": "cmd_vel",
"requestId": "browser-generated-id",
"data": {
"deviceKey": "ROBOT-001",
"linearX": 0.1,
"angularZ": 0
}
}
```
通用字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `type` | 是 | 消息类型 |
| `requestId` | 否 | 浏览器生成的请求 id后端回执会原样带回 |
| `data.deviceKey` | 是 | 控制目标机器人设备标识 |
心跳兼容字符串消息:浏览器收到 `ping` 后回复 `pong`;浏览器主动发送 `ping` 时后端回复 `pong`
## 后端回执
后端只确认消息是否成功转发到在线机器人,不等待机器人执行完成。
```json
{
"type": "control_result",
"requestId": "browser-generated-id",
"data": {
"sourceType": "cmd_vel",
"deviceKey": "ROBOT-001",
"accepted": true,
"message": "已下发"
},
"timestamp": 1782702000020
}
```
`accepted=false` 常见原因:`data` 缺失、`deviceKey` 缺失、机器人不在线。
## 底盘控制
消息类型只使用 `cmd_vel`
```json
{
"type": "cmd_vel",
"requestId": "cmd-1",
"data": {
"deviceKey": "ROBOT-001",
"linearX": 0.1,
"angularZ": 0
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `linearX` | 对应 ROS `geometry_msgs/Twist.linear.x` |
| `angularZ` | 对应 ROS `geometry_msgs/Twist.angular.z` |
按钮建议映射:
| 操作 | `linearX` | `angularZ` |
| --- | --- | --- |
| 前进 | `0.1` | `0` |
| 后退 | `-0.1` | `0` |
| 左转 | `0` | `0.1` |
| 右转 | `0` | `-0.1` |
| 停止 | `0` | `0` |
按钮按下时发送对应数值按钮松开、指针移出、页面失焦时发送停止值。ROS 端 WebSocket 适配器负责把 `linearX/angularZ` 转成 `/cmd_vel``geometry_msgs/Twist`
## 云台控制
ROS 包参考:`hikvision_camera_controller/srv/Ptz`
```text
string action
int32 speed
int32 duration_ms
```
浏览器协议只下发控制动作,`speed``duration_ms` 由 ROS 端适配器使用默认值。
消息类型:`ptz_control`
```json
{
"type": "ptz_control",
"requestId": "ptz-1",
"data": {
"deviceKey": "ROBOT-001",
"action": "up"
}
}
```
`action` 可选值:
| 值 | 含义 |
| --- | --- |
| `up` | 上 |
| `down` | 下 |
| `left` | 左 |
| `right` | 右 |
| `stop` | 停止 |
ROS 端调用 `/hikvision_camera/ptz` 时建议映射:
| 浏览器 `action` | ROS `action` | ROS `speed` | ROS `duration_ms` |
| --- | --- | --- | --- |
| `up` | `up` | 默认值,如 20 | 默认值,如 500 |
| `down` | `down` | 默认值,如 20 | 默认值,如 500 |
| `left` | `left` | 默认值,如 20 | 默认值,如 500 |
| `right` | `right` | 默认值,如 20 | 默认值,如 500 |
| `stop` | `stop` | 0 | 0 |
## 升降柱控制
ROS 包参考:`rs485_lift_controller/srv/ControlLift`
```text
int32 control_type
uint16 height
```
消息类型:`lift_control`
```json
{
"type": "lift_control",
"requestId": "lift-1",
"data": {
"deviceKey": "ROBOT-001",
"control_type": 4,
"height": 1200
}
}
```
`control_type` 可选值:
| 值 | 含义 | `height` |
| --- | --- | --- |
| `0` | 复位 | `0` |
| `1` | 上升 | `0` |
| `2` | 下降 | `0` |
| `3` | 停止 | `0` |
| `4` | 运行到指定高度 | 目标高度,单位 mm |
## 机器人端收到的消息
设备端连接:
```text
ws://{host}:{port}/ws/ros?deviceKey={deviceKey}
```
后端按相同 `type` 透传 `data` 给设备。例如浏览器发送升降柱控制,设备端收到:
```json
{
"type": "lift_control",
"data": {
"deviceKey": "ROBOT-001",
"control_type": 4,
"height": 1200
},
"timestamp": 1782702000010
}
```
设备端应自行做安全保护:连接断开、长时间未收到控制消息、收到停止命令时立即停止对应动作。

View File

@@ -67,6 +67,9 @@ export function createAlertPhoto(scene: AlertPhotoScene, accent: string) {
export const recentAlertPhotoSources = { export const recentAlertPhotoSources = {
"线缆破损": createAlertPhoto("cable", "#ff8f79"), "线缆破损": createAlertPhoto("cable", "#ff8f79"),
"电缆表面破损": createAlertPhoto("cable", "#ff8f79"),
"桥架断裂": createAlertPhoto("crack", "#89d8ff"),
"隧道积水": createAlertPhoto("heat", "#ffc86c"),
"局部高温": createAlertPhoto("heat", "#ffc86c"), "局部高温": createAlertPhoto("heat", "#ffc86c"),
"墙体裂缝": createAlertPhoto("crack", "#89d8ff"), "墙体裂缝": createAlertPhoto("crack", "#89d8ff"),
} as const; } as const;

203
src/views/login/index.vue Normal file
View File

@@ -0,0 +1,203 @@
<script lang="ts" setup>
import { computed, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useUserStore } from "@/store/modules/user.js";
import { encryptPasswordPayload } from '@/utils/passwordEncrypt.js'
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const form = reactive({
username: "",
password: "",
});
const loading = ref(false);
const errorMessage = ref("");
const redirectPath = computed(() => {
const redirect = route.query.redirect;
if (
typeof redirect === "string" &&
redirect.startsWith("/") &&
!redirect.startsWith("//")
) {
return redirect;
}
return "/dashboard/integrated-center";
});
// 校验登录表单,避免空账号或空密码提交到后端。
function validateLoginForm() {
if (!form.username.trim()) {
errorMessage.value = "请输入账号";
return false;
}
if (!form.password) {
errorMessage.value = "请输入密码";
return false;
}
return true;
}
// 提交登录信息,成功后回到登录前访问的页面。
async function handleLogin() {
if (!validateLoginForm() || loading.value) {
return;
}
loading.value = true;
errorMessage.value = "";
try {
const encryptedForm = await encryptPasswordPayload(form, ['password'])
await userStore.login(encryptedForm)
router.replace(redirectPath.value);
} catch (error) {
errorMessage.value =
error instanceof Error ? error.message : "登录失败,请检查账号或密码";
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="handleLogin">
<div class="login-card__eyebrow">Robot Manager</div>
<h1>系统登录</h1>
<p>请输入账号和密码进入巡检管理大屏</p>
<label class="login-field">
<span>账号</span>
<input v-model="form.username" autocomplete="username" placeholder="请输入账号" type="text" />
</label>
<label class="login-field">
<span>密码</span>
<input v-model="form.password" autocomplete="current-password" placeholder="请输入密码" type="password" />
</label>
<div v-if="errorMessage" class="login-error">{{ errorMessage }}</div>
<button class="login-button" :disabled="loading" type="submit">
{{ loading ? "登录中..." : "登录" }}
</button>
</form>
</div>
</template>
<style scoped>
.login-page {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #dff7ff;
background:
radial-gradient(circle at 50% 42%,
rgba(31, 126, 202, 0.32),
transparent 34%),
radial-gradient(circle at center, #15385d 0%, #061426 60%, #02070f 100%);
}
.login-card {
box-sizing: border-box;
width: 440px;
padding: 44px 48px 48px;
text-align: left;
border: 1px solid rgba(116, 236, 255, 0.28);
border-radius: 18px;
background: linear-gradient(180deg,
rgba(7, 31, 61, 0.9),
rgba(4, 15, 31, 0.86));
box-shadow:
0 0 40px rgba(69, 188, 255, 0.18),
inset 0 0 26px rgba(116, 236, 255, 0.08);
}
.login-card__eyebrow {
margin-bottom: 12px;
color: #74ecff;
font-size: 13px;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.login-card h1 {
margin: 0 0 12px;
font-size: 34px;
font-weight: 600;
letter-spacing: 0.08em;
}
.login-card p {
margin: 0 0 34px;
color: rgba(223, 247, 255, 0.72);
font-size: 15px;
}
.login-field {
display: block;
margin-bottom: 20px;
}
.login-field span {
display: block;
margin-bottom: 8px;
color: rgba(223, 247, 255, 0.78);
font-size: 14px;
}
.login-field input {
box-sizing: border-box;
width: 100%;
height: 46px;
padding: 0 16px;
color: #e8fbff;
border: 1px solid rgba(116, 236, 255, 0.26);
border-radius: 8px;
outline: none;
background: rgba(3, 15, 32, 0.74);
transition:
border-color 0.2s,
box-shadow 0.2s;
}
.login-field input:focus {
border-color: rgba(116, 236, 255, 0.78);
box-shadow: 0 0 18px rgba(116, 236, 255, 0.16);
}
.login-field input::placeholder {
color: rgba(223, 247, 255, 0.34);
}
.login-error {
min-height: 20px;
margin: -4px 0 16px;
color: #ff9c9c;
font-size: 14px;
}
.login-button {
width: 100%;
height: 48px;
color: #062239;
font-size: 17px;
font-weight: 600;
letter-spacing: 0.12em;
cursor: pointer;
border: 0;
border-radius: 8px;
background: linear-gradient(90deg, #74ecff, #46a7ff);
box-shadow: 0 0 24px rgba(70, 167, 255, 0.32);
}
.login-button:disabled {
cursor: not-allowed;
opacity: 0.68;
}
</style>

View File

@@ -17,5 +17,5 @@
"@/*": ["src/*"] "@/*": ["src/*"]
} }
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "src/api/rosCar.ts"]
} }

View File

@@ -1,15 +1,44 @@
import {defineConfig} from "vite"; import vue from '@vitejs/plugin-vue'
import vue from "@vitejs/plugin-vue"; import vueJsx from '@vitejs/plugin-vue-jsx'
import vueJsx from "@vitejs/plugin-vue-jsx"; import { resolve } from 'path'
import path from "path";
// import { viteSingleFile } from 'vite-plugin-singlefile'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default ({ command }) => {
base: "./", const isBuild = command === 'build'
plugins: [vue(), vueJsx()], return ({
plugins: [
vue(),
vueJsx(),
],
define: {
__VUE_OPTIONS_API__: false // 关闭 Vue2 中的 options选项API
},
base: './', // 使用相对路径
resolve: { resolve: {
alias: { alias: [{ find: '@', replacement: resolve(__dirname, 'src') }]
"@": path.resolve(__dirname, "./src"),
}, },
server: {
port: 6003,
host: true,
proxy: {
'/api': {
target: `http://192.168.3.110:16003/`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}, },
}); '/ws': {
target: `ws://192.168.3.110:16003/`,
changeOrigin: true,
ws: true
}
}
},
oxc: {
//清除全局的console.log和debug
drop: isBuild ? ['console', 'debugger'] : [],
}
})
}