Compare commits

...

3 Commits

Author SHA1 Message Date
龚皓 f456cdaa8c 告警大屏css初稿,大屏小图echart测试页 2024-11-01 13:19:38 +08:00
龚皓 e16bda93b1 大屏背景 2024-11-01 13:15:48 +08:00
龚皓 0fe9cf0e3e 大屏布局,dataV导入 2024-11-01 13:15:05 +08:00
17 changed files with 1217 additions and 158 deletions

View File

@ -252,11 +252,18 @@ router.beforeEach((to, from, next) => {
}
});
export default router;
```
### 组件页面
#### Layout.vue
## 本地告警首页ViewList布局
![image-20241030162011722](https://gitee.com/gonghao_git/draw-bed/raw/master/img/%E9%A6%96%E9%A1%B5ViewList%E5%B8%83%E5%B1%80-20241030162011722.png)
![image-20241031164625119](https://gitee.com/gonghao_git/draw-bed/raw/master/img/%E5%A4%A7%E5%B1%8F%E9%A1%B5%E9%9D%A2%E5%88%9D%E7%A8%BF-20241031164625119.png)

142
package-lock.json generated
View File

@ -8,6 +8,8 @@
"name": "local_alert",
"version": "0.0.0",
"dependencies": {
"@jiaminghi/data-view": "^2.10.0",
"@kjgl77/datav-vue3": "^1.7.4",
"axios": "^1.7.3",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
@ -57,6 +59,17 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.25.2",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.25.2.tgz",
@ -476,11 +489,135 @@
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.7.tgz",
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
},
"node_modules/@jiaminghi/bezier-curve": {
"version": "0.0.9",
"resolved": "https://registry.npmmirror.com/@jiaminghi/bezier-curve/-/bezier-curve-0.0.9.tgz",
"integrity": "sha512-u9xJPOEl6Dri2E9FfmJoGxYQY7vYJkURNX04Vj64tdi535tPrpkuf9Sm0lNr3QTKdHQh0DdNRsaa62FLQNQEEw==",
"dependencies": {
"@babel/runtime": "^7.5.5"
}
},
"node_modules/@jiaminghi/c-render": {
"version": "0.4.3",
"resolved": "https://registry.npmmirror.com/@jiaminghi/c-render/-/c-render-0.4.3.tgz",
"integrity": "sha512-FJfzj5hGj7MLqqqI2D7vEzHKbQ1Ynnn7PJKgzsjXaZpJzTqs2Yw5OSeZnm6l7Qj7jyPAP53lFvEQNH4o4j6s+Q==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"@jiaminghi/bezier-curve": "*",
"@jiaminghi/color": "*",
"@jiaminghi/transition": "*"
}
},
"node_modules/@jiaminghi/charts": {
"version": "0.2.18",
"resolved": "https://registry.npmmirror.com/@jiaminghi/charts/-/charts-0.2.18.tgz",
"integrity": "sha512-K+HXaOOeWG9OOY1VG6M4mBreeeIAPhb9X+khG651AbnwEwL6G2UtcAQ8GWCq6GzhczcLwwhIhuaHqRygwHC0sA==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"@jiaminghi/c-render": "^0.4.3"
}
},
"node_modules/@jiaminghi/color": {
"version": "1.1.3",
"resolved": "https://registry.npmmirror.com/@jiaminghi/color/-/color-1.1.3.tgz",
"integrity": "sha512-ZY3hdorgODk4OSTbxyXBPxAxHPIVf9rPlKJyK1C1db46a50J0reFKpAvfZG8zMG3lvM60IR7Qawgcu4ZDO3+Hg=="
},
"node_modules/@jiaminghi/data-view": {
"version": "2.10.0",
"resolved": "https://registry.npmmirror.com/@jiaminghi/data-view/-/data-view-2.10.0.tgz",
"integrity": "sha512-Cud2MTiMcqc5k2KWabR/svuVQmXHANqURo+yj40370/LdI/gyUJ6LG203hWXEnT1nMCeiv/SLVmxv3PXLScCeA==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"@jiaminghi/charts": "*"
}
},
"node_modules/@jiaminghi/transition": {
"version": "1.1.11",
"resolved": "https://registry.npmmirror.com/@jiaminghi/transition/-/transition-1.1.11.tgz",
"integrity": "sha512-owBggipoHMikDHHDW5Gc7RZYlVuvxHADiU4bxfjBVkHDAmmck+fCkm46n2JzC3j33hWvP9nSCAeh37t6stgWeg==",
"dependencies": {
"@babel/runtime": "^7.5.5"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
},
"node_modules/@kjgl77/datav-vue3": {
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@kjgl77/datav-vue3/-/datav-vue3-1.7.4.tgz",
"integrity": "sha512-zYVTVKkklUxwtiNKS1qPBilm4rTW+WItfp0zVpaRAI8wgXkLSPbDR9xPq2+UcU/Jft7/DVdMfBp709E2ResuPQ==",
"dependencies": {
"@jiaminghi/c-render": "^0.4.3",
"@jiaminghi/charts": "^0.2.18",
"@jiaminghi/color": "^1.1.3",
"@vueuse/core": "^10.11.1"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"node_modules/@kjgl77/datav-vue3/node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/@vueuse/metadata": {
"version": "10.11.1",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz",
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/@vueuse/shared": {
"version": "10.11.1",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz",
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@kjgl77/datav-vue3/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
@ -1477,6 +1614,11 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/rollup": {
"version": "4.20.0",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.20.0.tgz",

View File

@ -11,6 +11,8 @@
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@jiaminghi/data-view": "^2.10.0",
"@kjgl77/datav-vue3": "^1.7.4",
"axios": "^1.7.3",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",

BIN
public/bg01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

BIN
public/bg05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

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>
<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">
<el-image src="/turing.png" fit="contain" />
</div>
<el-menu :default-active="activeIndex" class="el-menu-part" router :collapse="isCollapse">
<el-menu-item index="/">
<el-icon><House /></el-icon>
<el-icon>
<House />
</el-icon>
<template #title><span>首页</span></template>
</el-menu-item>
<el-menu-item index="/alertManagement">
<el-icon><Management /></el-icon>
<el-icon>
<Management />
</el-icon>
<template #title><span>告警列表</span></template>
</el-menu-item>
<el-menu-item index="/dataStatistics">
<el-icon><TrendCharts /></el-icon>
<el-icon>
<TrendCharts />
</el-icon>
<template #title><span>数据统计</span></template>
</el-menu-item>
<el-menu-item index="/userList">
<el-icon><Avatar /></el-icon>
<el-icon>
<Avatar />
</el-icon>
<template #title><span>用户管理</span></template>
</el-menu-item>
<el-menu-item index="/settings">
<el-icon><Setting /></el-icon>
<el-icon>
<Setting />
</el-icon>
<template #title><span>告警设置</span></template>
</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">
<template #title>
<el-icon><Location /></el-icon>
@ -53,12 +69,16 @@
<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;">
<component :is="isCollapse ? Expand : Fold" />
</el-icon>
<div>
<el-icon @click="toggleCollapse" style="cursor: pointer; margin-right: 20px;">
<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">
<!-- 用户头像下拉菜单 -->
<el-dropdown trigger="click">
@ -69,10 +89,14 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="goToUserManagement">
<el-icon><User /></el-icon>
<el-icon>
<User />
</el-icon>
</el-dropdown-item>
<el-dropdown-item @click="onLogout">
<el-icon><SwitchButton /></el-icon> 退
<el-icon>
<SwitchButton />
</el-icon> 退
</el-dropdown-item>
</el-dropdown-menu>
</template>
@ -84,9 +108,13 @@
<el-main class="main-content">
<router-view />
</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>
</template>
@ -94,9 +122,9 @@
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {
Document, WarningFilled, Location, User, SwitchButton,
House, Management, TrendCharts, Avatar, Fold, Expand ,Setting
import {
Document, WarningFilled, Location, User, SwitchButton,
House, Management, TrendCharts, Avatar, Fold, Expand, Setting, FullScreen, Notification
} from '@element-plus/icons-vue';
import { BoxApi } from '@/utils/boxApi';
import { ElMessage } from 'element-plus';
@ -107,6 +135,7 @@ const activeIndex = ref(route.path);
const isCollapse = ref(false); //
const username = ref(''); //
const isFullScreen = ref(false);
const goToUserManagement = () => {
router.push('/userList'); //
};
@ -114,6 +143,7 @@ const goToUserManagement = () => {
onMounted(() => {
const storedUsername = localStorage.getItem('username');
username.value = storedUsername ? storedUsername : '用户';
});
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 = () => {
isCollapse.value = !isCollapse.value; //
isCollapse.value = !isCollapse.value;
};
const onLogout = async () => {
@ -167,6 +229,7 @@ const onLogout = async () => {
color: #fff;
border: 1px;
}
.el-menu-part .el-menu-item {
/* padding:30px 0; */
padding: 0px;
@ -178,6 +241,7 @@ const onLogout = async () => {
background-color: #001529;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* 悬停样式 */
.el-menu-part .el-menu-item:hover {
background-color: #001529;
@ -206,7 +270,8 @@ const onLogout = async () => {
display: flex;
flex-direction: column;
flex-grow: 1;
max-height: 100%;
/* max-height: 100%; */
height: 100vh;
/* overflow-x: auto; */
/* min-width: 0; */
}
@ -241,7 +306,7 @@ const onLogout = async () => {
.main-content {
background-color: #f5f8fc;
flex-grow: 1;
max-height: 95vh;
max-height: 100vh;
margin: 0;
padding: 0;
width: 100%;
@ -263,4 +328,19 @@ const onLogout = async () => {
padding: 0;
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>

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>
<div class="settings-container">
<el-row class="popup-row">
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange">开启弹窗</el-checkbox>
</el-row>
</div>
<div class="settings-container">
<el-row class="popup-row">
<el-checkbox v-model="isPopupEnabled" @change="handleCheckboxChange">
开启弹窗
</el-checkbox>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ref, inject, onMounted } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import type { GlobalWebSocket } from '../utils/useGlobalWebSocket'; //
//
const isPopupEnabled = ref(false);
let websocket: WebSocket | null = null;
let heartbeatInterval: number | null = null; //
const isPopupEnabled = ref(false); //
const globalWebSocket = inject<GlobalWebSocket>('globalWebSocket'); //
// globalWebSocket
if (!globalWebSocket) {
throw new Error('globalWebSocket 注入失败');
}
// localStorage
onMounted(() => {
const storedState = localStorage.getItem('isPopupEnabled');
isPopupEnabled.value = storedState === 'true';
if (isPopupEnabled.value) {
connectWebSocket();
// WebSocket WebSocket
if (isPopupEnabled.value && !globalWebSocket.isWebSocketConnected.value) {
globalWebSocket.connectWebSocket();
}
});
// WebSocket
onBeforeUnmount(() => {
closeWebSocket();
});
//
const handleCheckboxChange = () => {
if (isPopupEnabled.value) {
@ -40,106 +42,21 @@ const handleCheckboxChange = () => {
type: 'warning',
})
.then(() => {
// WebSocket
localStorage.setItem('isPopupEnabled', 'true'); // localStorage
connectWebSocket();
// WebSocket
localStorage.setItem('isPopupEnabled', 'true');
globalWebSocket.connectWebSocket();
})
.catch(() => {
//
//
isPopupEnabled.value = false;
localStorage.setItem('isPopupEnabled', 'false'); // localStorage
localStorage.setItem('isPopupEnabled', 'false');
});
} else {
// WebSocket
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;
// WebSocket
globalWebSocket.closeWebSocket();
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>
<style scoped>

View File

@ -13,10 +13,42 @@
告警总数:{{ displayTotalItems }}
</span>
</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-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" 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 label="告警类型" min-width="150">
<template v-slot="scope">
@ -35,9 +67,13 @@
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="100">
<el-table-column label="操作" min-width="150">
<template v-slot="scope">
<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>
</el-table-column>
</el-table>
@ -78,7 +114,7 @@
<p>持续时间: {{ duration }}</p>
</el-col>
<el-col :span="6">
<p>备注: {{ selectedRow.note }}</p>
<p>备注: {{ selectedRow.remark }}</p>
</el-col>
</el-row>
<!-- <div class="event-media">
@ -93,7 +129,7 @@
</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">
<p>告警关联视频</p>
@ -106,10 +142,27 @@
<el-image :src="snapshotFile" fit="contain" @click="openMediaDialog('image', snapshotFile)"></el-image>
</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>
<span slot="footer" class="dialog-footer">
<div class=""></div>
</span>
</el-dialog>
<el-dialog v-model="mediaDialogVisible" width="70%">
<div v-if="mediaType === 'image'">
@ -134,6 +187,7 @@ const boxApi = new BoxApi();
//
const tableData = ref([]);
const dialogVisible = ref(false);
const remark = ref("");
const mediaDialogVisible = ref(false);
const mediaType = ref('');
const mediaSrc = ref('');
@ -144,6 +198,7 @@ const duration = ref('');
const typeMapping = reactive({});
const statusMapping = {
'pending': '待处理',
'assigned': '处理中',
'closed': '已处理'
};
const currentPage = ref(1);
@ -151,6 +206,101 @@ const pageSize = ref(20);
const token = ref(null);
const totalItems = 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) => {
@ -226,12 +376,21 @@ const handleView = (row) => {
row.formatted_started_at = formatDateTime(row.started_at);
dialogVisible.value = true;
mediums.value = row.mediums || [];
remark.value = row.remark || "";
};
const closeDialog = () => {
remark.value = "";
};
//
const handlePageChange = (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');
await fetchTypeMapping(token.value);
await fetchEvents();
await fetchCameras();
});
</script>
@ -343,8 +503,8 @@ onMounted(async () => {
padding: 10px;
}
.table-col{
max-height:100%;
.table-col {
max-height: 100%;
}
.video-item video,
@ -374,4 +534,22 @@ onMounted(async () => {
::v-deep .pagination-container .el-pagination__classifier {
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>

View File

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

View File

@ -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;

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

@ -7,13 +7,18 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn';
// import axiosInstance from '@/utils/axios-config';
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)
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) => {

View File

@ -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 }
}
]
},

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> {
// 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<any> {
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()
}
};

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