From f456cdaa8c59a46de23d115cb02bb75c2fc7586c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9A=E7=9A=93?= <1736436516@qq.com> Date: Fri, 1 Nov 2024 13:19:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=91=8A=E8=AD=A6=E5=A4=A7=E5=B1=8Fcss?= =?UTF-8?q?=E5=88=9D=E7=A8=BF=EF=BC=8C=E5=A4=A7=E5=B1=8F=E5=B0=8F=E5=9B=BE?= =?UTF-8?q?echart=E6=B5=8B=E8=AF=95=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/viewListStyle.css | 217 +++++++++++++++++++++++ src/components/Layout.vue | 122 ++++++++++--- src/components/Max/LeftTop.vue | 55 ++++++ src/components/Settings.vue | 137 +++----------- src/html/AlertManagement.vue | 198 +++++++++++++++++++-- src/html/DataStatistics.vue | 1 + src/html/Home.vue | 6 +- src/html/ViewList.vue | 304 ++++++++++++++++++++++++++++++++ src/main.ts | 5 +- src/router/index.ts | 7 + src/utils/boxApi.ts | 68 +++++-- src/utils/useGlobalWebSocket.ts | 96 ++++++++++ 12 files changed, 1061 insertions(+), 155 deletions(-) create mode 100644 src/assets/viewListStyle.css create mode 100644 src/components/Max/LeftTop.vue create mode 100644 src/html/ViewList.vue create mode 100644 src/utils/useGlobalWebSocket.ts diff --git a/src/assets/viewListStyle.css b/src/assets/viewListStyle.css new file mode 100644 index 0000000..f5cb90d --- /dev/null +++ b/src/assets/viewListStyle.css @@ -0,0 +1,217 @@ +.list-view { + display: flex; + justify-content: center; + margin: 0; + height: 100vh; + width: 100vw; + padding: 10vh 10vw 10vh 1vw; + /* background-color: rgb(121, 184, 243); */ + /* background-image: url('/bg01.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; */ + position: relative; + color: black; + box-sizing: border-box; +} + +.background-overlay { + position: absolute; + /* 绝对定位 */ + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: url('/bg01.png'); + /* 设置背景图片 */ + background-size: cover; + /* 使背景图覆盖整个区域 */ + background-position: center; + /* 背景图居中显示 */ + background-repeat: no-repeat; + /* 不重复背景图 */ + opacity: 0.8; + /* 设置透明度(0 到 1 之间的值) */ + z-index: 0; + /* 确保在其他内容下方 */ +} + +.container { + display: flex; + flex-direction: column; + width: 80vw; + height: 100%; + gap: 1vh; + position: relative; + /* 为了在背景下显示 */ + z-index: 1; +} + +.header { + height: 6vh; + width: 81vw; + background-color: #333; + color: white; + text-align: center; + line-height: 6vh; +} + +.main-section { + height: 72vh; + display: flex; + flex-direction: row; + gap: 1vw; +} + +.left-section, +.right-section { + display: flex; + flex-direction: column; + width: 22vw; + height: 70vh; + gap: 2vh; + +} + +.top-left, +.middle-left, +.bottom-left, +.top-right, +.middle-right, +.bottom-right { + color: white; + text-align: center; + width: 22vw; + height: 22vh; + background-color: rgba(255, 255, 255, 0.1); + /* border: 3px solid rgba(0, 255, 255, 0.5); */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); +} + +.corner-style { + border: 2px solid rgba(73, 1, 95, 0.5); + position: relative; + + &::before { + position: absolute; + content: ""; + top: 0; + left: 0; + width: 20px; + height: 20px; + border-left: 5px solid rgba(40, 241, 241, 0.986); + border-top: 5px solid rgba(40, 241, 241, 0.986); + } + + &::after { + position: absolute; + content: ""; + top: 0; + right: 0; + width: 20px; + height: 20px; + border-right: 5px solid rgba(40, 241, 241, 0.986); + border-top: 5px solid rgba(40, 241, 241, 0.986); + } + + .hiden { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + + &::before { + position: absolute; + content: ""; + bottom: 0; + left: 0; + width: 20px; + height: 20px; + border-left: 5px solid rgba(40, 241, 241, 0.986); + border-bottom: 5px solid rgba(40, 241, 241, 0.986); + } + + &::after { + position: absolute; + content: ""; + bottom: 0; + right: 0; + width: 20px; + height: 20px; + border-right: 5px solid rgba(40, 241, 241, 0.986); + border-bottom: 5px solid rgba(40, 241, 241, 0.986); + } + } +} + + + +/* .top-left, .top-right { + margin-bottom: 1vh; +} + + +.bottom-left, .bottom-right { + margin-top: 1vh; +} */ + +.center-section { + display: flex; + flex-direction: column; + width: 35vw; + height: 70vh; + gap: 1vh; +} + +.center-top { + height: 47vh; + background-color: #555; + color: white; + display: flex; + flex-direction: column; +} + +.center-top-header { + height: 3vh; + width: 35vw; + text-align: center; + line-height: 3vh; + margin-bottom: 1vh; + background-color: rgba(0, 51, 102, 0.8); + border-radius: 3px; + /* 圆角 */ + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); + /* 添加阴影 */ +} + +.center-top-grids { + display: grid; + grid-template-columns: 17vw 17vw; + gap: 2vh 1vw; +} + +.grid-item { + width: 17vw; + height: 20vh; + background-color: #777; + color: white; + display: flex; + align-items: center; + justify-content: center; +} + +.center-bottom { + display: flex; + gap: 1vw; + flex-direction: row; +} + +.center-bottom-left, +.center-bottom-right { + width: 17vw; + height: 22vh; + background-color: #444; + color: white; + text-align: center; + /* margin-top: 1vh; */ +} \ No newline at end of file diff --git a/src/components/Layout.vue b/src/components/Layout.vue index 1fcb476..a15c71f 100644 --- a/src/components/Layout.vue +++ b/src/components/Layout.vue @@ -1,31 +1,47 @@ @@ -94,9 +122,9 @@ + + diff --git a/src/components/Settings.vue b/src/components/Settings.vue index dc8f4df..9669600 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -1,35 +1,37 @@ diff --git a/src/html/DataStatistics.vue b/src/html/DataStatistics.vue index 5dc9909..9d79608 100644 --- a/src/html/DataStatistics.vue +++ b/src/html/DataStatistics.vue @@ -96,6 +96,7 @@ onMounted(async () => { height: 56vh; margin-top: 60px; /* border: 1px solid #1E2E4A; */ + } .bottom-pan{ margin: 0; diff --git a/src/html/Home.vue b/src/html/Home.vue index ac7be23..74c8997 100644 --- a/src/html/Home.vue +++ b/src/html/Home.vue @@ -224,7 +224,7 @@ onBeforeUnmount(() => { /* 每个摄像头项目的样式 */ .camera-item { - margin-bottom: 8px; + margin-bottom: 10px; cursor: pointer; padding: 12px; border: 1px solid #458388; @@ -313,8 +313,8 @@ onBeforeUnmount(() => { width: 100%; height: 100%; /* background-color: #1E2E4A; */ - background: linear-gradient(to top, rgba(64, 226, 255, 0.7), rgba(211, 64, 248, 0.7)); - /* background: linear-gradient(to top, rgba(0, 7, 8, 0.9), rgba(12, 2, 155, 0.7)); */ + /* background: linear-gradient(to top, rgba(64, 226, 255, 0.7), rgba(211, 64, 248, 0.7)); */ + background: linear-gradient(to top, rgba(0, 7, 8, 0.9), rgba(12, 2, 155, 0.7)); border: 2px solid #0b4c5f; border-radius: 8px; position: relative; diff --git a/src/html/ViewList.vue b/src/html/ViewList.vue new file mode 100644 index 0000000..088c2ba --- /dev/null +++ b/src/html/ViewList.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/src/main.ts b/src/main.ts index f5b2c4b..929e653 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,8 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'; import axios from 'axios'; import '@/assets/global.css' import { useGlobalWebSocket } from './utils/useGlobalWebSocket'; +import DataVVue3 from '@kjgl77/datav-vue3'; +import '@kjgl77/datav-vue3/dist/style.css'; const app = createApp(App) @@ -15,7 +17,8 @@ const globalWebSocket = useGlobalWebSocket(); app.provide('globalWebSocket',globalWebSocket); // app.provide('axios', axiosInstance); app.use(ElementPlus, { locale: zhCn }); -app.use (router) +app.use (router); +app.use(DataVVue3); // 导航守卫,检查登录状态 router.beforeEach((to, from, next) => { diff --git a/src/router/index.ts b/src/router/index.ts index 13d561f..53892a2 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -11,6 +11,7 @@ import Home from '@/html/Home.vue'; import DataStatistics from '@/html/DataStatistics.vue'; import Cameras from '@/components/Cameras.vue'; import Settings from '@/components/Settings.vue'; +import ViewList from '@/html/ViewList.vue'; const routes = [ { @@ -72,6 +73,12 @@ const routes = [ name: 'Settings', component: Settings, meta: { requiresAuth: true } + }, + { + path:'/viewList', + name: 'ViewList', + component: ViewList, + meta: { requiresAuth: true } } ] }, diff --git a/src/utils/boxApi.ts b/src/utils/boxApi.ts index 0106201..153d282 100644 --- a/src/utils/boxApi.ts +++ b/src/utils/boxApi.ts @@ -313,6 +313,55 @@ class BoxApi { } } + public async getEventsByParams( + token: string | null = null, + pageSize: number = 20, + currentPage: number = 1, + timeBefore: string | null = null, + timeAfter: string | null = null, + types: string | null = null, + camera_id: number | null = null + ): Promise { + // 计算 offset + const offset = (currentPage - 1) * pageSize; + + // 构建请求的 URL + let url = `${this.apiEvents}?limit=${pageSize}&offset=${offset}`; + + // 如果有时间范围,则添加到 URL 中 + if (timeBefore && timeAfter) { + url += `&time_before=${encodeURIComponent(timeBefore)}&time_after=${encodeURIComponent(timeAfter)}`; + } + + // 如果 types 不为空,则添加到 URL 中 + if (types) { + url += `&types=${encodeURIComponent(types)}`; + } + if(camera_id){ + url += `&camera_id=${camera_id}`; + } + + try { + // 发送 GET 请求 + const res = await this.axios.get(url, this._authHeader(token)); + + // 判断请求是否成功 + if (res.data.err.ec === 0) { + return { + count: res.data.ret.count, + next: res.data.ret.next, + previous: res.data.ret.previous, + results: res.data.ret.results + }; + } else { + // 处理请求失败的情况 + throw new Error(res.data.err.dm); + } + } catch (error) { + // 抛出异常以便调用者处理 + throw error; + } + } // public async getOneEvent(token: string | null = null): Promise { // try { // return await this.getEvents(1, 0, token); @@ -329,18 +378,17 @@ class BoxApi { } } - public async setEventStatus(eventId: number, status: string, remark: string | null = null, token: string | null = null): Promise { const url = `${this.apiEvents}/${eventId}`; - const newRemark = remark ? remark : ""; - - const data = { - status: status, - remark: newRemark - }; - + + + const data: { status: string; remark?: string } = { status: status }; + if (remark && remark.trim() !== "") { + data.remark = remark; + } + try { - const res = await this.axios.patch(url, data, this._authHeader(token)) + const res = await this.axios.patch(url, data, this._authHeader(token)); if (res.data.err.ec === 0) { return res.data.ret; } else { @@ -387,7 +435,7 @@ class BoxApi { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', - 'Authorization': `Bearer ${accessToken}`, + 'Authorization': `Bearer ${accessToken}`, // 'X-CSRFToken': this.getCsrfToken() } }; diff --git a/src/utils/useGlobalWebSocket.ts b/src/utils/useGlobalWebSocket.ts new file mode 100644 index 0000000..a481c27 --- /dev/null +++ b/src/utils/useGlobalWebSocket.ts @@ -0,0 +1,96 @@ +// useGlobalWebSocket.ts +import { ref } from 'vue'; +import { ElMessage } from 'element-plus'; + +const websocket = ref(null); +const isWebSocketConnected = ref(false); +let heartbeatInterval: number | null = null; +const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1'; + +// 连接 WebSocket +const connectWebSocket = () => { + websocket.value = new WebSocket(`ws://${rememberedAddress}:8080/event/ws`); + websocket.value.onopen = () => { + ElMessage.success('全局 WebSocket 连接成功'); + isWebSocketConnected.value = true; + startHeartbeat(); + }; + websocket.value.onmessage = (event) => { + showNotification(event.data); + }; + websocket.value.onclose = handleClose; + websocket.value.onerror = handleError; +}; + +// 关闭 WebSocket +const closeWebSocket = () => { + if (websocket.value) { + websocket.value.close(); + ElMessage.info('全局 WebSocket 已关闭'); + stopHeartbeat(); + isWebSocketConnected.value = false; + } +}; + +// 心跳检测 +const startHeartbeat = () => { + if (heartbeatInterval) return; + heartbeatInterval = window.setInterval(() => { + if (websocket.value && websocket.value.readyState === WebSocket.OPEN) { + websocket.value.send('ping'); + } + }, 5000); +}; + +const stopHeartbeat = () => { + if (heartbeatInterval) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } +}; + +// 处理连接关闭 +const handleClose = () => { + ElMessage.warning('WebSocket 连接已关闭'); + isWebSocketConnected.value = false; + stopHeartbeat(); + localStorage.setItem('isPopupEnabled', 'False'); +}; + +const handleError = () => { + ElMessage.error('WebSocket 连接出错'); + isWebSocketConnected.value = false; + stopHeartbeat(); +}; + +// 显示通知 +const showNotification = (message: string) => { + if (Notification.permission === 'granted') { + new Notification('新消息', { + body: message, + icon: '/path/to/icon.png', + }); + } else if (Notification.permission !== 'denied') { + Notification.requestPermission().then((permission) => { + if (permission === 'granted') { + new Notification('新消息', { + body: message, + icon: '/path/to/icon.png', + }); + } + }); + } +}; + +// 导出类型和方法 +export interface GlobalWebSocket { + connectWebSocket: () => void; + closeWebSocket: () => void; + isWebSocketConnected: typeof isWebSocketConnected; +} + +export const useGlobalWebSocket = (): GlobalWebSocket => ({ + connectWebSocket, + closeWebSocket, + isWebSocketConnected, +});