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

372
README.md
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 959 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -35,6 +35,7 @@ import { BoxApi } from '@/utils/boxApi.ts';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
const activeName = ref('first'); const activeName = ref('first');
const hourlyCounts = ref(Array(24).fill(0)); const hourlyCounts = ref(Array(24).fill(0));
const dailyCounts = ref(Array(7).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(); timeAfter = dayjs().startOf('day').add(i, 'hour').format();
timeBefore = dayjs().startOf('day').add(i + 1, 'hour').format(); timeBefore = dayjs().startOf('day').add(i + 1, 'hour').format();
hourlyCounts.value[i] = await getEventCount(timeBefore, timeAfter); 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)); delayedInitChart(todayLineChartDom, createChartOption('今日告警趋势', Array.from({ length: 24 }, (_, i) => `${i}:00`), hourlyCounts.value));

View File

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

View File

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

View File

@ -72,7 +72,7 @@
const fetchCameras = async () => { const fetchCameras = async () => {
try { try {
const token = localStorage.getItem('alertToken'); const token = localStorage.getItem('alertToken');
const cameraData = await apiInstance.getMinCameras(token); const cameraData = await apiInstance.getAllCameras(token);
// filterStatus // filterStatus
if (filterStatus.value === "online") { if (filterStatus.value === "online") {
@ -112,13 +112,21 @@
} }
const token = localStorage.getItem('alertToken'); const token = localStorage.getItem('alertToken');
const rememberedAddress = localStorage.getItem('rememberedAddress');
if (!rememberedAddress) {
alert('主机地址获取异常,请确认地址配置');
console.error('主机地址获取异常');
return;
}
try { try {
const response = await apiInstance.startCameraStream(token, camera.id); const response = await apiInstance.startCameraStream(token, camera.id);
camera.streamPort = response.port; camera.streamPort = response.port;
camera.playing = true; camera.playing = true;
const url = `ws://192.168.28.33:${camera.streamPort}/`; const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
console.log('播放路径:', url); // const url = `ws://192.168.28.33:${camera.streamPort}/`;
// console.log(':', url);
if (window.JSMpeg) { if (window.JSMpeg) {
const player = new window.JSMpeg.Player(url, { const player = new window.JSMpeg.Player(url, {
@ -200,6 +208,7 @@
margin-top: 1vh; margin-top: 1vh;
} }
::v-deep .status-filter .el-select__wrapper { ::v-deep .status-filter .el-select__wrapper {
background-color: #001529; background-color: #001529;
box-shadow: 0 0 0 0 !important; box-shadow: 0 0 0 0 !important;

View File

@ -59,20 +59,17 @@
<el-dialog v-model="dialogVisible" title="告警详情" width="50%" class="dialog-container"> <el-dialog v-model="dialogVisible" title="告警详情" width="50%" class="dialog-container">
<div> <div>
<el-row class="dialog-row"> <el-row class="dialog-row">
<!-- 左侧告警图片和图片信息 -->
<el-col :span="12" class="dialog-left"> <el-col :span="12" class="dialog-left">
<el-row gutter class="dialog-image-container"> <el-row gutter class="dialog-image-container">
<!-- 根据 mediumType 显示视频或图片确保只显示一种或两种 -->
<template v-if="hasSnapshot"> <template v-if="hasSnapshot">
<el-image :src="snapshotFile"></el-image> <el-image :src="snapshotFile"></el-image>
</template> </template>
<template v-if="hasVideo"> <!-- <template v-if="hasVideo">
<video :src="videoFile" controls></video> <video :src="videoFile" controls></video>
</template> </template> -->
</el-row> </el-row>
</el-col> </el-col>
<!-- 右侧告警信息 -->
<el-col :span="11" class="dialog-right"> <el-col :span="11" class="dialog-right">
<el-row> <el-row>
<el-col :span="24"> <el-col :span="24">

View File

@ -207,6 +207,11 @@
}; };
}; };
const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', '');
};
const formatDateTime = (datetime) => { const formatDateTime = (datetime) => {
const date = new Date(datetime); const date = new Date(datetime);
const year = date.getFullYear(); const year = date.getFullYear();

View File

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

View File

@ -14,7 +14,7 @@
</span> </span>
</el-row> --> </el-row> -->
<el-row class="filter-row"> <el-row class="filter-row">
<el-col :span="4"> <el-col :span="5">
<el-form-item label="摄像头名称"> <el-form-item label="摄像头名称">
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable clearable> <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"> <el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
@ -22,14 +22,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="3"> <el-col :span="4">
<el-form-item label="告警类型"> <el-form-item label="告警类型">
<el-select v-model="filterParams.types" placeholder="请选择类型" clearable> <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-option v-for="(label, key) in typeMapping" :key="key" :label="label" :value="key"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="3"> <el-col :span="4">
<el-form-item label="处理情况"> <el-form-item label="处理情况">
<el-select v-model="filterParams.status" placeholder="全部" clearable> <el-select v-model="filterParams.status" placeholder="全部" clearable>
<el-option label="全部" value=""></el-option> <el-option label="全部" value=""></el-option>
@ -38,29 +38,36 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="5">
<el-form-item label="开始时间"> <el-form-item label="开始时间">
<el-date-picker v-model="filterParams.timeAfter" type="datetime" placeholder="请选择开始时间"></el-date-picker> <el-date-picker v-model="filterParams.timeAfter" type="datetime" placeholder="请选择开始时间"></el-date-picker>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="4"> <el-col :span="5">
<el-form-item label="结束时间"> <el-form-item label="结束时间">
<el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker> <el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker>
</el-form-item> </el-form-item>
</el-col> </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 type="primary" @click="handleFilter">查询</el-button>
<el-button @click="handleReset">重置</el-button> <el-button @click="handleReset">重置</el-button>
<el-button :disabled="isExporting" @click="exportData"> <el-button :disabled="isExporting" @click="exportData">
{{ isExporting ? '正在导出,请勿重复点击' : '导出' }} {{ isExporting ? '正在导出,请勿重复点击' : '导出' }}
</el-button> </el-button>
</el-col> </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>
<el-row class="table-row"> <el-row class="table-row">
<el-col :span="24" class="table-col"> <el-col :span="24" class="table-col">
<div class="table-container"> <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 prop="id" label="告警编号" min-width="100"></el-table-column>
<el-table-column label="告警类型" min-width="150"> <el-table-column label="告警类型" min-width="150">
<template v-slot="scope"> <template v-slot="scope">
@ -141,12 +148,13 @@
</div> </div>
</div> </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> <p>告警关联视频</p>
<video :src="videoFile" controls @click="openMediaDialog('video', videoFile)"></video> <video :src="videoFile" controls @click="openMediaDialog('video', videoFile)"></video>
</div> </div> -->
<!-- 告警关联图片 --> <!-- 告警关联图片 -->
<div v-if="hasSnapshot" class="media-container image-item"> <div v-if="hasSnapshot" class="media-container image-item">
@ -223,6 +231,26 @@ const totalItems = ref(0);
const displayTotalItems = ref(0); // const displayTotalItems = ref(0); //
const cameras = ref([]); 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) => { const formatDateTimeToISO = (datetime) => {
return new Date(datetime).toISOString().replace('.000', ''); return new Date(datetime).toISOString().replace('.000', '');
}; };
@ -326,8 +354,6 @@ const submitStatusUpdate = async (newStatus, row = null) => {
// console.console.log(row,row.id); // console.console.log(row,row.id);
const eventId = row ? row.id : selectedRow.value.id; const eventId = row ? row.id : selectedRow.value.id;
const remarkContent = remark.value && remark.value.trim() !== "" ? remark.value : null; const remarkContent = remark.value && remark.value.trim() !== "" ? remark.value : null;
// setEventStatus
await boxApi.setEventStatus(eventId, newStatus, remarkContent); await boxApi.setEventStatus(eventId, newStatus, remarkContent);
@ -551,7 +577,7 @@ onMounted(async () => {
justify-content: center; justify-content: center;
align-content: center; align-content: center;
height: 15vh; height: 15vh;
padding: 0px 0; padding: 0 0;
margin: 5vh 6vw 5vh 1vw; margin: 5vh 6vw 5vh 1vw;
gap: 8px; gap: 8px;
/* font-weight: bold; */ /* font-weight: bold; */
@ -594,7 +620,8 @@ onMounted(async () => {
.event-media { .event-media {
display: flex; display: flex;
justify-content: space-between; justify-content: center;
/* justify-content: space-between; */
flex-wrap: wrap; flex-wrap: wrap;
width: 100%; width: 100%;
} }

View File

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

View File

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

View File

@ -1,160 +1,144 @@
<template> <template>
<div class="settings-container"> <div class="settings-container">
<el-row class="popup-row"> <el-row class="popup-row">
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange" style="color: aliceblue;"> <el-col :sm="24" :md="24">弹窗设置</el-col>
开启弹窗 <el-col :sm="24" :md="24">
<el-checkbox v-model="isInteractivePopupEnabled" @change="handleInteractiveChange" style="color: aliceblue;">
开启交互式弹窗
</el-checkbox> </el-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>
<el-row class="channel-row"> <el-row class="channel-row">
<Channel /> <Channel />
</el-row> </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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, inject, onMounted } from 'vue'; import { ref, inject, onMounted } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus'; 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 Channel from '@/components/Channel.vue';
import { setDialogHandler } from '../utils/useGlobalWebSocket';
import { BoxApi } from '@/utils/boxApi';
// const apiInstance = new BoxApi(); const isInteractivePopupEnabled = ref(false); //
const isResponsivePopupEnabled = ref(false); //
const isPopupEnabled = ref(false);
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket'); 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) { if (!globalWebSocket) {
throw new Error('globalWebSocket 注入失败'); throw new Error('globalWebSocket 注入失败');
} }
// localStorage WebSocket //
onMounted(async () => { onMounted(() => {
const storedState = localStorage.getItem('isPopupEnabled'); isInteractivePopupEnabled.value = localStorage.getItem('isInteractivePopupEnabled') === 'true';
isPopupEnabled.value = storedState === 'true'; isResponsivePopupEnabled.value = localStorage.getItem('isResponsivePopupEnabled') === 'true';
if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) { updateWebSocketConnection();
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);
// }
}); });
// // const handleInteractiveChange = () => {
const handleCheckboxChange = () => { // if (isInteractivePopupEnabled.value) {
if (isPopupEnabled.value) { // isResponsivePopupEnabled.value = false;
ElMessageBox.confirm('是否开启弹窗提示?', '提示', { // 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: '是', confirmButtonText: '是',
cancelButtonText: '否', cancelButtonText: '否',
type: 'warning', type: 'warning',
}) })
.then(() => { .then(() => {
localStorage.setItem('isPopupEnabled', 'true'); isResponsivePopupEnabled.value = false;
globalWebSocket.connectWebSocket(); localStorage.setItem('isResponsivePopupEnabled', 'false');
localStorage.setItem('isInteractivePopupEnabled', 'true');
updateWebSocketConnection();
}) })
.catch(() => { .catch(() => {
isPopupEnabled.value = false; isInteractivePopupEnabled.value = false;
localStorage.setItem('isPopupEnabled', 'false');
}); });
} else { } else {
globalWebSocket.closeWebSocket(); ElMessageBox.confirm('是否关闭交互式弹窗?', '确认提示', {
localStorage.setItem('isPopupEnabled', 'false'); confirmButtonText: '是',
cancelButtonText: '否',
type: 'warning',
})
.then(() => {
localStorage.setItem('isInteractivePopupEnabled', 'false');
updateWebSocketConnection();
})
.catch(() => {
isInteractivePopupEnabled.value = true;
});
} }
}; };
// const showDialog = async (data: any) => { const handleResponsiveChange = () => {
// try { if (isResponsivePopupEnabled.value) {
// const token = localStorage.getItem('alertToken'); ElMessageBox.confirm('是否开启响应式弹窗提示?', '确认提示', {
// const eventDetails = await apiInstance.getEventById(data.id, token); 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'); // WebSocket
// const video = eventDetails.mediums.find((item: any) => item.name === 'video'); const updateWebSocketConnection = () => {
if (isInteractivePopupEnabled.value || isResponsivePopupEnabled.value) {
// dialogContent.value = { globalWebSocket.connectWebSocket();
// id: eventDetails.id, } else {
// camera_id: eventDetails.camera_id, globalWebSocket.closeWebSocket();
// 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;
// };
</script> </script>
<style scoped> <style scoped>
.settings-container { .settings-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #ffffff; background-color: #F1F1F1;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@ -165,7 +149,7 @@ const handleCheckboxChange = () => {
width: 80vw; width: 80vw;
padding: 1vh 1vw; padding: 1vh 1vw;
margin: 1vh 2vw; margin: 1vh 2vw;
background-color: #ffffff; background-color: #001529;
border-radius: 8px; border-radius: 8px;
color: white; color: white;
} }

View File

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