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));
|
||||
|
||||
|
|
|
@ -1,347 +1,356 @@
|
|||
<template>
|
||||
<div class="camera-container">
|
||||
<div class="top-header">
|
||||
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter" >
|
||||
<el-option label="全部" value="all"></el-option>
|
||||
<el-option label="在线" value="online"></el-option>
|
||||
<el-option label="离线" value="offline"></el-option>
|
||||
</el-select>
|
||||
<div class="top-text">警戒点位</div>
|
||||
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
|
||||
class="camera-select" >
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
<span>{{ camera.id }}.</span> 名称: {{ camera.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 播放列表,设置为可滚动 -->
|
||||
<div class="camera-grid">
|
||||
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
|
||||
<div class="stream-control">
|
||||
<p class="camera-name-title">{{ camera.name }}</p>
|
||||
<div class="close-button" @click="closeStream(camera)">×</div>
|
||||
</div>
|
||||
|
||||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
||||
<el-icon size="48">
|
||||
<VideoCameraFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="openDialog(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<div class="camera-container">
|
||||
<div class="top-header">
|
||||
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter">
|
||||
<el-option label="全部" value="all"></el-option>
|
||||
<el-option label="在线" value="online"></el-option>
|
||||
<el-option label="离线" value="offline"></el-option>
|
||||
</el-select>
|
||||
<div class="top-text">警戒点位</div>
|
||||
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
|
||||
class="camera-select">
|
||||
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
|
||||
<span>{{ camera.id }}.</span> 名称: {{ camera.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 播放列表,设置为可滚动 -->
|
||||
<div class="camera-grid">
|
||||
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
|
||||
<div class="stream-control">
|
||||
<p class="camera-name-title">{{ camera.name }}</p>
|
||||
<div class="close-button" @click="closeStream(camera)">×</div>
|
||||
</div>
|
||||
|
||||
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
|
||||
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
|
||||
<el-icon size="48">
|
||||
<VideoCameraFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="openDialog(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗播放视频 -->
|
||||
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
|
||||
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
|
||||
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const cameras = ref([]);
|
||||
const selectedCameras = ref([]);
|
||||
const showButton = ref(false);
|
||||
const apiInstance = new BoxApi();
|
||||
const canvasRefs = ref({});
|
||||
const selectedCameraId = ref(null);
|
||||
const dialogVisible = ref(false); // 控制弹窗的显示与隐藏
|
||||
const currentCamera = ref(null); // 当前选中的摄像头
|
||||
|
||||
// 弹窗中的canvas引用
|
||||
const dialogCanvas = ref(null);
|
||||
const filterStatus = ref("all");
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getMinCameras(token);
|
||||
|
||||
// 根据 filterStatus 筛选摄像头状态
|
||||
if (filterStatus.value === "online") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "online");
|
||||
} else if (filterStatus.value === "offline") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "offline");
|
||||
} else {
|
||||
cameras.value = cameraData; // 默认 "all" 显示全部摄像头
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const selectCameraById = (cameraId) => {
|
||||
const camera = cameras.value.find(c => c.id === cameraId);
|
||||
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
|
||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗并开始播放
|
||||
const openDialog = async (camera) => {
|
||||
currentCamera.value = camera;
|
||||
dialogVisible.value = true;
|
||||
await nextTick();
|
||||
startStreamInDialog(camera);
|
||||
};
|
||||
|
||||
// 在弹窗中播放视频
|
||||
const startStreamInDialog = async (camera) => {
|
||||
const canvas = dialogCanvas.value;
|
||||
|
||||
if (!camera || !canvas) {
|
||||
console.error('未找到对应的 canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
<!-- 弹窗播放视频 -->
|
||||
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
|
||||
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
|
||||
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const cameras = ref([]);
|
||||
const selectedCameras = ref([]);
|
||||
const showButton = ref(false);
|
||||
const apiInstance = new BoxApi();
|
||||
const canvasRefs = ref({});
|
||||
const selectedCameraId = ref(null);
|
||||
const dialogVisible = ref(false); // 控制弹窗的显示与隐藏
|
||||
const currentCamera = ref(null); // 当前选中的摄像头
|
||||
|
||||
// 弹窗中的canvas引用
|
||||
const dialogCanvas = ref(null);
|
||||
const filterStatus = ref("all");
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
const response = await apiInstance.startCameraStream(token, camera.id);
|
||||
camera.streamPort = response.port;
|
||||
camera.playing = true;
|
||||
|
||||
const url = `ws://192.168.28.33:${camera.streamPort}/`;
|
||||
console.log('播放路径:', url);
|
||||
|
||||
if (window.JSMpeg) {
|
||||
const player = new window.JSMpeg.Player(url, {
|
||||
canvas: canvas,
|
||||
autoplay: true,
|
||||
videoBufferSize: 15 * 1024 * 1024,
|
||||
audioBufferSize: 5 * 1024 * 1024,
|
||||
});
|
||||
camera.player = player;
|
||||
} else {
|
||||
console.error('JSMpeg 未加载');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动视频流失败:', error);
|
||||
const cameraData = await apiInstance.getAllCameras(token);
|
||||
|
||||
// 根据 filterStatus 筛选摄像头状态
|
||||
if (filterStatus.value === "online") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "online");
|
||||
} else if (filterStatus.value === "offline") {
|
||||
cameras.value = cameraData.filter(camera => camera.status === "offline");
|
||||
} else {
|
||||
cameras.value = cameraData; // 默认 "all" 显示全部摄像头
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗并停止视频播放
|
||||
const closeDialog = () => {
|
||||
if (currentCamera.value) {
|
||||
handleStopStream(currentCamera.value);
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const selectCameraById = (cameraId) => {
|
||||
const camera = cameras.value.find(c => c.id === cameraId);
|
||||
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
|
||||
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗并开始播放
|
||||
const openDialog = async (camera) => {
|
||||
currentCamera.value = camera;
|
||||
dialogVisible.value = true;
|
||||
await nextTick();
|
||||
startStreamInDialog(camera);
|
||||
};
|
||||
|
||||
// 在弹窗中播放视频
|
||||
const startStreamInDialog = async (camera) => {
|
||||
const canvas = dialogCanvas.value;
|
||||
|
||||
if (!camera || !canvas) {
|
||||
console.error('未找到对应的 canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('alertToken');
|
||||
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://${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, {
|
||||
canvas: canvas,
|
||||
autoplay: true,
|
||||
videoBufferSize: 15 * 1024 * 1024,
|
||||
audioBufferSize: 5 * 1024 * 1024,
|
||||
});
|
||||
camera.player = player;
|
||||
} else {
|
||||
console.error('JSMpeg 未加载');
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
currentCamera.value = null;
|
||||
};
|
||||
|
||||
const handleStopStream = async (camera) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
await apiInstance.stopCameraStream(token, camera.id);
|
||||
camera.playing = false;
|
||||
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
camera.player = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止视频流失败:', error);
|
||||
} catch (error) {
|
||||
console.error('启动视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗并停止视频播放
|
||||
const closeDialog = () => {
|
||||
if (currentCamera.value) {
|
||||
handleStopStream(currentCamera.value);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
currentCamera.value = null;
|
||||
};
|
||||
|
||||
const handleStopStream = async (camera) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
await apiInstance.stopCameraStream(token, camera.id);
|
||||
camera.playing = false;
|
||||
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
camera.player = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeStream = (camera) => {
|
||||
handleStopStream(camera);
|
||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCameras();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectedCameras.value.forEach(camera => {
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const closeStream = (camera) => {
|
||||
handleStopStream(camera);
|
||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCameras();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectedCameras.value.forEach(camera => {
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 5vw;
|
||||
margin-left: 1vh;
|
||||
margin-top: 1vh;
|
||||
|
||||
}
|
||||
::v-deep .status-filter .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__wrapper{
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.camera-select {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 12vw;
|
||||
margin-left: 0vh;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.top-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
width: 7vw;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0 0 0 10vw;
|
||||
/* justify-content: center; */
|
||||
align-content: center;
|
||||
background-color: #001529;
|
||||
line-height: 0px;
|
||||
color: aliceblue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.camera-name-title {
|
||||
padding: 0.2vh;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
margin: 0vh 1vh;
|
||||
padding: 0 0 2vh 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vh 0vh;
|
||||
width: 34vw;
|
||||
height: 39vh;
|
||||
max-height: 45vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
.camera-grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
margin: 1vh;
|
||||
position: relative;
|
||||
height: 17vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.camera-snapshot,
|
||||
.camera-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #000000;
|
||||
color: aliceblue;
|
||||
padding: 0 2px 2px 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.play-button-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 5vw;
|
||||
margin-left: 1vh;
|
||||
margin-top: 1vh;
|
||||
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__wrapper {
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__wrapper {
|
||||
background-color: #001529;
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::v-deep .camera-select .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
::v-deep .status-filter .el-select__selected-item {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.camera-select {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 12vw;
|
||||
margin-left: 0vh;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.top-text {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
width: 7vw;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0 0 0 10vw;
|
||||
/* justify-content: center; */
|
||||
align-content: center;
|
||||
background-color: #001529;
|
||||
line-height: 0px;
|
||||
color: aliceblue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stream-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.camera-name-title {
|
||||
padding: 0.2vh;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
margin: 0vh 1vh;
|
||||
padding: 0 0 2vh 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vh 0vh;
|
||||
width: 34vw;
|
||||
height: 39vh;
|
||||
max-height: 45vh;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
.camera-grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
margin: 1vh;
|
||||
position: relative;
|
||||
height: 17vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.camera-snapshot,
|
||||
.camera-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: #000000;
|
||||
color: aliceblue;
|
||||
padding: 0 2px 2px 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.play-button-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
|
||||
<el-row class="table-row">
|
||||
<el-col :span="24" class="table-col">
|
||||
<div class="table-container">
|
||||
|
@ -51,28 +51,25 @@
|
|||
</div>
|
||||
</el-col>
|
||||
</el-row> -->
|
||||
|
||||
|
||||
<!-- <div class="adjusted-widths-display">
|
||||
当前列宽: {{ adjustedWidths.join(', ') }}
|
||||
</div> -->
|
||||
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="告警详情" width="50%" class="dialog-container">
|
||||
<div>
|
||||
<el-row class="dialog-row">
|
||||
<!-- 左侧告警图片和图片信息 -->
|
||||
<el-col :span="12" class="dialog-left">
|
||||
<el-row gutter class="dialog-image-container">
|
||||
<!-- 根据 mediumType 显示视频或图片,确保只显示一种或两种 -->
|
||||
<template v-if="hasSnapshot">
|
||||
<el-image :src="snapshotFile"></el-image>
|
||||
</template>
|
||||
<template v-if="hasVideo">
|
||||
<!-- <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">
|
||||
|
@ -101,7 +98,7 @@
|
|||
<!-- <el-col :span="24">
|
||||
<p>备注: {{ selectedRow.remark }}</p>
|
||||
</el-col> -->
|
||||
|
||||
|
||||
<!-- <el-col :span="24">
|
||||
<el-form-item style="width: 90%;">
|
||||
<el-input v-model="remark" placeholder="请描述事件原因" type="textarea" rows="5" :disabled="true"></el-input>
|
||||
|
@ -113,50 +110,50 @@
|
|||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
|
||||
const boxApi = new BoxApi();
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const remark = ref("");
|
||||
const selectedRow = ref({});
|
||||
const typeMapping = reactive({});
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0);
|
||||
const token = ref(null);
|
||||
const apiInstance = new BoxApi();
|
||||
const statusMapping = {
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
|
||||
const boxApi = new BoxApi();
|
||||
const tableData = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const remark = ref("");
|
||||
const selectedRow = ref({});
|
||||
const typeMapping = reactive({});
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const totalItems = ref(0);
|
||||
const displayTotalItems = ref(0);
|
||||
const token = ref(null);
|
||||
const apiInstance = new BoxApi();
|
||||
const statusMapping = {
|
||||
'pending': '待处理',
|
||||
'assigned': '处理中',
|
||||
'closed': '已处理'
|
||||
};
|
||||
const duration = ref('');
|
||||
|
||||
const hasSnapshot = ref(false);
|
||||
const hasVideo = ref(false);
|
||||
const snapshotFile = ref("");
|
||||
const videoFile = ref("");
|
||||
const originalWidths = [97, 150, 160]; // 默认宽度
|
||||
const adjustedWidths = ref([...originalWidths]);
|
||||
const baseWidth = 2150;
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
};
|
||||
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 formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
const adjustColumnWidths = () => {
|
||||
};
|
||||
|
||||
|
||||
const adjustColumnWidths = () => {
|
||||
const currentWidth = window.innerWidth;
|
||||
// console.log(">>>>>>>>>>", currentWidth);
|
||||
const scale = currentWidth / baseWidth;
|
||||
// console.log(">>>>>>>>>>", scale);
|
||||
|
||||
|
||||
// const adjustedScale = Math.max(scale, 0.5);
|
||||
adjustedWidths.value = originalWidths.map(width => {
|
||||
return currentWidth < baseWidth
|
||||
|
@ -164,19 +161,19 @@ const adjustColumnWidths = () => {
|
|||
: width * scale; // 放大
|
||||
});
|
||||
// nextTick(() => {
|
||||
|
||||
|
||||
// });
|
||||
};
|
||||
|
||||
// const tableConfig = ref({
|
||||
// header: ['告警类型', '告警位置', '告警时间'],
|
||||
// data: [],
|
||||
// rowNum: 5,
|
||||
// columnWidth: [100, 160, 190],
|
||||
// carousel: 'single',
|
||||
// });
|
||||
|
||||
const fetchTypeMapping = async (token) => {
|
||||
};
|
||||
|
||||
// const tableConfig = ref({
|
||||
// header: ['告警类型', '告警位置', '告警时间'],
|
||||
// data: [],
|
||||
// rowNum: 5,
|
||||
// columnWidth: [100, 160, 190],
|
||||
// carousel: 'single',
|
||||
// });
|
||||
|
||||
const fetchTypeMapping = async (token) => {
|
||||
try {
|
||||
const algorithms = await boxApi.getAlgorithms(token);
|
||||
// console.log("Algorithms:", algorithms);
|
||||
|
@ -186,29 +183,29 @@ 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;
|
||||
let allEvents = [];
|
||||
let currentPage = 1;
|
||||
|
||||
|
||||
const { endOfToday, startOfToday } = getDateParams();
|
||||
|
||||
|
||||
const timeBefore = formatDateTimeToISO(endOfToday);
|
||||
const timeAfter = formatDateTimeToISO(startOfToday);
|
||||
// console.log("Start of today:", timeAfter);
|
||||
// const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
const firstResponse = await apiInstance.getEventsByParams(token, limit, currentPage,timeBefore, timeAfter);
|
||||
// console.log("Total items:", firstResponse);
|
||||
|
||||
|
||||
const total = firstResponse.count;
|
||||
totalItems.value = total;
|
||||
|
||||
|
||||
allEvents = firstResponse.results;
|
||||
|
||||
|
||||
while (allEvents.length < total) {
|
||||
currentPage += 1;
|
||||
const response = await apiInstance.getEventsByParams(token, limit, currentPage);
|
||||
|
@ -224,19 +221,19 @@ 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);
|
||||
|
||||
|
||||
return {
|
||||
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,28 +242,28 @@ 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);
|
||||
|
||||
|
||||
// 重置媒体类型
|
||||
hasSnapshot.value = false;
|
||||
hasVideo.value = false;
|
||||
snapshotFile.value = "";
|
||||
videoFile.value = "";
|
||||
|
||||
|
||||
// 判断并设置媒体文件路径
|
||||
row.mediums.forEach((medium) => {
|
||||
if (medium.name === "snapshot") {
|
||||
|
@ -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;
|
||||
// };
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
};
|
||||
|
||||
// const calculateColumnWidths = () => {
|
||||
// const containerWidth = document.querySelector('.table-container').clientWidth;
|
||||
// };
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
token.value = localStorage.getItem('alertToken');
|
||||
await fetchTypeMapping(token.value);
|
||||
await fetchEvents();
|
||||
await adjustColumnWidths();
|
||||
window.addEventListener('resize', adjustColumnWidths);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
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>
|
||||
|
|
@ -1,377 +1,382 @@
|
|||
<template>
|
||||
<div class="count-container">
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
<div class="count-container">
|
||||
<el-tabs v-model="activeTab" class="tab-div">
|
||||
<el-tab-pane label="所有" name="all"></el-tab-pane>
|
||||
<el-tab-pane label="今天" name="today"></el-tab-pane>
|
||||
<el-tab-pane label="7天" name="week"></el-tab-pane>
|
||||
<el-tab-pane label="30天" name="month"></el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-row class="top-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
通道总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOnline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
在线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOnlineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<CameraOffline />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
离线:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ cameraOfflineCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="bottom-row">
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventAll />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
事件总数:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ eventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventClosed />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
已处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ closedEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div>
|
||||
<el-row>
|
||||
<!-- 左侧占据一整行 -->
|
||||
<el-col :sm="24" :md="8">
|
||||
<EventPending />
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
<!-- 右侧分为两行 -->
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
未处理:
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24" class="inner-count-text">
|
||||
{{ pendingEventCount }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import CameraAll from '@/icons/CameraAll.vue';
|
||||
import CameraOnline from '@/icons/CameraOnline.vue';
|
||||
import CameraOffline from '@/icons/CameraOffline.vue';
|
||||
import EventAll from '@/icons/EventAll.vue';
|
||||
import EventClosed from '@/icons/EventClosed.vue';
|
||||
import EventPending from '@/icons/EventPending.vue';
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
const cameraCount = ref(0);
|
||||
const cameraOfflineCount = ref(0);
|
||||
const cameraOnlineCount = ref(0);
|
||||
const eventCount = ref(0);
|
||||
const pendingEventCount = ref(0);
|
||||
const closedEventCount = ref(0);
|
||||
|
||||
const activeTab = ref('all');
|
||||
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
<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 activeTab = ref('all');
|
||||
|
||||
const getTodayData = () => {
|
||||
const today = new Date();
|
||||
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfToday),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getWeekData = () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfWeek),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
};
|
||||
|
||||
const getMonthData = () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
const endOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
|
||||
|
||||
return {
|
||||
timeAfter: formatDateTime(startOfMonth),
|
||||
timeBefore: formatDateTime(endOfToday),
|
||||
};
|
||||
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
// console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.count-container{
|
||||
width:100%;
|
||||
height:100%;
|
||||
margin:1vh;
|
||||
padding: 2vh 0;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTimeToISO = (datetime) => {
|
||||
return new Date(datetime).toISOString().replace('.000', '');
|
||||
};
|
||||
|
||||
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
const formatDateTime = (datetime) => {
|
||||
const date = new Date(datetime);
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const limit = 20;
|
||||
let offset = 0;
|
||||
let allCameras = [];
|
||||
|
||||
const firstResponse = await apiInstance.getCameras(limit, offset, token);
|
||||
cameraCount.value = firstResponse.count;
|
||||
// console.log("总数》》》》》》》》》》》》》", cameraCount.value)
|
||||
allCameras = firstResponse.results;
|
||||
|
||||
|
||||
const total = cameraCount.value;
|
||||
while (offset + limit < total) {
|
||||
offset += limit;
|
||||
const response = await apiInstance.getCameras(limit, offset, token);
|
||||
allCameras = allCameras.concat(response.results);
|
||||
}
|
||||
|
||||
allCameras.forEach((camera) => {
|
||||
if (camera.status === 'online') {
|
||||
cameraOnlineCount.value++;
|
||||
} else if (camera.status === 'offline') {
|
||||
cameraOfflineCount.value++;
|
||||
} else {
|
||||
exceptionCount.value++;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching cameras:', error);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
};
|
||||
|
||||
|
||||
// const fetchEvents = async (timeAfter = null, timeBefore = null) => {
|
||||
// try {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
|
||||
// const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter);
|
||||
// eventCount.value = totalResponse.count;
|
||||
|
||||
// const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'pending');
|
||||
// pendingEventCount.value = pendingResponse.count;
|
||||
|
||||
// const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeBefore, timeAfter, null, null, 'closed');
|
||||
// closedEventCount.value = closedResponse.count;
|
||||
|
||||
// } catch (error) {
|
||||
// console.error('获取事件数据失败:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
let timeRange = { timeAfter: null, timeBefore: null };
|
||||
|
||||
// 根据 activeTab 设置时间范围
|
||||
if (activeTab.value === 'today') {
|
||||
timeRange = getTodayData();
|
||||
} else if (activeTab.value === 'week') {
|
||||
timeRange = getWeekData();
|
||||
} else if (activeTab.value === 'month') {
|
||||
timeRange = getMonthData();
|
||||
}
|
||||
|
||||
// 获取告警总数
|
||||
const totalResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter);
|
||||
eventCount.value = totalResponse.count;
|
||||
|
||||
// 获取状态为 pending 的告警数量
|
||||
const pendingResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'pending');
|
||||
pendingEventCount.value = pendingResponse.count;
|
||||
|
||||
// 获取状态为 closed 的告警数量
|
||||
const closedResponse = await apiInstance.getEventsByParams(token, 1, 1, timeRange.timeBefore, timeRange.timeAfter, null, null, 'closed');
|
||||
closedEventCount.value = closedResponse.count;
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取事件数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeTab, (newTab) => {
|
||||
fetchEvents();
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
// getTodayData();
|
||||
// getWeekData();
|
||||
// getMonthData();
|
||||
fetchCameras();
|
||||
fetchEvents();
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
.tab-div{
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.count-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 1vh;
|
||||
padding: 2vh 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.top-row,
|
||||
.bottom-row {
|
||||
background-color: #001529;
|
||||
color: aliceblue;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.inner-count-text {
|
||||
color: rgb(91, 224, 241);
|
||||
}
|
||||
|
||||
.tab-div {
|
||||
background-color: #001529;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
margin-left: 1vh;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
color: #2ea0ec;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.el-tabs__active-bar {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
/* width: 15vw; */
|
||||
position: static !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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)',
|
||||
}
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
stillShowZeroSum: false,
|
||||
}
|
||||
]
|
||||
};
|
||||
chart.value.setOption(option);
|
||||
};
|
||||
if (endTime <= startTime) {
|
||||
ElMessage.error("开始时间必须小于结束时间");
|
||||
timeSlots[index][1] = "00:00"; // 恢复为默认值
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
const updateChart = () => {
|
||||
if (chart.value && typeof chart.value.setOption === 'function') {
|
||||
chart.value.setOption({
|
||||
series: [
|
||||
{
|
||||
data: seriesData.value
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
console.error("ECharts instance is not initialized properly");
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(async () => {
|
||||
// 添加时间段
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果校验失败,阻止保存
|
||||
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', '');
|
||||
};
|
||||
|
@ -277,8 +305,8 @@ const exportData = async () => {
|
|||
saveAs(blob, 'Alert_data.csv');
|
||||
} catch (error) {
|
||||
console.error("导出数据错误:", error);
|
||||
}finally {
|
||||
isExporting.value = false;
|
||||
} 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%;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -614,7 +614,7 @@ onBeforeUnmount(() => {
|
|||
align-items: center;
|
||||
height: 15vh;
|
||||
/* background-color: #4587dd; */
|
||||
background: repeating-linear-gradient(-45deg, rgba(0, 21, 41), rgba(58, 61, 214, 0.3) 100px);
|
||||
background: repeating-linear-gradient(-45deg, rgba(0, 21, 41), rgba(58, 61, 214, 0.3) 100px);
|
||||
gap: 1vw;
|
||||
padding: 0 2vw;
|
||||
margin: 2vh 0vw 2vh 3vw;
|
||||
|
|
|
@ -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-checkbox>
|
||||
<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