告警大屏完成
This commit is contained in:
@@ -267,3 +267,6 @@ export default router;
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
220
SScode.md
Normal file
220
SScode.md
Normal file
@@ -0,0 +1,220 @@
|
||||
## 分批请求函数
|
||||
|
||||
- 入参方式
|
||||
- 1. (页码 + 页面大小)
|
||||
- 2. (偏移量 + 页面大小)
|
||||
|
||||
### 1》页码 + 页面大小
|
||||
|
||||
```
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20; // 每次请求 20 条数据
|
||||
let offset = 0; // 从 0 开始
|
||||
let allCameras = [];
|
||||
|
||||
// 第一次请求,用于获取总数
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
// 根据总数继续请求剩余的数据
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
cameras.value = allCameras;
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 2》 偏移量 + 页面大小
|
||||
|
||||
- .length
|
||||
|
||||
```
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 2;
|
||||
let currentPage = 1;
|
||||
let allEvents = [];
|
||||
let total = 0;
|
||||
|
||||
// 先请求一次以获取总条数
|
||||
const firstResponse = await apiInstance.getEvents(token, limit, currentPage);
|
||||
total = firstResponse.totalItems; // 总条数
|
||||
allEvents = firstResponse.tableData; // 第一次请求的数据
|
||||
|
||||
// 使用 while 循环来请求后续的数据
|
||||
while (allEvents.length < total) {
|
||||
currentPage += 1;
|
||||
const offset = (currentPage - 1) * limit;
|
||||
const response = await apiInstance.getEvents(token, limit, currentPage);
|
||||
|
||||
allEvents = allEvents.concat(response.tableData); // 追加数据
|
||||
}
|
||||
|
||||
tableData.value = allEvents; // 将所有数据赋值给 tableData
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
- offset + limit计算(初稿备份)
|
||||
|
||||
```
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 2000;
|
||||
let currentPage = 1;
|
||||
let allEvents = [];
|
||||
let total = 0;
|
||||
let offset = (currentPage - 1) * limit;
|
||||
|
||||
// 先请求一次以获取总条数
|
||||
const firstResponse = await apiInstance.getEvents(token, limit, currentPage);
|
||||
total = firstResponse.totalItems; // 总条数
|
||||
allEvents = firstResponse.tableData; // 第一次请求的数据
|
||||
|
||||
// 使用 while 循环来请求后续的数据
|
||||
while (offset+limit < total) {
|
||||
currentPage += 1;
|
||||
offset = (currentPage - 1) * limit;
|
||||
const response = await apiInstance.getEvents(token, limit, currentPage);
|
||||
|
||||
allEvents = allEvents.concat(response.tableData); // 追加数据
|
||||
}
|
||||
|
||||
tableData.value = allEvents; // 将所有数据赋值给 tableData
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 数组并行处理和排序
|
||||
|
||||
- 更改前(分两个数组直接存)
|
||||
|
||||
```
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
// 获取摄像头数据
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
// 获取所有摄像头信息
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
const cameraCount = firstResponse.count;
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
while (offset + limit < cameraCount) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
// 提取摄像头名称和对应的告警数量
|
||||
const cameraNames = [];
|
||||
const cameraCounts = [];
|
||||
|
||||
for (const camera of allCameras) {
|
||||
cameraNames.push(camera.name);
|
||||
|
||||
// 获取该摄像头的告警数量
|
||||
const eventsResponse = await apiInstance.getEventsByParams(token, 20, 1, null, null, null, camera.id);
|
||||
const count = eventsResponse.count || 0; // 确保即使没有事件也返回 0
|
||||
cameraCounts.push(count);
|
||||
}
|
||||
|
||||
// 更新图表的 Y 轴标签和系列数据
|
||||
option.value.yAxis[0].data = cameraNames;
|
||||
option.value.yAxis[1].data = cameraCounts.map(count => `${count}`);
|
||||
option.value.series[0].data = cameraCounts;
|
||||
option.value.series[1].data = cameraCounts;
|
||||
|
||||
// 设置图表选项并启动轮播
|
||||
myChart.setOption(option.value);
|
||||
startMoveDataZoom(); // 重新启动轮播效果
|
||||
} catch (error) {
|
||||
console.error("Error fetching cameras or events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
- 更改后
|
||||
|
||||
```
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
// 获取摄像头数据
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
// 获取所有摄像头信息
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
const cameraCount = firstResponse.count;
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
while (offset + limit < cameraCount) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
// 提取摄像头名称和对应的告警数量并存储在对象数组中
|
||||
const cameraData = [];
|
||||
|
||||
for (const camera of allCameras) {
|
||||
// 获取该摄像头的告警数量
|
||||
const eventsResponse = await apiInstance.getEventsByParams(token, 20, 1, null, null, null, camera.id);
|
||||
const count = eventsResponse.count || 0; // 确保即使没有事件也返回 0
|
||||
cameraData.push({ name: camera.name, count }); // 将数据存储为对象以便排序
|
||||
}
|
||||
|
||||
// 按照告警数量降序排序
|
||||
cameraData.sort((a, b) => b.count - a.count);
|
||||
|
||||
// 提取排序后的名称和数量
|
||||
const cameraNames = cameraData.map(item => item.name);
|
||||
const cameraCounts = cameraData.map(item => item.count);
|
||||
|
||||
// 更新图表的 Y 轴标签和系列数据
|
||||
option.value.yAxis[0].data = cameraNames;
|
||||
option.value.yAxis[1].data = cameraCounts.map(count => `${count}`);
|
||||
option.value.series[0].data = cameraCounts;
|
||||
option.value.series[1].data = cameraCounts;
|
||||
|
||||
// 设置图表选项并启动轮播
|
||||
myChart.setOption(option.value);
|
||||
startMoveDataZoom(); // 重新启动轮播效果
|
||||
} catch (error) {
|
||||
console.error("Error fetching cameras or events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
222
package-lock.json
generated
222
package-lock.json
generated
@@ -23,6 +23,8 @@
|
||||
"@types/node": "^20.14.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"npm-run-all2": "^6.2.0",
|
||||
"typescript": "~5.4.0",
|
||||
"vite": "^5.3.1",
|
||||
@@ -1193,6 +1195,18 @@
|
||||
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz",
|
||||
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-what": "^3.14.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -1276,6 +1290,19 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/errno": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/errno/-/errno-0.1.8.tgz",
|
||||
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"prr": "~1.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"errno": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
|
||||
@@ -1370,6 +1397,13 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
|
||||
@@ -1379,6 +1413,38 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"image-size": "bin/image-size.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-3.14.1.tgz",
|
||||
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -1399,6 +1465,58 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/less": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/less/-/less-4.2.0.tgz",
|
||||
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"copy-anything": "^2.0.1",
|
||||
"parse-node-version": "^1.0.1",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"lessc": "bin/lessc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"errno": "^0.1.1",
|
||||
"graceful-fs": "^4.1.2",
|
||||
"image-size": "~0.5.0",
|
||||
"make-dir": "^2.1.0",
|
||||
"mime": "^1.4.1",
|
||||
"needle": "^3.1.0",
|
||||
"source-map": "~0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/less-loader": {
|
||||
"version": "12.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/less-loader/-/less-loader-12.2.0.tgz",
|
||||
"integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/core": "0.x || 1.x",
|
||||
"less": "^3.5.0 || ^4.0.0",
|
||||
"webpack": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rspack/core": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -1427,6 +1545,30 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-2.1.0.tgz",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
@@ -1441,6 +1583,19 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -1498,6 +1653,23 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/needle": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz",
|
||||
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.3",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"needle": "bin/needle"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
@@ -1537,6 +1709,15 @@
|
||||
"npm": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-node-version": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz",
|
||||
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
@@ -1569,6 +1750,16 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.41.tgz",
|
||||
@@ -1601,6 +1792,13 @@
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",
|
||||
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/read-package-json-fast": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",
|
||||
@@ -1654,6 +1852,20 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz",
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz",
|
||||
@@ -1696,6 +1908,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"@types/node": "^20.14.5",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"npm-run-all2": "^6.2.0",
|
||||
"typescript": "~5.4.0",
|
||||
"vite": "^5.3.1",
|
||||
|
||||
408
src/bk/CenterBottom告警分布.vue
Normal file
408
src/bk/CenterBottom告警分布.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<!-- <el-card class="alert-card">
|
||||
<div class="alert-header">告警趋势</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third">
|
||||
<div ref="monthLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card> -->
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0));
|
||||
const dailyCounts = ref(Array(7).fill(0));
|
||||
const monthlyCounts = ref(Array(30).fill(0));
|
||||
|
||||
const todayLineChartDom = ref(null);
|
||||
const weekLineChartDom = ref(null);
|
||||
const monthLineChartDom = ref(null);
|
||||
|
||||
const chartInstanceToday = ref(null);
|
||||
const chartInstanceWeek = ref(null);
|
||||
const chartInstanceMonth = ref(null);
|
||||
|
||||
const weekDates = ref([]);
|
||||
const monthDates = ref([]);
|
||||
|
||||
let isTodayChartInitialized = false;
|
||||
let isWeekChartInitialized = false;
|
||||
let isMonthChartInitialized = false;
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const allEvents = ref([]);
|
||||
|
||||
// 日期格式化函数
|
||||
const formatDateTime = (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 获取时间参数
|
||||
const getDayParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.startOf('day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getWeeklyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(7, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getMonthlyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(30, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
|
||||
// 计算日期
|
||||
const calculateWeekDates = () => {
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
const calculateMonthDates = () => {
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 延迟初始化图表函数
|
||||
const delayedInitChart = debounce((chartInstance, domRef, option) => {
|
||||
nextTick(() => {
|
||||
if (domRef.value) {
|
||||
chartInstance.value = echarts.init(domRef.value);
|
||||
chartInstance.value.setOption(option);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 初始化图表选项配置
|
||||
const createTodayChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i}:00`),
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createWeekChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: weekDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMonthChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: monthDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: monthlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 获取和处理事件数据
|
||||
const fetchAndProcessEvents = async (timeParams) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 1000;
|
||||
const token = localStorage.getItem('alertToken');
|
||||
allEvents.value = [];
|
||||
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
const totalItems = firstResponse.count;
|
||||
allEvents.value.push(...firstResponse.results);
|
||||
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const response = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
allEvents.value.push(...response.results);
|
||||
}
|
||||
|
||||
processEventData(allEvents.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理事件数据
|
||||
const processEventData = (events) => {
|
||||
hourlyCounts.value.fill(0);
|
||||
dailyCounts.value.fill(0);
|
||||
monthlyCounts.value.fill(0);
|
||||
events.forEach((event) => {
|
||||
const hour = dayjs(event.ended_at).hour();
|
||||
const dayDiff = dayjs().diff(dayjs(event.ended_at), 'day');
|
||||
if (dayDiff < 1) hourlyCounts.value[hour] += 1;
|
||||
if (dayDiff < 7) dailyCounts.value[6 - dayDiff] += 1;
|
||||
if (dayDiff < 30) monthlyCounts.value[29 - dayDiff] += 1;
|
||||
});
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.setOption({ series: [{ data: hourlyCounts.value }] });
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.setOption({ series: [{ data: dailyCounts.value }] });
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.setOption({ series: [{ data: monthlyCounts.value }] });
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
let timeParams;
|
||||
if (tab.props.name === 'first' && !isTodayChartInitialized) {
|
||||
timeParams = getDayParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceToday, todayLineChartDom, createTodayChartOption());
|
||||
isTodayChartInitialized = true;
|
||||
} else if (tab.props.name === 'second' && !isWeekChartInitialized) {
|
||||
timeParams = getWeeklyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceWeek, weekLineChartDom, createWeekChartOption());
|
||||
isWeekChartInitialized = true;
|
||||
} else if (tab.props.name === 'third' && !isMonthChartInitialized) {
|
||||
timeParams = getMonthlyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceMonth, monthLineChartDom, createMonthChartOption());
|
||||
isMonthChartInitialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口调整时重新渲染图表
|
||||
const resizeCharts = debounce(() => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.resize();
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.resize();
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.resize();
|
||||
}, 300);
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates();
|
||||
calculateMonthDates();
|
||||
await handleClick({ props: { name: 'first' } });
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.alert-card {
|
||||
background-color: #001529;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
/* padding: 10px; */
|
||||
/* margin: 10px; */
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
background-color: #001529;
|
||||
margin: 0vh 1vw 1vh 1vw;
|
||||
/* margin-top: 0; */
|
||||
}
|
||||
|
||||
/* .alert-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
} */
|
||||
|
||||
.chart-container {
|
||||
/* min-height: 350px; */
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
/* ::v-deep .el-tabs___nav-wrap.is-top::after{
|
||||
border: none;
|
||||
} */
|
||||
|
||||
/* ::v-deep .el-tabs__active-bar.is-top{
|
||||
padding: 0 204px;
|
||||
box-sizing: border-box !important;
|
||||
background-clip: content-box !important;
|
||||
} */
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
347
src/bk/CenterTop警戒点位.vue
Normal file
347
src/bk/CenterTop警戒点位.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div class="camera-container">
|
||||
<div class="top-header">
|
||||
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter" >
|
||||
<el-option label="全部" value="all"></el-option>
|
||||
<el-option label="在线" value="online"></el-option>
|
||||
<el-option label="离线" value="offline"></el-option>
|
||||
</el-select>
|
||||
<div class="top-text">警戒点位</div>
|
||||
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
|
||||
class="camera-select" >
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
<span>{{ camera.id }}.</span> 名称: {{ camera.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 播放列表,设置为可滚动 -->
|
||||
<div class="camera-grid">
|
||||
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
|
||||
<div class="stream-control">
|
||||
<p class="camera-name-title">{{ camera.name }}</p>
|
||||
<div class="close-button" @click="closeStream(camera)">×</div>
|
||||
</div>
|
||||
|
||||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
||||
<el-icon size="48">
|
||||
<VideoCameraFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="openDialog(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗播放视频 -->
|
||||
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
|
||||
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
|
||||
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const cameras = ref([]);
|
||||
const selectedCameras = ref([]);
|
||||
const showButton = ref(false);
|
||||
const apiInstance = new BoxApi();
|
||||
const canvasRefs = ref({});
|
||||
const selectedCameraId = ref(null);
|
||||
const dialogVisible = ref(false); // 控制弹窗的显示与隐藏
|
||||
const currentCamera = ref(null); // 当前选中的摄像头
|
||||
|
||||
// 弹窗中的canvas引用
|
||||
const dialogCanvas = ref(null);
|
||||
const filterStatus = ref("all");
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getMinCameras(token);
|
||||
|
||||
// 根据 filterStatus 筛选摄像头状态
|
||||
if (filterStatus.value === "online") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "online");
|
||||
} else if (filterStatus.value === "offline") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "offline");
|
||||
} else {
|
||||
cameras.value = cameraData; // 默认 "all" 显示全部摄像头
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const selectCameraById = (cameraId) => {
|
||||
const camera = cameras.value.find(c => c.id === cameraId);
|
||||
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
|
||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗并开始播放
|
||||
const openDialog = async (camera) => {
|
||||
currentCamera.value = camera;
|
||||
dialogVisible.value = true;
|
||||
await nextTick();
|
||||
startStreamInDialog(camera);
|
||||
};
|
||||
|
||||
// 在弹窗中播放视频
|
||||
const startStreamInDialog = async (camera) => {
|
||||
const canvas = dialogCanvas.value;
|
||||
|
||||
if (!camera || !canvas) {
|
||||
console.error('未找到对应的 canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
const response = await apiInstance.startCameraStream(token, camera.id);
|
||||
camera.streamPort = response.port;
|
||||
camera.playing = true;
|
||||
|
||||
const url = `ws://192.168.28.33:${camera.streamPort}/`;
|
||||
console.log('播放路径:', url);
|
||||
|
||||
if (window.JSMpeg) {
|
||||
const player = new window.JSMpeg.Player(url, {
|
||||
canvas: canvas,
|
||||
autoplay: true,
|
||||
videoBufferSize: 15 * 1024 * 1024,
|
||||
audioBufferSize: 5 * 1024 * 1024,
|
||||
});
|
||||
camera.player = player;
|
||||
} else {
|
||||
console.error('JSMpeg 未加载');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗并停止视频播放
|
||||
const closeDialog = () => {
|
||||
if (currentCamera.value) {
|
||||
handleStopStream(currentCamera.value);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
currentCamera.value = null;
|
||||
};
|
||||
|
||||
const handleStopStream = async (camera) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
await apiInstance.stopCameraStream(token, camera.id);
|
||||
camera.playing = false;
|
||||
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
camera.player = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeStream = (camera) => {
|
||||
handleStopStream(camera);
|
||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCameras();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectedCameras.value.forEach(camera => {
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 5vw;
|
||||
margin-left: 1vh;
|
||||
margin-top: 1vh;
|
||||
|
||||
}
|
||||
::v-deep .status-filter .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.camera-select {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 12vw;
|
||||
margin-left: 0vh;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.top-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
width: 7vw;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0 0 0 10vw;
|
||||
/* justify-content: center; */
|
||||
align-content: center;
|
||||
background-color: #001529;
|
||||
line-height: 0px;
|
||||
color: aliceblue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.camera-name-title {
|
||||
padding: 0.2vh;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
margin: 0vh 1vh;
|
||||
padding: 0 0 2vh 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vh 0vh;
|
||||
width: 34vw;
|
||||
height: 39vh;
|
||||
max-height: 45vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
.camera-grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
margin: 1vh;
|
||||
position: relative;
|
||||
height: 17vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.camera-snapshot,
|
||||
.camera-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #000000;
|
||||
color: aliceblue;
|
||||
padding: 0 2px 2px 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.play-button-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
310
src/bk/LeftMiddle点位告警数量.vue
Normal file
310
src/bk/LeftMiddle点位告警数量.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="leftMiddle-box">
|
||||
<el-button @click="toggleDataZoomMove" class="show-bt">
|
||||
<el-icon><d-caret /></el-icon>
|
||||
{{ isDataZoomMoving ? '关闭轮播' : '开启轮播' }}
|
||||
</el-button>
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { DCaret } from '@element-plus/icons-vue';
|
||||
|
||||
const chartContainer = ref(null);
|
||||
let myChart = null;
|
||||
const cameras = ref([]);
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
const colorList = ['#FF4B01', '#FF7B00', '#FFE823', '#2379FF', '#2379FF', '#2379FF'];
|
||||
const colorListA = ['#FC7A24', '#FFC000', '#FFEA97', '#49B1FF', '#49B1FF', '#49B1FF'];
|
||||
let dataZoomMove = { start: 0, end: 8 };
|
||||
const isDataZoomMoving = ref(false);
|
||||
let dataZoomMoveTimer = null;
|
||||
|
||||
const option = ref({
|
||||
backgroundColor: "#001529",
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(21, 154, 255, 0.32)",
|
||||
textStyle: { color: "#fff" },
|
||||
borderColor: "#159AFF",
|
||||
axisPointer: { lineStyle: { color: "transparent" } },
|
||||
formatter: (params) => {
|
||||
const cameraName = params[0] ? params[0].name : '';
|
||||
const outerSeries = params.find(item => item.seriesName === '告警数量');
|
||||
|
||||
return outerSeries ? `<div style="font-weight: bold; margin-bottom: 5px;">摄像头: ${cameraName}</div>${outerSeries.marker} ${outerSeries.seriesName}: ${outerSeries.value} 次` : '';
|
||||
}
|
||||
|
||||
},
|
||||
dataZoom: [
|
||||
{ show: false, startValue: dataZoomMove.start, endValue: dataZoomMove.end, yAxisIndex: [0, 1] },
|
||||
{ type: "inside", yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseMove: true, moveOnMouseWheel: true }
|
||||
],
|
||||
grid: { containLabel: true, bottom: 20, left: 30, top: 20, right: 30 },
|
||||
xAxis: { type: "value", axisLabel: { show: false }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false } },
|
||||
yAxis: [
|
||||
{
|
||||
type: "category",
|
||||
data: [], // 动态填充摄像头名称
|
||||
inverse: true,
|
||||
axisLabel: {
|
||||
inside: true,
|
||||
verticalAlign: "bottom",
|
||||
lineHeight: 36,
|
||||
margin: 2,
|
||||
formatter(value) {
|
||||
const k = option.value.yAxis[0].data.indexOf(value);
|
||||
// return `{b|${k + 1}} {a|${value}}`;
|
||||
return `{a|${value}}`;
|
||||
},
|
||||
rich: {
|
||||
b: { color: "#fff", fontSize: 14 },
|
||||
a: { fontSize: 14, color: "#D0DEEE", padding: [4, 0, 0, 8] }
|
||||
}
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false }
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
data: [], // 动态填充告警数量
|
||||
inverse: true,
|
||||
axisLabel: {
|
||||
inside: true,
|
||||
verticalAlign: "bottom",
|
||||
lineHeight: 34,
|
||||
margin: 2,
|
||||
formatter(value, index) {
|
||||
return `{a|${value}}`;
|
||||
},
|
||||
rich: {
|
||||
a: { fontSize: 16, color: "#fff", padding: [4, 0, 0, 0], fontFamily: "DOUYU" }
|
||||
}
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "条形图",
|
||||
data: [], // 动态填充告警数量
|
||||
type: "bar",
|
||||
barWidth: 10,
|
||||
padding:[0],
|
||||
showBackground: true,
|
||||
barBorderRadius: [30, 0, 0, 30],
|
||||
backgroundStyle: { color: 'rgba(9, 68, 131, .2)' },
|
||||
itemStyle: {
|
||||
barBorderRadius: [3, 0, 0, 3],
|
||||
color(params) {
|
||||
const i = colorListA.length;
|
||||
const f = colorList.length;
|
||||
return {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: colorListA[params.dataIndex % i] },
|
||||
{ offset: 1, color: colorList[params.dataIndex % f] }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "告警数量",
|
||||
type: "scatter",
|
||||
symbol: "rect",
|
||||
symbolSize: [5, 16],
|
||||
itemStyle: { color: '#FFF', shadowColor: "rgba(255, 255, 255, 0.5)", shadowBlur: 5, borderWidth: 1, opacity: 1 },
|
||||
z: 2,
|
||||
data: [], // 动态填充告警数量
|
||||
animationDelay: 500
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const toggleDataZoomMove = () => {
|
||||
// console.log(`Data zoom move is now ${isDataZoomMoving.value}`);
|
||||
isDataZoomMoving.value = !isDataZoomMoving.value; // 切换状态
|
||||
// console.log(`转换${isDataZoomMoving.value}`);
|
||||
if (isDataZoomMoving.value) {
|
||||
startMoveDataZoom();// 关闭轮播
|
||||
} else {
|
||||
// 开启轮播
|
||||
stopMoveDataZoom();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// 关闭轮播
|
||||
const stopMoveDataZoom = () => {
|
||||
if (dataZoomMoveTimer) {
|
||||
// console.log('Stopping data zoom move...');
|
||||
clearInterval(dataZoomMoveTimer);
|
||||
dataZoomMoveTimer = null;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 启动数据轮播
|
||||
const startMoveDataZoom = () => {
|
||||
// if (isDataZoomMoving.value) return;
|
||||
// console.log('Starting ...');
|
||||
if (dataZoomMoveTimer !== null) {
|
||||
clearInterval(dataZoomMoveTimer);
|
||||
}
|
||||
if (isDataZoomMoving.value) {
|
||||
// console.log('Starting data zoom move...');
|
||||
dataZoomMoveTimer = setInterval(() => {
|
||||
dataZoomMove.start += 1;
|
||||
dataZoomMove.end += 1;
|
||||
if (dataZoomMove.end > option.value.yAxis[0].data.length - 1) {
|
||||
dataZoomMove.start = 0;
|
||||
dataZoomMove.end = 8;
|
||||
}
|
||||
myChart.setOption({
|
||||
dataZoom: [{ type: "slider", startValue: dataZoomMove.start, endValue: dataZoomMove.end }]
|
||||
});
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 获取摄像头信息并获取对应的告警数量
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
// 获取摄像头数据
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
// 获取所有摄像头信息
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
const cameraCount = firstResponse.count;
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
while (offset + limit < cameraCount) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
// 提取摄像头名称和对应的告警数量并存储在对象数组中
|
||||
const cameraData = [];
|
||||
|
||||
for (const camera of allCameras) {
|
||||
// 获取该摄像头的告警数量
|
||||
const eventsResponse = await apiInstance.getEventsByParams(token, 20, 1, null, null, null, camera.id);
|
||||
const count = eventsResponse.count || 0; // 确保即使没有事件也返回 0
|
||||
cameraData.push({ name: camera.name, count }); // 将数据存储为对象以便排序
|
||||
}
|
||||
|
||||
// 按照告警数量降序排序
|
||||
cameraData.sort((a, b) => b.count - a.count);
|
||||
|
||||
// 提取排序后的名称和数量
|
||||
const cameraNames = cameraData.map(item => item.name);
|
||||
const cameraCounts = cameraData.map(item => item.count);
|
||||
|
||||
// 更新图表的 Y 轴标签和系列数据
|
||||
option.value.yAxis[0].data = cameraNames;
|
||||
option.value.yAxis[1].data = cameraCounts.map(count => `${count}`);
|
||||
option.value.series[0].data = cameraCounts;
|
||||
option.value.series[1].data = cameraCounts;
|
||||
|
||||
// 设置图表选项并启动轮播
|
||||
myChart.setOption(option.value);
|
||||
startMoveDataZoom(); // 重新启动轮播效果
|
||||
} catch (error) {
|
||||
console.error("Error fetching cameras or events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理轮播和事件监听
|
||||
onBeforeUnmount(() => {
|
||||
if (dataZoomMoveTimer) {
|
||||
clearInterval(dataZoomMoveTimer);
|
||||
dataZoomMoveTimer = null; // 确保计时器被清空
|
||||
}
|
||||
if (myChart) {
|
||||
window.removeEventListener('resize', resizeChart); // 确保事件监听器被移除
|
||||
myChart.dispose();
|
||||
myChart = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 初始化图表
|
||||
onMounted(async () => {
|
||||
myChart = echarts.init(chartContainer.value);
|
||||
await fetchCameras();
|
||||
myChart.setOption(option.value);
|
||||
|
||||
// 监听窗口变化事件,调整图表大小
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
|
||||
const resizeChart = () => {
|
||||
if (myChart && !myChart.isDisposed()) {
|
||||
myChart.resize();
|
||||
} else {
|
||||
console.warn('Attempted to resize a disposed ECharts instance.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const resizeChart = () => {
|
||||
// if (myChart && !myChart.isDisposed()) {
|
||||
// myChart.resize();
|
||||
// if (dataZoomMoveTimer) {
|
||||
// clearInterval(dataZoomMoveTimer);
|
||||
// startMoveDataZoom();
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.leftMiddle-box{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 41vh;
|
||||
margin: 20px;
|
||||
}
|
||||
.show-bt{
|
||||
background-color: #001529;
|
||||
border: #001529;
|
||||
width: 6vw;
|
||||
height: 4vh;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bolder;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
.chart-container {
|
||||
/* margin: 0 1vw 0 1vw; */
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
height: 35vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
366
src/bk/LeftTop告警数据统计.vue
Normal file
366
src/bk/LeftTop告警数据统计.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendintingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import CameraAll from '@/icons/CameraAll.vue';
|
||||
import CameraOnline from '@/icons/CameraOnline.vue';
|
||||
import CameraOffline from '@/icons/CameraOffline.vue';
|
||||
import EventAll from '@/icons/EventAll.vue';
|
||||
import EventClosed from '@/icons/EventClosed.vue';
|
||||
import EventPending from '@/icons/EventPending.vue';
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendintingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
const activeTab = ref('all');
|
||||
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
}
|
||||
|
||||
.tab-div{
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
308
src/bk/RightTop类型分布.vue
Normal file
308
src/bk/RightTop类型分布.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
|
||||
<div class="search-row">
|
||||
<div class="bt-search">
|
||||
<el-button type="primary" @click="handleFilter" class="alert-bt">点击查询</el-button>
|
||||
</div>
|
||||
<div class="start-col">
|
||||
<el-date-picker v-model="filterParams.timeAfter" :teleported="false" type="datetime" placeholder="请选择开始时间" prefix-icon="CaretBottom" popper-class="popperClass" ></el-date-picker>
|
||||
</div>
|
||||
<div class="end-col">
|
||||
<el-date-picker v-model="filterParams.timeBefore" :teleported="false" type="datetime" placeholder="请选择结束时间" prefix-icon="CaretBottom" popper-class="popperClass" ></el-date-picker>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="3d-bar-chart" class="myPanChart" ></div>
|
||||
|
||||
<!-- <el-row>
|
||||
<el-col :span="24">
|
||||
<div class="statistics-container">
|
||||
<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: {{ typeCounts[code] || 0 }} 次
|
||||
</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted,onBeforeUnmount,computed} from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const typeCounts = reactive({}); // 存储每种类型的数量
|
||||
|
||||
const filterParams = reactive({
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
});
|
||||
|
||||
const pieChartData = computed(() => {
|
||||
return Object.keys(typeCounts).map(code => ({
|
||||
value: typeCounts[code] || 0,
|
||||
name: typeMapping[code]
|
||||
}));
|
||||
});
|
||||
|
||||
const renderChart = () => {
|
||||
const chartDom = document.getElementById('3d-bar-chart'); // 确保ID正确
|
||||
const chartInstance = echarts.init(chartDom);
|
||||
|
||||
const colorList = [
|
||||
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: "rgba(69,233,254,1)" },
|
||||
{ offset: 1, color: "rgba(69,233,254,0.3)" }
|
||||
]),
|
||||
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: "rgba(255,181,111,1)" },
|
||||
{ offset: 1, color: "rgba(255,181,111,0.3)" }
|
||||
]),
|
||||
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: "rgba(101,122,250,1)" },
|
||||
{ offset: 1, color: "rgba(101,122,250,0.3)" }
|
||||
]),
|
||||
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: "rgba(45,190,146,1)" },
|
||||
{ offset: 1, color: "rgba(45,190,146,0.3)" }
|
||||
]),
|
||||
|
||||
];
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
padding: [8, 16],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
},
|
||||
formatter: function (params) {
|
||||
return `${params.marker} <span style="color:${params.color}">${params.data['name']}\n${params.data['value']}</span>`;
|
||||
}
|
||||
},
|
||||
// title: {
|
||||
// text: '告警总数',
|
||||
// subtext: `${pieChartData.value.reduce((sum, item) => sum + item.value, 0)}`,
|
||||
// top: '45%',
|
||||
// left: 'center',
|
||||
// textStyle: {
|
||||
// color: '#F5F8FC',
|
||||
// fontSize: 20,
|
||||
// fontWeight: 400
|
||||
// },
|
||||
// subtextStyle: {
|
||||
// color: '#F5F8FC',
|
||||
// fontSize: 16,
|
||||
// fontWeight: 400,
|
||||
// }
|
||||
// },
|
||||
// legend: {
|
||||
// orient: 'vertical',
|
||||
// icon: "circle",
|
||||
// left: '0%',
|
||||
// bottom: '0%',
|
||||
// itemWidth: 30,
|
||||
// itemGap: 15 ,
|
||||
// // padding:[10],
|
||||
// textStyle: {
|
||||
// rich: {
|
||||
// a: { color: '#F5F8FC', fontSize: 15, padding: [0, 10, 0, 0] },
|
||||
// b: { color: '#F5F8FC', fontSize: 15, padding: [0, 10, 0, 10] }
|
||||
// }
|
||||
// },
|
||||
// formatter: function (name) {
|
||||
// const item = pieChartData.value.find(d => d.name === name);
|
||||
// return item ? `{a| ${name}}{b|${item.value}}` : '';
|
||||
// }
|
||||
// },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['10%', '90%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
padding:[0,10],
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: '{d}%',
|
||||
textStyle: {
|
||||
align: 'center',
|
||||
fontSize: 16,
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: params => colorList[params.dataIndex % colorList.length]
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: pieChartData.value
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 获取告警类型并初始化每种类型的count
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
typeCounts[algorithm.code_name] = 0; // 初始化count为0
|
||||
});
|
||||
|
||||
additionalMappings.forEach((item) => {
|
||||
typeMapping[item.code_name] = item.name;
|
||||
typeCounts[item.code_name] = 0;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 统计每种告警类型的数量
|
||||
const fetchTypeCounts = async (timeAfter = null, timeBefore = null) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
|
||||
console.log(">>>>>>>>>>>>>>>>", typeMapping)
|
||||
|
||||
// 遍历每种类型,根据code_name查询数据数量
|
||||
for (const code in typeMapping) {
|
||||
try {
|
||||
const { count } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, code);
|
||||
typeCounts[code] = count; // 存储每种类型的数量
|
||||
} catch (error) {
|
||||
console.error(`Error fetching count for type ${code}:`, error);
|
||||
}
|
||||
}
|
||||
renderChart();
|
||||
};
|
||||
|
||||
// 点击查询按钮时,添加时间条件并重新统计
|
||||
const handleFilter = () => {
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
|
||||
fetchTypeCounts(timeAfter, timeBefore); // 重新统计数量,添加时间条件
|
||||
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const resizeChart = () => {
|
||||
const chartDom = document.getElementById('3d-bar-chart');
|
||||
if (chartDom) {
|
||||
const chartInstance = echarts.getInstanceByDom(chartDom);
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
|
||||
resizeChart();
|
||||
};
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token);
|
||||
await fetchTypeCounts(); // 初次加载时不加时间条件
|
||||
window.addEventListener('resize', handleResize);
|
||||
await renderChart();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .statistics-container {
|
||||
margin-top: 20px;
|
||||
} */
|
||||
.alert-container{
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
/* padding: 2%; */
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
/* .filter-row{
|
||||
width: 100%;
|
||||
height: 5vh;
|
||||
} */
|
||||
.search-row{
|
||||
width: 95%;
|
||||
padding: 0;
|
||||
background-color: #001529;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: left;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
gap:1vh;
|
||||
|
||||
}
|
||||
|
||||
.start-col,.end-col,.bt-search{
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
height: 4vh;
|
||||
}
|
||||
|
||||
::v-deep .filter-buttons .el-button {
|
||||
background-color: #da681c;
|
||||
/* 查询按钮背景颜色 */
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.myPanChart{
|
||||
width: 95%;
|
||||
height: 25vh;
|
||||
}
|
||||
|
||||
::v-deep .search-row .el-input__wrapper{
|
||||
background-color: transparent;
|
||||
border-radius: 0px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.alert-bt{
|
||||
background-color: #001529;
|
||||
/* background-color: #4a4a4a; */
|
||||
border: #001529;
|
||||
width: 79px;
|
||||
height: 30px;
|
||||
margin-left: 3vh;
|
||||
color: #babcbe;
|
||||
}
|
||||
</style>
|
||||
424
src/bk/告警列表(letf-bottom).vue
Normal file
424
src/bk/告警列表(letf-bottom).vue
Normal file
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<div class="title-count">
|
||||
<!-- <el-row class="title-row">
|
||||
今日事件列表
|
||||
</el-row> -->
|
||||
<el-row class="total-row">
|
||||
<div class="total-text">
|
||||
今日告警
|
||||
</div>
|
||||
<div class="total-text">
|
||||
{{ totalItems }}条
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row class="table-row">
|
||||
<el-col :span="24" class="table-col">
|
||||
<div class="table-container">
|
||||
<el-table :data="tableData" @row-click="handleRowClick" class="table-part">
|
||||
<el-table-column v-show="false" prop="id" label="告警编号" v-if="false"></el-table-column>
|
||||
<el-table-column label="告警类型" :width="adjustedWidths[0]" align="center"
|
||||
:show-overflow-tooltip="true">
|
||||
<template v-slot="scope">
|
||||
{{ typeMapping[scope.row.types] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="camera.name" label="告警位置" :width="adjustedWidths[1]" align="center"
|
||||
:show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column label="告警时间" :width="adjustedWidths[2]" align="center"
|
||||
:show-overflow-tooltip="true">
|
||||
<template v-slot="scope">
|
||||
{{ formatDateTime(scope.row.ended_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="告警状态" v-if="false">
|
||||
<template v-slot="scope">
|
||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[scope.row.status]
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- <el-row class="table-row">
|
||||
<el-col :span="24" class="table-col">
|
||||
<div class="table-container">
|
||||
<dv-scroll-board :config="tableConfig" class="table-part" @row-click="handleRowClick"></dv-scroll-board>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
<!-- <div class="adjusted-widths-display">
|
||||
当前列宽: {{ adjustedWidths.join(', ') }}
|
||||
</div> -->
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="告警详情" width="50%">
|
||||
<div>
|
||||
<el-row class="dialog-row">
|
||||
<!-- 左侧告警图片和图片信息 -->
|
||||
<el-col :span="12" class="dialog-left">
|
||||
<el-row gutter class="dialog-image-container">
|
||||
<!-- 根据 mediumType 显示视频或图片,确保只显示一种或两种 -->
|
||||
<template v-if="hasSnapshot">
|
||||
<el-image :src="snapshotFile"></el-image>
|
||||
</template>
|
||||
<template v-if="hasVideo">
|
||||
<video :src="videoFile" controls></video>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧告警信息 -->
|
||||
<el-col :span="11" class="dialog-right">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<p>告警编号: {{ selectedRow.id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[selectedRow.status]
|
||||
}}</el-tag></p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>持续时间: {{ duration }}</p>
|
||||
</el-col>
|
||||
<!-- <el-col :span="24">
|
||||
<p>备注: {{ selectedRow.remark }}</p>
|
||||
</el-col> -->
|
||||
|
||||
<!-- <el-col :span="24">
|
||||
<el-form-item style="width: 90%;">
|
||||
<el-input v-model="remark" placeholder="请描述事件原因" type="textarea" rows="5" :disabled="true"></el-input>
|
||||
</el-form-item>
|
||||
</el-col> -->
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
|
||||
const boxApi = new BoxApi();
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const remark = ref("");
|
||||
const selectedRow = ref({});
|
||||
const typeMapping = reactive({});
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0);
|
||||
const token = ref(null);
|
||||
const apiInstance = new BoxApi();
|
||||
const statusMapping = {
|
||||
'pending': '待处理',
|
||||
'assigned': '处理中',
|
||||
'closed': '已处理'
|
||||
};
|
||||
const duration = ref('');
|
||||
|
||||
const hasSnapshot = ref(false);
|
||||
const hasVideo = ref(false);
|
||||
const snapshotFile = ref("");
|
||||
const videoFile = ref("");
|
||||
const originalWidths = [97, 150, 160]; // 默认宽度
|
||||
const adjustedWidths = ref([...originalWidths]);
|
||||
const baseWidth = 2150;
|
||||
|
||||
const adjustColumnWidths = () => {
|
||||
const currentWidth = window.innerWidth;
|
||||
console.log(">>>>>>>>>>", currentWidth);
|
||||
const scale = currentWidth / baseWidth;
|
||||
console.log(">>>>>>>>>>", scale);
|
||||
|
||||
// const adjustedScale = Math.max(scale, 0.5);
|
||||
adjustedWidths.value = originalWidths.map(width => {
|
||||
return currentWidth < baseWidth
|
||||
? width * scale // 缩小
|
||||
: width * scale; // 放大
|
||||
});
|
||||
// nextTick(() => {
|
||||
|
||||
// });
|
||||
};
|
||||
|
||||
// const tableConfig = ref({
|
||||
// header: ['告警类型', '告警位置', '告警时间'],
|
||||
// data: [],
|
||||
// rowNum: 5,
|
||||
// columnWidth: [100, 160, 190],
|
||||
// carousel: 'single',
|
||||
// });
|
||||
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token);
|
||||
// console.log("Algorithms:", algorithms);
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 1000;
|
||||
let allEvents = [];
|
||||
let currentPage = 1;
|
||||
|
||||
const { endOfToday, startOfToday } = getDateParams();
|
||||
|
||||
const timeBefore = formatDateTime(endOfToday);
|
||||
const timeAfter = formatDateTime(startOfToday);
|
||||
// console.log("Start of today:", timeAfter);
|
||||
// const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage,timeBefore, timeAfter);
|
||||
// console.log("Total items:", firstResponse);
|
||||
|
||||
const total = firstResponse.count;
|
||||
totalItems.value = total;
|
||||
|
||||
allEvents = firstResponse.results;
|
||||
|
||||
while (allEvents.length < total) {
|
||||
currentPage += 1;
|
||||
const response = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
allEvents = allEvents.concat(response.results);
|
||||
}
|
||||
tableData.value = allEvents;
|
||||
// tableConfig.value.data = allEvents.map(event => [
|
||||
// typeMapping[event.types],
|
||||
// event.camera.name,
|
||||
// formatDateTime(event.ended_at)
|
||||
// ]);
|
||||
// console.log(">>>>>>>>>>events IDs:", allEvents.map(event => event.id));
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getDateParams = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
startOfToday,
|
||||
endOfToday,
|
||||
};
|
||||
};
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const calculateDuration = (started_at, ended_at) => {
|
||||
const start = new Date(started_at);
|
||||
const end = new Date(ended_at);
|
||||
const durationMs = end - start;
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
||||
};
|
||||
|
||||
const handleRowClick = (row) => {
|
||||
selectedRow.value = row;
|
||||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||||
row.formatted_started_at = formatDateTime(row.started_at);
|
||||
|
||||
// 重置媒体类型
|
||||
hasSnapshot.value = false;
|
||||
hasVideo.value = false;
|
||||
snapshotFile.value = "";
|
||||
videoFile.value = "";
|
||||
|
||||
// 判断并设置媒体文件路径
|
||||
row.mediums.forEach((medium) => {
|
||||
if (medium.name === "snapshot") {
|
||||
hasSnapshot.value = true;
|
||||
snapshotFile.value = medium.file;
|
||||
} else if (medium.name === "video") {
|
||||
hasVideo.value = true;
|
||||
videoFile.value = medium.file;
|
||||
}
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// const calculateColumnWidths = () => {
|
||||
// const containerWidth = document.querySelector('.table-container').clientWidth;
|
||||
// };
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
await fetchEvents();
|
||||
await adjustColumnWidths();
|
||||
window.addEventListener('resize', adjustColumnWidths);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', adjustColumnWidths);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
height: 100%;
|
||||
margin: 0 0 0 1vw;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.table-continer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.title-count {
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
|
||||
/* background-color: transparent; */
|
||||
}
|
||||
|
||||
|
||||
/* .total-text{
|
||||
padding-right: 1vw;
|
||||
} */
|
||||
/* title-row内容水平上下居中 */
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
/* total-row左右等分,分别靠左和靠右 */
|
||||
.total-row {
|
||||
display: flex;
|
||||
/* justify-content: space-between; */
|
||||
justify-content: right;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding-right: 0vw;
|
||||
}
|
||||
|
||||
.dialog-row {
|
||||
gap: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dialog-image-container {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.el-dialog .el-image,
|
||||
.el-dialog video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* 基础表格样式 */
|
||||
::v-deep .el-table {
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
/* font-weight: bold; */
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 14vh;
|
||||
}
|
||||
|
||||
/* 表头和单元格基本样式 */
|
||||
|
||||
::v-deep .el-table td {
|
||||
border: none;
|
||||
background-color: #001529;
|
||||
transition: border 0.3s ease, background-color 0.3s ease;
|
||||
color: white;
|
||||
padding: 0;
|
||||
/* height: 10px; */
|
||||
/* white-space: nowrap; */
|
||||
/* overflow: hidden; */
|
||||
/* text-overflow: ellipsis; */
|
||||
}
|
||||
|
||||
/* 去掉中间数据的分割线 */
|
||||
/* ::v-deep .el-table .el-table__row>td{
|
||||
border: none;
|
||||
} */
|
||||
|
||||
.table-container >>> .el-table__row>td{
|
||||
border: none;
|
||||
}
|
||||
.table-container >>> .el-table th.is-leaf{
|
||||
border: none;
|
||||
}
|
||||
::v-deep .el-table__inner-wrapper::before{
|
||||
height: 0;
|
||||
}
|
||||
|
||||
::v-deep .el-table thead th {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
background-color: #001529;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::v-deep .el-table .el-table__cell:hover {
|
||||
background-color: transparent;
|
||||
color: rgb(238, 150, 49);
|
||||
/* 保持文本为白色 */
|
||||
}
|
||||
</style>
|
||||
|
||||
111
src/bk/时间段内告警类型数量.vue
Normal file
111
src/bk/时间段内告警类型数量.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<el-row class="filter-row">
|
||||
<el-col :span="11">
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="filterParams.timeAfter" type="datetime" placeholder="请选择开始时间"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="11">
|
||||
<el-form-item label="结束时间">
|
||||
<el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2" class="filter-buttons">
|
||||
<el-button type="primary" @click="handleFilter">查询</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<div class="statistics-container">
|
||||
<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: {{ typeCounts[code] || 0 }} 次
|
||||
</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const typeCounts = reactive({}); // 存储每种类型的数量
|
||||
|
||||
const filterParams = reactive({
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
});
|
||||
|
||||
// 获取告警类型并初始化每种类型的count
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
typeCounts[algorithm.code_name] = 0; // 初始化count为0
|
||||
});
|
||||
|
||||
additionalMappings.forEach((item) => {
|
||||
typeMapping[item.code_name] = item.name;
|
||||
typeCounts[item.code_name] = 0;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 统计每种告警类型的数量
|
||||
const fetchTypeCounts = async (timeAfter = null, timeBefore = null) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
|
||||
console.log(">>>>>>>>>>>>>>>>",typeMapping)
|
||||
|
||||
// 遍历每种类型,根据code_name查询数据数量
|
||||
for (const code in typeMapping) {
|
||||
try {
|
||||
const { count } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, code);
|
||||
typeCounts[code] = count; // 存储每种类型的数量
|
||||
} catch (error) {
|
||||
console.error(`Error fetching count for type ${code}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 点击查询按钮时,添加时间条件并重新统计
|
||||
const handleFilter = () => {
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
|
||||
fetchTypeCounts(timeAfter, timeBefore); // 重新统计数量,添加时间条件
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token);
|
||||
fetchTypeCounts(); // 初次加载时不加时间条件
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.statistics-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</el-icon>
|
||||
<template #title><span>大屏页</span></template>
|
||||
</el-menu-item>
|
||||
<!-- <el-sub-menu index="1">
|
||||
<el-sub-menu index="1">
|
||||
<template #title>
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>面板测试</span>
|
||||
@@ -51,7 +51,7 @@
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
<template #title><span>布局备份</span></template>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/alertChart">
|
||||
<!-- <el-menu-item index="/alertChart">
|
||||
<el-icon><WarningFilled /></el-icon>
|
||||
<template #title><span>功能点1测试</span></template>
|
||||
</el-menu-item>
|
||||
@@ -62,8 +62,8 @@
|
||||
<el-menu-item index="/cameras">
|
||||
<el-icon><Document /></el-icon>
|
||||
<template #title><span>功能点3测试</span></template>
|
||||
</el-menu-item>
|
||||
</el-sub-menu> -->
|
||||
</el-menu-item> -->
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
|
||||
408
src/components/Max/CenterBottom.vue
Normal file
408
src/components/Max/CenterBottom.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<!-- <el-card class="alert-card">
|
||||
<div class="alert-header">告警趋势</div>
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third">
|
||||
<div ref="monthLineChartDom" class="chart-container"></div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card> -->
|
||||
<el-tabs v-model="activeName" @tab-click="handleClick" class="alert-tabs">
|
||||
<el-tab-pane label="今日" name="first">
|
||||
<div ref="todayLineChartDom" class="chart-container"></div> <!-- 今日图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="本周" name="second">
|
||||
<div ref="weekLineChartDom" class="chart-container"></div> <!-- 本周图表 -->
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最近30天" name="third"> <!-- 新增最近30天选项卡 -->
|
||||
<div ref="monthLineChartDom" class="chart-container"></div> <!-- 最近30天图表 -->
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const activeName = ref('first');
|
||||
const hourlyCounts = ref(Array(24).fill(0));
|
||||
const dailyCounts = ref(Array(7).fill(0));
|
||||
const monthlyCounts = ref(Array(30).fill(0));
|
||||
|
||||
const todayLineChartDom = ref(null);
|
||||
const weekLineChartDom = ref(null);
|
||||
const monthLineChartDom = ref(null);
|
||||
|
||||
const chartInstanceToday = ref(null);
|
||||
const chartInstanceWeek = ref(null);
|
||||
const chartInstanceMonth = ref(null);
|
||||
|
||||
const weekDates = ref([]);
|
||||
const monthDates = ref([]);
|
||||
|
||||
let isTodayChartInitialized = false;
|
||||
let isWeekChartInitialized = false;
|
||||
let isMonthChartInitialized = false;
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const allEvents = ref([]);
|
||||
|
||||
// 日期格式化函数
|
||||
const formatDateTime = (date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 获取时间参数
|
||||
const getDayParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.startOf('day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getWeeklyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(7, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
const getMonthlyParams = () => {
|
||||
const today = dayjs();
|
||||
return { timeAfter: today.subtract(30, 'day').format(), timeBefore: today.endOf('day').format() };
|
||||
};
|
||||
|
||||
// 计算日期
|
||||
const calculateWeekDates = () => {
|
||||
weekDates.value = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
const calculateMonthDates = () => {
|
||||
monthDates.value = Array.from({ length: 30 }, (_, i) => dayjs().subtract(i, 'day').format('YYYY-MM-DD')).reverse();
|
||||
};
|
||||
|
||||
// 延迟初始化图表函数
|
||||
const delayedInitChart = debounce((chartInstance, domRef, option) => {
|
||||
nextTick(() => {
|
||||
if (domRef.value) {
|
||||
chartInstance.value = echarts.init(domRef.value);
|
||||
chartInstance.value.setOption(option);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
|
||||
// 初始化图表选项配置
|
||||
const createTodayChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: Array.from({ length: 24 }, (_, i) => `${i}:00`),
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: hourlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createWeekChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: weekDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: dailyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createMonthChartOption = () => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
axisPointer: { type: 'line' },
|
||||
formatter: (params) => {
|
||||
console.log('Tooltip params:', params);
|
||||
const date = params.name;
|
||||
const value = params.value;
|
||||
return `<strong>时间:${date}</strong><br/>告警数量: ${value}`; // 使用 <strong> 标签作为小标题
|
||||
},
|
||||
backgroundColor: 'rgba(50, 50, 50, 0.3)',
|
||||
borderWidth: 1,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontStyle: 'italic', // 字体样式,可以是 'normal', 'italic', 'oblique'
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'Arial, sans-serif', // 字体
|
||||
fontSize: 12
|
||||
},
|
||||
padding: 10
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
bottom: '20%',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: monthDates.value,
|
||||
axisLabel: { color: '#fff' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', minInterval: 1, axisLabel: { color: '#fff' },
|
||||
splitLine: {
|
||||
show: false,
|
||||
lineStyle: {
|
||||
color: '#cccccc',
|
||||
width: 1,
|
||||
type: 'solid',
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '告警数量',
|
||||
type: 'line',
|
||||
data: monthlyCounts.value,
|
||||
smooth: true,
|
||||
areaStyle: { color: 'rgba(74, 172, 178, 0.3)' },
|
||||
lineStyle: { color: 'rgb(60,178,239)', width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 获取和处理事件数据
|
||||
const fetchAndProcessEvents = async (timeParams) => {
|
||||
try {
|
||||
let currentPage = 1;
|
||||
const pageSize = 1000;
|
||||
const token = localStorage.getItem('alertToken');
|
||||
allEvents.value = [];
|
||||
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
const totalItems = firstResponse.count;
|
||||
allEvents.value.push(...firstResponse.results);
|
||||
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
while (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
const response = await apiInstance.getEventsByParams(token, pageSize, currentPage, timeParams.timeBefore, timeParams.timeAfter);
|
||||
allEvents.value.push(...response.results);
|
||||
}
|
||||
|
||||
processEventData(allEvents.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理事件数据
|
||||
const processEventData = (events) => {
|
||||
hourlyCounts.value.fill(0);
|
||||
dailyCounts.value.fill(0);
|
||||
monthlyCounts.value.fill(0);
|
||||
events.forEach((event) => {
|
||||
const hour = dayjs(event.ended_at).hour();
|
||||
const dayDiff = dayjs().diff(dayjs(event.ended_at), 'day');
|
||||
if (dayDiff < 1) hourlyCounts.value[hour] += 1;
|
||||
if (dayDiff < 7) dailyCounts.value[6 - dayDiff] += 1;
|
||||
if (dayDiff < 30) monthlyCounts.value[29 - dayDiff] += 1;
|
||||
});
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateCharts = () => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.setOption({ series: [{ data: hourlyCounts.value }] });
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.setOption({ series: [{ data: dailyCounts.value }] });
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.setOption({ series: [{ data: monthlyCounts.value }] });
|
||||
};
|
||||
|
||||
// 处理选项卡切换
|
||||
const handleClick = async (tab) => {
|
||||
let timeParams;
|
||||
if (tab.props.name === 'first' && !isTodayChartInitialized) {
|
||||
timeParams = getDayParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceToday, todayLineChartDom, createTodayChartOption());
|
||||
isTodayChartInitialized = true;
|
||||
} else if (tab.props.name === 'second' && !isWeekChartInitialized) {
|
||||
timeParams = getWeeklyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceWeek, weekLineChartDom, createWeekChartOption());
|
||||
isWeekChartInitialized = true;
|
||||
} else if (tab.props.name === 'third' && !isMonthChartInitialized) {
|
||||
timeParams = getMonthlyParams();
|
||||
await fetchAndProcessEvents(timeParams);
|
||||
delayedInitChart(chartInstanceMonth, monthLineChartDom, createMonthChartOption());
|
||||
isMonthChartInitialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口调整时重新渲染图表
|
||||
const resizeCharts = debounce(() => {
|
||||
if (chartInstanceToday.value) chartInstanceToday.value.resize();
|
||||
if (chartInstanceWeek.value) chartInstanceWeek.value.resize();
|
||||
if (chartInstanceMonth.value) chartInstanceMonth.value.resize();
|
||||
}, 300);
|
||||
|
||||
// 组件挂载时调用
|
||||
onMounted(async () => {
|
||||
calculateWeekDates();
|
||||
calculateMonthDates();
|
||||
await handleClick({ props: { name: 'first' } });
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.alert-card {
|
||||
background-color: #001529;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
/* padding: 10px; */
|
||||
/* margin: 10px; */
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
background-color: #001529;
|
||||
margin: 0vh 1vw 1vh 1vw;
|
||||
/* margin-top: 0; */
|
||||
}
|
||||
|
||||
/* .alert-header {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #3a4b5c;
|
||||
} */
|
||||
|
||||
.chart-container {
|
||||
/* min-height: 350px; */
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
/* ::v-deep .el-tabs___nav-wrap.is-top::after{
|
||||
border: none;
|
||||
} */
|
||||
|
||||
/* ::v-deep .el-tabs__active-bar.is-top{
|
||||
padding: 0 204px;
|
||||
box-sizing: border-box !important;
|
||||
background-clip: content-box !important;
|
||||
} */
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
347
src/components/Max/CenterTop.vue
Normal file
347
src/components/Max/CenterTop.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<div class="camera-container">
|
||||
<div class="top-header">
|
||||
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter" >
|
||||
<el-option label="全部" value="all"></el-option>
|
||||
<el-option label="在线" value="online"></el-option>
|
||||
<el-option label="离线" value="offline"></el-option>
|
||||
</el-select>
|
||||
<div class="top-text">警戒点位</div>
|
||||
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
|
||||
class="camera-select" >
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
<span>{{ camera.id }}.</span> 名称: {{ camera.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 播放列表,设置为可滚动 -->
|
||||
<div class="camera-grid">
|
||||
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
|
||||
<div class="stream-control">
|
||||
<p class="camera-name-title">{{ camera.name }}</p>
|
||||
<div class="close-button" @click="closeStream(camera)">×</div>
|
||||
</div>
|
||||
|
||||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
||||
<el-icon size="48">
|
||||
<VideoCameraFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="openDialog(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗播放视频 -->
|
||||
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
|
||||
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
|
||||
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const cameras = ref([]);
|
||||
const selectedCameras = ref([]);
|
||||
const showButton = ref(false);
|
||||
const apiInstance = new BoxApi();
|
||||
const canvasRefs = ref({});
|
||||
const selectedCameraId = ref(null);
|
||||
const dialogVisible = ref(false); // 控制弹窗的显示与隐藏
|
||||
const currentCamera = ref(null); // 当前选中的摄像头
|
||||
|
||||
// 弹窗中的canvas引用
|
||||
const dialogCanvas = ref(null);
|
||||
const filterStatus = ref("all");
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getMinCameras(token);
|
||||
|
||||
// 根据 filterStatus 筛选摄像头状态
|
||||
if (filterStatus.value === "online") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "online");
|
||||
} else if (filterStatus.value === "offline") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "offline");
|
||||
} else {
|
||||
cameras.value = cameraData; // 默认 "all" 显示全部摄像头
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const selectCameraById = (cameraId) => {
|
||||
const camera = cameras.value.find(c => c.id === cameraId);
|
||||
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
|
||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗并开始播放
|
||||
const openDialog = async (camera) => {
|
||||
currentCamera.value = camera;
|
||||
dialogVisible.value = true;
|
||||
await nextTick();
|
||||
startStreamInDialog(camera);
|
||||
};
|
||||
|
||||
// 在弹窗中播放视频
|
||||
const startStreamInDialog = async (camera) => {
|
||||
const canvas = dialogCanvas.value;
|
||||
|
||||
if (!camera || !canvas) {
|
||||
console.error('未找到对应的 canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
const response = await apiInstance.startCameraStream(token, camera.id);
|
||||
camera.streamPort = response.port;
|
||||
camera.playing = true;
|
||||
|
||||
const url = `ws://192.168.28.33:${camera.streamPort}/`;
|
||||
console.log('播放路径:', url);
|
||||
|
||||
if (window.JSMpeg) {
|
||||
const player = new window.JSMpeg.Player(url, {
|
||||
canvas: canvas,
|
||||
autoplay: true,
|
||||
videoBufferSize: 15 * 1024 * 1024,
|
||||
audioBufferSize: 5 * 1024 * 1024,
|
||||
});
|
||||
camera.player = player;
|
||||
} else {
|
||||
console.error('JSMpeg 未加载');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗并停止视频播放
|
||||
const closeDialog = () => {
|
||||
if (currentCamera.value) {
|
||||
handleStopStream(currentCamera.value);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
currentCamera.value = null;
|
||||
};
|
||||
|
||||
const handleStopStream = async (camera) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
await apiInstance.stopCameraStream(token, camera.id);
|
||||
camera.playing = false;
|
||||
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
camera.player = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeStream = (camera) => {
|
||||
handleStopStream(camera);
|
||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCameras();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectedCameras.value.forEach(camera => {
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 5vw;
|
||||
margin-left: 1vh;
|
||||
margin-top: 1vh;
|
||||
|
||||
}
|
||||
::v-deep .status-filter .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.camera-select {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 12vw;
|
||||
margin-left: 0vh;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.top-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
width: 7vw;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0 0 0 10vw;
|
||||
/* justify-content: center; */
|
||||
align-content: center;
|
||||
background-color: #001529;
|
||||
line-height: 0px;
|
||||
color: aliceblue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.camera-name-title {
|
||||
padding: 0.2vh;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
margin: 0vh 1vh;
|
||||
padding: 0 0 2vh 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vh 0vh;
|
||||
width: 34vw;
|
||||
height: 39vh;
|
||||
max-height: 45vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
.camera-grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
margin: 1vh;
|
||||
position: relative;
|
||||
height: 17vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.camera-snapshot,
|
||||
.camera-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #000000;
|
||||
color: aliceblue;
|
||||
padding: 0 2px 2px 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.play-button-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
431
src/components/Max/LeftBottom.vue
Normal file
431
src/components/Max/LeftBottom.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<div class="title-count">
|
||||
<!-- <el-row class="title-row">
|
||||
今日事件列表
|
||||
</el-row> -->
|
||||
<el-row class="total-row">
|
||||
<div class="total-text">
|
||||
今日告警
|
||||
</div>
|
||||
<div class="total-text">
|
||||
{{ totalItems }}条
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row class="table-row">
|
||||
<el-col :span="24" class="table-col">
|
||||
<div class="table-container">
|
||||
<el-table :data="tableData" @row-click="handleRowClick" class="table-part">
|
||||
<el-table-column v-show="false" prop="id" label="告警编号" v-if="false"></el-table-column>
|
||||
<el-table-column label="告警类型" :width="adjustedWidths[0]" align="center"
|
||||
:show-overflow-tooltip="true">
|
||||
<template v-slot="scope">
|
||||
{{ typeMapping[scope.row.types] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="camera.name" label="告警位置" :width="adjustedWidths[1]" align="center"
|
||||
:show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column label="告警时间" :width="adjustedWidths[2]" align="center"
|
||||
:show-overflow-tooltip="true">
|
||||
<template v-slot="scope">
|
||||
{{ formatDateTime(scope.row.ended_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="告警状态" v-if="false">
|
||||
<template v-slot="scope">
|
||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[scope.row.status]
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- <el-row class="table-row">
|
||||
<el-col :span="24" class="table-col">
|
||||
<div class="table-container">
|
||||
<dv-scroll-board :config="tableConfig" class="table-part" @row-click="handleRowClick"></dv-scroll-board>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
<!-- <div class="adjusted-widths-display">
|
||||
当前列宽: {{ adjustedWidths.join(', ') }}
|
||||
</div> -->
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="告警详情" width="50%" class="dialog-container">
|
||||
<div>
|
||||
<el-row class="dialog-row">
|
||||
<!-- 左侧告警图片和图片信息 -->
|
||||
<el-col :span="12" class="dialog-left">
|
||||
<el-row gutter class="dialog-image-container">
|
||||
<!-- 根据 mediumType 显示视频或图片,确保只显示一种或两种 -->
|
||||
<template v-if="hasSnapshot">
|
||||
<el-image :src="snapshotFile"></el-image>
|
||||
</template>
|
||||
<template v-if="hasVideo">
|
||||
<video :src="videoFile" controls></video>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧告警信息 -->
|
||||
<el-col :span="11" class="dialog-right">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<p>告警编号: {{ selectedRow.id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[selectedRow.status]
|
||||
}}</el-tag></p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<p>持续时间: {{ duration }}</p>
|
||||
</el-col>
|
||||
<!-- <el-col :span="24">
|
||||
<p>备注: {{ selectedRow.remark }}</p>
|
||||
</el-col> -->
|
||||
|
||||
<!-- <el-col :span="24">
|
||||
<el-form-item style="width: 90%;">
|
||||
<el-input v-model="remark" placeholder="请描述事件原因" type="textarea" rows="5" :disabled="true"></el-input>
|
||||
</el-form-item>
|
||||
</el-col> -->
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
|
||||
const boxApi = new BoxApi();
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const remark = ref("");
|
||||
const selectedRow = ref({});
|
||||
const typeMapping = reactive({});
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0);
|
||||
const token = ref(null);
|
||||
const apiInstance = new BoxApi();
|
||||
const statusMapping = {
|
||||
'pending': '待处理',
|
||||
'assigned': '处理中',
|
||||
'closed': '已处理'
|
||||
};
|
||||
const duration = ref('');
|
||||
|
||||
const hasSnapshot = ref(false);
|
||||
const hasVideo = ref(false);
|
||||
const snapshotFile = ref("");
|
||||
const videoFile = ref("");
|
||||
const originalWidths = [97, 150, 160]; // 默认宽度
|
||||
const adjustedWidths = ref([...originalWidths]);
|
||||
const baseWidth = 2150;
|
||||
|
||||
const adjustColumnWidths = () => {
|
||||
const currentWidth = window.innerWidth;
|
||||
// console.log(">>>>>>>>>>", currentWidth);
|
||||
const scale = currentWidth / baseWidth;
|
||||
// console.log(">>>>>>>>>>", scale);
|
||||
|
||||
// const adjustedScale = Math.max(scale, 0.5);
|
||||
adjustedWidths.value = originalWidths.map(width => {
|
||||
return currentWidth < baseWidth
|
||||
? width * scale // 缩小
|
||||
: width * scale; // 放大
|
||||
});
|
||||
// nextTick(() => {
|
||||
|
||||
// });
|
||||
};
|
||||
|
||||
// const tableConfig = ref({
|
||||
// header: ['告警类型', '告警位置', '告警时间'],
|
||||
// data: [],
|
||||
// rowNum: 5,
|
||||
// columnWidth: [100, 160, 190],
|
||||
// carousel: 'single',
|
||||
// });
|
||||
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token);
|
||||
// console.log("Algorithms:", algorithms);
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 1000;
|
||||
let allEvents = [];
|
||||
let currentPage = 1;
|
||||
|
||||
const { endOfToday, startOfToday } = getDateParams();
|
||||
|
||||
const timeBefore = formatDateTime(endOfToday);
|
||||
const timeAfter = formatDateTime(startOfToday);
|
||||
// console.log("Start of today:", timeAfter);
|
||||
// const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage,timeBefore, timeAfter);
|
||||
// console.log("Total items:", firstResponse);
|
||||
|
||||
const total = firstResponse.count;
|
||||
totalItems.value = total;
|
||||
|
||||
allEvents = firstResponse.results;
|
||||
|
||||
while (allEvents.length < total) {
|
||||
currentPage += 1;
|
||||
const response = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
allEvents = allEvents.concat(response.results);
|
||||
}
|
||||
tableData.value = allEvents;
|
||||
// tableConfig.value.data = allEvents.map(event => [
|
||||
// typeMapping[event.types],
|
||||
// event.camera.name,
|
||||
// formatDateTime(event.ended_at)
|
||||
// ]);
|
||||
// console.log(">>>>>>>>>>events IDs:", allEvents.map(event => event.id));
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const getDateParams = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
startOfToday,
|
||||
endOfToday,
|
||||
};
|
||||
};
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const calculateDuration = (started_at, ended_at) => {
|
||||
const start = new Date(started_at);
|
||||
const end = new Date(ended_at);
|
||||
const durationMs = end - start;
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
||||
};
|
||||
|
||||
const handleRowClick = (row) => {
|
||||
selectedRow.value = row;
|
||||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||||
row.formatted_started_at = formatDateTime(row.started_at);
|
||||
|
||||
// 重置媒体类型
|
||||
hasSnapshot.value = false;
|
||||
hasVideo.value = false;
|
||||
snapshotFile.value = "";
|
||||
videoFile.value = "";
|
||||
|
||||
// 判断并设置媒体文件路径
|
||||
row.mediums.forEach((medium) => {
|
||||
if (medium.name === "snapshot") {
|
||||
hasSnapshot.value = true;
|
||||
snapshotFile.value = medium.file;
|
||||
} else if (medium.name === "video") {
|
||||
hasVideo.value = true;
|
||||
videoFile.value = medium.file;
|
||||
}
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// const calculateColumnWidths = () => {
|
||||
// const containerWidth = document.querySelector('.table-container').clientWidth;
|
||||
// };
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
await fetchEvents();
|
||||
await adjustColumnWidths();
|
||||
window.addEventListener('resize', adjustColumnWidths);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', adjustColumnWidths);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
height: 100%;
|
||||
margin: 0 0 0 1vw;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.table-continer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.title-count {
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
|
||||
/* background-color: transparent; */
|
||||
}
|
||||
|
||||
|
||||
/* .total-text{
|
||||
padding-right: 1vw;
|
||||
} */
|
||||
/* title-row内容水平上下居中 */
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
/* total-row左右等分,分别靠左和靠右 */
|
||||
.total-row {
|
||||
display: flex;
|
||||
/* justify-content: space-between; */
|
||||
justify-content: right;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding-right: 0vw;
|
||||
}
|
||||
|
||||
.dialog-row {
|
||||
gap: 30px;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dialog-image-container {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.el-dialog .el-image,
|
||||
.el-dialog video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
/* 基础表格样式 */
|
||||
::v-deep .el-table {
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
/* font-weight: bold; */
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 14vh;
|
||||
}
|
||||
|
||||
/* 表头和单元格基本样式 */
|
||||
|
||||
::v-deep .el-table td {
|
||||
border: none;
|
||||
background-color: #001529;
|
||||
transition: border 0.3s ease, background-color 0.3s ease;
|
||||
color: white;
|
||||
padding: 0;
|
||||
/* height: 10px; */
|
||||
/* white-space: nowrap; */
|
||||
/* overflow: hidden; */
|
||||
/* text-overflow: ellipsis; */
|
||||
}
|
||||
|
||||
/* 去掉中间数据的分割线 */
|
||||
/* ::v-deep .el-table .el-table__row>td{
|
||||
border: none;
|
||||
} */
|
||||
|
||||
.table-container >>> .el-table__row>td{
|
||||
border: none;
|
||||
}
|
||||
.table-container >>> .el-table th.is-leaf{
|
||||
border: none;
|
||||
}
|
||||
::v-deep .el-table__inner-wrapper::before{
|
||||
height: 0;
|
||||
}
|
||||
|
||||
::v-deep .el-table thead th {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
background-color: #001529;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::v-deep .el-table .el-table__cell:hover {
|
||||
background-color: transparent;
|
||||
color: rgb(238, 150, 49);
|
||||
/* font-size: 26px; */
|
||||
/* 保持文本为白色 */
|
||||
}
|
||||
|
||||
.el-tooltip__popper {
|
||||
font-size: 26px;
|
||||
max-width:50%
|
||||
}
|
||||
</style>
|
||||
|
||||
310
src/components/Max/LeftMiddle.vue
Normal file
310
src/components/Max/LeftMiddle.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="leftMiddle-box">
|
||||
<el-button @click="toggleDataZoomMove" class="show-bt">
|
||||
<el-icon><d-caret /></el-icon>
|
||||
{{ isDataZoomMoving ? '关闭轮播' : '开启轮播' }}
|
||||
</el-button>
|
||||
<div ref="chartContainer" class="chart-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { DCaret } from '@element-plus/icons-vue';
|
||||
|
||||
const chartContainer = ref(null);
|
||||
let myChart = null;
|
||||
const cameras = ref([]);
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
const colorList = ['#FF4B01', '#FF7B00', '#FFE823', '#2379FF', '#2379FF', '#2379FF'];
|
||||
const colorListA = ['#FC7A24', '#FFC000', '#FFEA97', '#49B1FF', '#49B1FF', '#49B1FF'];
|
||||
let dataZoomMove = { start: 0, end: 8 };
|
||||
const isDataZoomMoving = ref(false);
|
||||
let dataZoomMoveTimer = null;
|
||||
|
||||
const option = ref({
|
||||
backgroundColor: "#001529",
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(21, 154, 255, 0.32)",
|
||||
textStyle: { color: "#fff" },
|
||||
borderColor: "#159AFF",
|
||||
axisPointer: { lineStyle: { color: "transparent" } },
|
||||
formatter: (params) => {
|
||||
const cameraName = params[0] ? params[0].name : '';
|
||||
const outerSeries = params.find(item => item.seriesName === '告警数量');
|
||||
|
||||
return outerSeries ? `<div style="font-weight: bold; margin-bottom: 5px;">摄像头: ${cameraName}</div>${outerSeries.marker} ${outerSeries.seriesName}: ${outerSeries.value} 次` : '';
|
||||
}
|
||||
|
||||
},
|
||||
dataZoom: [
|
||||
{ show: false, startValue: dataZoomMove.start, endValue: dataZoomMove.end, yAxisIndex: [0, 1] },
|
||||
{ type: "inside", yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseMove: true, moveOnMouseWheel: true }
|
||||
],
|
||||
grid: { containLabel: true, bottom: 20, left: 30, top: 20, right: 30 },
|
||||
xAxis: { type: "value", axisLabel: { show: false }, axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false } },
|
||||
yAxis: [
|
||||
{
|
||||
type: "category",
|
||||
data: [], // 动态填充摄像头名称
|
||||
inverse: true,
|
||||
axisLabel: {
|
||||
inside: true,
|
||||
verticalAlign: "bottom",
|
||||
lineHeight: 36,
|
||||
margin: 2,
|
||||
formatter(value) {
|
||||
const k = option.value.yAxis[0].data.indexOf(value);
|
||||
// return `{b|${k + 1}} {a|${value}}`;
|
||||
return `{a|${value}}`;
|
||||
},
|
||||
rich: {
|
||||
b: { color: "#fff", fontSize: 14 },
|
||||
a: { fontSize: 14, color: "#fff", padding: [4, 0, 0, 8] }
|
||||
}
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false }
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
data: [], // 动态填充告警数量
|
||||
inverse: true,
|
||||
axisLabel: {
|
||||
inside: true,
|
||||
verticalAlign: "bottom",
|
||||
lineHeight: 34,
|
||||
margin: 2,
|
||||
formatter(value, index) {
|
||||
return `{a|${value}}`;
|
||||
},
|
||||
rich: {
|
||||
a: { fontSize: 16, color: "#fff", padding: [4, 0, 0, 0], fontFamily: "DOUYU" }
|
||||
}
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "条形图",
|
||||
data: [], // 动态填充告警数量
|
||||
type: "bar",
|
||||
barWidth: 10,
|
||||
padding:[0],
|
||||
showBackground: true,
|
||||
barBorderRadius: [30, 0, 0, 30],
|
||||
backgroundStyle: { color: 'rgba(9, 68, 131, .2)' },
|
||||
itemStyle: {
|
||||
barBorderRadius: [3, 0, 0, 3],
|
||||
color(params) {
|
||||
const i = colorListA.length;
|
||||
const f = colorList.length;
|
||||
return {
|
||||
type: "linear",
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: colorListA[params.dataIndex % i] },
|
||||
{ offset: 1, color: colorList[params.dataIndex % f] }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "告警数量",
|
||||
type: "scatter",
|
||||
symbol: "rect",
|
||||
symbolSize: [5, 16],
|
||||
itemStyle: { color: '#FFF', shadowColor: "rgba(255, 255, 255, 0.5)", shadowBlur: 5, borderWidth: 1, opacity: 1 },
|
||||
z: 2,
|
||||
data: [], // 动态填充告警数量
|
||||
animationDelay: 500
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const toggleDataZoomMove = () => {
|
||||
// console.log(`Data zoom move is now ${isDataZoomMoving.value}`);
|
||||
isDataZoomMoving.value = !isDataZoomMoving.value; // 切换状态
|
||||
// console.log(`转换${isDataZoomMoving.value}`);
|
||||
if (isDataZoomMoving.value) {
|
||||
startMoveDataZoom();// 关闭轮播
|
||||
} else {
|
||||
// 开启轮播
|
||||
stopMoveDataZoom();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// 关闭轮播
|
||||
const stopMoveDataZoom = () => {
|
||||
if (dataZoomMoveTimer) {
|
||||
// console.log('Stopping data zoom move...');
|
||||
clearInterval(dataZoomMoveTimer);
|
||||
dataZoomMoveTimer = null;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 启动数据轮播
|
||||
const startMoveDataZoom = () => {
|
||||
// if (isDataZoomMoving.value) return;
|
||||
// console.log('Starting ...');
|
||||
if (dataZoomMoveTimer !== null) {
|
||||
clearInterval(dataZoomMoveTimer);
|
||||
}
|
||||
if (isDataZoomMoving.value) {
|
||||
// console.log('Starting data zoom move...');
|
||||
dataZoomMoveTimer = setInterval(() => {
|
||||
dataZoomMove.start += 1;
|
||||
dataZoomMove.end += 1;
|
||||
if (dataZoomMove.end > option.value.yAxis[0].data.length - 1) {
|
||||
dataZoomMove.start = 0;
|
||||
dataZoomMove.end = 8;
|
||||
}
|
||||
myChart.setOption({
|
||||
dataZoom: [{ type: "slider", startValue: dataZoomMove.start, endValue: dataZoomMove.end }]
|
||||
});
|
||||
}, 2500);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 获取摄像头信息并获取对应的告警数量
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
// 获取摄像头数据
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
// 获取所有摄像头信息
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
const cameraCount = firstResponse.count;
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
while (offset + limit < cameraCount) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
// 提取摄像头名称和对应的告警数量并存储在对象数组中
|
||||
const cameraData = [];
|
||||
|
||||
for (const camera of allCameras) {
|
||||
// 获取该摄像头的告警数量
|
||||
const eventsResponse = await apiInstance.getEventsByParams(token, 20, 1, null, null, null, camera.id);
|
||||
const count = eventsResponse.count || 0; // 确保即使没有事件也返回 0
|
||||
cameraData.push({ name: camera.name, count }); // 将数据存储为对象以便排序
|
||||
}
|
||||
|
||||
// 按照告警数量降序排序
|
||||
cameraData.sort((a, b) => b.count - a.count);
|
||||
|
||||
// 提取排序后的名称和数量
|
||||
const cameraNames = cameraData.map(item => item.name);
|
||||
const cameraCounts = cameraData.map(item => item.count);
|
||||
|
||||
// 更新图表的 Y 轴标签和系列数据
|
||||
option.value.yAxis[0].data = cameraNames;
|
||||
option.value.yAxis[1].data = cameraCounts.map(count => `${count}`);
|
||||
option.value.series[0].data = cameraCounts;
|
||||
option.value.series[1].data = cameraCounts;
|
||||
|
||||
// 设置图表选项并启动轮播
|
||||
myChart.setOption(option.value);
|
||||
startMoveDataZoom(); // 重新启动轮播效果
|
||||
} catch (error) {
|
||||
console.error("Error fetching cameras or events:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 清理轮播和事件监听
|
||||
onBeforeUnmount(() => {
|
||||
if (dataZoomMoveTimer) {
|
||||
clearInterval(dataZoomMoveTimer);
|
||||
dataZoomMoveTimer = null; // 确保计时器被清空
|
||||
}
|
||||
if (myChart) {
|
||||
window.removeEventListener('resize', resizeChart); // 确保事件监听器被移除
|
||||
myChart.dispose();
|
||||
myChart = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 初始化图表
|
||||
onMounted(async () => {
|
||||
myChart = echarts.init(chartContainer.value);
|
||||
await fetchCameras();
|
||||
myChart.setOption(option.value);
|
||||
|
||||
// 监听窗口变化事件,调整图表大小
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
|
||||
const resizeChart = () => {
|
||||
if (myChart && !myChart.isDisposed()) {
|
||||
myChart.resize();
|
||||
} else {
|
||||
console.warn('Attempted to resize a disposed ECharts instance.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const resizeChart = () => {
|
||||
// if (myChart && !myChart.isDisposed()) {
|
||||
// myChart.resize();
|
||||
// if (dataZoomMoveTimer) {
|
||||
// clearInterval(dataZoomMoveTimer);
|
||||
// startMoveDataZoom();
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.leftMiddle-box{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 41vh;
|
||||
margin: 20px;
|
||||
}
|
||||
.show-bt{
|
||||
background-color: #001529;
|
||||
border: #001529;
|
||||
width: 6vw;
|
||||
height: 4vh;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bolder;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
.chart-container {
|
||||
/* margin: 0 1vw 0 1vw; */
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
height: 35vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +1,377 @@
|
||||
<template>
|
||||
<div ref="chart" class="left-top-content" :style="style"></div>
|
||||
<div class="count-container">
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendintingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import CameraAll from '@/icons/CameraAll.vue';
|
||||
import CameraOnline from '@/icons/CameraOnline.vue';
|
||||
import CameraOffline from '@/icons/CameraOffline.vue';
|
||||
import EventAll from '@/icons/EventAll.vue';
|
||||
import EventClosed from '@/icons/EventClosed.vue';
|
||||
import EventPending from '@/icons/EventPending.vue';
|
||||
|
||||
const props = defineProps(['style']);
|
||||
const chart = ref(null);
|
||||
const myChart = ref(null);
|
||||
const apiInstance = new BoxApi();
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendintingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
const setupChart = () => {
|
||||
myChart.value = echarts.init(chart.value);
|
||||
const activeTab = ref('all');
|
||||
|
||||
const option = {
|
||||
title: {},
|
||||
tooltip: {},
|
||||
xAxis: {
|
||||
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
|
||||
},
|
||||
yAxis: {},
|
||||
series: [{
|
||||
name: '销量',
|
||||
type: 'bar',
|
||||
data: [5, 20, 36, 10, 10, 20]
|
||||
}]
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
myChart.value.setOption(option);
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setupChart();
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
myChart.value.resize(); // 调整图表大小
|
||||
});
|
||||
|
||||
resizeObserver.observe(chart.value.parentElement); // 观察父元素
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.disconnect();
|
||||
myChart.value.dispose(); // 清理 ECharts 实例
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-top-content {
|
||||
width: 100%; /* 与父容器宽度匹配 */
|
||||
height: 100%; /* 与父容器高度匹配 */
|
||||
position: relative; /* 允许子元素绝对定位 */
|
||||
.count-container{
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin:1vh;
|
||||
padding: 2vh 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
}
|
||||
|
||||
.tab-div{
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
308
src/components/Max/RightTop.vue
Normal file
308
src/components/Max/RightTop.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
|
||||
<div class="search-row">
|
||||
<div class="bt-search">
|
||||
<el-button type="primary" @click="handleFilter" class="alert-bt">点击查询</el-button>
|
||||
</div>
|
||||
<div class="start-col">
|
||||
<el-date-picker v-model="filterParams.timeAfter" :teleported="false" type="datetime" placeholder="请选择开始时间" prefix-icon="CaretBottom" popper-class="popperClass" ></el-date-picker>
|
||||
</div>
|
||||
<div class="end-col">
|
||||
<el-date-picker v-model="filterParams.timeBefore" :teleported="false" type="datetime" placeholder="请选择结束时间" prefix-icon="CaretBottom" popper-class="popperClass" ></el-date-picker>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="3d-bar-chart" class="myPanChart" ></div>
|
||||
|
||||
<!-- <el-row>
|
||||
<el-col :span="24">
|
||||
<div class="statistics-container">
|
||||
<p v-for="(name, code) in typeMapping" :key="code">
|
||||
{{ name }}: {{ typeCounts[code] || 0 }} 次
|
||||
</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted,onBeforeUnmount,computed} from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const typeMapping = reactive({});
|
||||
const typeCounts = reactive({}); // 存储每种类型的数量
|
||||
|
||||
const filterParams = reactive({
|
||||
timeAfter: null,
|
||||
timeBefore: null,
|
||||
});
|
||||
|
||||
const pieChartData = computed(() => {
|
||||
return Object.keys(typeCounts).map(code => ({
|
||||
value: typeCounts[code] || 0,
|
||||
name: typeMapping[code]
|
||||
}));
|
||||
});
|
||||
|
||||
const renderChart = () => {
|
||||
const chartDom = document.getElementById('3d-bar-chart'); // 确保ID正确
|
||||
const chartInstance = echarts.init(chartDom);
|
||||
|
||||
const colorList = [
|
||||
// new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
// { offset: 0, color: "rgba(69,233,254,1)" },
|
||||
// { offset: 1, color: "rgba(69,233,254,0.3)" }
|
||||
// ]),
|
||||
new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
||||
{ offset: 0, color: "rgba(255,181,111,1)" },
|
||||
{ offset: 1, color: "rgba(255,181,111,0.3)" }
|
||||
]),
|
||||
new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: "rgba(101,122,250,1)" },
|
||||
{ offset: 1, color: "rgba(101,122,250,0.3)" }
|
||||
]),
|
||||
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: "rgba(45,190,146,1)" },
|
||||
{ offset: 1, color: "rgba(45,190,146,0.3)" }
|
||||
]),
|
||||
|
||||
];
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#001529',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
padding: [8, 16],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 16
|
||||
},
|
||||
formatter: function (params) {
|
||||
return `${params.marker} <span style="color:${params.color}">${params.data['name']}\n${params.data['value']}</span>`;
|
||||
}
|
||||
},
|
||||
// title: {
|
||||
// text: '告警总数',
|
||||
// subtext: `${pieChartData.value.reduce((sum, item) => sum + item.value, 0)}`,
|
||||
// top: '45%',
|
||||
// left: 'center',
|
||||
// textStyle: {
|
||||
// color: '#F5F8FC',
|
||||
// fontSize: 20,
|
||||
// fontWeight: 400
|
||||
// },
|
||||
// subtextStyle: {
|
||||
// color: '#F5F8FC',
|
||||
// fontSize: 16,
|
||||
// fontWeight: 400,
|
||||
// }
|
||||
// },
|
||||
// legend: {
|
||||
// orient: 'vertical',
|
||||
// icon: "circle",
|
||||
// left: '0%',
|
||||
// bottom: '0%',
|
||||
// itemWidth: 30,
|
||||
// itemGap: 15 ,
|
||||
// // padding:[10],
|
||||
// textStyle: {
|
||||
// rich: {
|
||||
// a: { color: '#F5F8FC', fontSize: 15, padding: [0, 10, 0, 0] },
|
||||
// b: { color: '#F5F8FC', fontSize: 15, padding: [0, 10, 0, 10] }
|
||||
// }
|
||||
// },
|
||||
// formatter: function (name) {
|
||||
// const item = pieChartData.value.find(d => d.name === name);
|
||||
// return item ? `{a| ${name}}{b|${item.value}}` : '';
|
||||
// }
|
||||
// },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['10%', '90%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
padding:[0,10],
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
formatter: '{d}%',
|
||||
textStyle: {
|
||||
align: 'center',
|
||||
fontSize: 16,
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
color: params => colorList[params.dataIndex % colorList.length]
|
||||
},
|
||||
labelLine: { show: false },
|
||||
data: pieChartData.value
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 获取告警类型并初始化每种类型的count
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
typeCounts[algorithm.code_name] = 0; // 初始化count为0
|
||||
});
|
||||
|
||||
additionalMappings.forEach((item) => {
|
||||
typeMapping[item.code_name] = item.name;
|
||||
typeCounts[item.code_name] = 0;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 统计每种告警类型的数量
|
||||
const fetchTypeCounts = async (timeAfter = null, timeBefore = null) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
|
||||
// console.log(">>>>>>>>>>>>>>>>", typeMapping)
|
||||
|
||||
// 遍历每种类型,根据code_name查询数据数量
|
||||
for (const code in typeMapping) {
|
||||
try {
|
||||
const { count } = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, code);
|
||||
typeCounts[code] = count; // 存储每种类型的数量
|
||||
} catch (error) {
|
||||
console.error(`Error fetching count for type ${code}:`, error);
|
||||
}
|
||||
}
|
||||
renderChart();
|
||||
};
|
||||
|
||||
// 点击查询按钮时,添加时间条件并重新统计
|
||||
const handleFilter = () => {
|
||||
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
|
||||
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
|
||||
fetchTypeCounts(timeAfter, timeBefore); // 重新统计数量,添加时间条件
|
||||
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const resizeChart = () => {
|
||||
const chartDom = document.getElementById('3d-bar-chart');
|
||||
if (chartDom) {
|
||||
const chartInstance = echarts.getInstanceByDom(chartDom);
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
|
||||
resizeChart();
|
||||
};
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token);
|
||||
await fetchTypeCounts(); // 初次加载时不加时间条件
|
||||
window.addEventListener('resize', handleResize);
|
||||
await renderChart();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* .statistics-container {
|
||||
margin-top: 20px;
|
||||
} */
|
||||
.alert-container{
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
/* padding: 2%; */
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
/* .filter-row{
|
||||
width: 100%;
|
||||
height: 5vh;
|
||||
} */
|
||||
.search-row{
|
||||
width: 95%;
|
||||
padding: 0;
|
||||
background-color: #001529;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: left;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
gap:1vh;
|
||||
|
||||
}
|
||||
|
||||
.start-col,.end-col,.bt-search{
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
height: 4vh;
|
||||
}
|
||||
|
||||
::v-deep .filter-buttons .el-button {
|
||||
background-color: #da681c;
|
||||
/* 查询按钮背景颜色 */
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.myPanChart{
|
||||
width: 95%;
|
||||
height: 25vh;
|
||||
}
|
||||
|
||||
::v-deep .search-row .el-input__wrapper{
|
||||
background-color: transparent;
|
||||
border-radius: 0px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.alert-bt{
|
||||
background-color: #001529;
|
||||
/* background-color: #4a4a4a; */
|
||||
border: #001529;
|
||||
width: 79px;
|
||||
height: 30px;
|
||||
margin-left: 3vh;
|
||||
color: #babcbe;
|
||||
}
|
||||
</style>
|
||||
@@ -215,6 +215,7 @@ onBeforeUnmount(() => {
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #1E2E4A;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 10vh;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
|
||||
@@ -1,237 +1,210 @@
|
||||
<template>
|
||||
<div class="alert-container">
|
||||
<el-row class="top-pan">
|
||||
<el-col :sm="24" :md="12" class="panel-section">
|
||||
<statistics />
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="12" class="panel-section">
|
||||
<alertChart />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="middle-row">
|
||||
<span>
|
||||
告警总数:{{ displayTotalItems }}
|
||||
</span>
|
||||
</el-row>
|
||||
<el-row class="table-row">
|
||||
<el-col :span="24">
|
||||
<div class="table-container">
|
||||
<el-table :data="tableData" style="width:100%; height: 100%;" header-row-class-name="table-header" :fit="true">
|
||||
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
|
||||
<el-table-column label="告警类型" min-width="150">
|
||||
<template v-slot="scope">
|
||||
{{ typeMapping[scope.row.types] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
||||
<el-table-column label="告警时间" min-width="200">
|
||||
<template v-slot="scope">
|
||||
{{ formatDateTime(scope.row.ended_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="告警状态" min-width="100">
|
||||
<template v-slot="scope">
|
||||
<el-tag :type="scope.row.status === 'pending' ? 'warning' : 'success'">{{ statusMapping[scope.row.status]
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="100">
|
||||
<template v-slot="scope">
|
||||
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pagination-container">
|
||||
<el-pagination @size-change="handleSizeChange" @current-change="handlePageChange" :current-page="currentPage"
|
||||
:page-size="pageSize" :total="totalItems" layout="total, sizes, prev, pager, next, jumper">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog v-model="dialogVisible" title="告警详情" width="80%">
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
<p>告警编号: {{ selectedRow.id }}</p>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>告警类型: {{ typeMapping[selectedRow.types] }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>告警位置: {{ selectedRow.camera.name }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>告警时间: {{ selectedRow.formatted_started_at }}</p>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="6">
|
||||
<p>告警状态: <el-tag :type="selectedRow.status === 'pending' ? 'warning' : 'success'">{{
|
||||
statusMapping[selectedRow.status]
|
||||
}}</el-tag></p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>摄像头编号: {{ selectedRow.camera_id }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>持续时间: {{ duration }}</p>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<p>备注: {{ selectedRow.note }}</p>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- <div class="event-media">
|
||||
<div v-for="medium in selectedRow.mediums" :key="medium.id" class="media-container">
|
||||
<div v-if="medium.name === 'video'" class="media-item video-item">
|
||||
<p>告警关联视频</p>
|
||||
<video :src="medium.file" controls></video>
|
||||
</div>
|
||||
<div v-if="medium.name === 'snapshot'" class="media-item image-item">
|
||||
<p>告警关联图片</p>
|
||||
<el-image :src="medium.file" fit="contain"></el-image>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="event-media" :class="{ 'center-media': mediums.length === 1 }">
|
||||
<!-- 告警关联视频 -->
|
||||
<div v-if="hasVideo" class="media-container">
|
||||
<p>告警关联视频</p>
|
||||
<video :src="videoFile" controls></video>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 告警关联图片 -->
|
||||
<div v-if="hasSnapshot" class="media-container">
|
||||
<p>告警关联图片</p>
|
||||
<el-image :src="snapshotFile" fit="contain"></el-image>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendintingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<div class=""></div>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import Statistics from '@/components/Statistics.vue';
|
||||
import AlertChart from '@/components/AlertChart.vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts'; // 导入 BoxApi
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import CameraAll from '@/icons/CameraAll.vue';
|
||||
import CameraOnline from '@/icons/CameraOnline.vue';
|
||||
import CameraOffline from '@/icons/CameraOffline.vue';
|
||||
import EventAll from '@/icons/EventAll.vue';
|
||||
import EventClosed from '@/icons/EventClosed.vue';
|
||||
import EventPending from '@/icons/EventPending.vue';
|
||||
|
||||
// 创建 BoxApi 实例
|
||||
const boxApi = new BoxApi();
|
||||
const apiInstance = new BoxApi();
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendintingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
// 响应式数据
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const selectedRow = ref({});
|
||||
const mediums = ref([]);
|
||||
const duration = ref('');
|
||||
const typeMapping = reactive({});
|
||||
const statusMapping = {
|
||||
'pending': '待处理',
|
||||
'closed': '已处理'
|
||||
const activeTab = ref('all');
|
||||
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const token = ref(null);
|
||||
const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0); // 用于展示的数字
|
||||
|
||||
// 计算属性:根据 mediums 动态判断是否存在视频和图片
|
||||
const hasVideo = computed(() => mediums.value.some(medium => medium.name === 'video'));
|
||||
const hasSnapshot = computed(() => mediums.value.some(medium => medium.name === 'snapshot'));
|
||||
|
||||
// 获取视频和图片的文件
|
||||
const videoFile = computed(() => {
|
||||
const video = mediums.value.find(medium => medium.name === 'video');
|
||||
return video ? video.file : '';
|
||||
});
|
||||
|
||||
const snapshotFile = computed(() => {
|
||||
const snapshot = mediums.value.find(medium => medium.name === 'snapshot');
|
||||
return snapshot ? snapshot.file : '';
|
||||
});
|
||||
|
||||
// 获取类型映射
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token); // 使用 BoxApi 的 getAlgorithms 方法
|
||||
const additionalMappings = [{ code_name: 'minizawu:532', name: '杂物堆积' }];
|
||||
algorithms.forEach((algorithm) => {
|
||||
typeMapping[algorithm.code_name] = algorithm.name;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching algorithms:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取告警数据
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
|
||||
tableData.value = data;
|
||||
totalItems.value = total;
|
||||
animateNumberChange(total);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const animateNumberChange = (newTotal) => {
|
||||
const stepTime = 50; // 每次递增的时间间隔
|
||||
// const stepCount = Math.abs(newTotal - displayTotalItems.value);
|
||||
const stepCount = Math.abs(newTotal - displayTotalItems.value) / 20;
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
const incrementStep = Math.ceil((newTotal - displayTotalItems.value) / stepCount); // 每次递增的值
|
||||
|
||||
const step = () => {
|
||||
if (displayTotalItems.value < newTotal) {
|
||||
displayTotalItems.value = Math.min(displayTotalItems.value + incrementStep, newTotal);
|
||||
requestAnimationFrame(step);
|
||||
} else if (displayTotalItems.value > newTotal) {
|
||||
displayTotalItems.value = Math.max(displayTotalItems.value - incrementStep, newTotal);
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
step();
|
||||
};
|
||||
|
||||
// 查看告警详情
|
||||
const handleView = (row) => {
|
||||
selectedRow.value = row;
|
||||
duration.value = calculateDuration(row.started_at, row.ended_at);
|
||||
row.formatted_started_at = formatDateTime(row.started_at);
|
||||
dialogVisible.value = true;
|
||||
mediums.value = row.mediums || [];
|
||||
};
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page) => {
|
||||
currentPage.value = page;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// 页大小处理
|
||||
const handleSizeChange = (size) => {
|
||||
pageSize.value = size;
|
||||
fetchEvents();
|
||||
};
|
||||
|
||||
// 计算持续时间
|
||||
const calculateDuration = (started_at, ended_at) => {
|
||||
const start = new Date(started_at);
|
||||
const end = new Date(ended_at);
|
||||
const durationMs = end - start;
|
||||
const minutes = Math.floor(durationMs / 60000);
|
||||
const seconds = ((durationMs % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}分${seconds < 10 ? '0' : ''}${seconds}秒`;
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
@@ -243,113 +216,150 @@
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 页面加载后执行
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
await fetchEvents();
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendintingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alert-container {
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.top-pan {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 20px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 15px;
|
||||
transform: scale(0.8);
|
||||
transform-origin: center center;
|
||||
.tab-div{
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
.middle-row {
|
||||
min-height: 5vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(to bottom left, rgb(150, 151, 243), rgb(215, 214, 250));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
width: 100%;
|
||||
height: 650px;
|
||||
min-height: 60%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
background-color: #f7f8fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
::v-deep .el-table th.el-table__cell {
|
||||
background-color: #000;
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.event-media {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
.center-media {
|
||||
justify-content: center;
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.media-container {
|
||||
flex: 0 0 48%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.video-item video,
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.video-item p,
|
||||
.image-item p {
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #e8e9e4;
|
||||
}
|
||||
|
||||
::v-deep .pagination-container .el-pagination__total,
|
||||
::v-deep .pagination-container .el-pagination__goto,
|
||||
::v-deep .pagination-container .el-pagination__classifier {
|
||||
color: #000;
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<dv-decoration-11 class="custom-decoration" style="width:25vw;height:70px;">
|
||||
<div color-green font-700 bg="~ dark/0">
|
||||
<dv-decoration7 style="width:20vw;height:30px;">
|
||||
<div color-white font-300>
|
||||
本地告警大屏
|
||||
<div color-white font-400 >
|
||||
告警数据面板
|
||||
</div>
|
||||
</dv-decoration7>
|
||||
</div>
|
||||
@@ -57,32 +57,43 @@
|
||||
<dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left">
|
||||
<LeftTop />
|
||||
</dv-border-box-13>
|
||||
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left">点位告警数量(不同点位的数量)</dv-border-box-13>
|
||||
<dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表(告警详情)</dv-border-box-13>
|
||||
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left">不同点位告警的数量
|
||||
<LeftMiddle/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表(告警详情)
|
||||
|
||||
</dv-border-box-13> -->
|
||||
</div>
|
||||
|
||||
<!-- 中部区域 -->
|
||||
<div class="center-section">
|
||||
<dv-border-box8 class="center-top">
|
||||
<dv-border-box8 class="center-top-header">警戒画面</dv-border-box8>
|
||||
<CenterTop/>
|
||||
<!-- <dv-border-box8 class="center-top-header">警戒画面</dv-border-box8>
|
||||
<div class="center-top-grids">
|
||||
<div class="grid-item">栅格左上</div>
|
||||
<div class="grid-item">栅格右上</div>
|
||||
<div class="grid-item">栅格左下</div>
|
||||
<div class="grid-item">栅格右下</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</dv-border-box8>
|
||||
<div class="center-bottom">
|
||||
<!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="center-bottom-right">警戒点位列表</dv-border-box-13>
|
||||
<dv-border-box-13 class="center-bottom-right">告警数量分布情况
|
||||
<CenterBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="right-section">
|
||||
<dv-border-box-13 class="section top-right corner-style">告警类型概览</dv-border-box-13>
|
||||
<dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13>
|
||||
<dv-border-box-13 class="section bottom-right corner-style">告警种类划分</dv-border-box-13>
|
||||
<dv-border-box-13 class="section top-right corner-style">时间段告警总数分布
|
||||
<RightTop/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="section bottom-right corner-style">告警种类划分
|
||||
<LeftBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +106,11 @@ import { ref, onMounted, onBeforeUnmount, nextTick, computed } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
import LeftBottom from '@/components/Max/LeftBottom.vue';
|
||||
import LeftMiddle from '@/components/Max/LeftMiddle.vue';
|
||||
import RightTop from '@/components/Max/RightTop.vue';
|
||||
import CenterBottom from '@/components/Max/CenterBottom.vue';
|
||||
import CenterTop from '@/components/Max/CenterTop.vue';
|
||||
// import '/src/assets/viewListStyle.css'
|
||||
|
||||
</script>
|
||||
@@ -106,38 +122,41 @@ import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 4vh 10vw 10vh 1vw;
|
||||
padding: 4vh 10vw 10vh 7vw;
|
||||
/* background-color: rgb(121, 184, 243); */
|
||||
background-color: #001529;
|
||||
/* background-image: url('/bg05.png'); */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background: radial-gradient(circle, rgb(24, 64, 197), rgb(0, 7, 60));
|
||||
/* background: radial-gradient(circle, rgb(24, 64, 197), rgb(0, 7, 60)); */
|
||||
position: relative;
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* .custom-decoration{
|
||||
color:white;
|
||||
} */
|
||||
.custom-decoration{
|
||||
color: #70e5fa;;
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
|
||||
.background-overlay {
|
||||
/* .background-overlay {
|
||||
position: absolute;
|
||||
/* 绝对定位 */
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* background-image: url('/bg01.png'); */
|
||||
background-image: url('/bg01.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0.8;
|
||||
z-index: 0;
|
||||
|
||||
}
|
||||
} */
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
@@ -206,11 +225,11 @@ import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
|
||||
}
|
||||
|
||||
.top-left,
|
||||
.middle-left,
|
||||
|
||||
|
||||
.bottom-left,
|
||||
.top-right,
|
||||
.middle-right,
|
||||
/* .middle-right, */
|
||||
.bottom-right {
|
||||
color: white;
|
||||
text-align: center;
|
||||
@@ -225,13 +244,39 @@ import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
position: relative;
|
||||
padding: 1vh 1vw;
|
||||
display: flex;
|
||||
justify-content: center; /* 水平居中 */
|
||||
align-items: center; /* 垂直居中 */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
width: 20vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
.middle-left{
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.top-right{
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
/* padding: 1.5vh; */
|
||||
}
|
||||
|
||||
.bottom-left{
|
||||
/* color: white; */
|
||||
/* position: relative; */
|
||||
padding-left: 0.5vw;
|
||||
display: flex;
|
||||
/* justify-content: center; */
|
||||
/* align-items: center; */
|
||||
/* width: 20vw; */
|
||||
/* height: 20vh; */
|
||||
}
|
||||
|
||||
/* .top-left, .top-right {
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
@@ -257,17 +302,14 @@ import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.center-top-header {
|
||||
/* .center-top-header {
|
||||
height: 3vh;
|
||||
width: 35vw;
|
||||
text-align: center;
|
||||
line-height: 3vh;
|
||||
margin-bottom: 1vh;
|
||||
/* background-color: rgba(0, 51, 102, 0.8); */
|
||||
border-radius: 3px;
|
||||
/* 圆角 */
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
|
||||
/* 添加阴影 */
|
||||
}
|
||||
|
||||
.center-top-grids {
|
||||
@@ -275,16 +317,15 @@ import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
grid-template-columns: 17vw 17vw;
|
||||
gap: 2vh 1vw;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: 17vw;
|
||||
height: 20vh;
|
||||
/* background-color: #777; */
|
||||
background-color: #777;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
} */
|
||||
|
||||
.center-bottom {
|
||||
display: flex;
|
||||
|
||||
28
src/icons/CameraAll.vue
Normal file
28
src/icons/CameraAll.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="camera-all-icon" viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet">
|
||||
<path fill="currentColor"
|
||||
d="M12 17.5q1.875 0 3.188-1.312T16.5 13t-1.312-3.187T12 8.5T8.813 9.813T7.5 13t1.313 3.188T12 17.5m0-2q-1.05 0-1.775-.725T9.5 13t.725-1.775T12 10.5t1.775.725T14.5 13t-.725 1.775T12 15.5M4 21q-.825 0-1.412-.587T2 19V7q0-.825.588-1.412T4 5h3.15L8.4 3.65q.275-.3.663-.475T9.875 3h4.25q.425 0 .813.175t.662.475L16.85 5H20q.825 0 1.413.588T22 7v12q0 .825-.587 1.413T20 21z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.camera-all-icon {
|
||||
width: 2rem; /* 控制图标的宽度 */
|
||||
height: 2rem; /* 控制图标的高度 */
|
||||
fill: #29099c;
|
||||
opacity: 0.8;
|
||||
color: rgb(40, 222, 235);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
30
src/icons/CameraOffline.vue
Normal file
30
src/icons/CameraOffline.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 24 24" class="camera-offline-icon">
|
||||
<path fill="currentColor"
|
||||
d="m22 17.5l-4-4v1.675L6.825 4H16q.825 0 1.413.588T18 6v4.5l4-4zm-1.45 5.85L.65 3.45l1.4-1.4l19.9 19.9zM4 4l14 14q0 .825-.587 1.413T16 20H4q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.camera-offline-icon {
|
||||
width: 2rem;
|
||||
/* 控制图标的宽度 */
|
||||
height: 2rem;
|
||||
/* 控制图标的高度 */
|
||||
fill: #29099c;
|
||||
opacity: 0.8;
|
||||
color: rgb(40, 222, 235);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
30
src/icons/CameraOnline.vue
Normal file
30
src/icons/CameraOnline.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 24 24" class="camera-online-icon">
|
||||
<path fill="currentColor"
|
||||
d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h12q.825 0 1.413.588T18 6v4.5l4-4v11l-4-4V18q0 .825-.587 1.413T16 20z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.camera-online-icon {
|
||||
width: 2rem;
|
||||
/* 控制图标的宽度 */
|
||||
height: 2rem;
|
||||
/* 控制图标的高度 */
|
||||
fill: #29099c;
|
||||
opacity: 0.8;
|
||||
color: rgb(40, 222, 235);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
29
src/icons/EventAll.vue
Normal file
29
src/icons/EventAll.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 24 24" class="event-all-icon">
|
||||
<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.2q.325-.9 1.088-1.45T12 1t1.713.55T14.8 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm3-4h5q.425 0 .713-.288T14 16t-.288-.712T13 15H8q-.425 0-.712.288T7 16t.288.713T8 17m0-4h8q.425 0 .713-.288T17 12t-.288-.712T16 11H8q-.425 0-.712.288T7 12t.288.713T8 13m0-4h8q.425 0 .713-.288T17 8t-.288-.712T16 7H8q-.425 0-.712.288T7 8t.288.713T8 9m4-4.75q.325 0 .538-.213t.212-.537t-.213-.537T12 2.75t-.537.213t-.213.537t.213.538t.537.212" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.event-all-icon {
|
||||
width: 2rem;
|
||||
/* 控制图标的宽度 */
|
||||
height: 2rem;
|
||||
/* 控制图标的高度 */
|
||||
fill: #29099c;
|
||||
opacity: 0.8;
|
||||
color: rgb(40, 222, 235);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
29
src/icons/EventClosed.vue
Normal file
29
src/icons/EventClosed.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 24 24" class="event-closed-icon">
|
||||
<path fill="currentColor" d="M10.6 16.05L17.65 9l-1.4-1.4l-5.65 5.65l-2.85-2.85l-1.4 1.4zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.2q.325-.9 1.088-1.45T12 1t1.713.55T14.8 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm7-16.75q.325 0 .538-.213t.212-.537t-.213-.537T12 2.75t-.537.213t-.213.537t.213.538t.537.212" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.event-closed-icon {
|
||||
width: 2rem;
|
||||
/* 控制图标的宽度 */
|
||||
height: 2rem;
|
||||
/* 控制图标的高度 */
|
||||
fill: #29099c;
|
||||
opacity: 0.8;
|
||||
color: rgb(40, 222, 235);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
29
src/icons/EventPending.vue
Normal file
29
src/icons/EventPending.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 24 24" class="event-pending-icon">
|
||||
<path fill="currentColor"
|
||||
d="M12 17q.425 0 .713-.288T13 16t-.288-.712T12 15t-.712.288T11 16t.288.713T12 17m-1-4h2V7h-2zm-6 8q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h4.2q.325-.9 1.088-1.45T12 1t1.713.55T14.8 3H19q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm7-16.75q.325 0 .538-.213t.212-.537t-.213-.537T12 2.75t-.537.213t-.213.537t.213.538t.537.212" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.event-pending-icon {
|
||||
width: 2rem;
|
||||
/* 控制图标的宽度 */
|
||||
height: 2rem;
|
||||
/* 控制图标的高度 */
|
||||
fill: #29099c;
|
||||
opacity: 0.8;
|
||||
color: rgb(40, 222, 235);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
35
src/main.ts
35
src/main.ts
@@ -1,4 +1,4 @@
|
||||
import {createApp} from 'vue'
|
||||
import { createApp,ref, onBeforeUnmount} from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router';
|
||||
import ElementPlus from 'element-plus';
|
||||
@@ -41,4 +41,37 @@ router.beforeEach((to, from, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 定义转换函数
|
||||
app.config.globalProperties.xToh = (px: number): string => {
|
||||
const vh = (px / window.innerHeight) * 100;
|
||||
return `${vh}vh`;
|
||||
};
|
||||
|
||||
app.config.globalProperties.xTow = (px: number): string => {
|
||||
const vw = (px / window.innerWidth) * 100;
|
||||
return `${vw}vw`;
|
||||
};
|
||||
|
||||
app.config.globalProperties.vhToPx = (vh: number): number => {
|
||||
return (vh / 100) * window.innerHeight;
|
||||
};
|
||||
|
||||
app.config.globalProperties.vwToPx = (vw: number): number => {
|
||||
return (vw / 100) * window.innerWidth;
|
||||
};
|
||||
|
||||
// 响应式处理
|
||||
const updateDimensions = () => {
|
||||
app.config.globalProperties.windowHeight = window.innerHeight;
|
||||
app.config.globalProperties.windowWidth = window.innerWidth;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
updateDimensions(); // 初始化
|
||||
|
||||
// 清理事件监听器
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateDimensions);
|
||||
});
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -320,7 +320,8 @@ class BoxApi {
|
||||
timeBefore: string | null = null,
|
||||
timeAfter: string | null = null,
|
||||
types: string | null = null,
|
||||
camera_id: number | null = null
|
||||
camera_id: number | null = null,
|
||||
status?: 'pending' | 'closed' | null
|
||||
): Promise<any> {
|
||||
// 计算 offset
|
||||
const offset = (currentPage - 1) * pageSize;
|
||||
@@ -340,6 +341,9 @@ class BoxApi {
|
||||
if(camera_id){
|
||||
url += `&camera_id=${camera_id}`;
|
||||
}
|
||||
if (status) {
|
||||
url += `&status=${encodeURIComponent(status)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送 GET 请求
|
||||
|
||||
@@ -9,7 +9,7 @@ const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.
|
||||
|
||||
// 连接 WebSocket
|
||||
const connectWebSocket = () => {
|
||||
websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/event/ws`);
|
||||
websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/ws/event`);
|
||||
websocket.value.onopen = () => {
|
||||
ElMessage.success('全局 WebSocket 连接成功');
|
||||
isWebSocketConnected.value = true;
|
||||
|
||||
Reference in New Issue
Block a user