Compare commits
3 Commits
b80c991378
...
f456cdaa8c
Author | SHA1 | Date |
---|---|---|
|
f456cdaa8c | |
|
e16bda93b1 | |
|
0fe9cf0e3e |
11
README.md
11
README.md
|
@ -252,11 +252,18 @@ router.beforeEach((to, from, next) => {
|
|||
}
|
||||
});
|
||||
export default router;
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 组件页面
|
||||
|
||||
#### Layout.vue
|
||||
|
||||
## 本地告警首页ViewList布局
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 959 KiB |
Binary file not shown.
After Width: | Height: | Size: 521 KiB |
|
@ -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; */
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -96,6 +96,7 @@ onMounted(async () => {
|
|||
height: 56vh;
|
||||
margin-top: 60px;
|
||||
/* border: 1px solid #1E2E4A; */
|
||||
|
||||
}
|
||||
.bottom-pan{
|
||||
margin: 0;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
本地告警大屏
|
||||
</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>
|
|
@ -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) => {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
Loading…
Reference in New Issue