告警大屏css初稿,大屏小图echart测试页

This commit is contained in:
龚皓 2024-11-01 13:19:38 +08:00
parent e16bda93b1
commit f456cdaa8c
12 changed files with 1061 additions and 155 deletions

View File

@ -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; */
}

View File

@ -1,31 +1,47 @@
<template> <template>
<div id="layout" class="layout-container"> <div id="layout" class="layout-container">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<el-aside 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="/turing.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="/">
<el-icon><House /></el-icon> <el-icon>
<House />
</el-icon>
<template #title><span>首页</span></template> <template #title><span>首页</span></template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/alertManagement"> <el-menu-item index="/alertManagement">
<el-icon><Management /></el-icon> <el-icon>
<Management />
</el-icon>
<template #title><span>告警列表</span></template> <template #title><span>告警列表</span></template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/dataStatistics"> <el-menu-item index="/dataStatistics">
<el-icon><TrendCharts /></el-icon> <el-icon>
<TrendCharts />
</el-icon>
<template #title><span>数据统计</span></template> <template #title><span>数据统计</span></template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/userList"> <el-menu-item index="/userList">
<el-icon><Avatar /></el-icon> <el-icon>
<Avatar />
</el-icon>
<template #title><span>用户管理</span></template> <template #title><span>用户管理</span></template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/settings"> <el-menu-item index="/settings">
<el-icon><Setting /></el-icon> <el-icon>
<Setting />
</el-icon>
<template #title><span>告警设置</span></template> <template #title><span>告警设置</span></template>
</el-menu-item> </el-menu-item>
<el-menu-item index="/viewList">
<el-icon>
<Location />
</el-icon>
<template #title><span>大屏页</span></template>
</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>
@ -53,12 +69,16 @@
<div class="content-layout"> <div class="content-layout">
<!-- 头部区域 --> <!-- 头部区域 -->
<el-header class="nav-header"> <el-header v-if="!isFullScreen" class="nav-header">
<!-- 收缩/展开按钮 --> <!-- 收缩/展开按钮 -->
<el-icon @click="toggleCollapse" style="cursor: pointer; margin-right: 20px;"> <div>
<component :is="isCollapse ? Expand : Fold" /> <el-icon @click="toggleCollapse" style="cursor: pointer; margin-right: 20px;">
</el-icon> <component :is="isCollapse ? Expand : Fold" />
</el-icon>
<el-icon @click="toggleFullScreen" style="cursor: pointer; margin-right: 20px;">
<component :is="FullScreen" />
</el-icon>
</div>
<div class="header-right"> <div class="header-right">
<!-- 用户头像下拉菜单 --> <!-- 用户头像下拉菜单 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
@ -69,10 +89,14 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="goToUserManagement"> <el-dropdown-item @click="goToUserManagement">
<el-icon><User /></el-icon> <el-icon>
<User />
</el-icon>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click="onLogout"> <el-dropdown-item @click="onLogout">
<el-icon><SwitchButton /></el-icon> 退 <el-icon>
<SwitchButton />
</el-icon> 退
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@ -84,9 +108,13 @@
<el-main class="main-content"> <el-main class="main-content">
<router-view /> <router-view />
</el-main> </el-main>
<el-button v-if="isFullScreen" class="exit-fullscreen-button" type="primary" @click="toggleFullScreen">
<el-icon>
<Notification />
</el-icon>退
</el-button>
<!-- 页脚区域 --> <!-- 页脚区域 -->
<el-footer class="nav-footer">Powered by AI</el-footer> <el-footer v-if="!isFullScreen" class="nav-footer">Powered by AI</el-footer>
</div> </div>
</div> </div>
</template> </template>
@ -94,9 +122,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { import {
Document, WarningFilled, Location, User, SwitchButton, Document, WarningFilled, Location, User, SwitchButton,
House, Management, TrendCharts, Avatar, Fold, Expand ,Setting House, Management, TrendCharts, Avatar, Fold, Expand, Setting, FullScreen, Notification
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import { BoxApi } from '@/utils/boxApi'; import { BoxApi } from '@/utils/boxApi';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
@ -107,6 +135,7 @@ const activeIndex = ref(route.path);
const isCollapse = ref(false); // const isCollapse = ref(false); //
const username = ref(''); // const username = ref(''); //
const isFullScreen = ref(false);
const goToUserManagement = () => { const goToUserManagement = () => {
router.push('/userList'); // router.push('/userList'); //
}; };
@ -114,6 +143,7 @@ const goToUserManagement = () => {
onMounted(() => { onMounted(() => {
const storedUsername = localStorage.getItem('username'); const storedUsername = localStorage.getItem('username');
username.value = storedUsername ? storedUsername : '用户'; username.value = storedUsername ? storedUsername : '用户';
}); });
watch( watch(
@ -123,8 +153,40 @@ watch(
} }
); );
const toggleFullScreen = () => {
const elem = document.documentElement as HTMLElement & {
webkitRequestFullscreen?: () => Promise<void>;
msRequestFullscreen?: () => Promise<void>;
};
if (!isFullScreen.value) {
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen(); // Safari
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen(); // IE/Edge
}
} else {
const doc = document as Document & {
webkitExitFullscreen?: () => Promise<void>;
msExitFullscreen?: () => Promise<void>;
};
if (doc.exitFullscreen) {
doc.exitFullscreen();
} else if (doc.webkitExitFullscreen) {
doc.webkitExitFullscreen(); // Safari
} else if (doc.msExitFullscreen) {
doc.msExitFullscreen(); // IE/Edge
}
}
isFullScreen.value = !isFullScreen.value;
};
const toggleCollapse = () => { const toggleCollapse = () => {
isCollapse.value = !isCollapse.value; // isCollapse.value = !isCollapse.value;
}; };
const onLogout = async () => { const onLogout = async () => {
@ -167,6 +229,7 @@ const onLogout = async () => {
color: #fff; color: #fff;
border: 1px; border: 1px;
} }
.el-menu-part .el-menu-item { .el-menu-part .el-menu-item {
/* padding:30px 0; */ /* padding:30px 0; */
padding: 0px; padding: 0px;
@ -178,6 +241,7 @@ const onLogout = async () => {
background-color: #001529; background-color: #001529;
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
/* 悬停样式 */ /* 悬停样式 */
.el-menu-part .el-menu-item:hover { .el-menu-part .el-menu-item:hover {
background-color: #001529; background-color: #001529;
@ -206,7 +270,8 @@ const onLogout = async () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
max-height: 100%; /* max-height: 100%; */
height: 100vh;
/* overflow-x: auto; */ /* overflow-x: auto; */
/* min-width: 0; */ /* min-width: 0; */
} }
@ -241,7 +306,7 @@ const onLogout = async () => {
.main-content { .main-content {
background-color: #f5f8fc; background-color: #f5f8fc;
flex-grow: 1; flex-grow: 1;
max-height: 95vh; max-height: 100vh;
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
@ -263,4 +328,19 @@ const onLogout = async () => {
padding: 0; padding: 0;
border: 1px solid #001529; border: 1px solid #001529;
} }
.exit-fullscreen-button {
position: fixed;
bottom: 3vh;
left: 0px;
height: 50px;
/* border-radius: 25px; */
background-color: #1890ff;
color: white;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.15);
width: 100px;
border-radius: 0 25px 25px 0;
}
</style> </style>

View File

@ -0,0 +1,55 @@
<template>
<div ref="chart" class="left-top-content" :style="style"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import * as echarts from 'echarts';
const props = defineProps(['style']);
const chart = ref(null);
const myChart = ref(null);
const setupChart = () => {
myChart.value = echarts.init(chart.value);
const option = {
title: {},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};
myChart.value.setOption(option);
};
onMounted(() => {
setupChart();
const resizeObserver = new ResizeObserver(() => {
myChart.value.resize(); //
});
resizeObserver.observe(chart.value.parentElement); //
onBeforeUnmount(() => {
resizeObserver.disconnect();
myChart.value.dispose(); // ECharts
});
});
</script>
<style scoped>
.left-top-content {
width: 100%; /* 与父容器宽度匹配 */
height: 100%; /* 与父容器高度匹配 */
position: relative; /* 允许子元素绝对定位 */
}
</style>

View File

@ -1,35 +1,37 @@
<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">开启弹窗</el-checkbox> <el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange">
</el-row> 开启弹窗
</div> </el-checkbox>
</el-row>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } 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'; //
// const isPopupEnabled = ref(false); //
const isPopupEnabled = ref(false); const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket'); //
let websocket: WebSocket | null = null;
let heartbeatInterval: number | null = null; // // globalWebSocket
if (!globalWebSocket) {
throw new Error('globalWebSocket 注入失败');
}
// localStorage // localStorage
onMounted(() => { onMounted(() => {
const storedState = localStorage.getItem('isPopupEnabled'); const storedState = localStorage.getItem('isPopupEnabled');
isPopupEnabled.value = storedState === 'true'; isPopupEnabled.value = storedState === 'true';
if (isPopupEnabled.value) { // WebSocket WebSocket
connectWebSocket(); if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
globalWebSocket.connectWebSocket();
} }
}); });
// WebSocket
onBeforeUnmount(() => {
closeWebSocket();
});
// //
const handleCheckboxChange = () => { const handleCheckboxChange = () => {
if (isPopupEnabled.value) { if (isPopupEnabled.value) {
@ -40,106 +42,21 @@ const handleCheckboxChange = () => {
type: 'warning', type: 'warning',
}) })
.then(() => { .then(() => {
// WebSocket // WebSocket
localStorage.setItem('isPopupEnabled', 'true'); // localStorage localStorage.setItem('isPopupEnabled', 'true');
connectWebSocket(); globalWebSocket.connectWebSocket();
}) })
.catch(() => { .catch(() => {
// //
isPopupEnabled.value = false; isPopupEnabled.value = false;
localStorage.setItem('isPopupEnabled', 'false'); // localStorage localStorage.setItem('isPopupEnabled', 'false');
}); });
} else { } else {
// WebSocket // WebSocket
closeWebSocket(); globalWebSocket.closeWebSocket();
}
};
// WebSocket
const connectWebSocket = () => {
websocket = new WebSocket('ws://192.168.28.11:8080/event/ws');
websocket.onopen = () => {
ElMessage.success('弹窗告警开启成功');
startHeartbeat(); //
};
websocket.onmessage = (event) => {
//
showNotification(event.data);
};
websocket.onclose = () => {
ElMessage.warning('WebSocket 已关闭');
isPopupEnabled.value = false; //
localStorage.setItem('isPopupEnabled', 'false'); // localStorage
stopHeartbeat(); //
};
websocket.onerror = () => {
ElMessage.error('WebSocket 连接出错');
isPopupEnabled.value = false;
localStorage.setItem('isPopupEnabled', 'false'); localStorage.setItem('isPopupEnabled', 'false');
stopHeartbeat(); //
};
};
// WebSocket
const closeWebSocket = () => {
if (websocket) {
websocket.close();
ElMessage.info('WebSocket 连接已关闭');
localStorage.setItem('isPopupEnabled', 'false'); // localStorage
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',
});
}
});
}
};
// ping WebSocket
const startHeartbeat = () => {
if (heartbeatInterval) return;
heartbeatInterval = window.setInterval(() => {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send('ping'); //
}
}, 5000); // 5
};
//
const stopHeartbeat = () => {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
};
//
onMounted(() => {
if (Notification.permission !== 'granted') {
Notification.requestPermission();
}
});
</script> </script>
<style scoped> <style scoped>

View File

@ -13,10 +13,42 @@
告警总数:{{ displayTotalItems }} 告警总数:{{ displayTotalItems }}
</span> </span>
</el-row> </el-row>
<el-row class="filter-row">
<el-col :span="5">
<el-form-item label="摄像头名称">
<el-select v-model="filterParams.cameraId" placeholder="请选择摄像头" filterable>
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="告警类型">
<el-select v-model="filterParams.types" placeholder="请选择告警类型">
<el-option v-for="(label, key) in typeMapping" :key="key" :label="label" :value="key"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="开始时间">
<el-date-picker v-model="filterParams.timeAfter" type="datetime" placeholder="请选择开始时间"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="结束时间">
<el-date-picker v-model="filterParams.timeBefore" type="datetime" placeholder="请选择结束时间"></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="2" class="filter-buttons">
<el-button type="primary" @click="handleFilter">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-col>
</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">
<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">
@ -35,9 +67,13 @@
}}</el-tag> }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" min-width="100"> <el-table-column label="操作" min-width="150">
<template v-slot="scope"> <template v-slot="scope">
<el-button type="text" @click="handleView(scope.row)">查看</el-button> <el-button type="text" @click="handleView(scope.row)">查看</el-button>
<el-button type="text" v-if="scope.row.status === 'pending'"
@click="submitStatusUpdate('closed', scope.row)">
标记为已处理
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -78,7 +114,7 @@
<p>持续时间: {{ duration }}</p> <p>持续时间: {{ duration }}</p>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<p>备注: {{ selectedRow.note }}</p> <p>备注: {{ selectedRow.remark }}</p>
</el-col> </el-col>
</el-row> </el-row>
<!-- <div class="event-media"> <!-- <div class="event-media">
@ -93,7 +129,7 @@
</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 v-if="hasVideo" class="media-container video-item"> <div v-if="hasVideo" class="media-container video-item">
<p>告警关联视频</p> <p>告警关联视频</p>
@ -106,10 +142,27 @@
<el-image :src="snapshotFile" fit="contain" @click="openMediaDialog('image', snapshotFile)"></el-image> <el-image :src="snapshotFile" fit="contain" @click="openMediaDialog('image', snapshotFile)"></el-image>
</div> </div>
</div> </div>
<!-- 备注输入框 -->
<el-row>
<el-col :span="24">
<el-form-item>
<el-input v-model="remark" placeholder="请描述事件原因,处理过程方法,以及处理结果" type="textarea" rows="5"></el-input>
</el-form-item>
</el-col>
</el-row>
<!-- 条件显示按钮 -->
<el-row class="dialog-button">
<el-col :span="24" style="text-align: center;">
<el-button v-if="selectedRow.status === 'pending'" type="primary" @click="submitStatusUpdate('closed')">
标记为已处理
</el-button>
<el-button v-else type="primary" @click="submitStatusUpdate(selectedRow.status)">
提交
</el-button>
</el-col>
</el-row>
</div> </div>
<span slot="footer" class="dialog-footer">
<div class=""></div>
</span>
</el-dialog> </el-dialog>
<el-dialog v-model="mediaDialogVisible" width="70%"> <el-dialog v-model="mediaDialogVisible" width="70%">
<div v-if="mediaType === 'image'"> <div v-if="mediaType === 'image'">
@ -134,6 +187,7 @@ const boxApi = new BoxApi();
// //
const tableData = ref([]); const tableData = ref([]);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const remark = ref("");
const mediaDialogVisible = ref(false); const mediaDialogVisible = ref(false);
const mediaType = ref(''); const mediaType = ref('');
const mediaSrc = ref(''); const mediaSrc = ref('');
@ -144,6 +198,7 @@ const duration = ref('');
const typeMapping = reactive({}); const typeMapping = reactive({});
const statusMapping = { const statusMapping = {
'pending': '待处理', 'pending': '待处理',
'assigned': '处理中',
'closed': '已处理' 'closed': '已处理'
}; };
const currentPage = ref(1); const currentPage = ref(1);
@ -151,6 +206,101 @@ const pageSize = ref(20);
const token = ref(null); const token = ref(null);
const totalItems = ref(0); const totalItems = ref(0);
const displayTotalItems = ref(0); // const displayTotalItems = ref(0); //
const cameras = ref([]);
const fetchCameras = async () => {
try {
const token = localStorage.getItem('alertToken');
const limit = 20; // 20
let offset = 0; // 0
let allCameras = [];
//
const firstResponse = await boxApi.getCameras(limit, offset, token);
const cameraCount = firstResponse.count;
allCameras = firstResponse.results;
//
while (offset + limit < cameraCount) {
offset += limit;
const response = await boxApi.getCameras(limit, offset, token);
allCameras = allCameras.concat(response.results);
}
cameras.value = allCameras; //
} catch (error) {
console.error("Error fetching cameras:", error);
}
};
const filterParams = reactive({
types: null,
timeAfter: null,
timeBefore: null,
});
//
const submitStatusUpdate = async (newStatus, row = null) => {
try {
// console.console.log(row,row.id);
const eventId = row ? row.id : selectedRow.value.id;
const remarkContent = remark.value && remark.value.trim() !== "" ? remark.value : null;
// setEventStatus
await boxApi.setEventStatus(eventId, newStatus, remarkContent);
if (row) {
row.status = newStatus;
row.remark = remarkContent; //
} else {
selectedRow.value.status = newStatus;
selectedRow.value.remark = remarkContent;
dialogVisible.value = false; //
remark.value = ""; //
}
} catch (error) {
console.error("Error updating event status:", error);
}
};
const handleFilter = async () => {
try {
const types = filterParams.types || null;
// const timeAfter = filterParams.timeAfter ? new Date(filterParams.timeAfter).toISOString() : null;
// const timeBefore = filterParams.timeBefore ? new Date(filterParams.timeBefore).toISOString() : null;
const timeAfter = filterParams.timeAfter ? formatDateTime(new Date(filterParams.timeAfter)) : null;
const timeBefore = filterParams.timeBefore ? formatDateTime(new Date(filterParams.timeBefore)) : null;
const cameraId = filterParams.cameraId || null;
const { results, count } = await boxApi.getEventsByParams(
token.value,
pageSize.value,
currentPage.value,
timeBefore,
timeAfter,
types,
cameraId
);
tableData.value = results;
totalItems.value = count;
} catch (error) {
console.error("Error fetching filtered events:", error);
}
};
const handleReset = () => {
filterParams.types = null;
filterParams.timeAfter = null;
filterParams.timeBefore = null;
filterParams.cameraId = null;
fetchEvents(); //
};
const openMediaDialog = (type, src) => { const openMediaDialog = (type, src) => {
@ -226,12 +376,21 @@ const handleView = (row) => {
row.formatted_started_at = formatDateTime(row.started_at); row.formatted_started_at = formatDateTime(row.started_at);
dialogVisible.value = true; dialogVisible.value = true;
mediums.value = row.mediums || []; mediums.value = row.mediums || [];
remark.value = row.remark || "";
};
const closeDialog = () => {
remark.value = "";
}; };
// //
const handlePageChange = (page) => { const handlePageChange = (page) => {
currentPage.value = page; currentPage.value = page;
fetchEvents(); if (filterParams.types || filterParams.timeAfter || filterParams.timeBefore) {
handleFilter();
} else {
fetchEvents();
}
}; };
// //
@ -267,6 +426,7 @@ onMounted(async () => {
token.value = localStorage.getItem('alertToken'); token.value = localStorage.getItem('alertToken');
await fetchTypeMapping(token.value); await fetchTypeMapping(token.value);
await fetchEvents(); await fetchEvents();
await fetchCameras();
}); });
</script> </script>
@ -343,8 +503,8 @@ onMounted(async () => {
padding: 10px; padding: 10px;
} }
.table-col{ .table-col {
max-height:100%; max-height: 100%;
} }
.video-item video, .video-item video,
@ -374,4 +534,22 @@ onMounted(async () => {
::v-deep .pagination-container .el-pagination__classifier { ::v-deep .pagination-container .el-pagination__classifier {
color: #000; color: #000;
} }
.dialog-button {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.filter-row {
display: flex;
justify-content: center;
align-content: center;
height: 80px;
padding: 20px;
margin-top: 20px;
gap: 20px;
font-weight: bold;
}
</style> </style>

View File

@ -96,6 +96,7 @@ onMounted(async () => {
height: 56vh; height: 56vh;
margin-top: 60px; margin-top: 60px;
/* border: 1px solid #1E2E4A; */ /* border: 1px solid #1E2E4A; */
} }
.bottom-pan{ .bottom-pan{
margin: 0; margin: 0;

View File

@ -224,7 +224,7 @@ onBeforeUnmount(() => {
/* 每个摄像头项目的样式 */ /* 每个摄像头项目的样式 */
.camera-item { .camera-item {
margin-bottom: 8px; margin-bottom: 10px;
cursor: pointer; cursor: pointer;
padding: 12px; padding: 12px;
border: 1px solid #458388; border: 1px solid #458388;
@ -313,8 +313,8 @@ onBeforeUnmount(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
/* background-color: #1E2E4A; */ /* 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(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(0, 7, 8, 0.9), rgba(12, 2, 155, 0.7));
border: 2px solid #0b4c5f; border: 2px solid #0b4c5f;
border-radius: 8px; border-radius: 8px;
position: relative; position: relative;

304
src/html/ViewList.vue Normal file
View File

@ -0,0 +1,304 @@
<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-300>
&nbsp;本地告警大屏&nbsp;
</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">左中
<div class="section hiden"></div>
</div>
<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">点位告警数量不同点位的数量</dv-border-box-13>
<dv-border-box-13 title="今日告警列表(告警详情)" class="section bottom-left ">今日告警列表告警详情</dv-border-box-13>
</div>
<!-- 中部区域 -->
<div class="center-section">
<dv-border-box8 class="center-top">
<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">警戒点位列表</dv-border-box-13>
</div>
</div>
<!-- 右侧区域 -->
<div class="right-section">
<dv-border-box-13 class="section top-right corner-style">告警类型概览</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">告警种类划分</dv-border-box-13>
</div>
</div>
</div>
</div>
</template>
<script setup>
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 '/src/assets/viewListStyle.css'
</script>
<style scoped>
.list-view {
display: flex;
justify-content: center;
margin: 0;
height: 100vh;
width: 100vw;
padding: 4vh 10vw 10vh 1vw;
/* background-color: rgb(121, 184, 243); */
/* background-image: url('/bg05.png'); */
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;
box-sizing: border-box;
}
/* .custom-decoration{
color:white;
} */
.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;
z-index: 0;
}
.container {
display: flex;
flex-direction: column;
width: 80vw;
height: 100%;
gap: 1vh;
position: relative;
/* 为了在背景下显示 */
z-index: 1;
}
.header {
height: 12vh;
width: 81vw;
/* background-color: #000080; */
color: white;
text-align: center;
line-height: 12vh;
display: flex;
flex-direction: row;
margin-bottom: 20px;
}
.line-title {
display: flex;
flex-direction: column;
/* gap: 2vh; */
}
.title-left {
display: flex;
flex-direction: column;
}
.title-right {
display: flex;
flex-direction: column; /* 设置为垂直布局 */
}
.first-row {
display: flex;
justify-content: flex-start; /* 第一行内容靠左 */
}
.second-row {
display: flex;
justify-content: flex-end; /* 第二行内容靠右 */
}
.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(221, 204, 204, 0.5); */
}
.top-left{
position: relative;
padding: 1vh 1vw;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
flex-grow: 1;
width: 20vw;
height: 20vh;
}
/* .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: 35vw;
height: 22vh;
/* background-color: #444; */
color: white;
text-align: center;
/* margin-top: 1vh; */
}
</style>

View File

@ -8,6 +8,8 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn';
import axios from 'axios'; import axios from 'axios';
import '@/assets/global.css' import '@/assets/global.css'
import { useGlobalWebSocket } from './utils/useGlobalWebSocket'; import { useGlobalWebSocket } from './utils/useGlobalWebSocket';
import DataVVue3 from '@kjgl77/datav-vue3';
import '@kjgl77/datav-vue3/dist/style.css';
const app = createApp(App) const app = createApp(App)
@ -15,7 +17,8 @@ const globalWebSocket = useGlobalWebSocket();
app.provide('globalWebSocket',globalWebSocket); app.provide('globalWebSocket',globalWebSocket);
// app.provide('axios', axiosInstance); // app.provide('axios', axiosInstance);
app.use(ElementPlus, { locale: zhCn }); app.use(ElementPlus, { locale: zhCn });
app.use (router) app.use (router);
app.use(DataVVue3);
// 导航守卫,检查登录状态 // 导航守卫,检查登录状态
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {

View File

@ -11,6 +11,7 @@ import Home from '@/html/Home.vue';
import DataStatistics from '@/html/DataStatistics.vue'; import DataStatistics from '@/html/DataStatistics.vue';
import Cameras from '@/components/Cameras.vue'; import Cameras from '@/components/Cameras.vue';
import Settings from '@/components/Settings.vue'; import Settings from '@/components/Settings.vue';
import ViewList from '@/html/ViewList.vue';
const routes = [ const routes = [
{ {
@ -72,6 +73,12 @@ const routes = [
name: 'Settings', name: 'Settings',
component: Settings, component: Settings,
meta: { requiresAuth: true } meta: { requiresAuth: true }
},
{
path:'/viewList',
name: 'ViewList',
component: ViewList,
meta: { requiresAuth: true }
} }
] ]
}, },

View File

@ -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<any> {
// 计算 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<any> { // public async getOneEvent(token: string | null = null): Promise<any> {
// try { // try {
// return await this.getEvents(1, 0, token); // 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<any> { public async setEventStatus(eventId: number, status: string, remark: string | null = null, token: string | null = null): Promise<any> {
const url = `${this.apiEvents}/${eventId}`; const url = `${this.apiEvents}/${eventId}`;
const newRemark = remark ? remark : "";
const data = { const data: { status: string; remark?: string } = { status: status };
status: status, if (remark && remark.trim() !== "") {
remark: newRemark data.remark = remark;
}; }
try { 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) { if (res.data.err.ec === 0) {
return res.data.ret; return res.data.ret;
} else { } else {
@ -387,7 +435,7 @@ class BoxApi {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`, 'Authorization': `Bearer ${accessToken}`,
// 'X-CSRFToken': this.getCsrfToken() // 'X-CSRFToken': this.getCsrfToken()
} }
}; };

View File

@ -0,0 +1,96 @@
// useGlobalWebSocket.ts
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
const websocket = ref<WebSocket | null>(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,
});