other
This commit is contained in:
parent
9c5122f143
commit
fa85515d9b
372
README.md
372
README.md
|
@ -267,6 +267,378 @@ export default router;
|
|||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 告警规则设置
|
||||
|
||||
|
||||
|
||||
### 界面接口列表
|
||||
|
||||
- 获取所有摄像头信息 (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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
> 表单数据显示摄像id,name,mode对应的规则显示对应的id,name,mode,schedule
|
||||
>
|
||||
> 其中摄像mode有ON和OFF两种状态,规则中mode有On,OFF和schedule三中状态,规则模式只有在schedule状态才会显示schedule中内容type,
|
||||
>
|
||||
> schedule中type又分为每日daily和每周weekly两周状态,其中dayily直接设置,当日时间范围time_slots,weekly可选择周几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.弹窗仅图片显示(AlertManagement,LeftBottom,App)
|
||||
- 2.通道点播(Setting,Channel)
|
||||
- 摄像列表
|
||||
- 搜索/在线状态过滤
|
||||
- 3.通道规则设置(Channel,CameraRules)
|
||||
- 规则开关
|
||||
- 时间设置
|
||||
- 下阶段
|
||||
- 1.弹窗模式按钮
|
||||
- 交互式开关(已有,点击响应)
|
||||
- 响应式开关(###)
|
||||
- 2.光影效果
|
||||
- 声音联动(提示音)
|
||||
- 交互(首页)
|
||||
- 3.用户设定不可更改项
|
||||
- 测试调整(遗留代码冗余调整)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 2024.11.22
|
||||
|
||||
- 弹窗模式开关
|
||||
- 交互式开关(点击触发弹窗)
|
||||
- 响应式开关(触发对话框)
|
||||
- 弹窗现实
|
||||
- 格式化类型
|
||||
- 格式化时间
|
||||
- 现实时间
|
||||
- 弹窗布局调整
|
||||
- 控制台日志处理(部分)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
BIN
public/bg01.png
BIN
public/bg01.png
Binary file not shown.
Before Width: | Height: | Size: 959 KiB |
Binary file not shown.
Before Width: | Height: | Size: 335 KiB |
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
|
@ -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));
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
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") {
|
||||
|
@ -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, {
|
||||
|
@ -200,6 +208,7 @@
|
|||
margin-top: 1vh;
|
||||
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__wrapper {
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -207,6 +207,11 @@
|
|||
};
|
||||
};
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
|
|
|
@ -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> 通道号: </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>
|
||||
|
|
|
@ -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', '');
|
||||
};
|
||||
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue