首页接口

This commit is contained in:
renna 2025-06-05 18:21:18 +08:00
parent 8f230cf38a
commit 93db9389a2
20 changed files with 2796 additions and 264 deletions

371
package-lock.json generated
View File

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

View File

@ -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"

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

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

View File

@ -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>

View File

@ -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': '日常巡检'
};
// APIUI
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
};
// APIUI
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>

View File

@ -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>

View 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';
// offeranswer
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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -87,4 +87,5 @@ concoaiService.interceptors.response.use(
}
)
export { service }
export default service

427
src/views/Home copy.vue Normal file
View 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>

View File

@ -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 {

View File

@ -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");