告警大屏完成

This commit is contained in:
龚皓
2024-11-11 15:37:46 +08:00
parent f456cdaa8c
commit a2234922c8
30 changed files with 5551 additions and 440 deletions

View File

@@ -267,3 +267,6 @@ export default router;
![image-20241031164625119](https://gitee.com/gonghao_git/draw-bed/raw/master/img/%E5%A4%A7%E5%B1%8F%E9%A1%B5%E9%9D%A2%E5%88%9D%E7%A8%BF-20241031164625119.png)

220
SScode.md Normal file
View 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
View File

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

View File

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

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

View 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> &nbsp;名称: {{ 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>

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

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

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

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

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

View File

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

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

View 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> &nbsp;名称: {{ 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>

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

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

View File

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

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

View File

@@ -215,6 +215,7 @@ onBeforeUnmount(() => {
box-sizing: border-box;
border-right: 1px solid #1E2E4A;
padding-right: 10px;
padding-bottom: 10vh;
}
.search-input {

View File

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

View File

@@ -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>
&nbsp;本地告警大屏&nbsp;
<div color-white font-400 >
&nbsp;告警数据面板&nbsp;
</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
View 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>

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

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

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

View File

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

View File

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

View File

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