diff --git a/README.md b/README.md index ec8f23f..6b508d5 100644 --- a/README.md +++ b/README.md @@ -624,6 +624,30 @@ public async updateRule(token: string | null = null, rules: RuleData[]): Promise +### 2024.11.26 + +- 对话框大图显示 +- 删除告警按钮 +- 主页定时刷新 +- 布局滚动查看调整 +- 用户删除限制 + + + +### 2024.12.3 + +- 告警设置 + - 消息队列设置 + - 声音设置 + - 弹窗动画添加 + - 弹窗队列添加,错误重试机制(2s*5次) + + + + + + + diff --git a/src/App.vue b/src/App.vue index d5158b0..be7cbcc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,119 +1,14 @@ diff --git a/src/assets/global.css b/src/assets/global.css index 4519ed6..487536f 100644 --- a/src/assets/global.css +++ b/src/assets/global.css @@ -1,5 +1,5 @@ -body{ +body { margin: 0; padding: 0; - + } \ No newline at end of file diff --git a/src/components/GlobalDialog.vue b/src/components/GlobalDialog.vue new file mode 100644 index 0000000..d671a59 --- /dev/null +++ b/src/components/GlobalDialog.vue @@ -0,0 +1,220 @@ + + + + + + \ No newline at end of file diff --git a/src/components/Layout.vue b/src/components/Layout.vue index c912249..af8268f 100644 --- a/src/components/Layout.vue +++ b/src/components/Layout.vue @@ -42,7 +42,7 @@ --> - + - + - + diff --git a/src/components/Max/LeftTop.vue b/src/components/Max/LeftTop.vue index 5309838..67b6d7c 100644 --- a/src/components/Max/LeftTop.vue +++ b/src/components/Max/LeftTop.vue @@ -344,7 +344,7 @@ onBeforeUnmount(() => { width: 100%; height: 100%; margin: 1vh; - padding: 2vh 0; + padding: 1vh 1vw; overflow-y: scroll; scrollbar-width:none; } diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 6c842e5..0deeac5 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -1,17 +1,73 @@ diff --git a/src/html/UserList.vue b/src/html/UserList.vue index 9e9a7a8..37ceb55 100644 --- a/src/html/UserList.vue +++ b/src/html/UserList.vue @@ -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('刷新用户列表失败'); diff --git a/src/utils/boxApi.ts b/src/utils/boxApi.ts index e1885a4..bf71f6c 100644 --- a/src/utils/boxApi.ts +++ b/src/utils/boxApi.ts @@ -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; } } diff --git a/src/utils/useGlobalWebSocket.ts b/src/utils/useGlobalWebSocket.ts index e5b1b5a..fc4695a 100644 --- a/src/utils/useGlobalWebSocket.ts +++ b/src/utils/useGlobalWebSocket.ts @@ -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(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 = 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(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: `

告警编号:${data.id || '未知'}

-

告警点位:${data.camera?.name|| '未知'}

+

告警点位:${data.camera?.name || '未知'}

告警类型:${algorithmMap.value.get(data.types) || '未知'}

告警时间:${formattedTime || '未知'}

`, 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, });