latest-V.1
This commit is contained in:
parent
62a7971f7a
commit
3e4e455dbe
24
README.md
24
README.md
|
@ -624,6 +624,30 @@ public async updateRule(token: string | null = null, rules: RuleData[]): Promise
|
|||
|
||||
|
||||
|
||||
### 2024.11.26
|
||||
|
||||
- 对话框大图显示
|
||||
- 删除告警按钮
|
||||
- 主页定时刷新
|
||||
- 布局滚动查看调整
|
||||
- 用户删除限制
|
||||
|
||||
|
||||
|
||||
### 2024.12.3
|
||||
|
||||
- 告警设置
|
||||
- 消息队列设置
|
||||
- 声音设置
|
||||
- 弹窗动画添加
|
||||
- 弹窗队列添加,错误重试机制(2s*5次)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
114
src/App.vue
114
src/App.vue
|
@ -1,119 +1,14 @@
|
|||
<template>
|
||||
<div id="app" class="app-container">
|
||||
<router-view></router-view>
|
||||
<el-dialog title="告警提示" v-model="globalDialogVisible" width="50%" @close="handleDialogClose">
|
||||
<el-row style="margin-bottom: 2vh;">
|
||||
<el-col :span="17" style="text-align: center;">
|
||||
<img :src="globalDialogContent.snapshotUrl" alt="告警图片" v-if="globalDialogContent.snapshotUrl"
|
||||
style="max-width: 100%;" />
|
||||
<!-- 可选的视频展示 -->
|
||||
<!-- <video v-if="globalDialogContent.videoUrl" :src="globalDialogContent.videoUrl" controls style="max-width: 100%;"></video> -->
|
||||
</el-col>
|
||||
<el-col :span="7" >
|
||||
<el-row class="dialog-event-col">
|
||||
<el-col :span="24"><strong >告警编号</strong>:{{ globalDialogContent.id }}</el-col>
|
||||
<el-col :span="24"><strong >告警点位</strong>:{{ globalDialogContent.camera?.name }}</el-col>
|
||||
<el-col :span="24"><strong >告警类型</strong>:{{ globalDialogContent.types }}</el-col>
|
||||
<el-col :span="24"><strong >告警时间</strong>:{{ globalDialogContent.started_at }}</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
<GlobalDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface GlobalDialogContent {
|
||||
id: number | null;
|
||||
camera_id: number | null;
|
||||
camera: { name: string };
|
||||
types: string | null;
|
||||
started_at: string | null; // 支持 null 或字符串类型
|
||||
snapshotUrl: string;
|
||||
videoUrl: string;
|
||||
}
|
||||
import GlobalDialog from '@/components/GlobalDialog.vue';
|
||||
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
const globalDialogVisible = ref(false); // 控制全局对话框的可见性
|
||||
const globalDialogContent = ref<GlobalDialogContent>({
|
||||
id: null,
|
||||
camera_id: null,
|
||||
camera: { name: '' },
|
||||
types: null,
|
||||
started_at: null,
|
||||
snapshotUrl: '',
|
||||
videoUrl: ''
|
||||
});
|
||||
const algorithmMap = ref(new Map()); // 算法类型映射表
|
||||
|
||||
// 加载算法映射表
|
||||
const loadAlgorithms = async () => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
if (token) {
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
algorithmMap.value = new Map(
|
||||
algorithms.map((algo: { code_name: string; name: string }) => [algo.code_name, algo.name])
|
||||
);
|
||||
} else {
|
||||
console.error('Token 未找到,请登录');
|
||||
}
|
||||
};
|
||||
|
||||
// 显示告警详情对话框
|
||||
const showDialog = async (data: any) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
if (!token) {
|
||||
console.error('Token 未找到,请登录');
|
||||
return;
|
||||
}
|
||||
console.log('showDialog>>>>>>>>>>>>>', data);
|
||||
// 获取告警事件的详细信息
|
||||
const eventDetails = await apiInstance.getEventById(data.id, token);
|
||||
const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
|
||||
const video = eventDetails.mediums.find((item: any) => item.name === 'video');
|
||||
|
||||
// 更新对话框内容
|
||||
globalDialogContent.value = {
|
||||
id: eventDetails.id,
|
||||
camera_id: eventDetails.camera_id,
|
||||
camera: eventDetails.camera,
|
||||
types: algorithmMap.value.get(eventDetails.types),
|
||||
started_at: formatDateTime(eventDetails.started_at) ,
|
||||
snapshotUrl: snapshot?.file || '',
|
||||
videoUrl: video?.file || ''
|
||||
};
|
||||
|
||||
globalDialogVisible.value = true; // 显示对话框
|
||||
};
|
||||
|
||||
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 关闭对话框并重置内容
|
||||
const handleDialogClose = () => {
|
||||
globalDialogVisible.value = false;
|
||||
globalDialogContent.value = {
|
||||
id: null,
|
||||
camera_id: null,
|
||||
camera: { name: '' },
|
||||
types: null,
|
||||
started_at: null,
|
||||
snapshotUrl: '',
|
||||
videoUrl: ''
|
||||
};
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
loadAlgorithms(); // 加载算法数据
|
||||
eventBus.on('showDialog', showDialog); // 监听事件总线的 showDialog 事件
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -123,9 +18,4 @@ onMounted(() => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.dialog-event-col{
|
||||
font-size: 14px;
|
||||
gap: 30px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
body{
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
<template>
|
||||
<el-dialog title="告警提示" v-model="globalDialogVisible" width="50%" @close="handleDialogClose">
|
||||
<el-row style="margin-bottom: 2vh;">
|
||||
<el-col :span="17" style="text-align: center;">
|
||||
<img :src="globalDialogContent.snapshotUrl" alt="告警图片" v-if="globalDialogContent.snapshotUrl"
|
||||
style="max-width: 100%;" />
|
||||
<!-- 可选的视频展示 -->
|
||||
<!-- <video v-if="globalDialogContent.videoUrl" :src="globalDialogContent.videoUrl" controls style="max-width: 100%;"></video> -->
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<el-row class="dialog-event-col">
|
||||
<el-col :span="24"><strong>告警编号</strong>:{{ globalDialogContent.id }}</el-col>
|
||||
<el-col :span="24"><strong>告警点位</strong>:{{ globalDialogContent.camera?.name }}</el-col>
|
||||
<el-col :span="24"><strong>告警类型</strong>:{{ globalDialogContent.types }}</el-col>
|
||||
<el-col :span="24"><strong>告警时间</strong>:{{ globalDialogContent.started_at }}</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted,watch } from 'vue';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface GlobalDialogContent {
|
||||
id: number | null;
|
||||
camera_id: number | null;
|
||||
camera: { name: string };
|
||||
types: string | null;
|
||||
started_at: string | null; // 支持 null 或字符串类型
|
||||
snapshotUrl: string;
|
||||
videoUrl: string;
|
||||
}
|
||||
|
||||
const apiInstance = new BoxApi();
|
||||
|
||||
const globalDialogVisible = ref(false); // 控制全局对话框的可见性
|
||||
const globalDialogContent = ref<GlobalDialogContent>({
|
||||
id: null,
|
||||
camera_id: null,
|
||||
camera: { name: '' },
|
||||
types: null,
|
||||
started_at: null,
|
||||
snapshotUrl: '',
|
||||
videoUrl: ''
|
||||
});
|
||||
const algorithmMap = ref(new Map()); // 算法类型映射表
|
||||
|
||||
|
||||
const requestQueue: any[] = []; // 消息队列
|
||||
let isProcessing = false; // 标志是否正在处理队列
|
||||
|
||||
// 加载算法映射表
|
||||
const loadAlgorithms = async () => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
if (token) {
|
||||
const algorithms = await apiInstance.getAlgorithms(token);
|
||||
algorithmMap.value = new Map(
|
||||
algorithms.map((algo: { code_name: string; name: string }) => [algo.code_name, algo.name])
|
||||
);
|
||||
} else {
|
||||
console.error('Token 未找到,请登录');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchWithRetry = async (fetchFn: () => Promise<any>, retries = 5, delay = 2000): Promise<any> => {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await fetchFn();
|
||||
} catch (error) {
|
||||
console.error(`Retry ${i + 1} failed:`, error);
|
||||
if (i < retries - 1) await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
throw new Error('All retries failed');
|
||||
};
|
||||
|
||||
|
||||
// const showDialog = async (data: any) => {
|
||||
// const token = localStorage.getItem('alertToken');
|
||||
// if (!token) {
|
||||
// console.error('Token 未找到,请登录');
|
||||
// return;
|
||||
// }
|
||||
// console.log('弹窗接收>>>>>>>>>>>>>', data);
|
||||
|
||||
// const eventDetails = await apiInstance.getEventById(data.id, token);
|
||||
// console.log('showDialog>>>>>>>>>>>>>', eventDetails);
|
||||
// const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
|
||||
// const video = eventDetails.mediums.find((item: any) => item.name === 'video');
|
||||
|
||||
// globalDialogContent.value = {
|
||||
// id: eventDetails.id,
|
||||
// camera_id: eventDetails.camera_id,
|
||||
// camera: eventDetails.camera,
|
||||
// types: algorithmMap.value.get(eventDetails.types),
|
||||
// started_at: formatDateTime(eventDetails.started_at),
|
||||
// snapshotUrl: snapshot?.file || '',
|
||||
// videoUrl: video?.file || ''
|
||||
// };
|
||||
// globalDialogVisible.value = true;
|
||||
// };
|
||||
|
||||
const processQueue = async () => {
|
||||
if (isProcessing || requestQueue.length === 0) return; // 正在处理或队列为空时不执行
|
||||
isProcessing = true; // 标记正在处理
|
||||
const data = requestQueue.shift(); // 从队列中取出数据
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
if (!token) throw new Error('Token 未找到,请登录');
|
||||
|
||||
// 使用延时重试获取数据
|
||||
const eventDetails = await fetchWithRetry(() => apiInstance.getEventById(data.id, token));
|
||||
// console.log('processQueue>>>>>>>>>>>>>', eventDetails);
|
||||
if (!eventDetails || !eventDetails.mediums) {
|
||||
console.error('Event details or mediums not found:', eventDetails);
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = eventDetails.mediums.find((item: any) => item.name === 'snapshot');
|
||||
const video = eventDetails.mediums.find((item: any) => item.name === 'video');
|
||||
|
||||
globalDialogContent.value = {
|
||||
id: eventDetails.id,
|
||||
camera_id: eventDetails.camera_id,
|
||||
camera: eventDetails.camera,
|
||||
types: algorithmMap.value.get(eventDetails.types) || '未知类型',
|
||||
started_at: formatDateTime(eventDetails.started_at),
|
||||
snapshotUrl: snapshot?.file || '',
|
||||
videoUrl: video?.file || ''
|
||||
};
|
||||
|
||||
globalDialogVisible.value = true; // 显示对话框
|
||||
} catch (error) {
|
||||
console.error('Error processing data in queue:', error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
processQueue(); // 处理队列中的下一个请求
|
||||
}
|
||||
};
|
||||
|
||||
const enqueueRequest = (data: any) => {
|
||||
requestQueue.push(data);
|
||||
processQueue();
|
||||
};
|
||||
|
||||
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 关闭对话框并重置内容
|
||||
const handleDialogClose = () => {
|
||||
globalDialogVisible.value = false;
|
||||
globalDialogContent.value = {
|
||||
id: null,
|
||||
camera_id: null,
|
||||
camera: { name: '' },
|
||||
types: null,
|
||||
started_at: null,
|
||||
snapshotUrl: '',
|
||||
videoUrl: ''
|
||||
};
|
||||
};
|
||||
|
||||
watch(globalDialogContent, (newValue, oldValue) => {
|
||||
|
||||
if (newValue !== oldValue) {
|
||||
|
||||
const dialogElement = document.querySelector('.dialog-event-col');
|
||||
if (dialogElement) {
|
||||
dialogElement.classList.add('border-blink');
|
||||
|
||||
setTimeout(() => {
|
||||
dialogElement.classList.remove('border-blink');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await loadAlgorithms();
|
||||
// eventBus.on('showDialog', showDialog);
|
||||
eventBus.on('showDialog', enqueueRequest);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
// eventBus.off('showDialog', showDialog);
|
||||
eventBus.off('showDialog', enqueueRequest);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-event-col {
|
||||
font-size: 14px;
|
||||
gap: 30px;
|
||||
padding: 20px;
|
||||
/* transition: border 0.3s ease-in-out; */
|
||||
}
|
||||
|
||||
.border-blink {
|
||||
animation: borderBlink 1s infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes borderBlink {
|
||||
0% {
|
||||
border: 2px solid red;
|
||||
}
|
||||
50% {
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
100% {
|
||||
border: 2px solid red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
</el-icon>
|
||||
<template #title><span>大屏页</span></template>
|
||||
</el-menu-item> -->
|
||||
<el-sub-menu index="1">
|
||||
<!-- <el-sub-menu index="1">
|
||||
<template #title>
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>面板测试</span>
|
||||
|
@ -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 index="/statistics">
|
||||
<el-icon><Document /></el-icon>
|
||||
<template #title><span>功能点2测试</span></template>
|
||||
</el-menu-item>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -344,7 +344,7 @@ onBeforeUnmount(() => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 1vh;
|
||||
padding: 2vh 0;
|
||||
padding: 1vh 1vw;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width:none;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,73 @@
|
|||
<template>
|
||||
<div class="settings-container">
|
||||
<el-row class="popup-row">
|
||||
<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 class="tip-row">
|
||||
|
||||
<div class="model-row">
|
||||
<el-col :sm="24" :md="8">
|
||||
<el-card style="max-width: 480px" >
|
||||
<template #header>
|
||||
弹窗模式设置
|
||||
</template>
|
||||
<el-row>
|
||||
<el-col :sm="24" :md="24">
|
||||
<el-checkbox v-model="isInteractivePopupEnabled" @change="handleInteractiveChange">
|
||||
开启交互式弹窗
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24">
|
||||
<el-checkbox v-model="isResponsivePopupEnabled" @change="handleResponsiveChange">
|
||||
开启响应式弹窗
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>(Tips: 不支持同时生效,关闭状态无声音)</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="8">
|
||||
<el-card style="max-width: 480px" >
|
||||
<template #header>
|
||||
<span>弹窗队列设置</span>
|
||||
</template>
|
||||
<el-col :sm="24" :md="24" style="margin:1vh 0;">
|
||||
<el-button @click="loadNextNotification" size="small" type="primary">加载更多</el-button>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="24">
|
||||
<el-button @click="clearAllNotifications" size="small" type="danger">清空所有</el-button>
|
||||
</el-col>
|
||||
<template #footer>(Tips: 交互模式下加载/清空累积弹窗提示)</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="8" class="audio-card">
|
||||
<el-card style="max-width: 480px" >
|
||||
<template #header>
|
||||
<span>弹窗声音设置</span>
|
||||
</template>
|
||||
<el-col :sm="24" :md="24">
|
||||
开启提示声音:
|
||||
<el-switch v-model="isSoundEnabled" @change="handleSoundSwitch">开启振荡器发声</el-switch>
|
||||
</el-col>
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="5" >
|
||||
<span>音量设置: </span>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-slider v-model="volume" :min="0" :max="1" :step="0.01" :disabled="isSoundEnabled"
|
||||
@change="previewSound" size="small" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :md="5" >
|
||||
<span>音调设置: </span>
|
||||
</el-col>
|
||||
<el-col :sm="24" :md="16">
|
||||
<el-slider v-model="frequency" :min="300" :max="1200" :step="10" :disabled="isSoundEnabled"
|
||||
@change="previewSound" size="small" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>(Tips: 开启后锁定声音设置)</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-row class="channel-row">
|
||||
<Channel />
|
||||
|
@ -20,26 +76,90 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
import { ref, inject, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import type { GlobalWebSocket } from '@/utils/useGlobalWebSocket';
|
||||
import Channel from '@/components/Channel.vue';
|
||||
|
||||
|
||||
const isInteractivePopupEnabled = ref(false); // 交互式弹窗状态
|
||||
const isResponsivePopupEnabled = ref(false); // 响应式弹窗状态
|
||||
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
|
||||
|
||||
if (!globalWebSocket) {
|
||||
throw new Error('globalWebSocket 注入失败');
|
||||
}
|
||||
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket');
|
||||
if (!globalWebSocket) throw new Error('globalWebSocket 注入失败');
|
||||
|
||||
const { notificationSoundParams, setNotificationSoundParams } = globalWebSocket;
|
||||
const isSoundEnabled = ref(notificationSoundParams.isSoundEnabled);
|
||||
const volume = ref(notificationSoundParams.volume);
|
||||
const frequency = ref(notificationSoundParams.frequency);
|
||||
let audioContext: AudioContext | null = null;
|
||||
const loadNextNotification = () => {
|
||||
if (globalWebSocket) globalWebSocket.loadNextNotification();
|
||||
};
|
||||
|
||||
const clearAllNotifications = () => {
|
||||
if (globalWebSocket) globalWebSocket.clearAllNotifications();
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 初始化时加载弹窗模式状态
|
||||
onMounted(() => {
|
||||
isInteractivePopupEnabled.value = localStorage.getItem('isInteractivePopupEnabled') === 'true';
|
||||
isResponsivePopupEnabled.value = localStorage.getItem('isResponsivePopupEnabled') === 'true';
|
||||
isSoundEnabled.value = notificationSoundParams.isSoundEnabled;
|
||||
volume.value = notificationSoundParams.volume;
|
||||
frequency.value = notificationSoundParams.frequency;
|
||||
updateWebSocketConnection();
|
||||
});
|
||||
|
||||
const handleSoundSwitch = () => {
|
||||
setNotificationSoundParams({
|
||||
isSoundEnabled: isSoundEnabled.value,
|
||||
volume: volume.value,
|
||||
frequency: frequency.value,
|
||||
});
|
||||
ElMessage.success(isSoundEnabled.value ? '提示音已开启,当前参数已锁定' : '提示音已关闭');
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 预览提示音
|
||||
const previewSound = () => {
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
}
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.type = 'triangle';
|
||||
oscillator.frequency.setValueAtTime(frequency.value, audioContext.currentTime);
|
||||
gainNode.gain.setValueAtTime(volume.value, audioContext.currentTime);
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.start();
|
||||
setTimeout(() => {
|
||||
oscillator.stop();
|
||||
audioContext?.close();
|
||||
audioContext = null;
|
||||
}, 300); // 持续播放 300ms
|
||||
};
|
||||
|
||||
watch([isSoundEnabled, volume, frequency], () => {
|
||||
setNotificationSoundParams({
|
||||
isSoundEnabled: isSoundEnabled.value,
|
||||
volume: volume.value,
|
||||
frequency: frequency.value,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// const handleInteractiveChange = () => {
|
||||
// if (isInteractivePopupEnabled.value) {
|
||||
// isResponsivePopupEnabled.value = false;
|
||||
|
@ -132,6 +252,10 @@ const updateWebSocketConnection = () => {
|
|||
globalWebSocket.closeWebSocket();
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
audioContext?.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -143,7 +267,9 @@ const updateWebSocketConnection = () => {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
.tip-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
height: 20vh;
|
||||
width: 80vw;
|
||||
|
@ -154,6 +280,62 @@ const updateWebSocketConnection = () => {
|
|||
color: white;
|
||||
}
|
||||
|
||||
.tip-row .el-card{
|
||||
border: none !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tip-row .el-card .el-checkbox{
|
||||
color: white;
|
||||
}
|
||||
|
||||
::v-deep .tip-row .el-card__header {
|
||||
background-color: #001529;
|
||||
/* height: 3vh; */
|
||||
padding: 1vh 0 1vh 1vw;
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
::v-deep .tip-row .el-card__body {
|
||||
background-color: #001529;
|
||||
height: 12vh;
|
||||
font-size: 16px;
|
||||
/* border: 0px ; */
|
||||
}
|
||||
|
||||
::v-deep .tip-row .el-card .is-always-shadow{
|
||||
color: aqua;
|
||||
|
||||
}
|
||||
|
||||
::v-deep .tip-row .el-card__footer {
|
||||
background-color: #001529;
|
||||
|
||||
/* height: 3vh; */
|
||||
padding: 0 1vw 0 0;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border-top: none !important;
|
||||
color: #a8a3a3;
|
||||
}
|
||||
|
||||
|
||||
.model-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
::v-deep .audio-card .el-card__body {
|
||||
padding: 1vh 1vw;
|
||||
/* background-color: aqua; */
|
||||
}
|
||||
|
||||
|
||||
.channel-row {
|
||||
margin: 1vh 2vw;
|
||||
width: 80vw;
|
||||
|
|
|
@ -66,17 +66,20 @@
|
|||
<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" class="events-table" :fit="true"
|
||||
@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">
|
||||
<el-table-column type="selection" min-width="55" align="center"></el-table-column>
|
||||
<el-table-column type="index" label="序号" min-width="30"
|
||||
:index="(index) => index + 1 + (currentPage - 1) * pageSize" align="center">
|
||||
</el-table-column>
|
||||
<el-table-column prop="id" label="告警编号" min-width="100" align="center" v-if="showColumn"></el-table-column>
|
||||
<el-table-column label="告警类型" min-width="100" align="center">
|
||||
<template v-slot="scope">
|
||||
{{ typeMapping[scope.row.types] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="camera.name" label="告警位置" min-width="150"></el-table-column>
|
||||
<el-table-column label="告警时间" min-width="200">
|
||||
<el-table-column prop="camera.name" label="告警位置" min-width="100" align="center"></el-table-column>
|
||||
<el-table-column label="告警时间" min-width="200" align="center">
|
||||
<template v-slot="scope">
|
||||
{{ formatDateTime(scope.row.ended_at) }}
|
||||
</template>
|
||||
|
@ -233,6 +236,7 @@ const displayTotalItems = ref(0); // 用于展示的数字
|
|||
const cameras = ref([]);
|
||||
|
||||
const selectedAlerts = ref([]);
|
||||
const showColumn = ref(true);
|
||||
|
||||
const handleSelectionChange = (selectedRows) => {
|
||||
selectedAlerts.value = selectedRows.map(row => row.id);
|
||||
|
@ -266,7 +270,7 @@ const handleDelete = () => {
|
|||
try {
|
||||
// 调用 API 删除选中的告警
|
||||
const results = await boxApi.delEvents(token.value, selectedAlerts.value);
|
||||
|
||||
|
||||
// 检查结果,显示操作成功或失败
|
||||
const failedDeletions = results.filter(item => !item.success);
|
||||
if (failedDeletions.length === 0) {
|
||||
|
@ -278,7 +282,7 @@ const handleDelete = () => {
|
|||
.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 刷新表格数据
|
||||
await fetchEvents();
|
||||
selectedAlerts.value = []; // 清空选中项
|
||||
|
@ -491,9 +495,10 @@ const fetchTypeMapping = async (token) => {
|
|||
const fetchEvents = async () => {
|
||||
try {
|
||||
const { tableData: data, totalItems: total } = await boxApi.getEvents(token.value, pageSize.value, currentPage.value); // 使用 BoxApi 的 getEvents 方法
|
||||
tableData.value = data;
|
||||
// tableData.value = data;
|
||||
tableData.value = data.sort((a, b) => new Date(b.ended_at) - new Date(a.ended_at));
|
||||
totalItems.value = total;
|
||||
animateNumberChange(total);
|
||||
// animateNumberChange(total);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events data:", error);
|
||||
}
|
||||
|
@ -632,7 +637,8 @@ onMounted(async () => {
|
|||
|
||||
.table-container {
|
||||
max-width: 100%;
|
||||
height: 80%;
|
||||
height: 100%;
|
||||
height: 56vh;
|
||||
/* min-height: 50vh; */
|
||||
/* max-height: 70vh; */
|
||||
overflow-x: auto;
|
||||
|
@ -644,6 +650,11 @@ onMounted(async () => {
|
|||
|
||||
}
|
||||
|
||||
::v-deep .events-table .el-table__inner-wrapper{
|
||||
height: 55vh;
|
||||
max-height: 56vh;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
@ -652,10 +663,10 @@ onMounted(async () => {
|
|||
margin: 0vh 6vw 5vh 1vw;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
/* .table-header {
|
||||
background-color: #f7f8fa;
|
||||
font-weight: bold;
|
||||
}
|
||||
} */
|
||||
|
||||
::v-deep .el-table th.el-table__cell {
|
||||
background-color: #e9eefca4;
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
if (newVal) {
|
||||
// cameraDataZ.value = JSON.parse(JSON.stringify(newVal));
|
||||
Object.assign(cameraDataZ.value, JSON.parse(JSON.stringify(props.cameraData)));
|
||||
console.log("cameraDataZ:", cameraDataZ.value);
|
||||
// console.log("cameraDataZ:", cameraDataZ.value);
|
||||
formatCameraData(cameraDataZ.value);
|
||||
}
|
||||
}
|
||||
|
@ -299,7 +299,7 @@
|
|||
// mode: rule.mode,
|
||||
// })),
|
||||
};
|
||||
console.log("页面的cameraUpdate》》》》》》》》》》》:", cameraUpdate);
|
||||
// console.log("页面的cameraUpdate》》》》》》》》》》》:", cameraUpdate);
|
||||
|
||||
// 构造 ruleUpdate 数据
|
||||
const ruleUpdate = CameraDialog.value.rules.map((rule) => ({
|
||||
|
|
|
@ -1,51 +1,51 @@
|
|||
<template>
|
||||
<div class="list-view">
|
||||
<div class="background-overlay"></div>
|
||||
<div class="container">
|
||||
<!-- 头栏 -->
|
||||
<div class="header">
|
||||
<div class="title-left">
|
||||
<div small-bg>
|
||||
<dv-decoration8 :reverse="true" style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="line-title">
|
||||
<div text-2xl pt10>
|
||||
<div small-bg>
|
||||
<dv-decoration-11 class="custom-decoration" style="width:25vw;height:70px;">
|
||||
<div color-green font-700 bg="~ dark/0">
|
||||
<dv-decoration7 style="width:20vw;height:30px;">
|
||||
<div color-white font-400 >
|
||||
告警数据面板
|
||||
</div>
|
||||
</dv-decoration7>
|
||||
</div>
|
||||
</dv-decoration-11>
|
||||
</div>
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration5 :dur="2" style="width:25vw;height:80px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-right">
|
||||
<div small-bg class="first-row">
|
||||
<dv-decoration8 style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg class="second-row">
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="background-overlay"></div>
|
||||
<div class="container">
|
||||
<!-- 头栏 -->
|
||||
<div class="header">
|
||||
<div class="title-left">
|
||||
<div small-bg>
|
||||
<dv-decoration8 :reverse="true" style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-section">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="left-section">
|
||||
<!-- <div class="section top-left corner-style" >左上
|
||||
<div class="line-title">
|
||||
<div text-2xl pt10>
|
||||
<div small-bg>
|
||||
<dv-decoration-11 class="custom-decoration" style="width:25vw;height:70px;">
|
||||
<div color-green font-700 bg="~ dark/0">
|
||||
<dv-decoration7 style="width:20vw;height:30px;">
|
||||
<div color-white font-400>
|
||||
告警数据面板
|
||||
</div>
|
||||
</dv-decoration7>
|
||||
</div>
|
||||
</dv-decoration-11>
|
||||
</div>
|
||||
</div>
|
||||
<div small-bg>
|
||||
<dv-decoration5 :dur="2" style="width:25vw;height:80px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-right">
|
||||
<div small-bg class="first-row">
|
||||
<dv-decoration8 style="width:28vw;height:60px;" />
|
||||
</div>
|
||||
<div small-bg class="second-row">
|
||||
<dv-decoration3 style="width:250px;height:30px;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-section">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="left-section">
|
||||
<!-- <div class="section top-left corner-style" >左上
|
||||
<div class="section hiden"></div>
|
||||
</div>
|
||||
<div class="section middle-left corner-style">左中
|
||||
|
@ -54,49 +54,49 @@
|
|||
<div class="section bottom-left corner-style">左下
|
||||
<div class="section hiden"></div>
|
||||
</div> -->
|
||||
<dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left">
|
||||
<LeftTop />
|
||||
</dv-border-box-13>
|
||||
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left">不同点位告警的数量
|
||||
<LeftMiddle/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表(告警详情)
|
||||
<dv-border-box-13 title="告警数据概览(数据计算数字)" class="section top-left"><strong>告警数据统计</strong>
|
||||
<LeftTop />
|
||||
</dv-border-box-13>
|
||||
<dv-border-box-13 title="点位告警数量(不同点位的数量)" class="section middle-left"><strong>告警点位数据</strong>
|
||||
<LeftMiddle />
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表(告警详情)
|
||||
|
||||
</dv-border-box-13> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中部区域 -->
|
||||
<div class="center-section">
|
||||
<dv-border-box8 class="center-top">
|
||||
<CenterTop/>
|
||||
<!-- <dv-border-box8 class="center-top-header">警戒画面</dv-border-box8>
|
||||
<!-- 中部区域 -->
|
||||
<div class="center-section">
|
||||
<dv-border-box8 class="center-top">
|
||||
<CenterTop />
|
||||
<!-- <dv-border-box8 class="center-top-header">警戒画面</dv-border-box8>
|
||||
<div class="center-top-grids">
|
||||
<div class="grid-item">栅格左上</div>
|
||||
<div class="grid-item">栅格右上</div>
|
||||
<div class="grid-item">栅格左下</div>
|
||||
<div class="grid-item">栅格右下</div>
|
||||
</div> -->
|
||||
</dv-border-box8>
|
||||
<div class="center-bottom">
|
||||
<!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="center-bottom-right">告警数量分布情况
|
||||
<CenterBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="right-section">
|
||||
<dv-border-box-13 class="section top-right corner-style">时间段告警总数分布
|
||||
<RightTop/>
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="section bottom-right corner-style">告警种类划分
|
||||
<LeftBottom/>
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</dv-border-box8>
|
||||
<div class="center-bottom">
|
||||
<!-- <dv-border-box-13 class="center-bottom-left">中下左</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="center-bottom-right"><strong>告警数量分布</strong>
|
||||
<CenterBottom />
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="right-section">
|
||||
<dv-border-box-13 class="section top-right corner-style"><strong>告警种类数据</strong>
|
||||
<RightTop />
|
||||
</dv-border-box-13>
|
||||
<!-- <dv-border-box-13 class="section middle-right corner-style">告警数量分布</dv-border-box-13> -->
|
||||
<dv-border-box-13 class="section bottom-right corner-style"><strong>今日告警数据</strong>
|
||||
<LeftBottom />
|
||||
</dv-border-box-13>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
@ -105,12 +105,12 @@
|
|||
import { ref, onMounted, onBeforeUnmount, nextTick, computed } from 'vue';
|
||||
import { BoxApi } from '@/utils/boxApi.ts';
|
||||
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
|
||||
import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
import LeftBottom from '@/components/Max/LeftBottom.vue';
|
||||
import LeftMiddle from '@/components/Max/LeftMiddle.vue';
|
||||
import RightTop from '@/components/Max/RightTop.vue';
|
||||
import CenterBottom from '@/components/Max/CenterBottom.vue';
|
||||
import CenterTop from '@/components/Max/CenterTop.vue';
|
||||
import LeftTop from '@/components/Max/LeftTop.vue';
|
||||
import LeftBottom from '@/components/Max/LeftBottom.vue';
|
||||
import LeftMiddle from '@/components/Max/LeftMiddle.vue';
|
||||
import RightTop from '@/components/Max/RightTop.vue';
|
||||
import CenterBottom from '@/components/Max/CenterBottom.vue';
|
||||
import CenterTop from '@/components/Max/CenterTop.vue';
|
||||
// import '/src/assets/viewListStyle.css'
|
||||
|
||||
</script>
|
||||
|
@ -126,9 +126,9 @@ import CenterTop from '@/components/Max/CenterTop.vue';
|
|||
/* background-color: rgb(121, 184, 243); */
|
||||
background-color: #001529;
|
||||
/* background-image: url('/bg05.png'); */
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
/* background: radial-gradient(circle, rgb(24, 64, 197), rgb(0, 7, 60)); */
|
||||
position: relative;
|
||||
color: black;
|
||||
|
@ -136,8 +136,9 @@ import CenterTop from '@/components/Max/CenterTop.vue';
|
|||
|
||||
}
|
||||
|
||||
.custom-decoration{
|
||||
color: #70e5fa;;
|
||||
.custom-decoration {
|
||||
color: #70e5fa;
|
||||
;
|
||||
font-weight: bold;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
@ -196,27 +197,31 @@ import CenterTop from '@/components/Max/CenterTop.vue';
|
|||
|
||||
.title-right {
|
||||
display: flex;
|
||||
flex-direction: column; /* 设置为垂直布局 */
|
||||
flex-direction: column;
|
||||
/* 设置为垂直布局 */
|
||||
}
|
||||
|
||||
.first-row {
|
||||
display: flex;
|
||||
justify-content: flex-start; /* 第一行内容靠左 */
|
||||
justify-content: flex-start;
|
||||
/* 第一行内容靠左 */
|
||||
}
|
||||
|
||||
.second-row {
|
||||
display: flex;
|
||||
justify-content: flex-end; /* 第二行内容靠右 */
|
||||
justify-content: flex-end;
|
||||
/* 第二行内容靠右 */
|
||||
|
||||
}
|
||||
|
||||
.main-section {
|
||||
width: 84vw;
|
||||
height: 72vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1vw;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width:none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.left-section,
|
||||
|
@ -244,18 +249,20 @@ import CenterTop from '@/components/Max/CenterTop.vue';
|
|||
/* box-shadow: 0 2px 5px rgba(221, 204, 204, 0.5); */
|
||||
}
|
||||
|
||||
.top-left{
|
||||
position: relative;
|
||||
padding: 1vh 1vw;
|
||||
.top-left {
|
||||
/* position: relative; */
|
||||
color: white;
|
||||
text-align: center;
|
||||
/* padding: 1vh 1vw; */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
width: 20vw;
|
||||
height: 20vh;
|
||||
width: 22vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
.middle-left{
|
||||
.middle-left {
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 22vw;
|
||||
|
@ -263,14 +270,14 @@ import CenterTop from '@/components/Max/CenterTop.vue';
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.top-right{
|
||||
.top-right {
|
||||
width: 22vw;
|
||||
height: 46vh;
|
||||
display: flex;
|
||||
/* padding: 1.5vh; */
|
||||
}
|
||||
|
||||
.bottom-left{
|
||||
.bottom-left {
|
||||
/* color: white; */
|
||||
/* position: relative; */
|
||||
padding-left: 0.5vw;
|
||||
|
|
|
@ -1,616 +1,62 @@
|
|||
<template>
|
||||
<div class="camera-container">
|
||||
<div class="top-header">
|
||||
<div class="search-row">
|
||||
<el-input v-model="searchKeyword" placeholder="搜索摄像头名称" @input="filterCameras" class="search-input" />
|
||||
</div>
|
||||
<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 class="glowing-border">
|
||||
<div class="content">
|
||||
<!-- 在这里插入你想要的内容 -->
|
||||
</div>
|
||||
<div class="content-container">
|
||||
<div class="left-part">
|
||||
<div class="camera-list">
|
||||
<el-card v-for="camera in filteredCameras" :key="camera.id" class="camera-item"
|
||||
@click="selectCameraById(camera.id)">
|
||||
<template #header>
|
||||
<el-row class="row-id-name">
|
||||
<el-col :span="14" class="col-camera-id">
|
||||
{{ camera.name }}
|
||||
</el-col>
|
||||
<!-- <el-col :span="8" class="col-camera-name">
|
||||
{{ camera.name }}
|
||||
</el-col> -->
|
||||
<el-col :span="10" class="col-camera-setting">
|
||||
<el-button type="text" icon="el-icon-setting" class="settings-button"
|
||||
@click.stop="handleSettings(camera.id)">设置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<div class="div-content">
|
||||
<el-row class="row-content">
|
||||
<el-col :span="10" class="col-camera-snapshot">
|
||||
通道{{ camera.id }}
|
||||
</el-col>
|
||||
<el-col :span="14" class="col-camera-snapshot">
|
||||
<el-image :src="camera.snapshot" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2"
|
||||
:preview-src-list="camera.snapshot" class="camera-img" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</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>
|
||||
<el-image v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
|
||||
class="camera-snapshot" />
|
||||
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
|
||||
@click="openDialog(camera)">
|
||||
<el-icon>
|
||||
<VideoPlay v-if="!camera.playing" />
|
||||
<VideoPause v-if="camera.playing" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
|
||||
<CameraRules :visible="rulesDialogVisible" :cameraData="currentCameraData"
|
||||
@update:visible="rulesDialogVisible = $event" @save-result="handleSaveResult" />
|
||||
|
||||
|
||||
|
||||
</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';
|
||||
import CameraRules from '@/html/CameraRules.vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
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 filteredCameras = ref([]);
|
||||
const searchKeyword = ref('');
|
||||
|
||||
const currentCameraData = ref(null);
|
||||
const rulesDialogVisible = ref(false);
|
||||
|
||||
|
||||
const fetchCameras = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getAllCameras(token);
|
||||
// console.log("cameraData>>>>>>>>>>>>>>>", cameraData);
|
||||
|
||||
// 根据 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;
|
||||
// console.log("all cameras:", cameras.value);
|
||||
}
|
||||
filteredCameras.value = [...cameras.value];
|
||||
} catch (error) {
|
||||
console.error('获取摄像头列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const filterCameras = () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
filteredCameras.value = [...cameras.value];
|
||||
} else {
|
||||
filteredCameras.value = cameras.value.filter(camera =>
|
||||
camera.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// 名称+ID查询
|
||||
// const filterCameras = () => {
|
||||
// const keyword = searchKeyword.value.trim().toLowerCase();
|
||||
// if (!keyword) {
|
||||
// filteredCameras.value = [...cameras.value];
|
||||
// } else {
|
||||
// filteredCameras.value = cameras.value.filter(camera => {
|
||||
// const idMatch = camera.id.toString().toLowerCase().includes(keyword);
|
||||
// const nameMatch = camera.name.toLowerCase().includes(keyword);
|
||||
// return idMatch || nameMatch;
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
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 });
|
||||
// console.log("搜索摄像头的数组内含有", selectedCameras.value)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSettings = async (cameraId) => {
|
||||
try {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
const cameraData = await apiInstance.getCameraById(token, cameraId);
|
||||
// console.log("获取到的 cameraData:", cameraData);
|
||||
currentCameraData.value = cameraData;
|
||||
rulesDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取摄像头规则失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveResult = ({ success, message }) => {
|
||||
// console.log('收到子组件保存结果事件:', { success, message });
|
||||
|
||||
ElMessage({
|
||||
message,
|
||||
type: success ? 'success' : 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
rulesDialogVisible.value = false; // 关闭规则对话框
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗并开始播放
|
||||
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 未加载');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗并停止视频播放
|
||||
const closeDialog = () => {
|
||||
if (currentCamera.value) {
|
||||
handleStopStream(currentCamera.value);
|
||||
}
|
||||
dialogVisible.value = false;
|
||||
currentCamera.value = null;
|
||||
};
|
||||
|
||||
const handleStopStream = async (camera) => {
|
||||
const token = localStorage.getItem('alertToken');
|
||||
try {
|
||||
await apiInstance.stopCameraStream(token, camera.id);
|
||||
camera.playing = false;
|
||||
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
camera.player = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止视频流失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const closeStream = (camera) => {
|
||||
handleStopStream(camera);
|
||||
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCameras();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectedCameras.value.forEach(camera => {
|
||||
if (camera.player) {
|
||||
camera.player.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
<script lang="ts" setup>
|
||||
// 这里可以留空或者添加其他逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #F1F1F1;
|
||||
<style scoped>
|
||||
/* 外层容器 */
|
||||
.glowing-border {
|
||||
width: 400px; /* 设置容器宽度 */
|
||||
height: 200px; /* 设置容器高度 */
|
||||
background-color: #1a1a1a; /* 背景颜色 */
|
||||
border-radius: 15px; /* 圆角 */
|
||||
position: relative;
|
||||
box-shadow: 0 0 20px 5px rgba(138, 43, 226, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* .glowing-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: -20px;
|
||||
width: calc(100% + 40px);
|
||||
height: calc(100% + 40px);
|
||||
border-radius: 20px;
|
||||
width: 80vw;
|
||||
background: linear-gradient(45deg, rgba(138, 43, 226, 0.8), rgba(75, 0, 130, 0.8));
|
||||
filter: blur(15px);
|
||||
z-index: -1;
|
||||
animation: glow 3s infinite alternate;
|
||||
}
|
||||
|
||||
.top-header {
|
||||
/* width: 100%; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
}
|
||||
.search-row {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 10vw;
|
||||
margin-left: 1vh;
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
::v-deep .search-row input{
|
||||
color: #fffefe;
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner{
|
||||
background-color: #001529;
|
||||
/* background-color:red; */
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
::v-deep .search-input .el-input__wrapper{
|
||||
background-color: #001529;
|
||||
/* background-color: red; */
|
||||
box-shadow: 0 0 0 0px
|
||||
}
|
||||
|
||||
::v-deep .search-input .el-input__inner::placeholder{
|
||||
color: #fffefe;
|
||||
}
|
||||
|
||||
|
||||
.status-filter {
|
||||
position: relative;
|
||||
top: 0vh;
|
||||
left: 0vw;
|
||||
width: 10vw;
|
||||
margin-left: 1vh;
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
height: 58vh;
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.left-part {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 12vw;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.camera-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 1vh;
|
||||
}
|
||||
|
||||
.camera-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 40%;
|
||||
background-color: #001529;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
::v-deep .camera-item .el-card__header {
|
||||
border-radius: 10px 10px 0 0 ;
|
||||
/* background-color: rgb(41, 12, 150); */
|
||||
background: linear-gradient(to left top, rgb(18, 110, 196), rgba(3, 55, 153, 0.3));
|
||||
height: 4vh;
|
||||
/* min-height: 3vh;
|
||||
min-width: 13vw; */
|
||||
min-height: 50px;
|
||||
min-width: 300px;
|
||||
color: #ffffff;
|
||||
padding: 0;
|
||||
border: none;
|
||||
display: flex;
|
||||
border: none;
|
||||
/* align-items: center; */
|
||||
}
|
||||
|
||||
.row-id-name {
|
||||
display: flex;
|
||||
.content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.col-camera-id {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.col-camera-name {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-camera-setting {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
}
|
||||
.settings-button{
|
||||
color: #fffefe;
|
||||
}
|
||||
|
||||
::v-deep .camera-item .el-card__body {
|
||||
/* min-height: 10vh;
|
||||
min-width: 13vw; */
|
||||
border-radius: 0 0 10px 10px;
|
||||
background-color: #001529;
|
||||
padding: 1vh;
|
||||
}
|
||||
|
||||
.div-content,
|
||||
.row-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.col-camera-snapshot {
|
||||
text-align: center;
|
||||
background-color: #001529;
|
||||
width: 10vw;
|
||||
align-content: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.camera-img {
|
||||
/* object-fit: cover; */
|
||||
cursor: pointer;
|
||||
padding: 1vh;
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
::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;
|
||||
font-size: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.camera-grid {
|
||||
margin: 0vh 1vh;
|
||||
padding: 0 0 2vh 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1vh 0vh;
|
||||
/* height: 39vh;
|
||||
width: 34vw; */
|
||||
width: 68vw;
|
||||
height: 55vh;
|
||||
max-height: 58vh;
|
||||
/* overflow-y: scroll; */
|
||||
overflow-y: auto;
|
||||
scrollbar-width:thin;
|
||||
/* background-color: #ffffff; */
|
||||
background-color: #F1F1F1;
|
||||
/* background-color: #c71515; */
|
||||
}
|
||||
|
||||
/* .camera-grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
@keyframes glow {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
} */
|
||||
|
||||
.grid-item {
|
||||
margin: 1vh;
|
||||
position: relative;
|
||||
height: 25vh;
|
||||
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%;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -150,7 +150,7 @@ const refreshTable = async () => {
|
|||
id: index + 1,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
isLocked: ['turingvideo'].includes(user.username)
|
||||
isLocked: ['csmixc','admin'].includes(user.username)
|
||||
}));
|
||||
} catch (error) {
|
||||
ElMessage.error('刷新用户列表失败');
|
||||
|
|
|
@ -499,15 +499,15 @@ class BoxApi {
|
|||
...this._authHeader(token),
|
||||
params: params
|
||||
});
|
||||
|
||||
if (res.data.err.ec === 0) {
|
||||
// return res.data.ret.objects[0];
|
||||
|
||||
if (res.data.err.ec === 0 && res.data.ret.objects.length > 0) {
|
||||
return res.data.ret.objects[0];
|
||||
} else {
|
||||
throw new Error(res.data.err.dm);
|
||||
throw new Error(res.data.err.dm || '未获取到有效数据');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
console.error(`getEventById error for ID ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,52 @@
|
|||
import { ref } from 'vue';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage, ElNotification } from 'element-plus';
|
||||
import eventBus from '@/utils/eventBus';
|
||||
import dayjs from 'dayjs';
|
||||
import { BoxApi } from '@/utils/boxApi';
|
||||
const apiInstance = new BoxApi();
|
||||
const websocket = ref<WebSocket | null>(null); // WebSocket 实例
|
||||
const isWebSocketConnected = ref(false); // WebSocket 连接状态
|
||||
let heartbeatInterval: number | null = null; // 心跳定时器
|
||||
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
const algorithmMap = ref(new Map());
|
||||
const apiInstance = new BoxApi();
|
||||
const algorithmMap = ref(new Map());
|
||||
|
||||
const maxVisibleNotifications = 2; // 最多同时显示的通知数量
|
||||
const visibleNotifications: Map<string, any> = new Map(); // 当前显示的通知
|
||||
const notificationQueue: any[] = []; // 等待显示的通知队列
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
webkitAudioContext?: typeof AudioContext;
|
||||
}
|
||||
}
|
||||
// let notificationSoundParams = { volume: 0.5, frequency: 440 };
|
||||
// const setNotificationSoundParams = (params: { volume: number; frequency: number }) => {
|
||||
// notificationSoundParams = params;
|
||||
// };
|
||||
|
||||
const notificationSoundParams = reactive({
|
||||
volume: 0.5,
|
||||
frequency: 440,
|
||||
isSoundEnabled: false,
|
||||
});
|
||||
|
||||
const setNotificationSoundParams = (params: { volume?: number; frequency?: number; isSoundEnabled?: boolean }) => {
|
||||
Object.assign(notificationSoundParams, params);
|
||||
|
||||
// 更新 localStorage
|
||||
if (params.volume !== undefined) localStorage.setItem('volume', String(notificationSoundParams.volume));
|
||||
if (params.frequency !== undefined) localStorage.setItem('frequency', String(notificationSoundParams.frequency));
|
||||
if (params.isSoundEnabled !== undefined) localStorage.setItem('isSoundEnabled', String(notificationSoundParams.isSoundEnabled));
|
||||
};
|
||||
|
||||
// 初始化:从 localStorage 加载参数
|
||||
const initializeNotificationSoundParams = () => {
|
||||
notificationSoundParams.volume = parseFloat(localStorage.getItem('volume') || '0.5');
|
||||
notificationSoundParams.frequency = parseFloat(localStorage.getItem('frequency') || '440');
|
||||
notificationSoundParams.isSoundEnabled = localStorage.getItem('isSoundEnabled') === 'true';
|
||||
};
|
||||
initializeNotificationSoundParams();
|
||||
|
||||
// 加载算法映射表
|
||||
const loadAlgorithms = async () => {
|
||||
|
@ -20,17 +61,14 @@ const loadAlgorithms = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const websocket = ref<WebSocket | null>(null); // WebSocket 实例
|
||||
const isWebSocketConnected = ref(false); // WebSocket 连接状态
|
||||
let heartbeatInterval: number | null = null; // 心跳定时器
|
||||
const formatDateTime = (isoString: string): string => dayjs(isoString).format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 连接 WebSocket
|
||||
const connectWebSocket = () => {
|
||||
const connectWebSocket = async () => {
|
||||
const rememberedAddress = localStorage.getItem('rememberedAddress'); // 获取存储的主机地址
|
||||
if (rememberedAddress) {
|
||||
websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/ws/event`);
|
||||
// websocket.value = new WebSocket(`ws://192.168.28.33:8080/ws/event`);
|
||||
loadAlgorithms();
|
||||
// websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/ws/event`);
|
||||
websocket.value = new WebSocket(`ws://172.19.7.9:8080/ws/event`);
|
||||
await loadAlgorithms();
|
||||
websocket.value.onopen = () => {
|
||||
ElMessage.success('全局 WebSocket 连接成功');
|
||||
isWebSocketConnected.value = true;
|
||||
|
@ -38,6 +76,17 @@ const connectWebSocket = () => {
|
|||
};
|
||||
websocket.value.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data); // 解析收到的数据
|
||||
|
||||
const isInteractive = localStorage.getItem('isInteractivePopupEnabled') === 'true';
|
||||
const isResponsive = localStorage.getItem('isResponsivePopupEnabled') === 'true';
|
||||
if (!isInteractive && !isResponsive) {
|
||||
return; // 弹窗关闭,直接返回
|
||||
}
|
||||
|
||||
let num = 0;
|
||||
num += 1;
|
||||
// console.log(`${new Date().toISOString()}收到新的第${num}弹窗信息:`, data);
|
||||
playNotificationSound();
|
||||
handlePopupNotification(data); // 根据模式处理弹窗通知
|
||||
};
|
||||
websocket.value.onclose = handleClose; // 处理连接关闭
|
||||
|
@ -47,6 +96,33 @@ const connectWebSocket = () => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const playNotificationSound = () => {
|
||||
// if (!isWebSocketConnected.value) return;
|
||||
if (!notificationSoundParams.isSoundEnabled) return;
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext!)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.type = 'triangle';
|
||||
oscillator.frequency.setValueAtTime(notificationSoundParams.frequency, audioContext.currentTime);
|
||||
gainNode.gain.setValueAtTime(notificationSoundParams.volume, audioContext.currentTime);
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.start();
|
||||
setTimeout(() => {
|
||||
oscillator.stop();
|
||||
audioContext.close();
|
||||
}, 350); // 持续播放 200 毫秒
|
||||
} catch (error) {
|
||||
console.error('播放提示音时出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭 WebSocket
|
||||
const closeWebSocket = () => {
|
||||
if (websocket.value) {
|
||||
|
@ -90,30 +166,92 @@ const handleError = () => {
|
|||
stopHeartbeat(); // 停止心跳
|
||||
};
|
||||
|
||||
// 显示通知
|
||||
const showNotification = (notification: any) => {
|
||||
const id = `${Date.now()}-${Math.random()}`; // 唯一 ID
|
||||
notification.id = id;
|
||||
|
||||
if (visibleNotifications.size < maxVisibleNotifications) {
|
||||
// 如果未达到最大显示数量,直接显示
|
||||
const elNotification = ElNotification({
|
||||
...notification,
|
||||
onClose: () => {
|
||||
// 从显示列表中移除
|
||||
visibleNotifications.delete(id);
|
||||
|
||||
// 显示下一条队列中的通知
|
||||
if (notificationQueue.length > 0) {
|
||||
showNotification(notificationQueue.shift());
|
||||
}
|
||||
|
||||
// 调用用户定义的关闭回调(如果有)
|
||||
notification.onClose?.();
|
||||
},
|
||||
});
|
||||
|
||||
visibleNotifications.set(id, elNotification);
|
||||
} else {
|
||||
// 如果达到最大显示数量,存入队列
|
||||
notificationQueue.push(notification);
|
||||
}
|
||||
};
|
||||
|
||||
const loadNextNotification = () => {
|
||||
// 清空当前所有显示的通知
|
||||
visibleNotifications.forEach((notif) => notif.close());
|
||||
visibleNotifications.clear();
|
||||
|
||||
// 从队列中加载下一条通知
|
||||
if (notificationQueue.length > 0) {
|
||||
const nextNotification = notificationQueue.shift();
|
||||
if (nextNotification) {
|
||||
showNotification(nextNotification);
|
||||
}
|
||||
} else {
|
||||
// console.log('通知队列为空');
|
||||
}
|
||||
};
|
||||
// 手动清空所有通知
|
||||
const clearAllNotifications = () => {
|
||||
visibleNotifications.forEach((notif) => notif.close());
|
||||
visibleNotifications.clear();
|
||||
notificationQueue.length = 0;
|
||||
};
|
||||
|
||||
|
||||
const seenNotificationIds = new Set();
|
||||
// 根据弹窗模式处理通知
|
||||
const handlePopupNotification = (data: any) => {
|
||||
const isInteractive = localStorage.getItem('isInteractivePopupEnabled') === 'true'; // 是否为交互式弹窗
|
||||
const isResponsive = localStorage.getItem('isResponsivePopupEnabled') === 'true'; // 是否为响应式弹窗
|
||||
|
||||
if (seenNotificationIds.has(data.id)) return;
|
||||
seenNotificationIds.add(data.id);
|
||||
setTimeout(() => seenNotificationIds.delete(data.id), 30000);
|
||||
|
||||
|
||||
if (isResponsive) {
|
||||
// 响应式模式:直接显示对话框
|
||||
// console.log('handlePopupNotification triggered:', data);
|
||||
eventBus.emit('showDialog', data);
|
||||
} else if (isInteractive) {
|
||||
|
||||
|
||||
const formattedTime = formatDateTime(data.started_at);
|
||||
ElNotification({
|
||||
showNotification({
|
||||
title: '新告警',
|
||||
message: `
|
||||
<div style="max-height: 200px; overflow-y: auto;">
|
||||
<p><strong>告警编号:</strong>${data.id || '未知'}</p>
|
||||
<p><strong>告警点位:</strong>${data.camera?.name|| '未知'}</p>
|
||||
<p><strong>告警点位:</strong>${data.camera?.name || '未知'}</p>
|
||||
<p><strong>告警类型:</strong>${algorithmMap.value.get(data.types) || '未知'}</p>
|
||||
<p><strong>告警时间:</strong>${formattedTime || '未知'}</p>
|
||||
</div>
|
||||
`,
|
||||
dangerouslyUseHTMLString: true,
|
||||
duration: 5000,
|
||||
duration: 10000,
|
||||
customClass: 'custom-notification',
|
||||
position: 'bottom-right',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
eventBus.emit('showDialog', data); // 点击通知触发对话框
|
||||
}
|
||||
|
@ -126,10 +264,22 @@ export interface GlobalWebSocket {
|
|||
connectWebSocket: () => void; // 连接 WebSocket
|
||||
closeWebSocket: () => void; // 关闭 WebSocket
|
||||
isWebSocketConnected: typeof isWebSocketConnected; // WebSocket 连接状态
|
||||
loadNextNotification: () => void; // 加载下一条通知
|
||||
clearAllNotifications: () => void; // 清空所有通知
|
||||
setNotificationSoundParams: (params: { volume?: number; frequency?: number; isSoundEnabled?: boolean }) => void;
|
||||
notificationSoundParams: {
|
||||
volume: number;
|
||||
frequency: number;
|
||||
isSoundEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const useGlobalWebSocket = (): GlobalWebSocket => ({
|
||||
connectWebSocket,
|
||||
closeWebSocket,
|
||||
isWebSocketConnected,
|
||||
loadNextNotification, // 导出加载下一条通知的方法
|
||||
clearAllNotifications, // 导出清空所有通知的方法
|
||||
notificationSoundParams,
|
||||
setNotificationSoundParams,
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue