This commit is contained in:
龚皓 2024-11-22 18:02:19 +08:00
parent 9c5122f143
commit fa85515d9b
17 changed files with 1668 additions and 1163 deletions

372
README.md
View File

@ -267,6 +267,378 @@ 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)
## 告警规则设置
### 界面接口列表
- 获取所有摄像头信息 getAllCamera:/camera/cameras/get_all
- 通道流开启startCameraStream/camera/cameras/start_stream
- 通道流关闭stopCameraStream/camera/cameras/stop_stream
- 摄像实例
```
const cameraId = 1; // 示例摄像头 ID
const token = "your-auth-token";
const cameraJson = {
"id": 1,
"name": "421枪机",
"uri": "rtsp://admin:1234qwer@192.168.28.102:554/Streaming/Channels/202",
"mode": "on",
"status": "online",
"detect_params": { "threshold": 0.5 },
"default_params": {},
"should_push": false,
"config_params": {},
"sampling": false,
"note": {},
"snapshot": "http://127.0.0.1:8000/media/cameras/1/snapshot.jpg",
"remote_id": -1,
"raw_address": "0b8342ec52e62d01dd3273f583d326ec",
"ip": "admin:1234qwer@192.168.28.102:554",
"port": 8082,
"rules": [] // 包含的 rules 会被排除
};
await apiInstance.updateCamera(token, cameraId, cameraJson);
```
- 规则实例
```
const cameraId = 1; // 示例摄像头 ID
const token = "your-auth-token";
const cameraJson = {
"id": 1,
"name": "421枪机",
"uri": "rtsp://admin:1234qwer@192.168.28.102:554/Streaming/Channels/202",
"mode": "on",
"status": "online",
"detect_params": { "threshold": 0.5 },
"default_params": {},
"should_push": false,
"config_params": {},
"sampling": false,
"note": {},
"snapshot": "http://127.0.0.1:8000/media/cameras/1/snapshot.jpg",
"remote_id": -1,
"raw_address": "0b8342ec52e62d01dd3273f583d326ec",
"ip": "admin:1234qwer@192.168.28.102:554",
"port": 8082,
"rules": [] // 包含的 rules 会被排除
};
await apiInstance.updateCamera(token, cameraId, cameraJson);
```
```
单条camera实例部分内容
{
"id": 1,
"name": "421枪机",
"mode": "on",
"rules": [{规则1json数据},{规则2json数据}]
}
单条rule实例部分
{
"id": 3,
"camera": 1,
"name": "人员逗留",
"mode": "schedule",
"algo": "personnel_staying",
"params": {},
"params_base": "",
"unique_id": "7ce99a51-0ee4-4251-a754-bd897d671303",
"event_types": {
"personnel_staying": "人员逗留"
},
"schedule": {
"type": "weekly",
"time_slots": [
[
5,
1415
]
],
"week_day": "Monday"
}
}
```
> 表单数据显示摄像idnamemode对应的规则显示对应的id,name,mode,schedule
>
> 其中摄像mode有ON和OFF两种状态规则中mode有On,OFF和schedule三中状态规则模式只有在schedule状态才会显示schedule中内容type
>
> schedule中type又分为每日daily和每周weekly两周状态其中dayily直接设置当日时间范围time_slotsweekly可选择周几week_day和对应当天的当日时间范围time_slots
>
> time_slots为两个数字对应当天开始时间和结束时间计算交互方式为选择时间点
>
> 几点几分然后计算这个时间总计有多少分钟例如1点10分对应数字为70
- 结果返回信息修改(用户提示信息)
```
public async updateRule(token: string | null = null, rules: RuleData[]): Promise<any[]> {
const results: any[] = [];
for (const rule of rules) {
const url = `${this.superRule}/${rule.id}`;
try {
const res = await this.axios.patch(url, rule, this._authHeader(token));
if (res.data.err.ec === 0) {
results.push({
success: true,
ruleId: rule.id,
data: res.data.ret,
});
} else {
results.push({
success: false,
ruleId: rule.id,
error: res.data.err.dm,
});
}
} catch (error: any) {
results.push({
success: false,
ruleId: rule.id,
error: error.message || '未知错误',
});
}
}
return results;
}
```
- 时间段设置
> 添加时间段schedule的设置组件显示开始时间点和结束时间数据格式化方式为将代表累计分钟的数字转换为时间点每60分钟为1小时数字范围在0到1440实例[56,65]代表0:56到1:05组件显示将数字组转换为时间段请求时将组件时间段显示为数字组。
>
> 时间段结束时间点不能大于开始时间点,多时间段添加不能有重合部分。
>
> 设置规则只有在rule.mode为schedule时才可设置时间段携带参数日期类型和多个时间段数字组合在未保存切换rule。mode模式时间段不会因为隐藏组件而清空数值始终显示开始设定好的默认值当提交时处于时间段模式至少得设置一个时间段数组否则无法保存若处在schedule情况下保存携带参数请求若rule.mode处于on或者off,代表没有时间段设置那么schedule下的所有参数及时原来有值也需要伴随请求清空默认值。
> 重新修改这段CameraRule.vue代码 1.在rule.mode为schedule模式添加时间段time_slots的组件添加时间段类型输入框值默认为daily不可修改 2.开始时间和结束时间的组件,时间通过分钟数转换为格式化的时间(小时:分钟有删除按钮可删除临时的时间段设置使用数字范围0-1440单个时间点代表0点到设置的时间点累计的分钟数字 3.时间段的校验,结束时间不能小于开始时间,多时间段之间不能有重叠。 4.如果 rule.mode 为 schedule则必须包含至少一个时间段如果 rule.mode 是 on 或 off清空所有时间段和时间段类型
- 静态数据实例
```
"camera": {
"id": 1,
"name": "421枪机",
"mode": "on",
"status": "online",
"rules": [
{
"id": 1,
"camera": 1,
"name": "入侵test2",
"mode": "schedule",
"algo": "intrude",
"schedule": {
"type": "daily",
"time_slots": [
[
118,
898
],
[
0,
60
]
]
},
},
{
"id": 3,
"camera": 1,
"name": "人员逗留",
"mode": "schedule",
"algo": "personnel_staying",
"schedule": {
"type": "daily",
"time_slots": [
[
180,
420
]
]
},
}
],
"snapshot": "http://192.168.28.33:8000/media/cameras/1/snapshot.jpg",
}
```
```
模拟静态数据实例,
"camera": {
"id": 1,
"name": "421枪机",
"mode": "on",
"status": "online",
"rules": [
{
"id": 1,
"camera": 1,
"name": "入侵test2",
"mode": "schedule",
"algo": "intrude",
"schedule": {
"type": "daily",
"time_slots": [
[
118,
898
],
[
0,
60
]
]
},
},
{
"id": 3,
"camera": 1,
"name": "人员逗留",
"mode": "schedule",
"algo": "personnel_staying",
"schedule": {
"type": "daily",
"time_slots": [
[
180,
420
]
]
},
}
],
"snapshot": "http://192.168.28.33:8000/media/cameras/1/snapshot.jpg",
}
读取显示rules中多条数据每条rule中rule.mode对应el-radio三种状态在rule.model为schedule时
```
## 日志
### 2024.11.21
- 本地打包
- 服务器页面备份
- 服务器迭代,覆盖
- 版本提交云端
- 更新内容
- 1.弹窗仅图片显示AlertManagementLeftBottomApp
- 2.通道点播Setting,Channel
- 摄像列表
- 搜索/在线状态过滤
- 3.通道规则设置(Channel,CameraRules)
- 规则开关
- 时间设置
- 下阶段
- 1.弹窗模式按钮
- 交互式开关(已有,点击响应)
- 响应式开关(###
- 2.光影效果
- 声音联动(提示音)
- 交互(首页)
- 3.用户设定不可更改项
- 测试调整(遗留代码冗余调整)
### 2024.11.22
- 弹窗模式开关
- 交互式开关(点击触发弹窗)
- 响应式开关(触发对话框)
- 弹窗现实
- 格式化类型
- 格式化时间
- 现实时间
- 弹窗布局调整
- 控制台日志处理(部分)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -35,6 +35,7 @@ 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));
@ -135,7 +136,7 @@ const updateCounts = async (range) => {
timeAfter = dayjs().startOf('day').add(i, 'hour').format();
timeBefore = dayjs().startOf('day').add(i + 1, 'hour').format();
hourlyCounts.value[i] = await getEventCount(timeBefore, timeAfter);
console.log(`Hour ${i}: ${hourlyCounts.value[i]}`);
// console.log(`Hour ${i}: ${hourlyCounts.value[i]}`);
}
delayedInitChart(todayLineChartDom, createChartOption('今日告警趋势', Array.from({ length: 24 }, (_, i) => `${i}:00`), hourlyCounts.value));

View File

@ -3,7 +3,7 @@
<!-- 侧边栏 -->
<el-aside v-if="!isFullScreen" class="nav-sidebar" :style="{ width: isCollapse ? '64px' : '180px' }">
<div class="logo" v-if="!isCollapse">
<el-image src="/turing.png" fit="contain" />
<el-image src="/xlogo.png" fit="contain" />
</div>
<el-menu :default-active="activeIndex" class="el-menu-part" router :collapse="isCollapse">
<el-menu-item index="/">
@ -36,13 +36,13 @@
</el-icon>
<template #title><span>告警设置</span></template>
</el-menu-item>
<el-menu-item index="/viewList">
<!-- <el-menu-item index="/viewList">
<el-icon>
<Location />
</el-icon>
<template #title><span>大屏页</span></template>
</el-menu-item>
<el-sub-menu index="1">
</el-menu-item> -->
<!-- <el-sub-menu index="1">
<template #title>
<el-icon><Location /></el-icon>
<span>面板测试</span>
@ -50,20 +50,20 @@
<el-menu-item index="/test">
<el-icon><WarningFilled /></el-icon>
<template #title><span>布局备份</span></template>
</el-menu-item>
</el-menu-item> -->
<!-- <el-menu-item index="/alertChart">
<el-icon><WarningFilled /></el-icon>
<template #title><span>功能点1测试</span></template>
</el-menu-item>
<el-menu-item index="/statistics">
</el-menu-item> -->
<!-- <el-menu-item index="/statistics">
<el-icon><Document /></el-icon>
<template #title><span>功能点2测试</span></template>
</el-menu-item>
<el-menu-item index="/cameras">
</el-menu-item> -->
<!-- <el-menu-item index="/cameras">
<el-icon><Document /></el-icon>
<template #title><span>功能点3测试</span></template>
</el-menu-item> -->
</el-sub-menu>
<!-- </el-sub-menu> -->
</el-menu>
</el-aside>

View File

@ -136,7 +136,7 @@
timeAfter = dayjs().startOf('day').add(i, 'hour').format();
timeBefore = dayjs().startOf('day').add(i + 1, 'hour').format();
hourlyCounts.value[i] = await getEventCount(timeBefore, timeAfter);
console.log(`Hour ${i}: ${hourlyCounts.value[i]}`);
// console.log(`Hour ${i}: ${hourlyCounts.value[i]}`);
}
delayedInitChart(todayLineChartDom, createChartOption('今日告警趋势', Array.from({ length: 24 }, (_, i) => `${i}:00`), hourlyCounts.value));

View File

@ -1,14 +1,14 @@
<template>
<div class="camera-container">
<div class="top-header">
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter" >
<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" >
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>
@ -49,30 +49,30 @@
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
</el-dialog>
</div>
</template>
</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';
<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); //
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");
// canvas
const dialogCanvas = ref(null);
const filterStatus = ref("all");
const fetchCameras = async () => {
const fetchCameras = async () => {
try {
const token = localStorage.getItem('alertToken');
const cameraData = await apiInstance.getMinCameras(token);
const cameraData = await apiInstance.getAllCameras(token);
// filterStatus
if (filterStatus.value === "online") {
@ -85,25 +85,25 @@
} catch (error) {
console.error('获取摄像头列表失败:', error);
}
};
};
const selectCameraById = (cameraId) => {
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) => {
//
const openDialog = async (camera) => {
currentCamera.value = camera;
dialogVisible.value = true;
await nextTick();
startStreamInDialog(camera);
};
};
//
const startStreamInDialog = async (camera) => {
//
const startStreamInDialog = async (camera) => {
const canvas = dialogCanvas.value;
if (!camera || !canvas) {
@ -112,13 +112,21 @@
}
const token = localStorage.getItem('alertToken');
const rememberedAddress = localStorage.getItem('rememberedAddress');
if (!rememberedAddress) {
alert('主机地址获取异常,请确认地址配置');
console.error('主机地址获取异常');
return;
}
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);
const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
// const url = `ws://192.168.28.33:${camera.streamPort}/`;
// console.log(':', url);
if (window.JSMpeg) {
const player = new window.JSMpeg.Player(url, {
@ -134,18 +142,18 @@
} catch (error) {
console.error('启动视频流失败:', error);
}
};
};
//
const closeDialog = () => {
//
const closeDialog = () => {
if (currentCamera.value) {
handleStopStream(currentCamera.value);
}
dialogVisible.value = false;
currentCamera.value = null;
};
};
const handleStopStream = async (camera) => {
const handleStopStream = async (camera) => {
const token = localStorage.getItem('alertToken');
try {
await apiInstance.stopCameraStream(token, camera.id);
@ -158,40 +166,40 @@
} catch (error) {
console.error('停止视频流失败:', error);
}
};
};
const closeStream = (camera) => {
const closeStream = (camera) => {
handleStopStream(camera);
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
};
};
onMounted(() => {
onMounted(() => {
fetchCameras();
});
});
onBeforeUnmount(() => {
onBeforeUnmount(() => {
selectedCameras.value.forEach(camera => {
if (camera.player) {
camera.player.destroy();
}
});
});
</script>
});
</script>
<style scoped>
.camera-container {
<style scoped>
.camera-container {
display: flex;
flex-direction: column;
}
}
.top-header {
.top-header {
width: 100%;
display: flex;
flex-direction: row;
margin: 0;
}
}
.status-filter {
.status-filter {
position: relative;
top: 0vh;
left: 0vw;
@ -199,40 +207,41 @@
margin-left: 1vh;
margin-top: 1vh;
}
::v-deep .status-filter .el-select__wrapper{
}
::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{
::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 {
::v-deep .camera-select .el-select__selected-item {
color: #ffffff !important;
}
}
::v-deep .status-filter .el-select__selected-item {
::v-deep .status-filter .el-select__selected-item {
color: #ffffff !important;
}
}
.camera-select {
.camera-select {
position: relative;
top: 0vh;
left: 0vw;
width: 12vw;
margin-left: 0vh;
margin-top: 1vh;
}
}
.top-text {
.top-text {
display: block;
font-size: 15px;
width: 7vw;
@ -244,24 +253,24 @@
line-height: 0px;
color: aliceblue;
font-weight: bold;
}
}
.stream-control {
.stream-control {
display: flex;
align-items: center;
text-align: center;
background-color: black;
}
}
.camera-name-title {
.camera-name-title {
padding: 0.2vh;
margin: 0;
font-size: 13px;
display: block;
color: white;
}
}
.camera-grid {
.camera-grid {
margin: 0vh 1vh;
padding: 0 0 2vh 0;
display: grid;
@ -273,28 +282,28 @@
overflow-y: scroll;
scrollbar-width: none;
background-color: #001529;
}
}
.camera-grid::-webkit-scrollbar {
.camera-grid::-webkit-scrollbar {
display: none;
}
}
.grid-item {
.grid-item {
margin: 1vh;
position: relative;
height: 17vh;
display: flex;
flex-direction: column;
}
}
.camera-snapshot,
.camera-large {
.camera-snapshot,
.camera-large {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.close-button {
.close-button {
position: absolute;
top: 1px;
right: 1px;
@ -310,22 +319,22 @@
font-size: 13px;
cursor: pointer;
transition: background-color 0.2s ease;
}
}
.close-button:hover {
.close-button:hover {
background-color: red;
}
}
.play-button-container {
.play-button-container {
position: relative;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
.play-button {
.play-button {
position: absolute;
top: 50%;
left: 50%;
@ -333,15 +342,15 @@
z-index: 10;
opacity: 0.4;
transition: opacity 0.2s ease-in-out;
}
}
.play-button:hover {
.play-button:hover {
opacity: 1;
}
}
.dialog-canvas {
.dialog-canvas {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -59,20 +59,17 @@
<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">
<!-- <template v-if="hasVideo">
<video :src="videoFile" controls></video>
</template>
</template> -->
</el-row>
</el-col>
<!-- 右侧告警信息 -->
<el-col :span="11" class="dialog-right">
<el-row>
<el-col :span="24">
@ -113,45 +110,45 @@
</div>
</el-dialog>
</div>
</template>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
import { BoxApi } from '@/utils/boxApi.ts';
<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 = {
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 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 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 formatDateTimeToISO = (datetime) => {
const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', '');
};
};
const adjustColumnWidths = () => {
const adjustColumnWidths = () => {
const currentWidth = window.innerWidth;
// console.log(">>>>>>>>>>", currentWidth);
const scale = currentWidth / baseWidth;
@ -166,17 +163,17 @@ const adjustColumnWidths = () => {
// nextTick(() => {
// });
};
};
// const tableConfig = ref({
// header: ['', '', ''],
// data: [],
// rowNum: 5,
// columnWidth: [100, 160, 190],
// carousel: 'single',
// });
// const tableConfig = ref({
// header: ['', '', ''],
// data: [],
// rowNum: 5,
// columnWidth: [100, 160, 190],
// carousel: 'single',
// });
const fetchTypeMapping = async (token) => {
const fetchTypeMapping = async (token) => {
try {
const algorithms = await boxApi.getAlgorithms(token);
// console.log("Algorithms:", algorithms);
@ -186,9 +183,9 @@ const fetchTypeMapping = async (token) => {
} catch (error) {
console.error("Error fetching algorithms:", error);
}
};
};
const fetchEvents = async () => {
const fetchEvents = async () => {
try {
const token = localStorage.getItem('alertToken');
const limit = 1000;
@ -224,9 +221,9 @@ const fetchEvents = async () => {
} catch (error) {
console.error("Error fetching events data:", error);
}
};
};
const getDateParams = () => {
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);
@ -235,8 +232,8 @@ const getDateParams = () => {
startOfToday,
endOfToday,
};
};
const formatDateTime = (datetime) => {
};
const formatDateTime = (datetime) => {
const date = new Date(datetime);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
@ -245,18 +242,18 @@ const formatDateTime = (datetime) => {
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 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) => {
const handleRowClick = (row) => {
selectedRow.value = row;
duration.value = calculateDuration(row.started_at, row.ended_at);
row.formatted_started_at = formatDateTime(row.started_at);
@ -278,35 +275,35 @@ const handleRowClick = (row) => {
}
});
dialogVisible.value = true;
};
};
const handlePageChange = (page) => {
const handlePageChange = (page) => {
currentPage.value = page;
fetchEvents();
};
};
// const calculateColumnWidths = () => {
// const containerWidth = document.querySelector('.table-container').clientWidth;
// };
// const calculateColumnWidths = () => {
// const containerWidth = document.querySelector('.table-container').clientWidth;
// };
onMounted(async () => {
onMounted(async () => {
token.value = localStorage.getItem('alertToken');
await fetchTypeMapping(token.value);
await fetchEvents();
await adjustColumnWidths();
window.addEventListener('resize', adjustColumnWidths);
});
});
onBeforeUnmount(() => {
onBeforeUnmount(() => {
window.removeEventListener('resize', adjustColumnWidths);
});
</script>
});
</script>
<style scoped>
.alert-container {
<style scoped>
.alert-container {
display: flex;
flex-direction: column;
align-items: center;
@ -315,67 +312,67 @@ onBeforeUnmount(() => {
margin: 0 0 0 1vw;
box-sizing: border-box;
background-color: transparent;
}
}
.table-continer {
.table-continer {
width: 100%;
height: 100%;
box-sizing: border-box;
}
}
.table-row:hover {
.table-row:hover {
cursor: pointer;
}
}
.title-count {
.title-count {
width: 80%;
text-align: center;
/* background-color: transparent; */
}
}
/* .total-text{
/* .total-text{
padding-right: 1vw;
} */
/* title-row内容水平上下居中 */
.title-row {
} */
/* title-row内容水平上下居中 */
.title-row {
display: flex;
align-items: center;
justify-content: center;
}
}
/* total-row左右等分分别靠左和靠右 */
.total-row {
/* total-row左右等分分别靠左和靠右 */
.total-row {
display: flex;
/* justify-content: space-between; */
justify-content: right;
font-weight: bold;
font-size: 14px;
padding-right: 0vw;
}
}
.dialog-row {
.dialog-row {
gap: 30px;
display: flex;
text-align: left;
flex-direction: row;
}
}
.dialog-image-container {
.dialog-image-container {
gap: 20px;
}
}
.el-dialog .el-image,
.el-dialog video {
.el-dialog .el-image,
.el-dialog video {
width: 100%;
}
}
/* 基础表格样式 */
::v-deep .el-table {
/* 基础表格样式 */
::v-deep .el-table {
color: white;
font-size: 13px;
/* font-weight: bold; */
@ -383,11 +380,11 @@ onBeforeUnmount(() => {
padding: 0;
margin: 0;
height: 14vh;
}
}
/* 表头和单元格基本样式 */
/* 表头和单元格基本样式 */
::v-deep .el-table td {
::v-deep .el-table td {
border: none;
background-color: #001529;
transition: border 0.3s ease, background-color 0.3s ease;
@ -397,40 +394,40 @@ onBeforeUnmount(() => {
/* white-space: nowrap; */
/* overflow: hidden; */
/* text-overflow: ellipsis; */
}
}
/* 去掉中间数据的分割线 */
/* ::v-deep .el-table .el-table__row>td{
/* 去掉中间数据的分割线 */
/* ::v-deep .el-table .el-table__row>td{
border: none;
} */
} */
.table-container >>> .el-table__row>td{
.table-container >>> .el-table__row>td{
border: none;
}
.table-container >>> .el-table th.is-leaf{
}
.table-container >>> .el-table th.is-leaf{
border: none;
}
::v-deep .el-table__inner-wrapper::before{
}
::v-deep .el-table__inner-wrapper::before{
height: 0;
}
}
::v-deep .el-table thead th {
::v-deep .el-table thead th {
color: white;
font-weight: bold;
background-color: #001529;
padding: 0;
}
}
::v-deep .el-table .el-table__cell:hover {
::v-deep .el-table .el-table__cell:hover {
background-color: transparent;
color: rgb(238, 150, 49);
/* font-size: 26px; */
/* 保持文本为白色 */
}
}
.el-tooltip__popper {
.el-tooltip__popper {
font-size: 26px;
max-width:50%
}
</style>
}
</style>

View File

@ -153,28 +153,28 @@
</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';
<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 pendingEventCount = ref(0);
const closedEventCount = ref(0);
const apiInstance = new BoxApi();
const cameraCount = ref(0);
const cameraOfflineCount = ref(0);
const cameraOnlineCount = ref(0);
const eventCount = ref(0);
const pendingEventCount = ref(0);
const closedEventCount = ref(0);
const activeTab = ref('all');
const activeTab = ref('all');
const getTodayData = () => {
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);
@ -183,9 +183,9 @@
timeAfter: formatDateTime(startOfToday),
timeBefore: formatDateTime(endOfToday),
};
};
};
const getWeekData = () => {
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);
@ -194,9 +194,9 @@
timeAfter: formatDateTime(startOfWeek),
timeBefore: formatDateTime(endOfToday),
};
};
};
const getMonthData = () => {
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);
@ -205,9 +205,14 @@
timeAfter: formatDateTime(startOfMonth),
timeBefore: formatDateTime(endOfToday),
};
};
};
const formatDateTime = (datetime) => {
const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', '');
};
const formatDateTime = (datetime) => {
const date = new Date(datetime);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
@ -216,9 +221,9 @@
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 () => {
const fetchCameras = async () => {
try {
const token = localStorage.getItem('alertToken');
const limit = 20;
@ -250,29 +255,29 @@
} catch (error) {
console.error('Error fetching cameras:', error);
}
};
};
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
// try {
// const token = localStorage.getItem('alertToken');
// 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 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');
// pendingEventCount.value = pendingResponse.count;
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
// pendingEventCount.value = pendingResponse.count;
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
// closedEventCount.value = closedResponse.count;
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
// closedEventCount.value = closedResponse.count;
// } catch (error) {
// console.error(':', error);
// }
// };
// } catch (error) {
// console.error(':', error);
// }
// };
const fetchEvents = async () => {
const fetchEvents = async () => {
try {
const token = localStorage.getItem('alertToken');
let timeRange = { timeAfter: null, timeBefore: null };
@ -301,15 +306,15 @@
} catch (error) {
console.error('获取事件数据失败:', error);
}
};
};
watch(activeTab, (newTab) => {
watch(activeTab, (newTab) => {
fetchEvents();
});
});
onMounted(() => {
onMounted(() => {
// getTodayData();
// getWeekData();
// getMonthData();
@ -317,61 +322,61 @@
fetchEvents();
});
});
</script>
</script>
<style scoped>
.count-container{
width:100%;
height:100%;
margin:1vh;
<style scoped>
.count-container {
width: 100%;
height: 100%;
margin: 1vh;
padding: 2vh 0;
}
}
.top-row,
.bottom-row {
.top-row,
.bottom-row {
background-color: #001529;
color: aliceblue;
padding: 0;
margin: 0;
}
}
.inner-count-text {
.inner-count-text {
color: rgb(91, 224, 241);
}
}
.tab-div{
.tab-div {
background-color: #001529;
}
}
::v-deep .el-tabs__item {
::v-deep .el-tabs__item {
color: #fff;
font-size: 13px;
padding: 0;
margin-left: 1vh;
height: 20px;
}
}
::v-deep .el-tabs__item.is-active {
::v-deep .el-tabs__item.is-active {
color: #2ea0ec;
}
}
.el-tabs__active-bar {
.el-tabs__active-bar {
background-color: transparent !important;
}
}
::v-deep .el-tabs__nav-wrap::after {
::v-deep .el-tabs__nav-wrap::after {
/* width: 15vw; */
position: static !important;
}
</style>
}
</style>

View File

@ -1,204 +1,310 @@
<template>
<div>
<el-card class="stats-card">
<div class="stats-header">告警数量和类型分布</div>
<el-row :gutter="20" class="stats-row">
<el-col :span="24">
<div ref="chart" class="chart"></div>
</el-col>
</el-row>
</el-card>
<!-- 打开弹窗按钮 -->
<el-button type="primary" @click="openDialog">打开设置弹窗</el-button>
<!-- 弹窗 -->
<el-dialog title="设置规则" v-model="dialogVisible" width="50%" @close="resetDialog">
<!-- 显示摄像头基本信息 -->
<div v-if="CameraDialog.id">
<p><strong>摄像头名称</strong>{{ CameraDialog.name }} <strong> &nbsp;&nbsp;&nbsp;通道号: </strong>{{ CameraDialog.id }}</p>
<!-- <p><strong>摄像头状态</strong>{{ CameraDialog.status }}</p> -->
<!-- <img :src="CameraDialog.snapshot" alt="Snapshot" style="max-width: 100%; margin-bottom: 20px" /> -->
</div>
<el-radio-group v-model="CameraDialog.mode" @change="handleGlobalModeChange" style="margin-bottom: 20px">
<el-radio label="on">ON</el-radio>
<el-radio label="off">OFF</el-radio>
</el-radio-group>
<!-- 显示规则数据 -->
<div v-for="(rule, index) in CameraDialog.rules" :key="rule.id" class="rule-block">
<h3>{{ rule.name }} (模式: {{ rule.mode }})</h3>
<el-radio-group v-model="rule.mode" :disabled="CameraDialog.mode === 'off' " @change="handleRuleModeChange(rule)">
<el-radio label="on">ON</el-radio>
<el-radio label="off">OFF</el-radio>
<el-radio label="schedule">Schedule</el-radio>
</el-radio-group>
<div v-if="rule.mode === 'schedule'">
<!-- Schedule类型选择 -->
<el-select v-model="rule.schedule.type" placeholder="请选择" style="margin: 10px 0">
<el-option label="每日" value="daily"></el-option>
</el-select>
<!-- 时间段配置 -->
<div v-for="(time, timeIndex) in rule.schedule.time_slots" :key="timeIndex" class="time-period">
<el-time-picker v-model="rule.schedule.time_slots[timeIndex][0]" format="HH:mm" value-format="HH:mm"
placeholder="Start Time" @change="validateTime(rule.schedule.time_slots, timeIndex)" />
<el-time-picker v-model="rule.schedule.time_slots[timeIndex][1]" format="HH:mm" value-format="HH:mm"
placeholder="End Time" @change="validateTime(rule.schedule.time_slots, timeIndex)" />
<el-button type="danger" @click="removeTimeSlot(rule.schedule.time_slots, timeIndex)">Delete</el-button>
<!-- 提示用户填写完整时间 -->
<p v-if="!rule.schedule.time_slots[timeIndex][0] || !rule.schedule.time_slots[timeIndex][1]"
style="color: red; margin-top: 5px">
*
</p>
</div>
<el-button type="primary" @click="addTimeSlot(rule.schedule.time_slots)">Add Time Slot</el-button>
<p v-if="!rule.schedule.time_slots.length" style="color: red; margin-top: 10px">
请至少添加一个时间段
</p>
</div>
</div>
<!-- 弹窗底部操作 -->
<span slot="footer" class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="success" @click="saveRules">保存</el-button>
</span>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
import { BoxApi } from '@/utils/boxApi.ts'; // BoxApi
import { ref, watch, reactive} from "vue";
import { ElMessage } from "element-plus";
import { BoxApi } from "@/utils/boxApi.ts";
//
const chart = ref(null);
const seriesData = ref([]);
// BoxApi
// API
const apiInstance = new BoxApi();
const cameraData = ref({}); //
const CameraDialog = ref({}); //
//
const fetchTypeMapping = async (token) => {
const algorithms = await apiInstance.getAlgorithms(token);
let mapping = algorithms.map(algorithm => ({
value: 0,
code_name: algorithm.code_name,
name: algorithm.name
}));
//
const dialogVisible = ref(false);
//
const newMapping = [
{ code_name: "minizawu:532", name: "杂物堆积", value: 0 }
];
seriesData.value = mapping.concat(newMapping);
};
// seriesData
const fetchAndProcessEvents = async (token) => {
//
const openDialog = async () => {
try {
let currentPage = 1;
const pageSize = 1000; // 2000
let allEvents = [];
const token = localStorage.getItem("alertToken");
const cameraId = 1;
const camera = await apiInstance.getCameraById(token, cameraId);
cameraData.value = camera;
//
const { tableData: firstBatch, totalItems } = await apiInstance.getEvents(token, pageSize, currentPage);
allEvents = [...firstBatch];
console.log("摄像头数据:", cameraData.value);
//
const totalPages = Math.ceil(totalItems / pageSize);
//
while (currentPage < totalPages) {
currentPage++;
const { tableData: nextBatch } = await apiInstance.getEvents(token, pageSize, currentPage);
allEvents = [...allEvents, ...nextBatch];
//
processEventData(allEvents);
updateChart(); //
}
//
processEventData(allEvents);
updateChart(); //
formatCameraData();
dialogVisible.value = true;
} catch (error) {
console.error("Error fetching events:", error);
console.error("获取摄像头规则失败:", error);
}
};
// seriesData
const processEventData = (events) => {
//
seriesData.value.forEach(item => {
item.value = 0;
});
//
events.forEach(event => {
const matchAlgorithm = seriesData.value.find(item => item.code_name === event.types);
if (matchAlgorithm) {
matchAlgorithm.value += 1;
}
});
// cameraData CameraDialog
const formatCameraData = () => {
CameraDialog.value = {
id: cameraData.value.id,
name: cameraData.value.name,
status: cameraData.value.status,
mode: cameraData.value.mode,
// snapshot: cameraData.value.snapshot,
rules: cameraData.value.rules.map((rule) => ({
id: rule.id,
name: rule.name,
mode: rule.mode,
schedule: {
type: rule.schedule?.type || "",
time_slots: rule.schedule?.time_slots
? rule.schedule.time_slots.map(([start, end]) => [
start !== undefined ? convertMinutesToTime(start) : "00:00",
end !== undefined ? convertMinutesToTime(end) : "00:00",
])
: [],
},
})),
};
};
//
const initChart = () => {
// ECharts
if (!chart.value) {
console.error("Chart DOM element is not available");
const handleGlobalModeChange = () => {
if (CameraDialog.value.mode === "off") {
// off mode off
CameraDialog.value.rules.forEach((rule) => {
rule.mode = "off";
});
} else if (CameraDialog.value.mode === "on") {
// on
CameraDialog.value.rules.forEach((rule) => {
if (rule.mode === "schedule") {
rule.schedule.type = "daily"; // daily
rule.schedule.time_slots = []; //
} else if (rule.mode === "on" || rule.mode === "off") {
rule.schedule.type = ""; // type
rule.schedule.time_slots = []; //
}
});
}
};
const handleRuleModeChange = (rule) => {
if (rule.mode === "schedule") {
rule.schedule.type = "daily"; // daily
rule.schedule.time_slots = []; //
} else {
// schedule schedule
rule.schedule.type = ""; // type
rule.schedule.time_slots = []; //
}
};
const handleModeChange = (rule) => {
if (rule.mode === "schedule") {
rule.schedule.type = "daily";
}
};
//
const validateTime = (timeSlots, index) => {
const [startTime, endTime] = timeSlots[index];
if (!startTime || !endTime) {
ElMessage.error("请填写完整时间段");
return;
}
chart.value = echarts.init(chart.value);
const option = {
tooltip: {
trigger: 'item',
},
legend: {
orient: 'horizontal',
bottom: 10,
textStyle: {
color: '#fff',
},
itemGap: 20,
data: seriesData.value.map(item => item.name),
show : true
},
series: [
{
name: '告警类型',
type: 'pie',
radius: '50%',
center: ['50%', '50%'],
data: seriesData.value,
data: [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
if (endTime <= startTime) {
ElMessage.error("开始时间必须小于结束时间");
timeSlots[index][1] = "00:00"; //
}
if (endTime && startTime >= endTime) {
ElMessage.error("开始时间和结束时间不合理");
timeSlots[index][1] = "";
return;
}
for (let i = 0; i < timeSlots.length; i++) {
if (
i !== index &&
timeSlots[i][0] &&
timeSlots[i][1] &&
startTime &&
endTime &&
!(
endTime <= timeSlots[i][0] ||
startTime >= timeSlots[i][1]
)
) {
ElMessage.error("时间段重叠,请重新选择");
timeSlots[index][0] = "";
timeSlots[index][1] = "";
break;
}
},
label: {
show: false,
},
stillShowZeroSum: false,
}
]
};
chart.value.setOption(option);
};
//
const updateChart = () => {
if (chart.value && typeof chart.value.setOption === 'function') {
chart.value.setOption({
series: [
{
data: seriesData.value
//
const addTimeSlot = (timeSlots) => {
timeSlots.push([]);
};
//
const removeTimeSlot = (timeSlots, index) => {
timeSlots.splice(index, 1);
};
//
const convertMinutesToTime = (minutes) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
};
//
const convertTimeToMinutes = (timeString) => {
const [hours, minutes] = timeString.split(":").map(Number);
return hours * 60 + minutes;
};
//
const saveRules = () => {
let invalidSchedule = false;
// schedule
CameraDialog.value.rules.forEach((rule) => {
if (rule.mode === "schedule") {
if (rule.schedule.type && rule.schedule.time_slots.length === 0) {
//
ElMessage.error(`规则 "${rule.name}" 的时间段不能为空,请添加至少一个时间段`);
invalidSchedule = true;
}
}
]
});
} else {
console.error("ECharts instance is not initialized properly");
}
};
//
onMounted(async () => {
//
if (invalidSchedule) return;
// cameraUpdate
const cameraUpdate = {
id: CameraDialog.value.id,
name: CameraDialog.value.name,
mode: CameraDialog.value.mode,
// rules: CameraDialog.value.rules.map((rule) => ({
// id: rule.id,
// name: rule.name,
// mode: rule.mode,
// })),
};
console.log("页面的cameraUpdate》》》》》》》》》》》:", cameraUpdate);
// ruleUpdate
const ruleUpdate = CameraDialog.value.rules.map((rule) => ({
id: rule.id,
name: rule.name,
mode: CameraDialog.value.mode === "off" ? "off" : rule.mode,
schedule: rule.mode === "schedule" ? {
type: rule.schedule.type,
time_slots: rule.schedule.time_slots.map(([start, end]) => [
convertTimeToMinutes(start),
convertTimeToMinutes(end),
]),
} : {}, // schedule schedule
}));
try {
const token = localStorage.getItem('alertToken');
//
await fetchTypeMapping(token);
//
initChart();
//
await fetchAndProcessEvents(token);
// API camera rules
apiInstance.updateCamera(token, cameraUpdate.id, cameraUpdate); //
apiInstance.updateRule(token, ruleUpdate); //
ElMessage.success("规则保存成功");
dialogVisible.value = false;
} catch (error) {
console.error("Error fetching data:", error);
console.error("保存失败:", error);
ElMessage.error("保存失败,请重试");
}
});
};
const closeDialog = () => {
dialogVisible.value = false;
};
</script>
<style scoped>
.stats-card {
background-color: #304555;
/* background: linear-gradient(to top, rgba(16, 84, 194, 0.6), rgba(31, 48, 207, 0.7)); */
color: #fff;
border-radius: 8px;
/* margin: 10px; */
/* padding: 10px; */
/* height: 100vh; */
.time-period {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.stats-header {
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #3a4b5c;
padding-bottom: 10px;
.rule-block {
border: 1px solid #ddd;
padding: 20px;
margin-bottom: 20px;
}
.stats-row {
margin-top: 10px;
margin-bottom: 34px;
}
.chart {
width: 100%;
/* min-height: 365px; */
height: 41vh;
min-width: 40vw;
/* height: 445px; */
.dialog-footer {
text-align: right;
}
</style>

View File

@ -14,7 +14,7 @@
</span>
</el-row> -->
<el-row class="filter-row">
<el-col :span="4">
<el-col :span="5">
<el-form-item label="摄像头名称">
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable clearable>
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
@ -22,14 +22,14 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="3">
<el-col :span="4">
<el-form-item label="告警类型">
<el-select v-model="filterParams.types" placeholder="请选择类型" clearable>
<el-option v-for="(label, key) in typeMapping" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="3">
<el-col :span="4">
<el-form-item label="处理情况">
<el-select v-model="filterParams.status" placeholder="全部" clearable>
<el-option label="全部" value=""></el-option>
@ -38,29 +38,36 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-col :span="5">
<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="4">
<el-col :span="5">
<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="4" class="filter-buttons">
<el-col :span="4" class="filter-buttons" :offset="15">
<el-button type="primary" @click="handleFilter">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button :disabled="isExporting" @click="exportData">
{{ isExporting ? '正在导出,请勿重复点击' : '导出' }}
</el-button>
</el-col>
<el-col :span="1" :offset="0">
<el-button type="primary" :disabled="!selectedAlerts.length" @click="batchMarkAsProcessed">
批量标记为已处理
</el-button>
</el-col>
</el-row>
<el-row class="table-row">
<el-col :span="24" class="table-col">
<div class="table-container">
<el-table :data="tableData" header-row-class-name="table-header" :fit="true" height="580">
<el-table :data="tableData" header-row-class-name="table-header" :fit="true" height="580"
@selection-change="handleSelectionChange">
<el-table-column type="selection" min-width="55"></el-table-column>
<el-table-column prop="id" label="告警编号" min-width="100"></el-table-column>
<el-table-column label="告警类型" min-width="150">
<template v-slot="scope">
@ -141,12 +148,13 @@
</div>
</div>
</div> -->
<div class="event-media" :class="{ 'center-media': mediums.length === 1 }">
<!-- <div class="event-media" :class="{ 'center-media': mediums.length === 1 }"> -->
<div class="event-media" >
<!-- 告警关联视频 -->
<div v-if="hasVideo" class="media-container video-item">
<!-- <div v-if="hasVideo" class="media-container video-item">
<p>告警关联视频</p>
<video :src="videoFile" controls @click="openMediaDialog('video', videoFile)"></video>
</div>
</div> -->
<!-- 告警关联图片 -->
<div v-if="hasSnapshot" class="media-container image-item">
@ -223,6 +231,26 @@ const totalItems = ref(0);
const displayTotalItems = ref(0); //
const cameras = ref([]);
const selectedAlerts = ref([]);
const handleSelectionChange = (selectedRows) => {
selectedAlerts.value = selectedRows.map(row => row.id);
};
const batchMarkAsProcessed = async () => {
try {
for (const eventId of selectedAlerts.value) {
await boxApi.setEventStatus(eventId, 'closed', null, token.value);
}
await fetchEvents();
selectedAlerts.value = [];
} catch (error) {
console.error("Error in batch processing:", error);
}
};
const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', '');
};
@ -277,7 +305,7 @@ const exportData = async () => {
saveAs(blob, 'Alert_data.csv');
} catch (error) {
console.error("导出数据错误:", error);
}finally {
} finally {
isExporting.value = false;
}
};
@ -326,8 +354,6 @@ const submitStatusUpdate = async (newStatus, row = null) => {
// console.console.log(row,row.id);
const eventId = row ? row.id : selectedRow.value.id;
const remarkContent = remark.value && remark.value.trim() !== "" ? remark.value : null;
// setEventStatus
await boxApi.setEventStatus(eventId, newStatus, remarkContent);
@ -551,7 +577,7 @@ onMounted(async () => {
justify-content: center;
align-content: center;
height: 15vh;
padding: 0px 0;
padding: 0 0;
margin: 5vh 6vw 5vh 1vw;
gap: 8px;
/* font-weight: bold; */
@ -594,7 +620,8 @@ onMounted(async () => {
.event-media {
display: flex;
justify-content: space-between;
justify-content: center;
/* justify-content: space-between; */
flex-wrap: wrap;
width: 100%;
}

View File

@ -242,7 +242,7 @@ const handleFilter = async () => {
const timeAfter = filterParams.timeAfter ? formatDateTimeToISO(filterParams.timeAfter) : null;
const timeBefore = filterParams.timeBefore ? formatDateTimeToISO(filterParams.timeBefore) : null;
console.log('timeAfter:', timeAfter);
// console.log('timeAfter:', timeAfter);
await fetchTypeCounts(timeAfter, timeBefore);
await fetchAllCounts(timeAfter, timeBefore);

View File

@ -102,11 +102,11 @@ const onReset = () => {
<template>
<div class="main-layout">
<div class="login-form">
<div class="login-header">
<!-- <div class="login-header">
<el-row justify="center">
<el-image src="/logo.png" fit="contain" />
</el-row>
</div>
</div> -->
<div class="login-body">
<el-form :model="loginForm" style="max-width: 600px;">
<el-form-item v-show="loginForm.serverSettings">
@ -152,11 +152,11 @@ const onReset = () => {
</el-row>
</el-form>
</div>
<div class="login-footer">
<!-- <div class="login-footer">
<el-row justify="end">
<el-text class="plain-text">Powered by TuringAI</el-text>
<el-text class="plain-text"></el-text>
</el-row>
</div>
</div> -->
</div>
</div>
</template>
@ -168,7 +168,8 @@ const onReset = () => {
left: 0;
width: 100%;
height: 100%;
background-image: url('/login-bg.jpg');
/* background-color: #000; */
background-image: url('/login-bg.png');
background-size: cover;
}
@ -183,7 +184,7 @@ const onReset = () => {
}
.login-body {
padding-top: 30px;
padding-top: 60px;
padding-bottom: 5px;
padding-left: 30px;
padding-right: 30px;

View File

@ -1,160 +1,144 @@
<template>
<div class="settings-container">
<el-row class="popup-row">
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange" style="color: aliceblue;">
开启弹窗
<el-col :sm="24" :md="24">弹窗设置</el-col>
<el-col :sm="24" :md="24">
<el-checkbox v-model="isInteractivePopupEnabled" @change="handleInteractiveChange" style="color: aliceblue;">
开启交互式弹窗
</el-checkbox>
</el-col>
<el-col :sm="24" :md="24">
<el-checkbox v-model="isResponsivePopupEnabled" @change="handleResponsiveChange" style="color: aliceblue;">
开启响应式弹窗
</el-checkbox>
</el-col>
</el-row>
<el-row class="channel-row">
<Channel />
</el-row>
<!-- el-dialog 用于显示通知详情 -->
<!-- <el-dialog
title="告警详情"
v-model="dialogVisible"
width="50%"
@close="handleDialogClose">
<el-row :gutter="30" style="margin-bottom: 2vh;">
<el-col span="8">告警编号{{ dialogContent.id }}</el-col>
<el-col span="24">摄像头编号{{ dialogContent.camera_id }}</el-col>
<el-col span="8">摄像头名称{{ dialogContent.camera?.name }}</el-col>
<el-col span="8">告警类型{{ algorithmMap.get(dialogContent.types) || '未知类型' }}</el-col>
<el-col span="8">告警时间{{ dialogContent.started_at }}</el-col>
</el-row>
<img :src="dialogContent.snapshotUrl" alt="告警图片" v-if="dialogContent.snapshotUrl" style="max-width: 100%;" />
<video v-if="dialogContent.videoUrl" :src="dialogContent.videoUrl" controls style="max-width: 100%;"></video>
</el-dialog> -->
</div>
</template>
<script lang="ts" setup>
import { ref, inject, onMounted } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import type { GlobalWebSocket } from '../utils/useGlobalWebSocket';
import type { GlobalWebSocket } from '@/utils/useGlobalWebSocket';
import Channel from '@/components/Channel.vue';
import { setDialogHandler } from '../utils/useGlobalWebSocket';
import { BoxApi } from '@/utils/boxApi';
// const apiInstance = new BoxApi();
const isPopupEnabled = ref(false);
const isInteractivePopupEnabled = ref(false); //
const isResponsivePopupEnabled = ref(false); //
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
// const dialogVisible = ref(false);
// const dialogContent = ref({
// id: null,
// camera_id: null,
// camera: { name: '' },
// types: null,
// started_at: null,
// snapshotUrl: '',
// videoUrl: ''
// });
// const algorithmMap = ref(new Map());
if (!globalWebSocket) {
throw new Error('globalWebSocket 注入失败');
}
// localStorage WebSocket
onMounted(async () => {
const storedState = localStorage.getItem('isPopupEnabled');
isPopupEnabled.value = storedState === 'true';
if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
globalWebSocket.connectWebSocket();
}
// try {
// const token = localStorage.getItem('alertToken');
// const algorithms = await apiInstance.getAlgorithms(token);
// algorithmMap.value = new Map(algorithms.map((algo: { code_name: string, name: string }) => [algo.code_name, algo.name]));
// setDialogHandler(showDialog);
// if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
// globalWebSocket.connectWebSocket();
// }
// } catch (error) {
// ElMessage.error('');
// console.error(error);
// }
//
onMounted(() => {
isInteractivePopupEnabled.value = localStorage.getItem('isInteractivePopupEnabled') === 'true';
isResponsivePopupEnabled.value = localStorage.getItem('isResponsivePopupEnabled') === 'true';
updateWebSocketConnection();
});
//
const handleCheckboxChange = () => {
if (isPopupEnabled.value) {
ElMessageBox.confirm('是否开启弹窗提示?', '提示', {
// const handleInteractiveChange = () => {
// if (isInteractivePopupEnabled.value) {
// isResponsivePopupEnabled.value = false;
// localStorage.setItem('isResponsivePopupEnabled', 'false');
// }
// localStorage.setItem('isInteractivePopupEnabled', String(isInteractivePopupEnabled.value));
// updateWebSocketConnection();
// };
// const handleResponsiveChange = () => {
// if (isResponsivePopupEnabled.value) {
// isInteractivePopupEnabled.value = false;
// localStorage.setItem('isInteractivePopupEnabled', 'false');
// }
// localStorage.setItem('isResponsivePopupEnabled', String(isResponsivePopupEnabled.value));
// updateWebSocketConnection();
// };
const handleInteractiveChange = () => {
if (isInteractivePopupEnabled.value) {
ElMessageBox.confirm('是否开启交互式弹窗提示?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
localStorage.setItem('isPopupEnabled', 'true');
globalWebSocket.connectWebSocket();
isResponsivePopupEnabled.value = false;
localStorage.setItem('isResponsivePopupEnabled', 'false');
localStorage.setItem('isInteractivePopupEnabled', 'true');
updateWebSocketConnection();
})
.catch(() => {
isPopupEnabled.value = false;
localStorage.setItem('isPopupEnabled', 'false');
isInteractivePopupEnabled.value = false;
});
} else {
globalWebSocket.closeWebSocket();
localStorage.setItem('isPopupEnabled', 'false');
ElMessageBox.confirm('是否关闭交互式弹窗?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
localStorage.setItem('isInteractivePopupEnabled', 'false');
updateWebSocketConnection();
})
.catch(() => {
isInteractivePopupEnabled.value = true;
});
}
};
// const showDialog = async (data: any) => {
// try {
// const token = localStorage.getItem('alertToken');
// const eventDetails = await apiInstance.getEventById(data.id, token);
const handleResponsiveChange = () => {
if (isResponsivePopupEnabled.value) {
ElMessageBox.confirm('是否开启响应式弹窗提示?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
isInteractivePopupEnabled.value = false;
localStorage.setItem('isInteractivePopupEnabled', 'false');
localStorage.setItem('isResponsivePopupEnabled', 'true');
updateWebSocketConnection();
})
.catch(() => {
isResponsivePopupEnabled.value = false;
});
} else {
ElMessageBox.confirm('是否关闭响应式弹窗?', '确认提示', {
confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
localStorage.setItem('isResponsivePopupEnabled', 'false');
updateWebSocketConnection();
})
.catch(() => {
isResponsivePopupEnabled.value = true;
});
}
};
// const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
// const video = eventDetails.mediums.find((item: any) => item.name === 'video');
// dialogContent.value = {
// id: eventDetails.id,
// camera_id: eventDetails.camera_id,
// camera: eventDetails.camera,
// types: eventDetails.types,
// started_at: eventDetails.started_at,
// snapshotUrl: snapshot?.file || '',
// videoUrl: video?.file || ''
// };
// dialogVisible.value = true;
// } catch (error) {
// ElMessage.error('');
// console.error(error);
// }
// };
// const handleDialogClose = () => {
// dialogContent.value = {
// id: null,
// camera_id: null,
// camera: { name: '' },
// types: null,
// started_at: null,
// snapshotUrl: '',
// videoUrl: ''
// };
// dialogVisible.value = false;
// };
// WebSocket
const updateWebSocketConnection = () => {
if (isInteractivePopupEnabled.value || isResponsivePopupEnabled.value) {
globalWebSocket.connectWebSocket();
} else {
globalWebSocket.closeWebSocket();
}
};
</script>
<style scoped>
.settings-container {
display: flex;
flex-direction: column;
background-color: #ffffff;
background-color: #F1F1F1;
width: 100%;
height: 100%;
}
@ -165,7 +149,7 @@ const handleCheckboxChange = () => {
width: 80vw;
padding: 1vh 1vw;
margin: 1vh 2vw;
background-color: #ffffff;
background-color: #001529;
border-radius: 8px;
color: white;
}

View File

@ -11,6 +11,8 @@ import { useGlobalWebSocket } from './utils/useGlobalWebSocket';
import DataVVue3 from '@kjgl77/datav-vue3';
import '@kjgl77/datav-vue3/dist/style.css';
import mitt from 'mitt';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css';
const app = createApp(App)
@ -20,6 +22,7 @@ app.provide('globalWebSocket', globalWebSocket);
app.use(ElementPlus, { locale: zhCn });
app.use(router);
app.use(DataVVue3);
app.use(Antd);
// 导航守卫,检查登录状态
router.beforeEach((to, from, next) => {