首页接口
This commit is contained in:
parent
8f230cf38a
commit
93db9389a2
371
package-lock.json
generated
371
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"echarts": "^5.6.0",
|
||||
"video.js": "^8.23.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.5.1",
|
||||
"webrtc-adapter": "^9.0.3"
|
||||
@ -32,6 +33,14 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
@ -685,6 +694,51 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/http-streaming": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/http-streaming/-/http-streaming-3.17.0.tgz",
|
||||
"integrity": "sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"aes-decrypter": "^4.0.2",
|
||||
"global": "^4.4.0",
|
||||
"m3u8-parser": "^7.2.0",
|
||||
"mpd-parser": "^1.3.1",
|
||||
"mux.js": "7.1.0",
|
||||
"video.js": "^7 || ^8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"video.js": "^8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/vhs-utils": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
|
||||
"integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/@videojs/xhr": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/xhr/-/xhr-2.7.0.tgz",
|
||||
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"global": "~4.4.0",
|
||||
"is-function": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
|
||||
@ -805,6 +859,25 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
|
||||
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aes-decrypter": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
|
||||
"integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"global": "^4.4.0",
|
||||
"pkcs7": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@ -897,6 +970,11 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
|
||||
@ -1117,6 +1195,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/global": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"dependencies": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -1180,6 +1267,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@ -1203,6 +1295,16 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/m3u8-parser": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
|
||||
"integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
@ -1255,6 +1357,44 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/min-document": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||
"dependencies": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mpd-parser": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/mpd-parser/-/mpd-parser-1.3.1.tgz",
|
||||
"integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.0.0",
|
||||
"@xmldom/xmldom": "^0.8.3",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"mpd-to-m3u8-json": "bin/parse.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mux.js": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mux.js/-/mux.js-7.1.0.tgz",
|
||||
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"bin": {
|
||||
"muxjs-transmux": "bin/transmux.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=5"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
@ -1297,6 +1437,17 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkcs7": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5"
|
||||
},
|
||||
"bin": {
|
||||
"pkcs7": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@ -1324,6 +1475,14 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@ -1409,6 +1568,53 @@
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
},
|
||||
"node_modules/video.js": {
|
||||
"version": "8.23.3",
|
||||
"resolved": "https://registry.npmmirror.com/video.js/-/video.js-8.23.3.tgz",
|
||||
"integrity": "sha512-Toe0VLlDZcUhiaWfcePS1OEdT3ATfktm0hk/PELfD7zUoPDHeT+cJf/wZmCy5M5eGVwtGUg25RWPCj1L/1XufA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "^3.17.0",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"@videojs/xhr": "2.7.0",
|
||||
"aes-decrypter": "^4.0.2",
|
||||
"global": "4.4.0",
|
||||
"m3u8-parser": "^7.2.0",
|
||||
"mpd-parser": "^1.3.1",
|
||||
"mux.js": "^7.0.1",
|
||||
"videojs-contrib-quality-levels": "4.1.0",
|
||||
"videojs-font": "4.2.0",
|
||||
"videojs-vtt.js": "0.15.5"
|
||||
}
|
||||
},
|
||||
"node_modules/videojs-contrib-quality-levels": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
|
||||
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
|
||||
"dependencies": {
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"video.js": "^8"
|
||||
}
|
||||
},
|
||||
"node_modules/videojs-font": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-font/-/videojs-font-4.2.0.tgz",
|
||||
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ=="
|
||||
},
|
||||
"node_modules/videojs-vtt.js": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
|
||||
"dependencies": {
|
||||
"global": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
@ -1517,6 +1723,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw=="
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
@ -1793,6 +2004,40 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"@videojs/http-streaming": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/http-streaming/-/http-streaming-3.17.0.tgz",
|
||||
"integrity": "sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"aes-decrypter": "^4.0.2",
|
||||
"global": "^4.4.0",
|
||||
"m3u8-parser": "^7.2.0",
|
||||
"mpd-parser": "^1.3.1",
|
||||
"mux.js": "7.1.0",
|
||||
"video.js": "^7 || ^8"
|
||||
}
|
||||
},
|
||||
"@videojs/vhs-utils": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
|
||||
"integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"@videojs/xhr": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/@videojs/xhr/-/xhr-2.7.0.tgz",
|
||||
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"global": "~4.4.0",
|
||||
"is-function": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@vitejs/plugin-vue": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz",
|
||||
@ -1904,6 +2149,22 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
|
||||
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
|
||||
},
|
||||
"@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="
|
||||
},
|
||||
"aes-decrypter": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
|
||||
"integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"global": "^4.4.0",
|
||||
"pkcs7": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@ -1972,6 +2233,11 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"dom-walk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz",
|
||||
@ -2125,6 +2391,15 @@
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/global/-/global-4.4.0.tgz",
|
||||
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||
"requires": {
|
||||
"min-document": "^2.19.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -2164,6 +2439,11 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-function": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-function/-/is-function-1.0.2.tgz",
|
||||
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@ -2181,6 +2461,16 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"m3u8-parser": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
|
||||
"integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
|
||||
@ -2218,6 +2508,34 @@
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"min-document": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/min-document/-/min-document-2.19.0.tgz",
|
||||
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||
"requires": {
|
||||
"dom-walk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"mpd-parser": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/mpd-parser/-/mpd-parser-1.3.1.tgz",
|
||||
"integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^4.0.0",
|
||||
"@xmldom/xmldom": "^0.8.3",
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"mux.js": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mux.js/-/mux.js-7.1.0.tgz",
|
||||
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
@ -2242,6 +2560,14 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"pkcs7": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
@ -2252,6 +2578,11 @@
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@ -2309,6 +2640,46 @@
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
},
|
||||
"video.js": {
|
||||
"version": "8.23.3",
|
||||
"resolved": "https://registry.npmmirror.com/video.js/-/video.js-8.23.3.tgz",
|
||||
"integrity": "sha512-Toe0VLlDZcUhiaWfcePS1OEdT3ATfktm0hk/PELfD7zUoPDHeT+cJf/wZmCy5M5eGVwtGUg25RWPCj1L/1XufA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "^3.17.0",
|
||||
"@videojs/vhs-utils": "^4.1.1",
|
||||
"@videojs/xhr": "2.7.0",
|
||||
"aes-decrypter": "^4.0.2",
|
||||
"global": "4.4.0",
|
||||
"m3u8-parser": "^7.2.0",
|
||||
"mpd-parser": "^1.3.1",
|
||||
"mux.js": "^7.0.1",
|
||||
"videojs-contrib-quality-levels": "4.1.0",
|
||||
"videojs-font": "4.2.0",
|
||||
"videojs-vtt.js": "0.15.5"
|
||||
}
|
||||
},
|
||||
"videojs-contrib-quality-levels": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
|
||||
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
|
||||
"requires": {
|
||||
"global": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"videojs-font": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-font/-/videojs-font-4.2.0.tgz",
|
||||
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ=="
|
||||
},
|
||||
"videojs-vtt.js": {
|
||||
"version": "0.15.5",
|
||||
"resolved": "https://registry.npmmirror.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
|
||||
"requires": {
|
||||
"global": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"vite": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"echarts": "^5.6.0",
|
||||
"video.js": "^8.23.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.5.1",
|
||||
"webrtc-adapter": "^9.0.3"
|
||||
|
||||
@ -1,16 +1,67 @@
|
||||
import request, { service } from '@/utils/request'
|
||||
import { service } from '../utils/request'
|
||||
|
||||
// 机器人相关接口
|
||||
export const robotApi = {
|
||||
// 获取机器人统计信息
|
||||
getRobotStatistics: () => {
|
||||
export const homeApi = {
|
||||
// 获取机器人列表
|
||||
getRobotList: (data) => {
|
||||
return service({
|
||||
url: '/api/v1/events/robotST',
|
||||
method: 'get'
|
||||
url: '/api/v1/event/getRobotList',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
},
|
||||
// 告警事件统计
|
||||
getAlarmStatistics: (day) => {
|
||||
// 根据不同时间段参数构建不同的URL
|
||||
return service({
|
||||
url: `/api/v1/events/stMessages/${day}`,
|
||||
method: 'get',
|
||||
})
|
||||
},
|
||||
// 获取告警信息数量信息 返回已处理未处理的数量
|
||||
getAlertCount: () => {
|
||||
return service({
|
||||
url: '/api/v1/events/alertCount',
|
||||
method: 'get',
|
||||
})
|
||||
},
|
||||
// 获取未处理告警消息列表
|
||||
getNoSolve: (params) => {
|
||||
return service({
|
||||
url: '/api/v1/events/messagesUnhandled',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
},
|
||||
// 获取已处理告警消息列表
|
||||
getSolveList:() =>{
|
||||
return service({
|
||||
url: '/api/v1/events/messagesHandled',
|
||||
method: 'get',
|
||||
|
||||
})
|
||||
},
|
||||
// 处理单个告警事件
|
||||
getAlarmEvent:(data)=>{
|
||||
return service({
|
||||
url: '/api/v1/events/handleOcrAlert',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
},
|
||||
// 一键处理所有事件
|
||||
handleAllEvent:()=>{
|
||||
return service({
|
||||
url: '/api/v1/events/handleOcrAlerts',
|
||||
method: 'get',
|
||||
})
|
||||
},
|
||||
// 获取厂区级机器人实时监控
|
||||
getFactoryRobotRealTime:()=>{
|
||||
console.log('调用getFactoryRobotRealTime接口');
|
||||
return service({
|
||||
url: '/api/v1/events/getMonitor',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
// 机器人状态列表
|
||||
|
||||
//
|
||||
|
||||
}
|
||||
BIN
src/assets/img/alert3.png
Normal file
BIN
src/assets/img/alert3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
src/assets/img/delete.png
Normal file
BIN
src/assets/img/delete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/img/empty.png
Normal file
BIN
src/assets/img/empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/img/empty_list.png
Normal file
BIN
src/assets/img/empty_list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.7 KiB |
@ -5,49 +5,121 @@
|
||||
v-for="period in periods"
|
||||
:key="period.value"
|
||||
:class="['filter-item', { active: currentPeriod === period.value }]"
|
||||
@click="currentPeriod = period.value"
|
||||
@click="handleManualSelect(period.value)"
|
||||
>
|
||||
{{ period.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" ref="chartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { homeApi } from '../api/index';
|
||||
import emptyList from '../assets/img/empty_list.png';
|
||||
|
||||
const chartRef = ref(null);
|
||||
let chart = null;
|
||||
let autoSwitchTimer = null;
|
||||
let checkInterval = null;
|
||||
const isLoading = ref(false);
|
||||
const isManualMode = ref(false); // 是否处于手动选择模式
|
||||
|
||||
// 模拟数据
|
||||
const chartData = {
|
||||
xAxis: [
|
||||
"公共设施巡检",
|
||||
"闸门开关",
|
||||
"阀门开闭",
|
||||
"空气质量预警",
|
||||
"语音未按时",
|
||||
"日常巡检",
|
||||
],
|
||||
series: [2, 8, 28, 18, 8, 8],
|
||||
// 告警事件类型映射
|
||||
const eventTypeMap = {
|
||||
'0': '读表告警',
|
||||
'1': '高温感知报警',
|
||||
'2': '吸烟报警',
|
||||
'3': '长时间滞留报警',
|
||||
'4': '空气质量报警',
|
||||
'5': '急停按下',
|
||||
'6': '语音未接通',
|
||||
'7': '日常巡检'
|
||||
};
|
||||
|
||||
// 存储API返回的数据
|
||||
const apiData = ref({
|
||||
'1': [], // 天
|
||||
'7': [], // 周
|
||||
'30': [], // 月
|
||||
});
|
||||
|
||||
// 判断当前时间段是否有数据
|
||||
const hasData = computed(() => {
|
||||
return apiData.value[currentPeriod.value] && apiData.value[currentPeriod.value].length > 0;
|
||||
});
|
||||
|
||||
// 转换API数据为图表格式
|
||||
const transformData = (data) => {
|
||||
const xAxisData = [];
|
||||
const seriesData = [];
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return { xAxis: [], series: [] };
|
||||
}
|
||||
|
||||
// 按事件类型分组并排序
|
||||
data.forEach(item => {
|
||||
const eventTypeName = eventTypeMap[item.eventType] || `未知类型(${item.eventType})`;
|
||||
xAxisData.push(eventTypeName);
|
||||
seriesData.push(item.count);
|
||||
});
|
||||
|
||||
return {
|
||||
xAxis: xAxisData,
|
||||
series: seriesData
|
||||
};
|
||||
};
|
||||
|
||||
const periods = [
|
||||
{ label: "天", value: "day" },
|
||||
{ label: "周", value: "week" },
|
||||
{ label: "月", value: "month" },
|
||||
{ label: "天", value: "1" },
|
||||
{ label: "周", value: "7" },
|
||||
{ label: "月", value: "30" },
|
||||
];
|
||||
|
||||
const currentPeriod = ref("day");
|
||||
const currentPeriod = ref("1");
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
// 获取指定时间段的告警统计数据
|
||||
const fetchAlarmStatistics = async (day) => {
|
||||
console.log(`获取${day}天的数据`);
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const res = await homeApi.getAlarmStatistics(day);
|
||||
if (res.code === 200) {
|
||||
apiData.value[day] = res.data || [];
|
||||
console.log(`获取${day}天告警统计数据成功:`, res.data);
|
||||
} else {
|
||||
console.error(`获取${day}天告警统计数据失败:`, res);
|
||||
apiData.value[day] = [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`获取${day}天告警统计数据错误:`, err);
|
||||
apiData.value[day] = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
||||
// 确保当前还是这个时间段才更新图表
|
||||
if (currentPeriod.value === day) {
|
||||
updateChart();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
chart = echarts.init(chartRef.value);
|
||||
// 更新图表数据
|
||||
const updateChart = () => {
|
||||
if (!chartRef.value || !chart) return;
|
||||
|
||||
console.log(`更新图表数据,当前时间段: ${currentPeriod.value}`);
|
||||
|
||||
// 获取当前时间段的数据
|
||||
const chartData = transformData(apiData.value[currentPeriod.value]);
|
||||
|
||||
// 基础配置,无论是否有数据都会显示
|
||||
const option = {
|
||||
// backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: "10%",
|
||||
left: "5%",
|
||||
@ -57,7 +129,8 @@ const initChart = () => {
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: chartData.xAxis,
|
||||
// 如果没有数据,使用空数组但仍显示坐标轴
|
||||
data: chartData.xAxis.length > 0 ? chartData.xAxis : ['暂无数据'],
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
@ -102,7 +175,8 @@ const initChart = () => {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: chartData.series,
|
||||
// 当没有数据时,显示一个空数据点
|
||||
data: chartData.series.length > 0 ? chartData.series : [0],
|
||||
type: "line",
|
||||
smooth: true,
|
||||
symbol: "circle",
|
||||
@ -149,7 +223,7 @@ const initChart = () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
markPoint: {
|
||||
markPoint: chartData.series.length > 0 ? {
|
||||
symbol: "circle",
|
||||
symbolSize: 50,
|
||||
itemStyle: {
|
||||
@ -181,12 +255,78 @@ const initChart = () => {
|
||||
label: { show: true, color: "#FF8A00", fontSize: 12 },
|
||||
},
|
||||
],
|
||||
},
|
||||
} : null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
console.log('设置图表选项');
|
||||
chart.setOption(option, true); // 使用true参数完全覆盖之前的配置
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
}
|
||||
|
||||
chart = echarts.init(chartRef.value);
|
||||
updateChart(); // 初始更新图表
|
||||
};
|
||||
|
||||
// 开始自动轮播
|
||||
const startAutoSwitch = () => {
|
||||
stopAutoSwitch(); // 先停止现有的定时器
|
||||
|
||||
autoSwitchTimer = setInterval(async () => {
|
||||
// 如果处于手动模式,不自动切换
|
||||
if (isManualMode.value) return;
|
||||
|
||||
const currentIndex = periods.findIndex(p => p.value === currentPeriod.value);
|
||||
const nextIndex = (currentIndex + 1) % periods.length;
|
||||
const nextPeriod = periods[nextIndex].value;
|
||||
|
||||
console.log(`自动切换到: ${periods[nextIndex].label}(${nextPeriod})`);
|
||||
|
||||
// 先切换时间段
|
||||
currentPeriod.value = nextPeriod;
|
||||
|
||||
// 然后获取数据
|
||||
await fetchAlarmStatistics(nextPeriod);
|
||||
}, 10000); // 每10秒切换一次
|
||||
};
|
||||
|
||||
// 停止自动轮播
|
||||
const stopAutoSwitch = () => {
|
||||
if (autoSwitchTimer) {
|
||||
clearInterval(autoSwitchTimer);
|
||||
autoSwitchTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理手动选择
|
||||
const handleManualSelect = async (period) => {
|
||||
console.log(`手动选择时间段: ${period}`);
|
||||
|
||||
// 进入手动模式
|
||||
isManualMode.value = true;
|
||||
|
||||
// 如果点击的是当前已选择的时间段,不做任何操作
|
||||
if (currentPeriod.value === period) return;
|
||||
|
||||
// 更新当前选中时间段
|
||||
currentPeriod.value = period;
|
||||
|
||||
// 获取该时间段的数据
|
||||
await fetchAlarmStatistics(period);
|
||||
|
||||
// 5秒后恢复自动切换
|
||||
setTimeout(() => {
|
||||
console.log('恢复自动模式');
|
||||
isManualMode.value = false;
|
||||
}, 30000); // 30秒后恢复自动模式
|
||||
};
|
||||
|
||||
// 监听窗口大小变化
|
||||
@ -194,14 +334,55 @@ const handleResize = () => {
|
||||
chart && chart.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 监听当前选中的时间段变化
|
||||
watch(currentPeriod, (newPeriod) => {
|
||||
console.log(`当前选中时间段变为: ${newPeriod}`);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('组件挂载');
|
||||
|
||||
// 初始化时获取默认数据(1天)
|
||||
await fetchAlarmStatistics('1');
|
||||
|
||||
// 然后初始化其他时间段数据
|
||||
Promise.all([
|
||||
fetchAlarmStatistics('7'),
|
||||
fetchAlarmStatistics('30')
|
||||
]);
|
||||
|
||||
initChart();
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// 启动自动轮播
|
||||
console.log('开始自动轮播');
|
||||
startAutoSwitch();
|
||||
|
||||
// 定期检查轮播是否还在进行
|
||||
checkInterval = setInterval(() => {
|
||||
if (!autoSwitchTimer) {
|
||||
console.log('检测到轮播已停止,重新启动');
|
||||
startAutoSwitch();
|
||||
}
|
||||
}, 20000); // 每20秒检查一次
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
chart && chart.dispose();
|
||||
// 清理定时器
|
||||
stopAutoSwitch();
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
checkInterval = null;
|
||||
}
|
||||
|
||||
// 销毁图表
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
chart = null;
|
||||
}
|
||||
|
||||
window.removeEventListener("resize", handleResize);
|
||||
console.log('组件卸载,停止自动轮播');
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -209,22 +390,25 @@ onUnmounted(() => {
|
||||
.alarm-statistics {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background: rgba(0, 21, 31, 0.3);
|
||||
border-radius: 4px; */
|
||||
/* padding: 10px; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.time-filter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
/* margin-bottom: 15px; */
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
@ -236,11 +420,10 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
/* background: rgba(0, 145, 169, 0.3);
|
||||
*/
|
||||
padding: 2px 10px;
|
||||
padding: 2px 10px;
|
||||
background: url("../assets/img/alarm_tri.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #00ffff;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -16,56 +16,72 @@
|
||||
<div class="header-item status">状态</div>
|
||||
</div>
|
||||
<div class="alarm-list" ref="scrollContainer">
|
||||
<!-- 避障警报 -->
|
||||
<div class="alarm-category" v-show="shouldShowAlarm('obstacle')">
|
||||
<div class="category-header">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-name">避障警报</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in filteredObstacleAlarms" :key="'obstacle-'+index" class="alarm-item" @click="showAlarmDetail(item)">
|
||||
<div class="item-event">{{ item.event }}</div>
|
||||
<div class="item-robot-id">{{ item.robotId }}</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
<div class="item-status">
|
||||
<button v-if="!item.handled" class="handle-btn" @click="handleAlarm(item)">处理</button>
|
||||
<span v-else class="handled-text">已处理</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="loading-state">
|
||||
<div class="loading-text">数据加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 高温感知报警 -->
|
||||
<div class="alarm-category" v-show="shouldShowAlarm('temperature')">
|
||||
<div class="category-header high-temp">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-name">高温感知报警</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in filteredTemperatureAlarms" :key="'temp-'+index" class="alarm-item" @click="showAlarmDetail(item)">
|
||||
<div class="item-event">{{ item.event }}</div>
|
||||
<div class="item-robot-id">{{ item.robotId }}</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
<div class="item-status">
|
||||
<button v-if="!item.handled" class="handle-btn" @click="handleAlarm(item)">处理</button>
|
||||
<span v-else class="handled-text">已处理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 空状态展示 -->
|
||||
<EmptyState
|
||||
v-else-if="(currentTab === 'pending' && pendingAlarmsCount === 0) || (currentTab === 'done' && doneAlarmsCount === 0)"
|
||||
:iconSrc="emptyList"
|
||||
title="暂无告警事件"
|
||||
subtitle="所有告警已处理完成"
|
||||
/>
|
||||
|
||||
<!-- 日常巡检 -->
|
||||
<div class="alarm-category" v-show="shouldShowAlarm('routine')">
|
||||
<div class="category-header">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-name">日常巡检</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in filteredRoutineAlarms" :key="'routine-'+index" class="alarm-item" @click="showAlarmDetail(item)">
|
||||
<div class="item-event">{{ item.event }}</div>
|
||||
<div class="item-robot-id">{{ item.robotId }}</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
<div class="item-status">
|
||||
<button v-if="!item.handled" class="handle-btn" @click="handleAlarm(item)">处理</button>
|
||||
<span v-else class="handled-text">已处理</span>
|
||||
<!-- 告警列表 -->
|
||||
<template v-else>
|
||||
<!-- 避障警报 -->
|
||||
<div v-if="shouldShowAlarm('obstacle')" class="alarm-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-name">避障警报</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in filteredObstacleAlarms" :key="'obstacle-'+item.id+index" class="alarm-item" @click="showAlarmDetail(item)">
|
||||
<div class="item-event">{{ item.event }}</div>
|
||||
<div class="item-robot-id">{{ item.robotId }}</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
<div class="item-status">
|
||||
<button v-if="!item.handled" class="handle-btn" @click.stop="handleAlarm(item)">处理</button>
|
||||
<span v-else class="handled-text">已处理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高温感知报警 -->
|
||||
<div v-if="shouldShowAlarm('temperature')" class="alarm-category">
|
||||
<div class="category-header high-temp">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-name">高温感知报警</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in filteredTemperatureAlarms" :key="'temp-'+item.id+index" class="alarm-item" @click="showAlarmDetail(item)">
|
||||
<div class="item-event">{{ item.event }}</div>
|
||||
<div class="item-robot-id">{{ item.robotId }}</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
<div class="item-status">
|
||||
<button v-if="!item.handled" class="handle-btn" @click.stop="handleAlarm(item)">处理</button>
|
||||
<span v-else class="handled-text">已处理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日常巡检 -->
|
||||
<div v-if="shouldShowAlarm('routine')" class="alarm-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-name">日常巡检</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in filteredRoutineAlarms" :key="'routine-'+item.id+index" class="alarm-item" @click="showAlarmDetail(item)">
|
||||
<div class="item-event">{{ item.event }}</div>
|
||||
<div class="item-robot-id">{{ item.robotId }}</div>
|
||||
<div class="item-time">{{ item.time }}</div>
|
||||
<div class="item-status">
|
||||
<button v-if="!item.handled" class="handle-btn" @click.stop="handleAlarm(item)">处理</button>
|
||||
<span v-else class="handled-text">已处理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 引入告警详情弹窗 -->
|
||||
@ -75,88 +91,308 @@
|
||||
@confirm="handleConfirm"
|
||||
@report="handleReport"
|
||||
/>
|
||||
|
||||
<!-- 确认对话框 -->
|
||||
<ConfirmDialog
|
||||
v-model:visible="confirmDialogVisible"
|
||||
:title="confirmDialogTitle"
|
||||
:message="confirmDialogMessage"
|
||||
@confirm="onConfirmProcessAll"
|
||||
@cancel="confirmDialogVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import AlarmActions from './AlarmActions.vue'
|
||||
import AlarmDetailModal from './dialog/AlarmDetailModal.vue'
|
||||
import ConfirmDialog from './dialog/ConfirmDialog.vue'
|
||||
import EmptyState from './common/EmptyState.vue'
|
||||
import emptyList from '../assets/img/empty_list.png'
|
||||
import { homeApi } from '../api/index'
|
||||
|
||||
// 模拟数据 - 添加handled属性来跟踪处理状态
|
||||
const obstacleAlarms = ref([
|
||||
{ event: '紧急避障', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
{ event: '紧急避障', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
{ event: '紧急避障', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
])
|
||||
// 告警数量统计
|
||||
const pendingAlarmsCount = ref(0);
|
||||
const doneAlarmsCount = ref(0);
|
||||
|
||||
const temperatureAlarms = ref([
|
||||
{ event: '设备高温', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
{ event: 'ACS监测高温', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
])
|
||||
// 确认对话框状态
|
||||
const confirmDialogVisible = ref(false);
|
||||
const confirmDialogTitle = ref('确认处理');
|
||||
const confirmDialogMessage = ref('是否确认处理所有未处理的告警事件?');
|
||||
|
||||
const routineAlarms = ref([
|
||||
{ event: 'C区通信异常', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
{ event: '充电桩故障', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
{ event: 'B区通信异常', robotId: 'X32305000043', time: '2025/09/07 12:23:45', handled: false },
|
||||
])
|
||||
// 存储API返回的原始告警数据
|
||||
const apiAlarmData = ref({
|
||||
// 未处理告警
|
||||
unhandled: {
|
||||
high_temperature_message: [],
|
||||
smoke_message: [],
|
||||
long_stay_message: [],
|
||||
air_quality_message: [],
|
||||
stop_emergency_message: [],
|
||||
voice_connect_message: [],
|
||||
daily_inspect_message: []
|
||||
},
|
||||
// 已处理告警
|
||||
handled: {
|
||||
high_temperature_message: [],
|
||||
smoke_message: [],
|
||||
long_stay_message: [],
|
||||
air_quality_message: [],
|
||||
stop_emergency_message: [],
|
||||
voice_connect_message: [],
|
||||
daily_inspect_message: []
|
||||
}
|
||||
});
|
||||
|
||||
// 当前页面参数
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(999);
|
||||
|
||||
// 告警类型映射表
|
||||
const eventTypeMap = {
|
||||
'0': '读表告警',
|
||||
'1': '高温感知报警',
|
||||
'2': '吸烟报警',
|
||||
'3': '长时间滞留报警',
|
||||
'4': '空气质量报警',
|
||||
'5': '急停按下',
|
||||
'6': '语音未接通',
|
||||
'7': '日常巡检'
|
||||
};
|
||||
|
||||
// 告警API分类到UI分类的映射
|
||||
const categoryMapping = {
|
||||
'high_temperature_message': 'temperature', // 高温感知报警 -> temperature
|
||||
'smoke_message': 'temperature', // 吸烟报警 -> temperature (也可以分到其他类别)
|
||||
'long_stay_message': 'routine', // 长时间滞留报警 -> routine
|
||||
'air_quality_message': 'routine', // 空气质量报警 -> routine
|
||||
'stop_emergency_message': 'obstacle', // 急停按下 -> obstacle
|
||||
'voice_connect_message': 'routine', // 语音未接通 -> routine
|
||||
'daily_inspect_message': 'routine' // 日常巡检 -> routine
|
||||
};
|
||||
|
||||
// 转换API告警数据为UI所需格式
|
||||
const transformAlarmData = (apiData, isHandled) => {
|
||||
// 初始化转换后的数据结构
|
||||
const result = {
|
||||
obstacle: [], // 避障警报
|
||||
temperature: [], // 高温感知报警
|
||||
routine: [] // 日常巡检
|
||||
};
|
||||
|
||||
// 遍历所有告警类型
|
||||
Object.entries(apiData).forEach(([category, alarms]) => {
|
||||
if (!alarms || alarms.length === 0) return;
|
||||
|
||||
// 获取UI分类
|
||||
const uiCategory = categoryMapping[category] || 'routine';
|
||||
|
||||
// 转换每个告警项
|
||||
alarms.forEach(alarm => {
|
||||
// 构建UI需要的告警对象
|
||||
const uiAlarm = {
|
||||
id: alarm.messageId,
|
||||
eventId: alarm.eventId,
|
||||
event: eventTypeMap[alarm.eventType] || '未知告警',
|
||||
robotId: alarm.number || alarm.name || '未知设备',
|
||||
time: formatTime(alarm.createTime),
|
||||
handled: isHandled,
|
||||
type: alarm.eventType,
|
||||
image: alarm.imageUrl && alarm.imageUrl.length > 0 ? alarm.imageUrl[0] : '',
|
||||
subImages: alarm.imageUrl || [],
|
||||
temperature: alarm.temperature || '',
|
||||
remark: alarm.remark || '',
|
||||
messageType: alarm.messageType,
|
||||
raw: alarm // 保存原始数据以备后用
|
||||
};
|
||||
|
||||
// 添加到对应分类中
|
||||
result[uiCategory].push(uiAlarm);
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '';
|
||||
|
||||
try {
|
||||
// 将ISO时间格式转换为易读格式
|
||||
const date = new Date(timeStr);
|
||||
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
|
||||
} catch (e) {
|
||||
console.error('时间格式化错误:', e);
|
||||
return timeStr;
|
||||
}
|
||||
};
|
||||
|
||||
const scrollContainer = ref(null)
|
||||
let scrollInterval = null
|
||||
|
||||
const currentTab = ref('pending')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 使用transformAlarmData的结果创建告警数据结构
|
||||
const alarmData = ref({
|
||||
obstacle: [],
|
||||
temperature: [],
|
||||
routine: []
|
||||
});
|
||||
|
||||
// 过滤函数
|
||||
const filterAlarms = (alarms) => {
|
||||
return alarms.value.filter(alarm =>
|
||||
currentTab.value === 'pending' ? !alarm.handled : alarm.handled
|
||||
)
|
||||
}
|
||||
const filterAlarms = (category) => {
|
||||
return alarmData.value[category] || [];
|
||||
};
|
||||
|
||||
// 过滤后的告警列表
|
||||
const filteredObstacleAlarms = computed(() => filterAlarms(obstacleAlarms))
|
||||
const filteredTemperatureAlarms = computed(() => filterAlarms(temperatureAlarms))
|
||||
const filteredRoutineAlarms = computed(() => filterAlarms(routineAlarms))
|
||||
const filteredObstacleAlarms = computed(() => filterAlarms('obstacle'));
|
||||
const filteredTemperatureAlarms = computed(() => filterAlarms('temperature'));
|
||||
const filteredRoutineAlarms = computed(() => filterAlarms('routine'));
|
||||
|
||||
// 计算未处理和已处理的数量
|
||||
const pendingAlarmsCount = computed(() => {
|
||||
return [...obstacleAlarms.value, ...temperatureAlarms.value, ...routineAlarms.value]
|
||||
.filter(alarm => !alarm.handled).length
|
||||
})
|
||||
// 获取告警数量统计
|
||||
const fetchAlarmCount = async () => {
|
||||
try {
|
||||
const res = await homeApi.getAlertCount();
|
||||
if (res.code === 200) {
|
||||
pendingAlarmsCount.value = res.data.alert_count || 0;
|
||||
doneAlarmsCount.value = res.data.processed_count || 0;
|
||||
console.log('获取告警数量成功:', res.data);
|
||||
} else {
|
||||
console.error('获取告警数量失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取告警数量错误:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const doneAlarmsCount = computed(() => {
|
||||
return [...obstacleAlarms.value, ...temperatureAlarms.value, ...routineAlarms.value]
|
||||
.filter(alarm => alarm.handled).length
|
||||
})
|
||||
// 获取未处理告警列表
|
||||
const fetchUnhandledAlarms = async () => {
|
||||
if (currentTab.value !== 'pending') return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await homeApi.getNoSolve({
|
||||
skip: (currentPage.value - 1) * pageSize.value,
|
||||
limit: pageSize.value
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
apiAlarmData.value.unhandled = res.data || {};
|
||||
// 转换为UI所需格式
|
||||
alarmData.value = transformAlarmData(apiAlarmData.value.unhandled, false);
|
||||
console.log('获取未处理告警成功:', alarmData.value);
|
||||
} else {
|
||||
console.error('获取未处理告警失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取未处理告警错误:', err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const unreadCount = computed(() => 5) // 计算未读数量
|
||||
// 获取已处理告警列表
|
||||
const fetchHandledAlarms = async () => {
|
||||
if (currentTab.value !== 'done') return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const res = await homeApi.getSolveList();
|
||||
|
||||
if (res.code === 200) {
|
||||
apiAlarmData.value.handled = res.data || {};
|
||||
// 转换为UI所需格式
|
||||
alarmData.value = transformAlarmData(apiAlarmData.value.handled, true);
|
||||
console.log('获取已处理告警成功:', alarmData.value);
|
||||
} else {
|
||||
console.error('获取已处理告警失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取已处理告警错误:', err);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理单个告警
|
||||
const handleSingleAlarm = async (alarm) => {
|
||||
try {
|
||||
const res = await homeApi.getAlarmEvent({
|
||||
messageId: alarm.id,
|
||||
remark: alarm.remark || '已处理'
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
console.log('处理告警成功:', alarm.id);
|
||||
// 刷新数据
|
||||
fetchAlarmCount();
|
||||
fetchUnhandledAlarms();
|
||||
return true;
|
||||
} else {
|
||||
console.error('处理告警失败:', res);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('处理告警错误:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 一键处理所有告警
|
||||
const handleAllAlarms = async () => {
|
||||
try {
|
||||
const res = await homeApi.handleAllEvent();
|
||||
|
||||
if (res.code === 200) {
|
||||
console.log('一键处理所有告警成功');
|
||||
// 刷新数据
|
||||
fetchAlarmCount();
|
||||
fetchUnhandledAlarms();
|
||||
return true;
|
||||
} else {
|
||||
console.error('一键处理所有告警失败:', res);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('一键处理所有告警错误:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const unreadCount = computed(() => 0) // 未读数量暂时设为0,后续可扩展
|
||||
|
||||
const handleViewAll = () => {
|
||||
currentTab.value = 'all'
|
||||
}
|
||||
|
||||
const handleProcessAll = () => {
|
||||
obstacleAlarms.value.forEach(alarm => alarm.handled = true)
|
||||
temperatureAlarms.value.forEach(alarm => alarm.handled = true)
|
||||
routineAlarms.value.forEach(alarm => alarm.handled = true)
|
||||
// 显示确认对话框
|
||||
confirmDialogVisible.value = true;
|
||||
}
|
||||
|
||||
const handleAlarm = (alarm) => {
|
||||
alarm.handled = true
|
||||
// 确认一键处理所有告警
|
||||
const onConfirmProcessAll = async () => {
|
||||
const success = await handleAllAlarms();
|
||||
if (success) {
|
||||
// 成功后可以显示提示或其他操作
|
||||
console.log('所有告警已成功处理');
|
||||
}
|
||||
}
|
||||
|
||||
const handleAlarm = async (alarm) => {
|
||||
const success = await handleSingleAlarm(alarm);
|
||||
if (success) {
|
||||
// 告警已处理,可以直接从列表中移除或标记为已处理
|
||||
alarm.handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
const shouldShowAlarm = (type) => {
|
||||
switch(type) {
|
||||
case 'obstacle':
|
||||
return filteredObstacleAlarms.value.length > 0
|
||||
case 'temperature':
|
||||
return filteredTemperatureAlarms.value.length > 0
|
||||
case 'routine':
|
||||
return filteredRoutineAlarms.value.length > 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return filterAlarms(type).length > 0;
|
||||
};
|
||||
|
||||
const startScroll = () => {
|
||||
scrollInterval = setInterval(() => {
|
||||
@ -182,28 +418,74 @@ const currentAlarm = ref({});
|
||||
const showAlarmDetail = (alarm) => {
|
||||
currentAlarm.value = {
|
||||
...alarm,
|
||||
title: alarm.event, // 使用事件名称作为标题
|
||||
mainImage: alarm.image,
|
||||
subImages: [alarm.image, alarm.image], // 示例:可以根据实际数据调整
|
||||
type: alarm.type,
|
||||
subImages: alarm.subImages || [], // 使用API返回的图片数组
|
||||
type: eventTypeMap[alarm.type] || alarm.event, // 使用事件类型映射
|
||||
time: alarm.time,
|
||||
robotName: alarm.robotId,
|
||||
status: alarm.status
|
||||
status: alarm.handled ? '已处理' : '未处理',
|
||||
temperature: alarm.temperature || '',
|
||||
messageId: alarm.id // 用于处理时传递
|
||||
};
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const handleConfirm = (data) => {
|
||||
const handleConfirm = async (data) => {
|
||||
console.log('确认处理:', data);
|
||||
|
||||
// 如果是未处理的告警,则进行处理
|
||||
if (!data.handled && data.messageId) {
|
||||
const success = await handleSingleAlarm({
|
||||
id: data.messageId,
|
||||
remark: data.remark || '已处理'
|
||||
});
|
||||
|
||||
if (success) {
|
||||
// 重新加载数据
|
||||
fetchAlarmCount();
|
||||
if (currentTab.value === 'pending') {
|
||||
fetchUnhandledAlarms();
|
||||
} else {
|
||||
fetchHandledAlarms();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const handleReport = (data) => {
|
||||
const handleReport = async (data) => {
|
||||
console.log('处理并上报:', data);
|
||||
|
||||
// 处理逻辑与handleConfirm相同,可以根据需要修改
|
||||
await handleConfirm(data);
|
||||
|
||||
// 这里可以添加上报的额外逻辑
|
||||
console.log('上报功能待实现');
|
||||
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
startScroll()
|
||||
// 监听标签页切换,加载对应数据
|
||||
watch(currentTab, (newValue) => {
|
||||
console.log('标签页切换:', newValue);
|
||||
if (newValue === 'pending') {
|
||||
fetchUnhandledAlarms();
|
||||
} else if (newValue === 'done') {
|
||||
fetchHandledAlarms();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载告警数量
|
||||
await fetchAlarmCount();
|
||||
|
||||
// 加载未处理告警(默认标签页)
|
||||
await fetchUnhandledAlarms();
|
||||
|
||||
// 启动滚动
|
||||
startScroll();
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -337,7 +619,46 @@ onUnmounted(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* .handle-btn {
|
||||
.handled-text {
|
||||
color: #00ffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #B9E8FF;
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.loading-text:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(42, 185, 208, 0.5);
|
||||
border-top: 2px solid #2AB9D0;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: translateY(-50%) rotate(0deg); }
|
||||
100% { transform: translateY(-50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
.handle-btn {
|
||||
background: #52213C;
|
||||
border: 1px solid #CB1A49;
|
||||
color: white;
|
||||
@ -345,10 +666,5 @@ onUnmounted(() => {
|
||||
padding: 2px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
} */
|
||||
|
||||
.handled-text {
|
||||
color: #00ffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -3,7 +3,12 @@
|
||||
<div class="list-header">
|
||||
<div class="search-wrapper">
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入机器人搜索">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入机器人搜索"
|
||||
v-model="searchText"
|
||||
@input="handleSearchInput"
|
||||
>
|
||||
<img src="../assets/img/fdj.png" alt="搜索" class="search-icon" />
|
||||
</div>
|
||||
<CustomSelect
|
||||
@ -16,91 +21,197 @@
|
||||
</div>
|
||||
|
||||
<div class="list-content" ref="scrollContainer" @wheel="handleWheel">
|
||||
<div v-for="(category, categoryIndex) in groupedRobots" :key="categoryIndex" class="robot-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-title">{{ category.title }} ({{ category.robots.length }})</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(item, index) in category.robots" :key="index" class="list-item">
|
||||
<div class="list-item-content" @click="goToRobotDetail(item.id)">
|
||||
<div class="robot-id">{{ item.id }}</div>
|
||||
<div class="robot-info">
|
||||
<BatteryIndicator :battery="item.battery" />
|
||||
<div class="robot-status" :class="item.status.toLowerCase()">{{ item.status }}</div>
|
||||
</div>
|
||||
<div v-if="item.tag" class="alarm-wrapper">
|
||||
<img src="../assets/img/tip.png" alt="告警" class="alarm-tip" />
|
||||
<span class="alarm-text">告警</span>
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error-state">
|
||||
<div class="error-icon">!</div>
|
||||
<div class="error-text">{{ error }}</div>
|
||||
<button class="retry-button" @click="fetchRobotList">重试</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="groupedRobots.length === 0" class="empty-state">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<div class="empty-text">没有找到机器人</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-for="(category, categoryIndex) in groupedRobots" :key="categoryIndex" class="robot-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon"></div>
|
||||
<div class="category-title">{{ category.title }} ({{ category.robots.length }})</div>
|
||||
</div>
|
||||
|
||||
<div v-for="(item, index) in category.robots" :key="index" class="list-item">
|
||||
<div class="list-item-content" @click="goToRobotDetail(item.id)">
|
||||
<div class="robot-id">{{ item.id }}</div>
|
||||
<div class="robot-info">
|
||||
<BatteryIndicator :battery="item.battery" />
|
||||
<div class="robot-status" :class="item.status.toLowerCase().replace(/\s+/g, '-')">{{ item.status }}</div>
|
||||
</div>
|
||||
<div v-if="item.tag" class="alarm-wrapper">
|
||||
<img src="../assets/img/tip.png" alt="告警" class="alarm-tip" />
|
||||
<span class="alarm-text">{{ item.tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BatteryIndicator from './common/BatteryIndicator.vue'
|
||||
import CustomSelect from './common/CustomSelect.vue'
|
||||
import { homeApi } from '../api/index'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 模拟数据
|
||||
const robotsData = [
|
||||
{ id: 'X32305000019', battery: 16, status: '故障', type: 'outdoor', tag: '紧急' },
|
||||
{ id: 'X32305000019', battery: 28, status: '离线', type: 'outdoor', tag: '紧急' },
|
||||
{ id: 'X32305000019', battery: 64, status: '在线', type: 'outdoor' },
|
||||
{ id: 'X32305000019', battery: 86, status: '故障', type: 'outdoor' },
|
||||
{ id: 'X32305000019', battery: 16, status: '故障', type: 'transport', tag: '告警' },
|
||||
{ id: 'X32305000019', battery: 28, status: '离线', type: 'transport', tag: '告警' },
|
||||
{ id: 'X32305000019', battery: 73, status: '在线', type: 'transport' },
|
||||
{ id: 'X32305000019', battery: 16, status: '故障', type: 'emergency' }
|
||||
]
|
||||
// 搜索和筛选
|
||||
const searchText = ref('')
|
||||
const filterOptions = ['全部', '在线', '离线', '故障']
|
||||
const selectedFilter = ref('全部')
|
||||
|
||||
// 机器人列表数据
|
||||
const robotList = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
// 获取机器人列表
|
||||
const fetchRobotList = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const res = await homeApi.getRobotList({
|
||||
tenantInfoId: '4fff5d4bcc4b4239941ff077a0da8958', // 租户id
|
||||
number: null, // 机器人名
|
||||
status: null, // 是否故障
|
||||
onlineStatus: null // 在线状态
|
||||
})
|
||||
|
||||
if (res.code === 200) {
|
||||
// 处理返回的数据
|
||||
const allRobots = []
|
||||
|
||||
// 遍历分组数据
|
||||
Object.entries(res.data).forEach(([groupName, robots]) => {
|
||||
robots.forEach(robot => {
|
||||
// 添加分组名称
|
||||
const robotData = {
|
||||
id: robot.number,
|
||||
robotId: robot.robotId,
|
||||
groupName: groupName,
|
||||
groupId: robot.groupingId,
|
||||
battery: parseInt(robot.robotInfo?.power || 0),
|
||||
status: getStatusText(robot.onlineStatus, robot.status),
|
||||
type: getGroupType(groupName),
|
||||
tag: robot.onlineStatus === '1' && robot.status === '3' ? '故障' : null
|
||||
}
|
||||
|
||||
allRobots.push(robotData)
|
||||
})
|
||||
})
|
||||
|
||||
robotList.value = allRobots
|
||||
console.log('获取机器人列表成功:', robotList.value)
|
||||
} else {
|
||||
error.value = res.msg || '获取机器人列表失败'
|
||||
console.error('获取机器人列表失败:', res)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = '获取机器人列表失败'
|
||||
console.error('获取机器人列表错误:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态码获取状态文本
|
||||
const getStatusText = (onlineStatus, status) => {
|
||||
if (onlineStatus === '0') return '离线'
|
||||
|
||||
// 在线状态下,根据status判断
|
||||
const statusMap = {
|
||||
'0': '充电中',
|
||||
'1': '待机中',
|
||||
'2': '运行中',
|
||||
'3': '故障'
|
||||
}
|
||||
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 根据分组名称获取类型
|
||||
const getGroupType = (groupName) => {
|
||||
// 根据分组名称映射到类型
|
||||
const typeMap = {
|
||||
'室外巡检': 'outdoor',
|
||||
'物流运输': 'transport',
|
||||
'应急救援': 'emergency'
|
||||
}
|
||||
|
||||
return typeMap[groupName] || 'outdoor'
|
||||
}
|
||||
|
||||
const robotList = ref(robotsData)
|
||||
const scrollContainer = ref(null)
|
||||
let scrollInterval = null
|
||||
let canManualScroll = ref(false)
|
||||
|
||||
const filterOptions = ['全部', '在线', '离线', '故障'];
|
||||
const selectedFilter = ref('全部');
|
||||
|
||||
// 按类型分组
|
||||
const groupedRobots = computed(() => {
|
||||
// 先根据筛选条件过滤
|
||||
let filtered = robotList.value
|
||||
|
||||
// 根据搜索文本过滤
|
||||
if (searchText.value) {
|
||||
filtered = filtered.filter(robot =>
|
||||
robot.id.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// 根据状态过滤
|
||||
if (selectedFilter.value !== '全部') {
|
||||
const statusMap = {
|
||||
'在线': ['运行中', '待机中', '充电中'],
|
||||
'离线': ['离线'],
|
||||
'故障': ['故障']
|
||||
}
|
||||
|
||||
const statusList = statusMap[selectedFilter.value] || []
|
||||
filtered = filtered.filter(robot => statusList.includes(robot.status))
|
||||
}
|
||||
|
||||
// 按类型分组
|
||||
const grouped = [
|
||||
{
|
||||
type: 'outdoor',
|
||||
title: '室外巡检',
|
||||
robots: robotList.value.filter(robot => robot.type === 'outdoor')
|
||||
robots: filtered.filter(robot => robot.type === 'outdoor')
|
||||
},
|
||||
{
|
||||
type: 'transport',
|
||||
title: '物流运输',
|
||||
robots: robotList.value.filter(robot => robot.type === 'transport')
|
||||
robots: filtered.filter(robot => robot.type === 'transport')
|
||||
},
|
||||
{
|
||||
type: 'emergency',
|
||||
title: '应急救援',
|
||||
robots: robotList.value.filter(robot => robot.type === 'emergency')
|
||||
robots: filtered.filter(robot => robot.type === 'emergency')
|
||||
}
|
||||
]
|
||||
return grouped
|
||||
|
||||
// 过滤掉没有机器人的分组
|
||||
return grouped.filter(group => group.robots.length > 0)
|
||||
})
|
||||
|
||||
const getBatteryClass = (battery) => {
|
||||
if (battery <= 50) return 'critical'
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
const getCategoryIconClass = (type) => {
|
||||
return {
|
||||
'outdoor': 'icon-outdoor',
|
||||
'transport': 'icon-transport',
|
||||
'emergency': 'icon-emergency'
|
||||
}[type] || ''
|
||||
const handleSearchInput = (e) => {
|
||||
searchText.value = e.target.value
|
||||
}
|
||||
|
||||
const handleWheel = (e) => {
|
||||
@ -131,17 +242,23 @@ const stopScroll = () => {
|
||||
canManualScroll.value = true
|
||||
}
|
||||
|
||||
const goToRobotDetail = (robotId) => {
|
||||
router.push(`/robot/${robotId}`)
|
||||
const goToRobotDetail = (id) => {
|
||||
router.push(`/robot/${id}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRobotList()
|
||||
startScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopScroll()
|
||||
})
|
||||
|
||||
// 监听筛选条件变化,重新获取数据
|
||||
watch(selectedFilter, () => {
|
||||
console.log('筛选条件变化:', selectedFilter.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -356,4 +473,89 @@ onUnmounted(() => {
|
||||
background: rgba(185, 232, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #B9E8FF;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(185, 232, 255, 0.2);
|
||||
border-top: 3px solid #B9E8FF;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.error-text,
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.error-icon,
|
||||
.empty-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
background-color: rgba(255, 77, 79, 0.2);
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-top: 15px;
|
||||
padding: 5px 15px;
|
||||
background-color: rgba(0, 145, 169, 0.2);
|
||||
border: 1px solid #0091A9;
|
||||
border-radius: 4px;
|
||||
color: #B9E8FF;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.retry-button:hover {
|
||||
background-color: rgba(0, 145, 169, 0.4);
|
||||
}
|
||||
|
||||
.robot-status.故障 {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.robot-status.离线 {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.robot-status.运行中 {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.robot-status.充电中 {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.robot-status.待机中 {
|
||||
color: #00ffff;
|
||||
}
|
||||
</style>
|
||||
298
src/components/common/CustomWebRTCPlayer.vue
Normal file
298
src/components/common/CustomWebRTCPlayer.vue
Normal file
@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="webrtc-player">
|
||||
<EmptyState
|
||||
v-if="!streamUrl"
|
||||
subtitle="暂无信息"
|
||||
:iconSrc="empty"
|
||||
class="video-empty-state"
|
||||
/>
|
||||
<div v-else class="player-container">
|
||||
<video
|
||||
ref="videoRef"
|
||||
autoplay
|
||||
muted
|
||||
:class="['video-element', fillModeClass]"
|
||||
></video>
|
||||
<div v-if="isLoading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import EmptyState from './EmptyState.vue';
|
||||
import empty from '../../assets/img/empty.png';
|
||||
|
||||
const props = defineProps({
|
||||
streamUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fillMode: {
|
||||
type: String,
|
||||
default: 'fill', // 'fill', 'cover' 或 'stretch-height'
|
||||
validator: (value) => ['fill', 'cover', 'stretch-height'].includes(value)
|
||||
},
|
||||
clientIp: {
|
||||
type: String,
|
||||
default: '127.0.0.1'
|
||||
}
|
||||
});
|
||||
|
||||
const videoRef = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const pc = ref(null); // WebRTC连接
|
||||
|
||||
// 根据填充模式计算CSS类
|
||||
const fillModeClass = computed(() => {
|
||||
switch (props.fillMode) {
|
||||
case 'fill': return 'video-fill';
|
||||
case 'cover': return 'video-cover';
|
||||
case 'stretch-height': return 'video-stretch-height';
|
||||
default: return 'video-fill';
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化WebRTC连接
|
||||
const initWebRTC = async () => {
|
||||
if (!props.streamUrl || !videoRef.value) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// 清理之前的连接
|
||||
cleanupWebRTC();
|
||||
|
||||
// 创建新的RTCPeerConnection
|
||||
pc.value = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' }
|
||||
]
|
||||
});
|
||||
|
||||
// 监听ICE候选
|
||||
pc.value.onicecandidate = (event) => {
|
||||
if (event.candidate) {
|
||||
console.log('ICE候选:', event.candidate);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听连接状态
|
||||
pc.value.onconnectionstatechange = () => {
|
||||
console.log('连接状态:', pc.value.connectionState);
|
||||
};
|
||||
|
||||
// 监听ICE连接状态
|
||||
pc.value.oniceconnectionstatechange = () => {
|
||||
console.log('ICE连接状态:', pc.value.iceConnectionState);
|
||||
|
||||
// 如果连接失败,尝试重新连接
|
||||
if (pc.value.iceConnectionState === 'failed' || pc.value.iceConnectionState === 'disconnected') {
|
||||
console.log('WebRTC连接失败,尝试重新连接...');
|
||||
setTimeout(() => {
|
||||
initWebRTC();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听媒体轨道
|
||||
pc.value.ontrack = (event) => {
|
||||
if (videoRef.value && event.streams && event.streams[0]) {
|
||||
videoRef.value.srcObject = event.streams[0];
|
||||
isLoading.value = false;
|
||||
console.log(`WebRTC流 ${props.title} 连接成功`);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建offer
|
||||
const offer = await pc.value.createOffer({
|
||||
offerToReceiveAudio: true,
|
||||
offerToReceiveVideo: true
|
||||
});
|
||||
|
||||
// 设置本地描述
|
||||
await pc.value.setLocalDescription(offer);
|
||||
|
||||
// 构建API URL
|
||||
const apiUrl = 'https://qvs-live.thirdmonitor.concoai.com:447/rtc/v1/play';
|
||||
|
||||
// 发送offer到服务器并获取answer
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientip: props.clientIp,
|
||||
sdp: pc.value.localDescription.sdp,
|
||||
streamurl: props.streamUrl
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !data.sdp) {
|
||||
throw new Error('服务器未返回有效SDP');
|
||||
}
|
||||
|
||||
// 设置远程描述
|
||||
await pc.value.setRemoteDescription(new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: data.sdp
|
||||
}));
|
||||
|
||||
// 设置超时处理
|
||||
setTimeout(() => {
|
||||
if (isLoading.value) {
|
||||
isLoading.value = false;
|
||||
console.error('WebRTC连接超时');
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
} catch (err) {
|
||||
console.error('WebRTC连接错误:', err);
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 清理WebRTC连接
|
||||
const cleanupWebRTC = () => {
|
||||
if (pc.value) {
|
||||
pc.value.close();
|
||||
pc.value = null;
|
||||
}
|
||||
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = null;
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
};
|
||||
|
||||
// 监听流URL变化
|
||||
watch(() => props.streamUrl, (newUrl) => {
|
||||
if (newUrl) {
|
||||
initWebRTC();
|
||||
} else {
|
||||
cleanupWebRTC();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.streamUrl) {
|
||||
initWebRTC();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupWebRTC();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.webrtc-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #033347;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-fill {
|
||||
object-fit: fill; /* 拉伸填充,可能会变形 */
|
||||
}
|
||||
|
||||
.video-cover {
|
||||
object-fit: cover; /* 保持比例填充,可能会裁剪 */
|
||||
}
|
||||
|
||||
.video-stretch-height {
|
||||
object-fit: none; /* 不调整大小 */
|
||||
width: 100%;
|
||||
height: auto !important; /* 自动调整高度 */
|
||||
min-height: 100%; /* 确保至少填满容器高度 */
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%); /* 垂直居中 */
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(3, 51, 71, 0.7);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(42, 185, 208, 0.3);
|
||||
border-top: 3px solid #2AB9D0;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #B9E8FF;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.video-empty-state {
|
||||
padding-top: 30px;
|
||||
background: #033347;
|
||||
}
|
||||
|
||||
:deep(.empty-icon) {
|
||||
width: 100px;
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
:deep(.empty-title) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.empty-subtitle) {
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
57
src/components/common/EmptyState.vue
Normal file
57
src/components/common/EmptyState.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon" :style="{ backgroundImage: `url(${iconSrc})` }"></div>
|
||||
<div class="empty-title" v-if="title">{{ title }}</div>
|
||||
<div class="empty-subtitle" v-if="subtitle">{{ subtitle }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import emptyList from '../../assets/img/empty_list.png'
|
||||
defineProps({
|
||||
iconSrc: {
|
||||
type: String,
|
||||
default: emptyList
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '暂无告警事件'
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '所有告警已处理完成'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding-top: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
color: #2AB9D0;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
color: #2AB9D0;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
370
src/components/common/VideoPlayer.vue
Normal file
370
src/components/common/VideoPlayer.vue
Normal file
@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="video-player">
|
||||
<EmptyState
|
||||
v-if="!streamUrl"
|
||||
subtitle="暂无信息"
|
||||
:iconSrc="empty"
|
||||
class="video-empty-state"
|
||||
/>
|
||||
<div v-else class="video-container">
|
||||
<video
|
||||
ref="videoRef"
|
||||
class="video-element"
|
||||
:class="fillModeClass"
|
||||
muted
|
||||
autoplay
|
||||
></video>
|
||||
|
||||
|
||||
|
||||
<!-- 视频控制栏 -->
|
||||
<div class="video-controls" v-if="streamUrl">
|
||||
<div class="control-left">
|
||||
<button @click="togglePlay" class="control-btn">
|
||||
<i :class="isPlaying ? 'icon-pause' : 'icon-play'"></i>
|
||||
</button>
|
||||
<div class="volume-control">
|
||||
<button @click="toggleMute" class="control-btn">
|
||||
<i :class="isMuted ? 'icon-mute' : 'icon-volume'"></i>
|
||||
</button>
|
||||
<!-- <div class="volume-slider">
|
||||
<div class="volume-track">
|
||||
<div class="volume-fill" :style="{width: volume + '%'}"></div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-right">
|
||||
<button @click="toggleFullscreen" class="control-btn">
|
||||
<i class="icon-fullscreen"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import EmptyState from './EmptyState.vue';
|
||||
import empty from '../../assets/img/empty.png';
|
||||
|
||||
const props = defineProps({
|
||||
streamUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fillMode: {
|
||||
type: String,
|
||||
default: 'fill', // 'fill', 'cover' 或 'stretch-height'
|
||||
validator: (value) => ['fill', 'cover', 'stretch-height'].includes(value)
|
||||
}
|
||||
});
|
||||
|
||||
const videoRef = ref(null);
|
||||
const isPlaying = ref(false);
|
||||
const isMuted = ref(true);
|
||||
const volume = ref(100);
|
||||
const isLiveStream = ref(false);
|
||||
|
||||
// 根据填充模式计算CSS类
|
||||
const fillModeClass = computed(() => {
|
||||
switch (props.fillMode) {
|
||||
case 'fill': return 'video-fill';
|
||||
case 'cover': return 'video-cover';
|
||||
case 'stretch-height': return 'video-stretch-height';
|
||||
default: return 'video-fill';
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否为直播流
|
||||
const checkIfLiveStream = (url) => {
|
||||
if (!url) return false;
|
||||
// 检查是否为m3u8格式
|
||||
return url.includes('.m3u8');
|
||||
};
|
||||
|
||||
// 初始化视频流
|
||||
const initVideoStream = () => {
|
||||
if (!props.streamUrl || !videoRef.value) return;
|
||||
|
||||
try {
|
||||
const video = videoRef.value;
|
||||
|
||||
// 检查是否为直播流
|
||||
isLiveStream.value = checkIfLiveStream(props.streamUrl);
|
||||
|
||||
// 设置视频源
|
||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// 原生支持HLS
|
||||
video.src = props.streamUrl;
|
||||
} else if (window.Hls && window.Hls.isSupported()) {
|
||||
// 使用hls.js播放
|
||||
const hls = new window.Hls();
|
||||
hls.loadSource(props.streamUrl);
|
||||
hls.attachMedia(video);
|
||||
} else {
|
||||
// 尝试直接播放
|
||||
video.src = props.streamUrl;
|
||||
}
|
||||
|
||||
// 设置事件监听
|
||||
video.addEventListener('play', () => { isPlaying.value = true; });
|
||||
video.addEventListener('pause', () => { isPlaying.value = false; });
|
||||
|
||||
// 尝试播放
|
||||
video.play().catch(err => {
|
||||
console.error('视频播放失败:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('初始化视频流错误:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 播放/暂停切换
|
||||
const togglePlay = () => {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
if (videoRef.value.paused) {
|
||||
videoRef.value.play();
|
||||
isPlaying.value = true;
|
||||
} else {
|
||||
videoRef.value.pause();
|
||||
isPlaying.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 静音切换
|
||||
const toggleMute = () => {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
videoRef.value.muted = !videoRef.value.muted;
|
||||
isMuted.value = videoRef.value.muted;
|
||||
};
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = () => {
|
||||
if (!videoRef.value) return;
|
||||
|
||||
try {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
videoRef.value.requestFullscreen();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('全屏切换失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听流URL变化
|
||||
watch(() => props.streamUrl, (newUrl) => {
|
||||
if (newUrl) {
|
||||
// 检查是否为直播流
|
||||
isLiveStream.value = checkIfLiveStream(newUrl);
|
||||
|
||||
// 延迟一帧再初始化,确保DOM已更新
|
||||
setTimeout(() => {
|
||||
initVideoStream();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 添加全局hls.js脚本
|
||||
if (!window.Hls) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
|
||||
script.onload = () => {
|
||||
if (props.streamUrl) {
|
||||
initVideoStream();
|
||||
}
|
||||
};
|
||||
} else if (props.streamUrl) {
|
||||
initVideoStream();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听
|
||||
if (videoRef.value) {
|
||||
videoRef.value.removeEventListener('play', () => { isPlaying.value = true; });
|
||||
videoRef.value.removeEventListener('pause', () => { isPlaying.value = false; });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #033347;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-fill {
|
||||
object-fit: fill; /* 拉伸填充,可能会变形 */
|
||||
}
|
||||
|
||||
.video-cover {
|
||||
object-fit: cover; /* 保持比例填充,可能会裁剪 */
|
||||
}
|
||||
|
||||
.video-stretch-height {
|
||||
object-fit: none; /* 不调整大小 */
|
||||
width: 100%;
|
||||
height: auto !important; /* 自动调整高度 */
|
||||
min-height: 100%; /* 确保至少填满容器高度 */
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%); /* 垂直居中 */
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 视频控制栏 */
|
||||
.video-controls {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
height: 30px; /* 控制底部高度 */
|
||||
}
|
||||
|
||||
.video-container:hover .video-controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control-left,
|
||||
.control-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.control-btn i {
|
||||
font-size: 18px;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.icon-play {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>');
|
||||
}
|
||||
|
||||
.icon-pause {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>');
|
||||
}
|
||||
|
||||
.icon-volume {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>');
|
||||
}
|
||||
|
||||
.icon-mute {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>');
|
||||
}
|
||||
|
||||
.icon-fullscreen {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>');
|
||||
}
|
||||
|
||||
/* 音量控制样式 */
|
||||
.volume-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume-track {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.volume-fill {
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.video-empty-state {
|
||||
padding-top: 30px;
|
||||
background: #033347;
|
||||
}
|
||||
|
||||
:deep(.empty-icon) {
|
||||
width: 100px;
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
:deep(.empty-title) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.empty-subtitle) {
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
@ -2,8 +2,9 @@
|
||||
<div v-if="visible" class="confirm-dialog-overlay">
|
||||
<div class="confirm-dialog">
|
||||
<div class="confirm-dialog-header">
|
||||
<span class="title">{{ title }}</span>
|
||||
<button class="close-btn" @click="handleCancel">×</button>
|
||||
<span class="title textjb">{{ title }}</span>
|
||||
<!-- <button class="close-btn" @click="handleCancel">×</button> -->
|
||||
<img src="../../assets/img/close.png" @click="handleCancel" alt="" class="close-btn">
|
||||
</div>
|
||||
<div class="confirm-dialog-content">
|
||||
{{ message }}
|
||||
@ -60,23 +61,32 @@ const handleCancel = () => {
|
||||
}
|
||||
|
||||
.confirm-dialog {
|
||||
width: 400px;
|
||||
background: url("../../assets/img/alert.png") no-repeat;
|
||||
width:700px;
|
||||
height:310px;
|
||||
background: url("../../assets/img/alert3.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
/* border-radius: 4px; */
|
||||
padding:10px 20px;
|
||||
letter-spacing: 2px;
|
||||
|
||||
}
|
||||
|
||||
.confirm-dialog-header {
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
/* margin-bottom: 40px; */
|
||||
|
||||
/* line-height: 40px; */
|
||||
letter-spacing: 2px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #B9E8FF;
|
||||
font-size: 18px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@ -86,11 +96,21 @@ const handleCancel = () => {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.confirm-dialog-content::before{
|
||||
content: url("../../assets/img/delete.png");
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.confirm-dialog-content {
|
||||
height: 70%;
|
||||
color: #B9E8FF;
|
||||
margin-bottom: 20px;
|
||||
min-height: 60px;
|
||||
/* margin-bottom: 20px; */
|
||||
min-height: 40px;
|
||||
font-size: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -107,9 +127,10 @@ const handleCancel = () => {
|
||||
height: 32px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-size: 18px;
|
||||
background-size: 100% 100%;
|
||||
color: #C6F4FF;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn.cancel {
|
||||
@ -118,7 +139,7 @@ const handleCancel = () => {
|
||||
}
|
||||
|
||||
.btn.confirm {
|
||||
background: url("../../assets/img/confirm.png") no-repeat;
|
||||
background: url("../../assets/img/report.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
</style>
|
||||
@ -75,8 +75,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import BatteryIndicator from '../common/BatteryIndicator.vue';
|
||||
import { homeApi } from '../../api/index';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@ -105,7 +106,7 @@ const handleTabClick = (type) => {
|
||||
};
|
||||
|
||||
// 类型选择相关
|
||||
const robotTypes = ['全部类型', '安防机器人', '巡检机器人', '消防机器人'];
|
||||
const robotTypes = ref(['全部类型']);
|
||||
const selectedType = ref('全部类型');
|
||||
const selectOpen = ref(false);
|
||||
|
||||
@ -122,17 +123,81 @@ const selectType = (type) => {
|
||||
selectOpen.value = false;
|
||||
};
|
||||
|
||||
// 模拟数据
|
||||
const robotList = ref([
|
||||
{ id: 'X32305000001', name: '安防机器人', status: '运行中', battery: 98 },
|
||||
{ id: 'X32305000002', name: '巡检机器人', status: '运行中', battery: 85 },
|
||||
{ id: 'X32305000003', name: '消防机器人', status: '离线', battery: 12 },
|
||||
{ id: 'X32305000004', name: '安防机器人', status: '充电中', battery: 28 },
|
||||
{ id: 'X32305000005', name: '巡检机器人', status: '运行中', battery: 75 },
|
||||
{ id: 'X32305000006', name: '消防机器人', status: '离线', battery: 0 },
|
||||
{ id: 'X32305000007', name: '安防机器人', status: '运行中', battery: 92 },
|
||||
{ id: 'X32305000008', name: '巡检机器人', status: '充电中', battery: 45 },
|
||||
]);
|
||||
// 机器人列表数据
|
||||
const robotList = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
// 获取机器人列表
|
||||
const fetchRobotList = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const res = await homeApi.getRobotList({
|
||||
tenantInfoId: '4fff5d4bcc4b4239941ff077a0da8958', // 租户id
|
||||
number: null, // 机器人名
|
||||
status: null, // 是否故障
|
||||
onlineStatus: null // 在线状态
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
// 处理返回的数据
|
||||
const allRobots = [];
|
||||
const groupTypes = new Set(['全部类型']);
|
||||
|
||||
// 遍历分组数据
|
||||
Object.entries(res.data).forEach(([groupName, robots]) => {
|
||||
robots.forEach(robot => {
|
||||
// 添加分组名称
|
||||
const robotData = {
|
||||
id: robot.number,
|
||||
name: groupName,
|
||||
groupId: robot.groupingId,
|
||||
robotId: robot.robotId,
|
||||
status: getStatusText(robot.onlineStatus, robot.status),
|
||||
battery: robot.robotInfo?.power || 0,
|
||||
position: robot.robotInfo?.positon || '',
|
||||
temperature: robot.robotInfo?.temperature || '',
|
||||
humidity: robot.robotInfo?.humidity || '',
|
||||
updateTime: robot.robotInfo?.updateTime || ''
|
||||
};
|
||||
|
||||
allRobots.push(robotData);
|
||||
groupTypes.add(groupName);
|
||||
});
|
||||
});
|
||||
|
||||
robotList.value = allRobots;
|
||||
robotTypes.value = ['全部类型', ...Array.from(groupTypes).filter(type => type !== '全部类型')];
|
||||
|
||||
console.log('获取机器人列表成功:', robotList.value);
|
||||
} else {
|
||||
error.value = res.msg || '获取机器人列表失败';
|
||||
console.error('获取机器人列表失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = '获取机器人列表失败';
|
||||
console.error('获取机器人列表错误:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 根据状态码获取状态文本
|
||||
const getStatusText = (onlineStatus, status) => {
|
||||
if (onlineStatus === '0') return '离线';
|
||||
|
||||
// 在线状态下,根据status判断
|
||||
const statusMap = {
|
||||
'0': '充电中',
|
||||
'1': '待机中',
|
||||
'2': '运行中',
|
||||
'3': '故障'
|
||||
};
|
||||
|
||||
return statusMap[status] || '未知';
|
||||
};
|
||||
|
||||
// 过滤逻辑
|
||||
const filteredRobots = computed(() => {
|
||||
@ -141,11 +206,13 @@ const filteredRobots = computed(() => {
|
||||
// 状态过滤
|
||||
if (currentTab.value !== 'all') {
|
||||
const statusMap = {
|
||||
online: '运行中',
|
||||
offline: '离线',
|
||||
fault: '故障'
|
||||
online: ['运行中', '待机中'],
|
||||
offline: ['离线'],
|
||||
fault: ['故障']
|
||||
};
|
||||
result = result.filter(robot => robot.status === statusMap[currentTab.value]);
|
||||
|
||||
const statusList = statusMap[currentTab.value] || [];
|
||||
result = result.filter(robot => statusList.includes(robot.status));
|
||||
}
|
||||
|
||||
// 类型过滤
|
||||
@ -161,13 +228,28 @@ const getTabCount = (type) => {
|
||||
if (type === 'all') return robotList.value.length;
|
||||
|
||||
const statusMap = {
|
||||
online: '运行中',
|
||||
offline: '离线',
|
||||
fault: '故障'
|
||||
online: ['运行中', '待机中'],
|
||||
offline: ['离线'],
|
||||
fault: ['故障']
|
||||
};
|
||||
|
||||
return robotList.value.filter(robot => robot.status === statusMap[type]).length;
|
||||
const statusList = statusMap[type] || [];
|
||||
return robotList.value.filter(robot => statusList.includes(robot.status)).length;
|
||||
};
|
||||
|
||||
// 监听弹窗显示状态,显示时获取数据
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
fetchRobotList();
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
fetchRobotList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -87,4 +87,5 @@ concoaiService.interceptors.response.use(
|
||||
}
|
||||
)
|
||||
|
||||
export { service }
|
||||
export default service
|
||||
|
||||
427
src/views/Home copy.vue
Normal file
427
src/views/Home copy.vue
Normal file
@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="main">
|
||||
<!-- 渐变边框 -->
|
||||
<div class="border-gradient top"></div>
|
||||
<div class="border-gradient bottom"></div>
|
||||
<div class="border-gradient left"></div>
|
||||
<div class="border-gradient right"></div>
|
||||
|
||||
<!-- 顶部组件 -->
|
||||
<TopHeader />
|
||||
|
||||
<!-- 左侧内容 -->
|
||||
<div class="left_content">
|
||||
<TitleBlock title="机器人统计">机器人统计</TitleBlock>
|
||||
<div class="statistics-content">
|
||||
<div class="statistics-grid">
|
||||
<StatisticCard
|
||||
v-for="item in statistics"
|
||||
:key="item.title"
|
||||
:iconSrc="item.icon"
|
||||
:value="item.value"
|
||||
:title="item.title"
|
||||
@click="handleStatisticClick(item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TitleBlock title="机器人状态列表">机器人状态列表</TitleBlock>
|
||||
<div class="status-list-content">
|
||||
<RobotStatusList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="right_content">
|
||||
<TitleBlock title="最新告警">最新告警</TitleBlock>
|
||||
<div class="alert-content">
|
||||
<LatestAlarms />
|
||||
</div>
|
||||
|
||||
<TitleBlock title="告警事件统计">告警事件统计</TitleBlock>
|
||||
<div class="event-list-content">
|
||||
<AlarmStatistics />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部监控区域 -->
|
||||
<div class="bottom_content">
|
||||
<TitleBlock title="厂区及机器人实时监控">厂区及机器人实时监控</TitleBlock>
|
||||
<div class="monitor-content">
|
||||
<div v-if="displayCameras.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<!-- <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30 10C31.3 10 32.4 11.1 32.4 12.4V15.2C35.5 15.9 38.4 17.4 40.6 19.6C43.5 22.5 45 26.4 45 30.5C45 38.9 38.4 45.5 30 45.5C21.6 45.5 15 38.9 15 30.5C15 22.7 20.6 16.4 28 15.3V12.4C28 11.1 29.1 10 30.4 10H30ZM30 19C24.5 19 20 23.5 20 29C20 34.5 24.5 39 30 39C35.5 39 40 34.5 40 29C40 23.5 35.5 19 30 19ZM25.5 25.5C26.3 25.5 27 26.2 27 27V31C27 31.8 26.3 32.5 25.5 32.5C24.7 32.5 24 31.8 24 31V27C24 26.2 24.7 25.5 25.5 25.5ZM34.5 25.5C35.3 25.5 36 26.2 36 27V31C36 31.8 35.3 32.5 34.5 32.5C33.7 32.5 33 31.8 33 31V27C33 26.2 33.7 25.5 34.5 25.5Z" fill="#B9E8FF" fill-opacity="0.5"/>
|
||||
</svg> -->
|
||||
</div>
|
||||
<div class="empty-text">暂无信息</div>
|
||||
</div>
|
||||
<div v-else class="camera-grid">
|
||||
<div v-for="(camera, index) in displayCameras" :key="index" class="camera-item">
|
||||
<div class="camera-header">
|
||||
<div class="camera-title">
|
||||
<img :src="camera.src" alt="摄像头" class="camera-icon" />
|
||||
<p>{{camera.title}}</p>
|
||||
</div>
|
||||
<CustomSelect
|
||||
v-model="selectedViews[camera.title]"
|
||||
:options="viewOptions"
|
||||
size="small"
|
||||
variant="search"
|
||||
font="small"
|
||||
/>
|
||||
</div>
|
||||
<div class="camera-feed">
|
||||
<img />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 机器人列表弹窗 -->
|
||||
<RobotListModal v-model:visible="showRobotListModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import TopHeader from '../components/common/TopHeader.vue'
|
||||
import TitleBlock from '../components/common/TitleBlock.vue'
|
||||
import StatisticCard from '../components/StatisticCard.vue'
|
||||
import RobotStatusList from '../components/RobotStatusList.vue'
|
||||
import AlarmStatistics from '../components/AlarmStatistics.vue'
|
||||
import LatestAlarms from '../components/LatestAlarms.vue'
|
||||
import RobotListModal from '../components/dialog/RobotListModal.vue'
|
||||
import CustomSelect from '../components/common/CustomSelect.vue'
|
||||
import { homeApi } from '../api/index'
|
||||
import Icon1 from '../assets/img/icon1.png'
|
||||
import Icon2 from '../assets/img/icon2.png'
|
||||
import Icon3 from '../assets/img/icon3.png'
|
||||
import Icon4 from '../assets/img/icon4.png'
|
||||
import jkA from '../assets/img/jkA.png'
|
||||
import jkRobot from '../assets/img/jkRobot.png'
|
||||
|
||||
// 统计数据
|
||||
const statistics = ref([
|
||||
{ icon: Icon1, value: 0, title: '总数量' },
|
||||
{ icon: Icon2, value: 0, title: '在线数量' },
|
||||
{ icon: Icon3, value: 0, title: '离线数量' },
|
||||
{ icon: Icon4, value: 0, title: '故障数量' }
|
||||
]);
|
||||
|
||||
// 获取机器人统计数据
|
||||
const fetchRobotStatistics = async () => {
|
||||
try {
|
||||
// 获取机器人列表
|
||||
const res = await homeApi.getRobotList({
|
||||
tenantInfoId: '4fff5d4bcc4b4239941ff077a0da8958', // 租户id
|
||||
number: null, // 机器人名
|
||||
status: null, // 是否故障
|
||||
onlineStatus: null // 在线状态
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
// 计算统计数据
|
||||
let totalCount = 0;
|
||||
let onlineCount = 0;
|
||||
let offlineCount = 0;
|
||||
let faultCount = 0;
|
||||
|
||||
// 遍历分组数据
|
||||
Object.entries(res.data).forEach(([groupName, robots]) => {
|
||||
robots.forEach(robot => {
|
||||
totalCount++;
|
||||
|
||||
if (robot.onlineStatus === '0') {
|
||||
offlineCount++;
|
||||
} else {
|
||||
onlineCount++;
|
||||
|
||||
// 检查故障状态
|
||||
if (robot.status === '3') {
|
||||
faultCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 更新统计数据
|
||||
statistics.value = [
|
||||
{ icon: Icon1, value: totalCount, title: '总数量' },
|
||||
{ icon: Icon2, value: onlineCount, title: '在线数量' },
|
||||
{ icon: Icon3, value: offlineCount, title: '离线数量' },
|
||||
{ icon: Icon4, value: faultCount, title: '故障数量' }
|
||||
];
|
||||
|
||||
console.log('获取机器人统计数据成功:', statistics.value);
|
||||
} else {
|
||||
console.error('获取机器人统计数据失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取机器人统计数据错误:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const cameras = ref([
|
||||
{ title: '厂区大门', src: jkA },
|
||||
{ title: '生产车间', src: jkA },
|
||||
{ title: '仓库区域', src: jkRobot },
|
||||
{ title: '室外区域', src: jkRobot }
|
||||
]);
|
||||
|
||||
const selectedViews = ref({
|
||||
'厂区大门': '视角1',
|
||||
'生产车间': '视角1',
|
||||
'仓库区域': '视角1',
|
||||
'室外区域': '视角1'
|
||||
});
|
||||
|
||||
const viewOptions = ['视角1', '视角2', '视角3'];
|
||||
|
||||
const showRobotListModal = ref(false);
|
||||
|
||||
const handleStatisticClick = (item) => {
|
||||
if (item.title === '总数量') {
|
||||
showRobotListModal.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchRobotStatistics();
|
||||
});
|
||||
|
||||
// 测试用:设置为 true 显示空状态,设置为 false 显示正常状态
|
||||
const showEmptyState = ref(false);
|
||||
|
||||
// 计算属性:根据测试变量决定是否显示摄像头
|
||||
const displayCameras = computed(() => {
|
||||
return showEmptyState.value ? [] : cameras.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: url("../assets/img/bg.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.left_content,
|
||||
.right_content {
|
||||
position: absolute;
|
||||
top: 6.25rem;
|
||||
width: 400px;
|
||||
/* height: 85%; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.left_content {
|
||||
left: 32px;
|
||||
}
|
||||
|
||||
.right_content {
|
||||
right: 32px;
|
||||
}
|
||||
|
||||
.border-gradient {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.border-gradient.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 200px;
|
||||
background: linear-gradient(to bottom, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||||
}
|
||||
|
||||
.border-gradient.bottom {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 300px;
|
||||
background: linear-gradient(to top, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||||
}
|
||||
|
||||
.border-gradient.left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 35rem;
|
||||
background: linear-gradient(to right, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||||
}
|
||||
|
||||
.border-gradient.right {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 35rem;
|
||||
background: linear-gradient(to left, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||||
}
|
||||
|
||||
.statistics-content,
|
||||
.status-list-content,
|
||||
.alert-content {
|
||||
width: 100%;
|
||||
/* padding: 15px;
|
||||
border: 1px solid red; */
|
||||
/* flex: 1; */
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
width: 100%;
|
||||
/* padding: 15px;
|
||||
background: rgba(0, 21, 31, 0.5);
|
||||
border-radius: 8px; */
|
||||
}
|
||||
|
||||
.statistics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.status-list-content {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
/* background: rgba(0, 21, 31, 0.5); */
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.event-list-content {
|
||||
width: 100%;
|
||||
height: 20rem;
|
||||
}
|
||||
|
||||
.bottom_content {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: calc(100% - 900px);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.monitor-title {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.title-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monitor-title span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #B9E8FF;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.monitor-content {
|
||||
margin-top: 10px;
|
||||
/* background: rgba(0, 21, 31, 0.3); */
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.camera-item {
|
||||
background: rgba(0, 21, 31, 0.5);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.camera-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding:4px 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: #B9E8FF;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.camera-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.camera-feed {
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.camera-feed img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
background: rgba(0, 21, 31, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 100px;
|
||||
height: 74px;
|
||||
margin-bottom: 10px;
|
||||
/* opacity: 0.8; */
|
||||
background: url('../assets/img/empty.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
||||
@ -59,12 +59,17 @@
|
||||
v-model="selectedViews[camera.title]"
|
||||
:options="viewOptions"
|
||||
size="small"
|
||||
variant="search"
|
||||
font="small"
|
||||
variant="search"
|
||||
font="small"
|
||||
@change="handleViewChange(camera.title)"
|
||||
/>
|
||||
</div>
|
||||
<div class="camera-feed">
|
||||
<img />
|
||||
<VideoPlayer
|
||||
:streamUrl="camera.streamUrl"
|
||||
:title="camera.title"
|
||||
fillMode="stretch-height"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -77,7 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, computed, watch } from "vue";
|
||||
import TopHeader from '../components/common/TopHeader.vue'
|
||||
import TitleBlock from '../components/common/TitleBlock.vue'
|
||||
import StatisticCard from '../components/StatisticCard.vue'
|
||||
@ -86,6 +91,8 @@ import AlarmStatistics from '../components/AlarmStatistics.vue'
|
||||
import LatestAlarms from '../components/LatestAlarms.vue'
|
||||
import RobotListModal from '../components/dialog/RobotListModal.vue'
|
||||
import CustomSelect from '../components/common/CustomSelect.vue'
|
||||
import VideoPlayer from '../components/common/VideoPlayer.vue'
|
||||
import { homeApi } from '../api/index'
|
||||
import Icon1 from '../assets/img/icon1.png'
|
||||
import Icon2 from '../assets/img/icon2.png'
|
||||
import Icon3 from '../assets/img/icon3.png'
|
||||
@ -93,29 +100,129 @@ import Icon4 from '../assets/img/icon4.png'
|
||||
import jkA from '../assets/img/jkA.png'
|
||||
import jkRobot from '../assets/img/jkRobot.png'
|
||||
|
||||
// 统计数据
|
||||
const statistics = ref([
|
||||
{ icon: Icon1, value: 0, title: '总数量' },
|
||||
{ icon: Icon2, value: 0, title: '在线数量' },
|
||||
{ icon: Icon3, value: 0, title: '离线数量' },
|
||||
{ icon: Icon4, value: 0, title: '故障数量' }
|
||||
]);
|
||||
|
||||
const statistics = [
|
||||
{ icon: Icon1, value: 100, title: '总数量' },
|
||||
{ icon: Icon2, value: 60, title: '在线数量' },
|
||||
{ icon: Icon3, value: 30, title: '离线数量' },
|
||||
{ icon: Icon4, value: 10, title: '故障数量' }
|
||||
];
|
||||
// 获取机器人统计数据
|
||||
const fetchRobotStatistics = async () => {
|
||||
try {
|
||||
// 获取机器人列表
|
||||
const res = await homeApi.getRobotList({
|
||||
tenantInfoId: '4fff5d4bcc4b4239941ff077a0da8958', // 租户id
|
||||
number: null, // 机器人名
|
||||
status: null, // 是否故障
|
||||
onlineStatus: null // 在线状态
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
// 计算统计数据
|
||||
let totalCount = 0;
|
||||
let onlineCount = 0;
|
||||
let offlineCount = 0;
|
||||
let faultCount = 0;
|
||||
|
||||
// 遍历分组数据
|
||||
Object.entries(res.data).forEach(([groupName, robots]) => {
|
||||
robots.forEach(robot => {
|
||||
totalCount++;
|
||||
|
||||
if (robot.onlineStatus === '0') {
|
||||
offlineCount++;
|
||||
} else {
|
||||
onlineCount++;
|
||||
|
||||
// 检查故障状态
|
||||
if (robot.status === '3') {
|
||||
faultCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 更新统计数据
|
||||
statistics.value = [
|
||||
{ icon: Icon1, value: totalCount, title: '总数量' },
|
||||
{ icon: Icon2, value: onlineCount, title: '在线数量' },
|
||||
{ icon: Icon3, value: offlineCount, title: '离线数量' },
|
||||
{ icon: Icon4, value: faultCount, title: '故障数量' }
|
||||
];
|
||||
|
||||
console.log('获取机器人统计数据成功:', statistics.value);
|
||||
} else {
|
||||
console.error('获取机器人统计数据失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取机器人统计数据错误:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const cameras = ref([
|
||||
{ title: '厂区大门', src: jkA },
|
||||
{ title: '生产车间', src: jkA },
|
||||
{ title: '仓库区域', src: jkRobot },
|
||||
{ title: '室外区域', src: jkRobot }
|
||||
{ title: 'A区厂区监控', src: jkA, streamUrl: '' },
|
||||
{ title: 'B区厂区监控', src: jkA, streamUrl: '' },
|
||||
{ title: '追随机器人监控', src: jkRobot, streamUrl: '' },
|
||||
{ title: '室外机器人监控', src: jkRobot, streamUrl: '' }
|
||||
]);
|
||||
|
||||
const selectedViews = ref({
|
||||
'厂区大门': '视角1',
|
||||
'生产车间': '视角1',
|
||||
'仓库区域': '视角1',
|
||||
'室外区域': '视角1'
|
||||
'A区厂区监控': '视角1',
|
||||
'B区厂区监控': '视角1',
|
||||
'追随机器人监控': '视角1',
|
||||
'室外机器人监控': '视角1'
|
||||
});
|
||||
|
||||
const viewOptions = ['视角1', '视角2', '视角3'];
|
||||
const viewOptions = ['视角1', '视角2'];
|
||||
|
||||
// 存储所有视频流数据
|
||||
const monitorStreams = ref({});
|
||||
|
||||
// 获取监控视频流数据
|
||||
const fetchMonitorStreams = async () => {
|
||||
try {
|
||||
const res = await homeApi.getFactoryRobotRealTime();
|
||||
|
||||
if (res.code === 200) {
|
||||
monitorStreams.value = res.data || {};
|
||||
console.log('获取监控视频流成功:', JSON.stringify(monitorStreams.value));
|
||||
|
||||
// 更新摄像头流URL
|
||||
updateCameraStreams();
|
||||
} else {
|
||||
console.error('获取监控视频流失败:', res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取监控视频流错误:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新摄像头流URL
|
||||
const updateCameraStreams = () => {
|
||||
cameras.value.forEach(camera => {
|
||||
const cameraData = monitorStreams.value[camera.title];
|
||||
console.log(`处理摄像头 ${camera.title}:`, cameraData);
|
||||
|
||||
if (cameraData) {
|
||||
const viewName = selectedViews.value[camera.title];
|
||||
const newStreamUrl = cameraData[viewName] || '';
|
||||
console.log(`设置 ${camera.title} 的 ${viewName} 流URL:`, newStreamUrl);
|
||||
camera.streamUrl = newStreamUrl;
|
||||
} else {
|
||||
console.warn(`未找到 ${camera.title} 的数据`);
|
||||
}
|
||||
});
|
||||
|
||||
// 打印更新后的摄像头数据
|
||||
console.log('更新后的摄像头数据:', JSON.stringify(cameras.value));
|
||||
};
|
||||
|
||||
// 监听视角选择变化
|
||||
watch(selectedViews, (newViews) => {
|
||||
updateCameraStreams();
|
||||
}, { deep: true });
|
||||
|
||||
const showRobotListModal = ref(false);
|
||||
|
||||
@ -124,6 +231,25 @@ const handleStatisticClick = (item) => {
|
||||
showRobotListModal.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理视角切换
|
||||
const handleViewChange = (cameraTitle) => {
|
||||
const camera = cameras.value.find(c => c.title === cameraTitle);
|
||||
if (camera) {
|
||||
const cameraData = monitorStreams.value[cameraTitle];
|
||||
if (cameraData) {
|
||||
const viewName = selectedViews.value[cameraTitle];
|
||||
camera.streamUrl = cameraData[viewName] || '';
|
||||
console.log(`切换 ${cameraTitle} 到 ${viewName}, URL: ${camera.streamUrl}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchRobotStatistics();
|
||||
fetchMonitorStreams();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -286,6 +412,7 @@ const handleStatisticClick = (item) => {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* border: 1px solid rgba(0, 168, 255, 0.2); */
|
||||
}
|
||||
|
||||
.camera-header {
|
||||
@ -297,8 +424,8 @@ const handleStatisticClick = (item) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding:4px 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 6px 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #B9E8FF;
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -307,15 +434,25 @@ const handleStatisticClick = (item) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.camera-title img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.camera-title p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.camera-feed {
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
/* background: rgba(0, 21, 31, 0.5); */
|
||||
background: #033347;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.camera-feed img {
|
||||
|
||||
@ -109,7 +109,14 @@
|
||||
@mouseenter="handleMouseEnter('alarmList')"
|
||||
@mouseleave="handleMouseLeave('alarmList')"
|
||||
>
|
||||
<EmptyState
|
||||
v-if="filteredEventAlarms.length === 0"
|
||||
title="暂无告警事件"
|
||||
subtitle="所有告警已处理完成"
|
||||
:iconSrc="emptyList"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-for="alarm in filteredEventAlarms"
|
||||
:key="alarm.id"
|
||||
class="alarm-item"
|
||||
@ -148,7 +155,14 @@
|
||||
@mouseenter="handleMouseEnter('meterList')"
|
||||
@mouseleave="handleMouseLeave('meterList')"
|
||||
>
|
||||
<EmptyState
|
||||
v-if="filteredMeterAlarms.length === 0"
|
||||
title="暂无仪表异常"
|
||||
subtitle="暂无需要处理的仪表异常"
|
||||
:iconSrc="require('../assets/img/empty_list.png')"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-for="alarm in filteredMeterAlarms"
|
||||
:key="alarm.id"
|
||||
class="alarm-item"
|
||||
@ -235,7 +249,8 @@ import { robotApi } from '@/api/detail';
|
||||
import WebSocketClient from '@/utils/websocket';
|
||||
import WebRTCPlayer from '@/components/detail/WebRTCPlayer.vue';
|
||||
import ConfirmDialog from '@/components/dialog/ConfirmDialog.vue';
|
||||
|
||||
import EmptyState from '@/components/common/EmptyState.vue';
|
||||
import emptyList from '@/assets/img/empty_list.png';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const robotId = ref(route.params.id || "X32305000019");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user