init: 初始化前端项目
1
web/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules
|
5
web/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
13
web/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
25
web/package.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "analytics-dashboard",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@kjgl77/datav-vue3": "^1.7.4",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"echarts-liquidfill": "^3.1.0",
|
||||||
|
"element-plus": "^2.10.4",
|
||||||
|
"vue": "^3.5.17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"sass": "^1.89.2",
|
||||||
|
"sass-embedded": "^1.89.2",
|
||||||
|
"vite": "^7.0.4"
|
||||||
|
}
|
||||||
|
}
|
1597
web/pnpm-lock.yaml
generated
Normal file
1
web/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
362
web/src/App.vue
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container" ref="dashboardContainer">
|
||||||
|
<!-- DataV顶部标题栏 -->
|
||||||
|
<DashboardHeader :active-tab="activeTab" @tab-change="handleTabChange" />
|
||||||
|
|
||||||
|
<!-- 全屏按钮 -->
|
||||||
|
<div class="fullscreen-btn" @click="toggleFullscreen">
|
||||||
|
<svg v-if="!isFullscreen" viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path fill="currentColor" d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else viewBox="0 0 24 24" width="24" height="24">
|
||||||
|
<path fill="currentColor" d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 根据当前标签显示不同内容 -->
|
||||||
|
<div v-if="activeTab === 'overview'" class="main-content">
|
||||||
|
<!-- 原有数据总览内容 -->
|
||||||
|
<div class="left-section">
|
||||||
|
<dv-border-box-1>
|
||||||
|
<MainKPI />
|
||||||
|
</dv-border-box-1>
|
||||||
|
</div>
|
||||||
|
<div class="center-section">
|
||||||
|
<AlarmTrend />
|
||||||
|
<CameraStats />
|
||||||
|
</div>
|
||||||
|
<div class="right-section">
|
||||||
|
<dv-border-box-1>
|
||||||
|
<EventHotSpots />
|
||||||
|
<AlgorithmStats />
|
||||||
|
</dv-border-box-1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 监控视角内容 -->
|
||||||
|
<div v-if="activeTab === 'monitor'" class="monitor-content">
|
||||||
|
<!-- 监控列表 -->
|
||||||
|
<MonitorDetail :visible="showMonitorDetail" :monitor="currentMonitor" @close="closeMonitorDetail" />
|
||||||
|
<div class="monitor-list">
|
||||||
|
<div class="monitor-grid">
|
||||||
|
<div v-for="monitor in monitors" :key="monitor.id">
|
||||||
|
<MonitorThumbnail :monitor="monitor" @click="selectMonitor(monitor)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 算法中心内容 -->
|
||||||
|
<div v-if="activeTab === 'algorithms'" class="algorithm-content">
|
||||||
|
<AlgorithmCenter />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 告警管理 -->
|
||||||
|
<div v-if="activeTab === 'alarms'" class="alarm-content">
|
||||||
|
<AlarmManagement />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 事件中心 -->
|
||||||
|
<div v-if="activeTab === 'events'" class="event-content">
|
||||||
|
<EventCenter />
|
||||||
|
</div>
|
||||||
|
<!-- 设备管理 -->
|
||||||
|
<div v-if="activeTab === 'devices'" class="device-content">
|
||||||
|
<DeviceManagement />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import MainKPI from './components/MainKPI.vue'
|
||||||
|
import AlarmTrend from './components/AlarmTrend.vue'
|
||||||
|
import EventHotSpots from './components/EventHotSpots.vue'
|
||||||
|
import AlgorithmStats from './components/AlgorithmStats.vue'
|
||||||
|
import CameraStats from './components/CameraStats.vue'
|
||||||
|
import DashboardHeader from './components/DashboardHeader.vue'
|
||||||
|
import MonitorThumbnail from './components/monitor/MonitorThumbnail.vue'
|
||||||
|
import MonitorDetail from './components/monitor/MonitorDetail.vue'
|
||||||
|
import AlgorithmCenter from './components/algorithm/AlgorithmCenter.vue'
|
||||||
|
import AlarmManagement from './components/alarm/AlarmManagement.vue'
|
||||||
|
import EventCenter from './components/event/EventCenter.vue'
|
||||||
|
import DeviceManagement from './components/device/DeviceManagement.vue'
|
||||||
|
|
||||||
|
const activeTab = ref('overview')
|
||||||
|
const dashboardContainer = ref(null)
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
|
||||||
|
const showMonitorDetail = ref(false)
|
||||||
|
|
||||||
|
const currentMonitor = ref({
|
||||||
|
id: 1,
|
||||||
|
name: '港口区主监控',
|
||||||
|
location: '港口区', // 与图片一致
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-main.mp4'
|
||||||
|
})
|
||||||
|
|
||||||
|
const monitors = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '港口区监控1',
|
||||||
|
location: '港口A区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-1.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'person', x: 25, y: 35, width: 40, height: 80 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '港口区监控2',
|
||||||
|
location: '港口B区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-2.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '港口区监控2',
|
||||||
|
location: '港口B区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-2.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: '港口区监控2',
|
||||||
|
location: '港口B区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-2.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: '港口区监控1',
|
||||||
|
location: '港口A区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-1.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'person', x: 25, y: 35, width: 40, height: 80 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: '港口区监控2',
|
||||||
|
location: '港口B区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-2.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: '港口区监控2',
|
||||||
|
location: '港口B区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-2.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: '港口区监控2',
|
||||||
|
location: '港口B区',
|
||||||
|
status: 'online',
|
||||||
|
videoSrc: '/videos/port-2.mp4',
|
||||||
|
detections: [
|
||||||
|
{ type: 'vehicle', x: 40, y: 50, width: 80, height: 60 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
// 切换标签
|
||||||
|
const handleTabChange = (tab) => {
|
||||||
|
console.log(tab)
|
||||||
|
activeTab.value = tab
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择监控画面
|
||||||
|
const selectMonitor = (monitor) => {
|
||||||
|
currentMonitor.value = monitor
|
||||||
|
showMonitorDetail.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭大屏监控
|
||||||
|
const closeMonitorDetail = () => {
|
||||||
|
showMonitorDetail.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换全屏
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
dashboardContainer.value.requestFullscreen().catch(err => {
|
||||||
|
console.error(`全屏错误: ${err.message}`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听全屏状态变化
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
isFullscreen.value = !!document.fullscreenElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加事件监听
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移除事件监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.dashboard-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #0a1d3c;
|
||||||
|
color: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.fullscreen-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
color: #7cd0ff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.left-section {
|
||||||
|
width: 25%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
height: 84vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-section {
|
||||||
|
height: 84vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-section {
|
||||||
|
height: 84vh;
|
||||||
|
width: 25%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 监控视角样式 */
|
||||||
|
.monitor-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-list {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.monitor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备管理样式 */
|
||||||
|
.device-content {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: rgba(16, 42, 87, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.algorithm-content {
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-content {
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.event-content {
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全屏样式 */
|
||||||
|
:fullscreen .dashboard-container {
|
||||||
|
background-color: #0a1d3c;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:fullscreen .main-content {
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:fullscreen .monitor-content,
|
||||||
|
:fullscreen .algorithm-content,
|
||||||
|
:fullscreen .alarm-content,
|
||||||
|
:fullscreen .event-content {
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
}
|
||||||
|
</style>
|
BIN
web/src/assets/images/header-center-in@1x.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
web/src/assets/images/header-center-out.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
web/src/assets/images/header-left@1x.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
web/src/assets/images/header-right@1x.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
web/src/assets/images/header-tab-bg@1x.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
web/src/assets/images/l-lx@1x.png
Normal file
After Width: | Height: | Size: 354 B |
BIN
web/src/assets/images/r-lx@1x.png
Normal file
After Width: | Height: | Size: 362 B |
BIN
web/src/assets/images/st-home@1x.png
Normal file
After Width: | Height: | Size: 996 B |
46
web/src/assets/styles/main.scss
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// 全局变量
|
||||||
|
$primary-color: #409eff;
|
||||||
|
$background-color: #0a1d3c;
|
||||||
|
$text-color: #fff;
|
||||||
|
$text-secondary: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
// 重置样式
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
background-color: $background-color;
|
||||||
|
color: $text-color;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片通用样式
|
||||||
|
.card {
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图表容器
|
||||||
|
.chart-container {
|
||||||
|
@extend .card;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
1
web/src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
172
web/src/components/AlarmTrend.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div class="alert-trend-container">
|
||||||
|
<dv-border-box-10 class="border-box">
|
||||||
|
<div class="chart-title">告警趋势</div>
|
||||||
|
<div ref="chart" class="chart"></div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const chart = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const myChart = echarts.init(chart.value)
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
borderColor: 'rgba(64, 158, 255, 0.8)',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: { color: '#fff' }
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['P0告警', 'P1告警', 'P2告警', 'P3告警'],
|
||||||
|
textStyle: { color: '#fff', fontSize: 12 },
|
||||||
|
itemWidth: 12,
|
||||||
|
itemHeight: 8,
|
||||||
|
itemGap: 15,
|
||||||
|
right: 20,
|
||||||
|
top: 10
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
top: '20%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: ['1月', '2月', '3月', '4月', '5月'],
|
||||||
|
axisLine: { lineStyle: { color: 'rgba(124, 208, 255, 0.5)' } },
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
axisTick: { show: false }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 550,
|
||||||
|
interval: 50,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: { color: 'rgba(124, 208, 255, 0.5)' }
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.1)',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'P0告警',
|
||||||
|
type: 'line',
|
||||||
|
smooth: false,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#FF4D4F' },
|
||||||
|
itemStyle: {
|
||||||
|
color: '#FF4D4F',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
data: [300, 100, 400, 250, 200]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P1告警',
|
||||||
|
type: 'line',
|
||||||
|
smooth: false,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#FFA500' },
|
||||||
|
itemStyle: {
|
||||||
|
color: '#FFA500',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
data: [300, 100, 400, 250, 200]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P2告警',
|
||||||
|
type: 'line',
|
||||||
|
smooth: false,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#A020F0' },
|
||||||
|
itemStyle: {
|
||||||
|
color: '#A020F0',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
data: [350, 150, 550, 50, 110]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'P3告警',
|
||||||
|
type: 'line',
|
||||||
|
smooth: false,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2, color: '#00FF00' },
|
||||||
|
itemStyle: {
|
||||||
|
color: '#00FF00',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
data: [200, 300, 400, 100, 50]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
myChart.setOption(option)
|
||||||
|
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
myChart.resize()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.alert-trend-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 50vh; /* 增加高度 */
|
||||||
|
padding: 15px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 50px; /* 增加上方间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* padding: 20px; */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
}
|
||||||
|
</style>
|
152
web/src/components/AlgorithmStats.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div class="radar-chart-container">
|
||||||
|
<dv-border-box-10 class="border-box">
|
||||||
|
<div class="chart-title">算法命中率统计</div>
|
||||||
|
<div ref="radarChart" class="chart"></div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const radarChart = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const radarOption = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
|
borderColor: 'rgba(124, 208, 255, 0.8)',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['目标识别算法', '行为识别算法'],
|
||||||
|
bottom: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
itemWidth: 12,
|
||||||
|
itemHeight: 8,
|
||||||
|
itemGap: 20
|
||||||
|
},
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{ name: '命中率', max: 100 },
|
||||||
|
{ name: '误报率', max: 100 },
|
||||||
|
{ name: '触发器', max: 100 },
|
||||||
|
{ name: '处理时延', max: 100 },
|
||||||
|
{ name: '稳定性', max: 100 }
|
||||||
|
],
|
||||||
|
radius: '65%',
|
||||||
|
axisName: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
|
fontSize: 12,
|
||||||
|
padding: [3, 5]
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.2)',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.5)',
|
||||||
|
width: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
type: 'radar',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: [90, 85, 95, 80, 88],
|
||||||
|
name: '目标识别算法',
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: '#A020F0' // 紫色
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#A020F0',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(160, 32, 240, 0.2)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: [80, 75, 85, 70, 82],
|
||||||
|
name: '行为识别算法',
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
color: '#1E90FF' // 蓝色
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1E90FF',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(30, 144, 255, 0.2)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartInstance = echarts.init(radarChart.value)
|
||||||
|
chartInstance.setOption(radarOption)
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
chartInstance.resize()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.radar-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 40vh; /* 高度占一半 */
|
||||||
|
padding: 15px 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* padding: 20px; */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
126
web/src/components/CameraStats.vue
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stats-container">
|
||||||
|
<dv-border-box-10>
|
||||||
|
<div class="stat-title">区域/设备类指标</div>
|
||||||
|
<div class="gauge-container">
|
||||||
|
<div class="gauge">
|
||||||
|
<div ref="onlineRateChart" class="gauge-chart"></div>
|
||||||
|
<div class="gauge-value">58%</div>
|
||||||
|
<div class="gauge-label">摄像头在线率</div>
|
||||||
|
</div>
|
||||||
|
<div class="gauge">
|
||||||
|
<div ref="coverageChart" class="gauge-chart"></div>
|
||||||
|
<div class="gauge-value">40%</div>
|
||||||
|
<div class="gauge-label">算法部署覆盖率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import 'echarts-liquidfill' // 引入水波图扩展
|
||||||
|
|
||||||
|
const onlineRateChart = ref(null)
|
||||||
|
const coverageChart = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 水波图配置
|
||||||
|
const createLiquidOption = (color) => ({
|
||||||
|
series: [{
|
||||||
|
type: 'liquidFill',
|
||||||
|
radius: '70%',
|
||||||
|
center: ['50%', '50%'],
|
||||||
|
data: [0.338, 0.48], // 水波高度
|
||||||
|
color: [color],
|
||||||
|
backgroundStyle: {
|
||||||
|
color: 'rgba(16, 42, 87, 0.5)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(124, 208, 255, 0.3)'
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
waveAnimation: true,
|
||||||
|
animationDuration: 2000,
|
||||||
|
animationEasing: 'linear',
|
||||||
|
amplitude: 6,
|
||||||
|
waveLength: '80%'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化图表
|
||||||
|
const onlineRate = echarts.init(onlineRateChart.value)
|
||||||
|
onlineRate.setOption(createLiquidOption('#3A8BFF'))
|
||||||
|
|
||||||
|
const coverage = echarts.init(coverageChart.value)
|
||||||
|
coverage.setOption(createLiquidOption('#FFA726'))
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
onlineRate.resize()
|
||||||
|
coverage.resize()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 样式保持不变 */
|
||||||
|
.stats-container {
|
||||||
|
|
||||||
|
height: 30vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-box {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-chart {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
margin: 10px 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
</style>
|
281
web/src/components/DashboardHeader.vue
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<div class="header-top">
|
||||||
|
<div class="header-left">
|
||||||
|
<img src="/src/assets/images/header-left@1x.png"></img>
|
||||||
|
<div class="left-tabs">
|
||||||
|
<template v-if="isOverviewOrMonitor">
|
||||||
|
<div
|
||||||
|
class="tab-item overview-tab"
|
||||||
|
:class="{active: activeTab === 'overview'}"
|
||||||
|
@click="handleTabClick('overview')"
|
||||||
|
>
|
||||||
|
数据总览
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="tab-item monitor-tab"
|
||||||
|
:class="{active: activeTab === 'monitor'}"
|
||||||
|
@click="handleTabClick('monitor')"
|
||||||
|
>
|
||||||
|
监控视角
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
src="/src/assets/images/st-home@1x.png"
|
||||||
|
@click="handleTabClick('overview')"
|
||||||
|
class="home-icon"
|
||||||
|
></img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-center">
|
||||||
|
<div class="center-top">
|
||||||
|
<img class="out-bg" src="/src/assets/images/header-center-out.png"></img>
|
||||||
|
<img class="in-bg"src="/src/assets/images/header-center-in@1x.png"></img>
|
||||||
|
<div class="l-lx">
|
||||||
|
<img src="/src/assets/images/l-lx@1x.png" ></img>
|
||||||
|
<img src="/src/assets/images/l-lx@1x.png" ></img>
|
||||||
|
<img src="/src/assets/images/l-lx@1x.png" ></img>
|
||||||
|
</div>
|
||||||
|
<div class="r-lx">
|
||||||
|
<img src="/src/assets/images/r-lx@1x.png" ></img>
|
||||||
|
<img src="/src/assets/images/r-lx@1x.png" ></img>
|
||||||
|
<img src="/src/assets/images/r-lx@1x.png" ></img>
|
||||||
|
</div>
|
||||||
|
<div class="title">视频智能分析系统</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<img src="/src/assets/images/header-right@1x.png"></img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center-tabs">
|
||||||
|
<img src="/src/assets/images/header-tab-bg@1x.png"></img>
|
||||||
|
<div class="item">
|
||||||
|
<a @click="handleTabClick('devices')">设备管理</a>
|
||||||
|
<a @click="handleTabClick('algorithms')">算法中心</a>
|
||||||
|
<a class="has-alarm" @click="handleTabClick('alarms')">
|
||||||
|
告警管理<span class="alarm-badge">99</span>
|
||||||
|
</a>
|
||||||
|
<a @click="handleTabClick('events')">事件中心</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
activeTab: {
|
||||||
|
type: String,
|
||||||
|
default: 'overview'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['tab-change'])
|
||||||
|
|
||||||
|
const isOverviewOrMonitor = computed(() => {
|
||||||
|
return props.activeTab === 'overview' || props.activeTab === 'monitor'
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTabClick = (tab) => {
|
||||||
|
emit('tab-change', tab)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dashboard-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.header-top{
|
||||||
|
width: 95%;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: #0a1d3c;
|
||||||
|
.header-left{
|
||||||
|
width: 28%;
|
||||||
|
img{
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.left-tabs {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
padding: 0 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: rgba(16,16,16,1);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255,255,255,1);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 2px;
|
||||||
|
&.overview-tab{
|
||||||
|
background-color: #3a3a42;
|
||||||
|
opacity: 0.8;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.overview-tab.active {
|
||||||
|
background-color: rgba(38,140,140,1);
|
||||||
|
}
|
||||||
|
&.monitor-tab {
|
||||||
|
background-color: #3a3a42;
|
||||||
|
opacity: 0.8;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.monitor-tab.active {
|
||||||
|
background-color: rgba(38,140,140,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
height: 24px;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header-center{
|
||||||
|
width:48%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.center-top{
|
||||||
|
.out-bg{
|
||||||
|
position: absolute;
|
||||||
|
width: 104%;
|
||||||
|
height: auto;
|
||||||
|
left: -2%;
|
||||||
|
}
|
||||||
|
.in-bg{
|
||||||
|
position: absolute;
|
||||||
|
width: 102%;
|
||||||
|
height: auto;
|
||||||
|
top:8%;
|
||||||
|
left: -1%;
|
||||||
|
}
|
||||||
|
.l-lx{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25%;
|
||||||
|
display: flex;
|
||||||
|
justify-content:flex-end;
|
||||||
|
align-items: right;
|
||||||
|
img{
|
||||||
|
width: 25%;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.r-lx{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25%;
|
||||||
|
display: flex;
|
||||||
|
right: -0.5%;
|
||||||
|
justify-content:flex-start;
|
||||||
|
img{
|
||||||
|
width: 25%;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title{
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 15px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-shadow: 0 0 10px rgba(124, 208, 255, 0.7);
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.header-right{
|
||||||
|
width: 28%;
|
||||||
|
img{
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.center-tabs{
|
||||||
|
width: 32%;
|
||||||
|
position: relative;
|
||||||
|
margin-top: -4px;
|
||||||
|
img{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 80%;
|
||||||
|
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-shadow: 0 0 8px rgba(124, 208, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-alarm {
|
||||||
|
.alarm-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: 2px;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 4px rgba(255, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
167
web/src/components/EventHotSpots.vue
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="half-height-chart-container">
|
||||||
|
<dv-border-box-10 class="border-box">
|
||||||
|
<div class="chart-title">事件高发场景/区域分布</div>
|
||||||
|
<div ref="barChart" class="chart"></div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const barChart = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const barOption = {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||||
|
borderColor: 'rgba(64, 158, 255, 0.8)',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: { color: '#fff' }
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['检测数据1', '检测数据2'],
|
||||||
|
bottom: 10, // 距离底部10px
|
||||||
|
left: 'center', // 水平居中
|
||||||
|
textStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
itemWidth: 12,
|
||||||
|
itemHeight: 8,
|
||||||
|
itemGap: 30
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '2%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '15%', // 增加底部间距给图例留空间
|
||||||
|
top: '5%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'value',
|
||||||
|
min: 0,
|
||||||
|
max: 250,
|
||||||
|
interval: 50,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.5)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.1)',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: [
|
||||||
|
'办公走廊/楼道区',
|
||||||
|
'会议室区域',
|
||||||
|
'值班岗亭区',
|
||||||
|
'边检通道区',
|
||||||
|
'港口泊位区'
|
||||||
|
],
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.5)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(124, 208, 255, 0.8)',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '检测数据1',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: 15,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'right',
|
||||||
|
color: '#FFA500',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: '{c}'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#FFA500',
|
||||||
|
borderRadius: [0, 4, 4, 0]
|
||||||
|
},
|
||||||
|
data: [120, 150, 200, 100, 150]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '检测数据2',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: 15,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'right',
|
||||||
|
color: '#00FFC0',
|
||||||
|
fontSize: 12,
|
||||||
|
formatter: '{c}'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#00FFC0',
|
||||||
|
borderRadius: [0, 4, 4, 0]
|
||||||
|
},
|
||||||
|
data: [100, 120, 100, 80, 100]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartInstance = echarts.init(barChart.value)
|
||||||
|
chartInstance.setOption(barOption)
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
chartInstance.resize()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.half-height-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 40vh; /* 关键修改:高度设为视口高度的50% */
|
||||||
|
padding: 15px 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* padding: 20px; */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
223
web/src/components/MainKPI.vue
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<div class="kpi-container">
|
||||||
|
<dv-border-box-10 class="kpi-card">
|
||||||
|
<div class="chart-title">今日告警</div>
|
||||||
|
<div class="kpi-value">6549.69<span class="monitor-unit">个</span></div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
<dv-border-box-10 class="kpi-card online-monitor">
|
||||||
|
<div class="chart-title">在线监控数</div>
|
||||||
|
<div class="kpi-value">
|
||||||
|
<dv-border-box-12 class="monitor-box">
|
||||||
|
<div class="monitor-title">当前在线监控</div>
|
||||||
|
<div class="monitor-value">13,365<span class="monitor-unit">台</span></div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
<dv-border-box-10 class="kpi-card algorithm-card">
|
||||||
|
<div class="chart-title">算法执行</div>
|
||||||
|
<div class="algorithm-content">
|
||||||
|
<div class="algorithm-subtitle">累计执行次数</div>
|
||||||
|
<div class="flip-counter">
|
||||||
|
<div class="flip-digit" v-for="(digit, index) in digits" :key="index">
|
||||||
|
<div class="digit-scroll" :style="{ transform: `translateY(${-digit.current * 10}%)` }">
|
||||||
|
<span v-for="n in 10" :key="n">{{ n }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-10>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// 初始显示22415
|
||||||
|
const digits = ref([
|
||||||
|
{ current: 2, target: 2 },
|
||||||
|
{ current: 2, target: 2 },
|
||||||
|
{ current: 4, target: 4 },
|
||||||
|
{ current: 1, target: 6 },
|
||||||
|
{ current: 5, target: 4 },
|
||||||
|
{ current: 5, target: 4 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const animateDigits = () => {
|
||||||
|
// 3秒后开始变化到22464
|
||||||
|
setTimeout(() => {
|
||||||
|
digits.value.forEach((digit, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let steps = Math.abs(digit.target - digit.current)
|
||||||
|
if (steps > 5) steps = 10 - steps
|
||||||
|
|
||||||
|
const duration = 1000
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const elapsed = Date.now() - startTime
|
||||||
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
|
|
||||||
|
digit.current = Math.floor(digit.current + progress * (digit.target - digit.current))
|
||||||
|
if (digit.current > 9) digit.current = 0
|
||||||
|
if (digit.current < 0) digit.current = 9
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
} else {
|
||||||
|
digit.current = digit.target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animate()
|
||||||
|
}, index * 300)
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
animateDigits()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 基础样式 */
|
||||||
|
.kpi-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 80vh;
|
||||||
|
padding: 20px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
width: 91%;
|
||||||
|
height: 25vh;
|
||||||
|
margin: 10px 0;
|
||||||
|
/* display: flex; */
|
||||||
|
/* flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 在线监控数卡片样式 */
|
||||||
|
.online-monitor {
|
||||||
|
border-color: rgba(124, 208, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-monitor ::v-deep .border-box-title {
|
||||||
|
color: #7cd0ff !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-box {
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #094290;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-title {
|
||||||
|
color: #7cd0ff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-value {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-unit {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 6px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 算法执行卡片样式 */
|
||||||
|
.algorithm-card ::v-deep .border-box-title {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
margin-top: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-subtitle {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数字翻牌器样式 */
|
||||||
|
.flip-counter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-digit {
|
||||||
|
width: 40px;
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #1a5fb4, #3584e4);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 10px rgba(53, 132, 228, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-scroll {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-scroll span {
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
74
web/src/components/alarm/AlarmItem.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!-- src/components/alarm/AlarmItem.vue -->
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="alarm-item"
|
||||||
|
:class="{active: isActive, 'has-new': alarm.hasNew}"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<div class="a-content">
|
||||||
|
{{ alarm.name }}
|
||||||
|
<span v-if="alarm.hasNew" class="new-dot"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
alarm: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['click'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.alarm-item {
|
||||||
|
width: 90%;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 1px solid #09D2FF;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(58, 139, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-left: 3px solid #ff4d4f;
|
||||||
|
background: rgba(58, 139, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-item .a-content{
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-time {
|
||||||
|
color: rgba(124, 208, 255, 0.7);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-dot {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
534
web/src/components/alarm/AlarmManagement.vue
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
<!-- src/components/alarm/AlarmManagement.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="alarm-management">
|
||||||
|
<!-- 左侧P0紧急告警 -->
|
||||||
|
<div class="alarm-column p0-alarms">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="column-header">
|
||||||
|
<span class="level-tag p0">P0</span>
|
||||||
|
<span class="column-title">紧急告警</span>
|
||||||
|
</div>
|
||||||
|
<div class="alarm-list">
|
||||||
|
<AlarmItem v-for="alarm in p0Alarms" :key="alarm.id" :alarm="alarm" @click="selectAlarm(alarm)" />
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
<!-- 中间告警详情 -->
|
||||||
|
<div class="alarm-detail">
|
||||||
|
<div class="center-top">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="detail-container">
|
||||||
|
<div class="video-container">
|
||||||
|
<video autoplay loop muted>
|
||||||
|
<source :src="selectedAlarm.videoSrc" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div class="alarm-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">事件名称:</span>
|
||||||
|
<span class="info-value">{{ selectedAlarm.name }}</span>
|
||||||
|
<span class="info-label">时间:</span>
|
||||||
|
<span class="info-value">{{ selectedAlarm.time }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">告警等级:</span>
|
||||||
|
<span class="level-tag p0">P0</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">详细信息:</span>
|
||||||
|
<span class="info-label">1231312312312321312312</span>
|
||||||
|
</div>
|
||||||
|
<button class="confirm-btn" @click="openConfirmDialog">人员二次确认</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center-bottom">
|
||||||
|
<div class="bottom-column">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="column-header">
|
||||||
|
<span class="level-tag p1">P1</span>
|
||||||
|
<span class="column-title">高级告警</span>
|
||||||
|
</div>
|
||||||
|
<div class="alarm-list">
|
||||||
|
<AlarmItem v-for="alarm in p1Alarms" :key="alarm.id" :alarm="alarm"
|
||||||
|
@click="selectAlarm(alarm)" />
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-column">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="column-header">
|
||||||
|
<span class="level-tag p2">P2</span>
|
||||||
|
<span class="column-title">一般告警</span>
|
||||||
|
</div>
|
||||||
|
<div class="alarm-list">
|
||||||
|
<AlarmItem v-for="alarm in p2Alarms" :key="alarm.id" :alarm="alarm"
|
||||||
|
@click="selectAlarm(alarm)" />
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧P3提示信息 -->
|
||||||
|
<div class="alarm-column p3-notices">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="column-header">
|
||||||
|
<span class="level-tag p3">P3</span>
|
||||||
|
<span class="column-title">提示信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="notice-list">
|
||||||
|
<NoticeItem v-for="notice in p3Notices" :key="notice.id" :notice="notice" />
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showConfirmDialog" class="confirm-dialog-overlay">
|
||||||
|
<div class="confirm-dialog">
|
||||||
|
<div class="dialog-header">告警二次确认</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-label">唯一ID:</span>
|
||||||
|
<input type="text" v-model="uniqueId" placeholder="请输入" class="id-input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="cancel-btn" @click="closeDialog">取消</button>
|
||||||
|
<button class="confirm-btn" @click="handleConfirm">保存应用设置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import AlarmItem from './AlarmItem.vue'
|
||||||
|
import NoticeItem from './NoticeItem.vue'
|
||||||
|
const showConfirmDialog = ref(false)
|
||||||
|
const uniqueId = ref('')
|
||||||
|
const selectedAlarm = ref({
|
||||||
|
name: '',
|
||||||
|
time: '',
|
||||||
|
confirmed: true,
|
||||||
|
videoSrc: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const p0Alarms = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '非法登轮事件识别',
|
||||||
|
time: '2025/10/15 15:48',
|
||||||
|
hasNew: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '暴力行为识别',
|
||||||
|
time: '2025/10/15 16:12',
|
||||||
|
hasNew: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '执法岗长时间无人值守',
|
||||||
|
time: '2025/10/15 16:30',
|
||||||
|
hasNew: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: '黑名单人员出现在控制区域',
|
||||||
|
time: '2025/10/15 17:05',
|
||||||
|
hasNew: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: '越界穿越警戒线',
|
||||||
|
time: '2025/10/15 17:22',
|
||||||
|
hasNew: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: '重点物品丢失检测',
|
||||||
|
time: '2025/10/15 17:45',
|
||||||
|
hasNew: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: '电脑屏幕长时间未锁定+无人状态',
|
||||||
|
time: '2025/10/15 18:03',
|
||||||
|
hasNew: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: '可疑行为',
|
||||||
|
time: '2025/10/15 18:20',
|
||||||
|
hasNew: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const p1Alarms = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '员工办公室内长时间离岗',
|
||||||
|
time: '2025/10/15 09:30',
|
||||||
|
hasNew: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '电脑屏幕未锁定+短时无人',
|
||||||
|
time: '2025/10/15 10:15',
|
||||||
|
hasNew: false
|
||||||
|
},
|
||||||
|
// 其他P1告警...
|
||||||
|
])
|
||||||
|
|
||||||
|
const p2Alarms = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '会议期间频繁查看手机',
|
||||||
|
time: '2025/10/15 11:20',
|
||||||
|
hasNew: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '门口区域逗留/张望行为',
|
||||||
|
time: '2025/10/15 12:45',
|
||||||
|
hasNew: false
|
||||||
|
},
|
||||||
|
// 其他P2告警...
|
||||||
|
])
|
||||||
|
const p3Notices = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: '摄像头恢复在线',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: '摄像头恢复在线',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
content: '摄像头恢复在线',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
content: '摄像头恢复在线',
|
||||||
|
},
|
||||||
|
// 其他P3通知...
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectAlarm = (alarm) => {
|
||||||
|
selectedAlarm.value = alarm
|
||||||
|
// 标记为已读
|
||||||
|
alarm.hasNew = false
|
||||||
|
}
|
||||||
|
const openConfirmDialog = () => {
|
||||||
|
showConfirmDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showConfirmDialog.value = false
|
||||||
|
uniqueId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
// 处理确认逻辑
|
||||||
|
console.log('确认ID:', uniqueId.value)
|
||||||
|
closeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改confirm-btn的点击事件
|
||||||
|
const confirmBtn = document.querySelector('.confirm-btn')
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', openConfirmDialog)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.alarm-management {
|
||||||
|
display: flex;
|
||||||
|
height: 84vh;
|
||||||
|
background: #0a1d3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-column {
|
||||||
|
width: 25%;
|
||||||
|
padding: 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.p0-alarms {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p0-alarms .alarm-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-detail {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.center-top {
|
||||||
|
padding: 20px;
|
||||||
|
height: 35vh;
|
||||||
|
|
||||||
|
.detail-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
width: 40%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm-info {
|
||||||
|
position: relative; // 添加相对定位
|
||||||
|
width: 60%;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: rgba(124, 208, 255, 0.8);
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tag.p0 {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px; // 距离底部20px
|
||||||
|
left: 50%; // 水平居中
|
||||||
|
transform: translateX(-50%); // 精确居中
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-bottom {
|
||||||
|
height: 48vh;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
|
||||||
|
.bottom-column {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.alarm-list {
|
||||||
|
height: calc(100% - 50px);
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
width: 95%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.p0 {
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.p3 {
|
||||||
|
background: #00ffc0;
|
||||||
|
color: #0a1d3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.p1 {
|
||||||
|
background: #FFA500; // 橙色
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.p2 {
|
||||||
|
background: #FFFF00; // 黄色
|
||||||
|
color: #0a1d3c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在style中添加
|
||||||
|
.confirm-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
width: 400px;
|
||||||
|
background: #0a1d3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: white;
|
||||||
|
background: rgba(16, 42, 87, 0.8);
|
||||||
|
border-bottom: 1px solid rgba(124, 208, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
color: white;
|
||||||
|
width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(124, 208, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid rgba(124, 208, 255, 0.2);
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.cancel-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
color: #7cd0ff;
|
||||||
|
margin-right: 15px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.confirm-btn {
|
||||||
|
background: #03051a;
|
||||||
|
border: 1px solid #00ffc0;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #00e6ac;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
35
web/src/components/alarm/NoticeItem.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!-- src/components/alarm/NoticeItem.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="notice-item">
|
||||||
|
<div class="notice-content">
|
||||||
|
{{ notice.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
notice: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notice-item {
|
||||||
|
width: 90%;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background: #081f50;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 1px solid #09d2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
102
web/src/components/algorithm/AlgorithmCard.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<!-- src/components/algorithm/AlgorithmCard.vue -->
|
||||||
|
<template>
|
||||||
|
<dv-border-box-13 class="algorithm-card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="algorithm-name">{{ algorithm.name }}</div>
|
||||||
|
<div class="task-types">
|
||||||
|
<div class="task-title">任务类型</div>
|
||||||
|
<div class="type-list">
|
||||||
|
<div v-for="(type, index) in algorithm.taskTypes"
|
||||||
|
:key="index"
|
||||||
|
class="task-type">
|
||||||
|
{{ type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detection-count">
|
||||||
|
<span class="count-text">成功检测</span>
|
||||||
|
<span class="count-number">{{ algorithm.detectionCount }}</span>
|
||||||
|
<span class="count-unit">次</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-13>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
algorithm: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => ({
|
||||||
|
name: '',
|
||||||
|
taskTypes: [],
|
||||||
|
detectionCount: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.algorithm-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
::v-deep .border-box-content {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #7cd0ff;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-types {
|
||||||
|
flex: 1;
|
||||||
|
width: 90%;
|
||||||
|
.task-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.type-list{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
.task-type {
|
||||||
|
border: 1px solid #7cd0ff; /* 浅蓝色边框 */
|
||||||
|
color: #00ffc0; /* 蓝绿色文字 */
|
||||||
|
background: rgba(16, 42, 87, 0.5); /* 半透明深蓝背景 */
|
||||||
|
padding: 4px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-count {
|
||||||
|
width: 90%;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-top: 30px;
|
||||||
|
align-items: center;
|
||||||
|
.count-number {
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #7cd0ff; /* 大号蓝色数字 */
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
95
web/src/components/algorithm/AlgorithmCenter.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<!-- src/components/algorithm/AlgorithmCenter.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="algorithm-center">
|
||||||
|
<div class="algorithm-grid">
|
||||||
|
<AlgorithmCard
|
||||||
|
v-for="(algorithm, index) in algorithms"
|
||||||
|
:key="index"
|
||||||
|
:algorithm="algorithm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AlgorithmCard from './AlgorithmCard.vue'
|
||||||
|
|
||||||
|
const algorithms = [
|
||||||
|
{
|
||||||
|
name: '非法登轮识别算法',
|
||||||
|
taskTypes: ['区域入侵检测', '行为识别'],
|
||||||
|
detectionCount: '68,000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '靠泊状态检测算法',
|
||||||
|
taskTypes: ['靠泊检测', '静态目标跟踪'],
|
||||||
|
detectionCount: '5,124'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '打瞌睡检测算法',
|
||||||
|
taskTypes: ['姿态识别', '行为识别'],
|
||||||
|
detectionCount: '1,025'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '值班离岗检测算法',
|
||||||
|
taskTypes: ['手部动作识别', '行为识别'],
|
||||||
|
detectionCount: '90,128'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '佩戴证件识别算法',
|
||||||
|
taskTypes: ['属性识别', '目标特征比对'],
|
||||||
|
detectionCount: '6,094'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '非法律行识别算法',
|
||||||
|
taskTypes: ['人体轨迹跟踪', '滞留时长分析'],
|
||||||
|
detectionCount: '5,080'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '暴力动作检测算法',
|
||||||
|
taskTypes: ['多人姿态动作识别', '非正常行为判定'],
|
||||||
|
detectionCount: '8,300'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '异常靠近设备算法',
|
||||||
|
taskTypes: ['电脑未锁屏', '人脸识别'],
|
||||||
|
detectionCount: '3,456'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '违章品携带检测',
|
||||||
|
taskTypes: ['接近行为追踪', '人体-物体关系识别'],
|
||||||
|
detectionCount: '7,890'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '注意力评估算法',
|
||||||
|
taskTypes: ['可疑动作追踪', '头部朝向识别'],
|
||||||
|
detectionCount: '4,321'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '危险区域闯入检测',
|
||||||
|
taskTypes: ['行为持续性分析', '区域入侵检测'],
|
||||||
|
detectionCount: '9,876'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '异常聚集检测算法',
|
||||||
|
taskTypes: ['人群密度分析', '行为识别'],
|
||||||
|
detectionCount: '5,678'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.algorithm-center {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
</style>
|
150
web/src/components/device/AddDeviceDialog.vue
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :model-value="visible" @update:modelValue="$emit('update:visible', $event)" title="视频设备接入设置"
|
||||||
|
width="900px" :class="['custom-dialog', 'add-scene-dialog']">
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>摄像头名称:</label>
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>摄像头ID:</label>
|
||||||
|
<el-input v-model="formData.id" placeholder="请输入" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>摄像头IP地址:</label>
|
||||||
|
<el-input v-model="formData.ip" placeholder="请输入" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>摄像头通道号:</label>
|
||||||
|
<el-input v-model="formData.channel" placeholder="请输入" />
|
||||||
|
</div>
|
||||||
|
<div class="form-item">
|
||||||
|
<label>所属分组与场景:</label>
|
||||||
|
<el-select v-model="formData.group" placeholder="请选择">
|
||||||
|
<el-option v-for="scene in scenes" :key="scene.id" :label="scene.name" :value="scene.id" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-preview">
|
||||||
|
<div class="video-placeholder">
|
||||||
|
<svg viewBox="0 0 24 24" width="60" height="60">
|
||||||
|
<path fill="#7cd0ff"
|
||||||
|
d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />
|
||||||
|
</svg>
|
||||||
|
<p>视频区域</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button class="cancel-btn" @click="$emit('update:visible', false)">测试接入效果</el-button>
|
||||||
|
<el-button class="confirm-btn" @click="saveDevice">保存应用设置</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
scenes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'save'])
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
name: '',
|
||||||
|
id: '',
|
||||||
|
ip: '',
|
||||||
|
channel: '',
|
||||||
|
group: '',
|
||||||
|
algorithm: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveDevice = () => {
|
||||||
|
emit('save', formData.value)
|
||||||
|
emit('update:visible', false)
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
id: '',
|
||||||
|
ip: '',
|
||||||
|
channel: '',
|
||||||
|
group: '',
|
||||||
|
algorithm: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 150px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-preview {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border: 1px dashed #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.video-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
color: #7cd0ff;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
.cancel-btn {
|
||||||
|
background: transparent;
|
||||||
|
border-color: #7cd0ff;
|
||||||
|
color: #7cd0ff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
background: #00d2ff;
|
||||||
|
border-color: #00d2ff;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
666
web/src/components/device/DeviceManagement.vue
Normal file
@ -0,0 +1,666 @@
|
|||||||
|
<!-- src/components/device/DeviceManagement.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="device-management">
|
||||||
|
<template v-if="!editingDevice">
|
||||||
|
<!-- 左侧导航栏 -->
|
||||||
|
<div class="device-nav">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="nav-header">
|
||||||
|
<div class="nav-title">分组与场景管理</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-actions">
|
||||||
|
<div class="all-scenes" :class="{ active: activeScene === 'all' }" @click="selectScene('all')">
|
||||||
|
全部
|
||||||
|
</div>
|
||||||
|
<div class="add-scene" @click="showAddSceneDialog">
|
||||||
|
<svg viewBox="0 0 30 30" width="40" height="40">
|
||||||
|
<path fill="#ffffff" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scene-list">
|
||||||
|
<div v-for="scene in scenes" :key="scene.id" class="scene-item"
|
||||||
|
:class="{ active: activeScene === scene.id }" @click="selectScene(scene.id)">
|
||||||
|
<div class="scene-name">{{ scene.name }}</div>
|
||||||
|
<div class="scene-indicator" v-if="activeScene === scene.id"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧设备列表 -->
|
||||||
|
<div class="device-list">
|
||||||
|
<dv-border-box-8>
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="device-header">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" placeholder="搜索设备名称、IP或分组" v-model="searchText" />
|
||||||
|
<div class="search-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="add-device-btn" @click="showAddDeviceDialog">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||||
|
</svg>
|
||||||
|
接入视频设备
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 设备表格 -->
|
||||||
|
<div class="device-table-container">
|
||||||
|
<table class="device-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>分组</th>
|
||||||
|
<th>设备ID</th>
|
||||||
|
<th>IP地址</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>算法任务</th>
|
||||||
|
<th>算法状态</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="device in filteredDevices" :key="device.id">
|
||||||
|
<tr v-for="(algorithm, index) in device.algorithms" :key="index">
|
||||||
|
<td v-if="index === 0" :rowspan="device.algorithms.length">{{ device.group }}
|
||||||
|
</td>
|
||||||
|
<td v-if="index === 0" :rowspan="device.algorithms.length">{{ device.id }}</td>
|
||||||
|
<td v-if="index === 0" :rowspan="device.algorithms.length">{{ device.ip }}</td>
|
||||||
|
<td v-if="index === 0" :rowspan="device.algorithms.length">
|
||||||
|
<span class="status-badge" :class="device.status">
|
||||||
|
{{ device.status === 'online' ? '在线' : '离线' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ algorithm.name }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="algorithm-status" :class="{ enabled: algorithm.enabled }">
|
||||||
|
{{ algorithm.enabled ? '启用' : '停用' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td v-if="index === 0" :rowspan="device.algorithms.length">
|
||||||
|
<button class="action-btn view-btn" @click="viewDevice(device)">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn edit-btn" @click="editDevice(device)">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="action-btn delete-btn" @click="deleteDevice(device)">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</dv-border-box-8>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加场景对话框 -->
|
||||||
|
<el-dialog v-model="showSceneDialog" title="添加新场景" width="30%"
|
||||||
|
:class="['custom-dialog', 'add-scene-dialog']">
|
||||||
|
<div class="dialog-content">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>场景名称</label>
|
||||||
|
<el-input v-model="newScene.name" placeholder="例如:XXXXX场景007" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button class="cancel-btn" @click="showSceneDialog = false">取消</el-button>
|
||||||
|
<el-button class="confirm-btn" type="primary" @click="confirmAddScene">确认添加</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
<!-- 添加设备对话框 -->
|
||||||
|
<AddDeviceDialog v-model:visible="showAddDialog" :scenes="scenes" @save="handleSaveDevice" />
|
||||||
|
</template>
|
||||||
|
<!-- 当处于编辑状态时显示事件编辑界面 -->
|
||||||
|
<EventEditor v-else :device-data="editingDevice" @close="cancelEdit" @save="saveEvent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import AddDeviceDialog from './AddDeviceDialog.vue'
|
||||||
|
import EventEditor from './EventEditor.vue'
|
||||||
|
const searchText = ref('')
|
||||||
|
const activeScene = ref('all')
|
||||||
|
const showSceneDialog = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
|
||||||
|
const editingDevice = ref(null)
|
||||||
|
|
||||||
|
const newScene = ref({
|
||||||
|
name: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 场景数据
|
||||||
|
const scenes = ref([
|
||||||
|
{ id: 'scene-001', name: 'XXXXX场景001' },
|
||||||
|
{ id: 'scene-002', name: 'XXXXX场景002' },
|
||||||
|
{ id: 'scene-003', name: 'XXXXX场景003' },
|
||||||
|
{ id: 'scene-004', name: 'XXXXX场景004' },
|
||||||
|
{ id: 'scene-005', name: 'XXXXX场景005' },
|
||||||
|
{ id: 'scene-006', name: 'XXXXX场景006' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 模拟设备数据
|
||||||
|
const devices = ref([
|
||||||
|
{
|
||||||
|
id: 'VIDEO_214IOISHEK454',
|
||||||
|
group: '港口区',
|
||||||
|
ip: '192.268.0.1',
|
||||||
|
status: 'online',
|
||||||
|
algorithms: [
|
||||||
|
{ name: '目标检测(如靠泊、登轮)', enabled: true },
|
||||||
|
{ name: '行为识别(如玩手机、离岗)', enabled: true },
|
||||||
|
{ name: '目标检测(计数)', enabled: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CAM_002',
|
||||||
|
group: 'XXXXX场景002',
|
||||||
|
ip: '192.168.1.102',
|
||||||
|
status: 'online',
|
||||||
|
algorithms: [
|
||||||
|
{ name: '越界检测', enabled: true },
|
||||||
|
{ name: '车辆识别', enabled: false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CAM_003',
|
||||||
|
group: 'XXXXX场景003',
|
||||||
|
ip: '192.168.1.103',
|
||||||
|
status: 'offline',
|
||||||
|
algorithms: [
|
||||||
|
{ name: '货物识别', enabled: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 过滤设备
|
||||||
|
const filteredDevices = computed(() => {
|
||||||
|
let result = devices.value
|
||||||
|
if (activeScene.value && activeScene.value !== 'all') {
|
||||||
|
const activeSceneName = scenes.value.find(s => s.id === activeScene.value)?.name || ''
|
||||||
|
result = result.filter(device => device.group === activeSceneName)
|
||||||
|
}
|
||||||
|
if (searchText.value) {
|
||||||
|
const search = searchText.value.toLowerCase()
|
||||||
|
result = result.filter(device =>
|
||||||
|
device.id.toLowerCase().includes(search) ||
|
||||||
|
device.ip.toLowerCase().includes(search) ||
|
||||||
|
device.group.toLowerCase().includes(search) ||
|
||||||
|
device.algorithm.toLowerCase().includes(search)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择场景
|
||||||
|
const selectScene = (sceneId) => {
|
||||||
|
activeScene.value = sceneId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示添加场景对话框
|
||||||
|
const showAddSceneDialog = () => {
|
||||||
|
newScene.value = {
|
||||||
|
name: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
showSceneDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认添加场景
|
||||||
|
const confirmAddScene = () => {
|
||||||
|
const newId = `scene-${String(scenes.value.length + 1).padStart(3, '0')}`
|
||||||
|
scenes.value.push({
|
||||||
|
id: newId,
|
||||||
|
name: newScene.value.name || `XXXXX场景${String(scenes.value.length + 1).padStart(3, '0')}`,
|
||||||
|
description: newScene.value.description
|
||||||
|
})
|
||||||
|
showSceneDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示添加设备对话框
|
||||||
|
const showAddDeviceDialog = () => {
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存设备
|
||||||
|
const handleSaveDevice = (deviceData) => {
|
||||||
|
devices.value.push({
|
||||||
|
id: deviceData.id,
|
||||||
|
group: scenes.value.find(s => s.id === deviceData.group)?.name || '',
|
||||||
|
ip: deviceData.ip,
|
||||||
|
status: 'online',
|
||||||
|
algorithms: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
const viewDevice = (device) => {
|
||||||
|
console.log('查看设备:', device)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editDevice = (device) => {
|
||||||
|
editingDevice.value = {
|
||||||
|
...device,
|
||||||
|
eventName: device.eventName || '',
|
||||||
|
ip: device.ip || '192.168.5.44', // 示例IP
|
||||||
|
status: device.status || '在线'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
editingDevice.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEvent = (eventData) => {
|
||||||
|
// 更新设备数据
|
||||||
|
const deviceIndex = devices.value.findIndex(d => d.id === editingDevice.value.id)
|
||||||
|
if (deviceIndex !== -1) {
|
||||||
|
devices.value[deviceIndex] = {
|
||||||
|
...devices.value[deviceIndex],
|
||||||
|
...eventData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editingDevice.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDevice = (device) => {
|
||||||
|
console.log('删除设备:', device)
|
||||||
|
const index = devices.value.findIndex(d => d.id === device.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
devices.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.device-management {
|
||||||
|
display: flex;
|
||||||
|
height: 85vh;
|
||||||
|
background: #0a1d3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧导航栏 */
|
||||||
|
.device-nav {
|
||||||
|
width: 300px;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.nav-header {
|
||||||
|
padding: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid #09D2FF;
|
||||||
|
padding: 5px 0;
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.all-scenes {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-scene {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-list {
|
||||||
|
padding: 15px 0;
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.scene-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin: 8px 0;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.scene-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: -15px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #FFD700;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-tip {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
border-top: 1px dashed rgba(124, 208, 255, 0.2);
|
||||||
|
|
||||||
|
.tip-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: #FFD700;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 215, 0, 0.1);
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.tip-signature {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧设备列表 */
|
||||||
|
.device-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.device-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 15px;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 35px 8px 15px;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(124, 208, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 55%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #7cd0ff;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: linear-gradient(90deg, #3a7bd5, #00d2ff);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table-container {
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-table {
|
||||||
|
width: 98%;
|
||||||
|
color: white;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid rgba(124, 208, 255, 0.2);
|
||||||
|
border-right: 1px solid rgba(124, 208, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: rgba(16, 42, 87, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: rgba(124, 208, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.algorithm-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.enabled {
|
||||||
|
background-color: rgba(0, 255, 0, 0.2);
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.enabled) {
|
||||||
|
background-color: rgba(255, 0, 0, 0.2);
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.online {
|
||||||
|
background-color: #00ffc0;
|
||||||
|
color: #0a1d3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offline {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.view-btn {
|
||||||
|
background-color: rgba(124, 208, 255, 0.2);
|
||||||
|
color: #7cd0ff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(124, 208, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.edit-btn {
|
||||||
|
background-color: rgba(255, 193, 7, 0.2);
|
||||||
|
color: #ffc107;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 193, 7, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.delete-btn {
|
||||||
|
background-color: rgba(255, 77, 79, 0.2);
|
||||||
|
color: #ff4d4f;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 77, 79, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框样式 */
|
||||||
|
:deep(.custom-dialog.add-scene-dialog) {
|
||||||
|
background: #0a1d3c;
|
||||||
|
border: 1px solid #1a5fb4;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
border-bottom: 1px solid rgba(124, 208, 255, 0.2);
|
||||||
|
padding: 15px 20px;
|
||||||
|
|
||||||
|
.el-dialog__title {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
border-top: 1px solid rgba(124, 208, 255, 0.2);
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.el-button.cancel-btn {
|
||||||
|
background: transparent;
|
||||||
|
border-color: #7cd0ff;
|
||||||
|
color: #7cd0ff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.confirm-btn {
|
||||||
|
background: #00d2ff;
|
||||||
|
border-color: #00d2ff;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
343
web/src/components/device/EventEditor.vue
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-editor-container">
|
||||||
|
<div class="editor-content">
|
||||||
|
<!-- 左侧视频区域 -->
|
||||||
|
<div class="video-section">
|
||||||
|
<!-- 上部视频预览区域 (55%) -->
|
||||||
|
<div class="video-preview">
|
||||||
|
<div class="video-placeholder">
|
||||||
|
<svg viewBox="0 0 24 24" width="60" height="60">
|
||||||
|
<path fill="#7cd0ff"
|
||||||
|
d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />
|
||||||
|
</svg>
|
||||||
|
<p>视频区域</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-actions">
|
||||||
|
<el-button class="upload-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||||
|
</svg>
|
||||||
|
上传模拟视频
|
||||||
|
</el-button>
|
||||||
|
<el-button class="clear-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor" d="M19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||||
|
</svg>
|
||||||
|
清空模拟视频
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下部设备信息区域 (45%) -->
|
||||||
|
<div class="device-info">
|
||||||
|
<h4>设备默认信息显示区域</h4>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">分组:</span>
|
||||||
|
<span class="value">港口区</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">摄像头ID:</span>
|
||||||
|
<span class="value">VIDEO_214IOISHEK454</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">设备状态:</span>
|
||||||
|
<span class="value">在线</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">IP地址:</span>
|
||||||
|
<span class="value">192.168.5.44</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧事件设置区域 -->
|
||||||
|
<div class="event-section">
|
||||||
|
<!-- 事件1 -->
|
||||||
|
<div class="event-group">
|
||||||
|
<div class="event-header">
|
||||||
|
<h4>事件绑定设置</h4>
|
||||||
|
<el-button class="add-event-btn">新增事件</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>事件名称:</label>
|
||||||
|
<el-input v-model="eventData.name" placeholder="例如:船舶登轮" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>电子围栏区域:</label>
|
||||||
|
<el-select v-model="eventData.area" placeholder="例如:整个监控画面">
|
||||||
|
<el-option label="整个监控画面" value="整个监控画面" />
|
||||||
|
<el-option label="自定义画面" value="自定义画面" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>告警等级:</label>
|
||||||
|
<el-select v-model="eventData.level" placeholder="例如:P0紧急告警">
|
||||||
|
<el-option label="P0紧急告警" value="P0" />
|
||||||
|
<el-option label="P1重要告警" value="P1" />
|
||||||
|
<el-option label="P2一般告警" value="P2" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>电子围栏名称:</label>
|
||||||
|
<el-input v-model="eventData.fenceName" placeholder="请输入" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>算法绑定:</label>
|
||||||
|
<el-select v-model="eventData.algorithm" placeholder="例如:算法XXX001">
|
||||||
|
<el-option v-for="alg in algorithms" :key="alg.id" :label="alg.name" :value="alg.id" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-hint">
|
||||||
|
<p>触发算法条件待定</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<div class="editor-footer">
|
||||||
|
<el-button class="preview-btn">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||||||
|
</svg>
|
||||||
|
查看模拟效果
|
||||||
|
</el-button>
|
||||||
|
<el-button class="save-btn" @click="saveEvent">
|
||||||
|
保存应用设置
|
||||||
|
</el-button>
|
||||||
|
<el-button class="cancel-btn" @click="closeEditor">
|
||||||
|
返回设备列表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
deviceData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'save'])
|
||||||
|
|
||||||
|
const algorithms = ref([
|
||||||
|
{ id: 'alg-001', name: '算法XXX001' },
|
||||||
|
{ id: 'alg-002', name: '算法XXX002' },
|
||||||
|
{ id: 'alg-003', name: '算法XXX003' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const eventData = ref({
|
||||||
|
name: props.deviceData.eventName || '船舶登轮',
|
||||||
|
area: '整个监控画面',
|
||||||
|
level: 'P0',
|
||||||
|
fenceName: '',
|
||||||
|
algorithm: 'alg-001',
|
||||||
|
crossAreaName: '',
|
||||||
|
crossAlgorithm: 'alg-001'
|
||||||
|
})
|
||||||
|
|
||||||
|
const closeEditor = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEvent = () => {
|
||||||
|
emit('save', eventData.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.event-editor-container {
|
||||||
|
height: 100vh;
|
||||||
|
background: #0a1d3c;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-content {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
/* 减去上下padding */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧视频区域 */
|
||||||
|
.video-section {
|
||||||
|
width: 25%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 视频预览区域 (55%) */
|
||||||
|
.video-preview {
|
||||||
|
height: 55%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border: 1px dashed #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder p {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-actions .el-button {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
border-color: #7cd0ff;
|
||||||
|
color: #7cd0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
background: rgba(255, 77, 79, 0.2);
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设备信息区域 (45%) */
|
||||||
|
.device-info {
|
||||||
|
height: 45%;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info h4 {
|
||||||
|
color: #7cd0ff;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .label {
|
||||||
|
color: #7cd0ff;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.event-section {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.event-group {
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.event-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: #7cd0ff;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-event-btn {
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
border-color: #7cd0ff;
|
||||||
|
color: #7cd0ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
color: rgba(124, 208, 255, 0.7);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
&.preview-btn {
|
||||||
|
background: rgba(0, 210, 255, 0.2);
|
||||||
|
border-color: #00d2ff;
|
||||||
|
color: #00d2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.save-btn {
|
||||||
|
background: #00d2ff;
|
||||||
|
border-color: #00d2ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cancel-btn {
|
||||||
|
background: transparent;
|
||||||
|
border-color: #7cd0ff;
|
||||||
|
color: #7cd0ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
457
web/src/components/event/EventCenter.vue
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
<template>
|
||||||
|
<div class="event-center">
|
||||||
|
<!-- 左侧年月日导航 -->
|
||||||
|
<div class="year-month-nav">
|
||||||
|
<dv-border-box-12>
|
||||||
|
<div class="nav-header">事件记录时间</div>
|
||||||
|
|
||||||
|
<!-- 年份列表 - 一级菜单 -->
|
||||||
|
<div class="year-list">
|
||||||
|
<div
|
||||||
|
v-for="year in years"
|
||||||
|
:key="year"
|
||||||
|
class="year-item"
|
||||||
|
:class="{ active: selectedYear === year }"
|
||||||
|
@click="toggleYear(year)"
|
||||||
|
>
|
||||||
|
<div class="item-content">
|
||||||
|
{{ year }}年
|
||||||
|
<span class="arrow-icon" v-if="selectedYear === year">▼</span>
|
||||||
|
<span class="arrow-icon" v-else>▲</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 月份列表 - 二级菜单 -->
|
||||||
|
<div class="month-list" v-if="selectedYear === year">
|
||||||
|
<div
|
||||||
|
v-for="month in months"
|
||||||
|
:key="month"
|
||||||
|
class="month-item"
|
||||||
|
:class="{ active: selectedMonth === month }"
|
||||||
|
@click.stop="toggleMonth(month)"
|
||||||
|
>
|
||||||
|
<div class="item-content">
|
||||||
|
{{ month }}月
|
||||||
|
<span class="arrow-icon" v-if="selectedMonth === month">▼</span>
|
||||||
|
<span class="arrow-icon" v-else>▲</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期列表 - 三级菜单 -->
|
||||||
|
<div class="day-list" v-if="selectedMonth === month">
|
||||||
|
<div
|
||||||
|
v-for="day in days"
|
||||||
|
:key="day"
|
||||||
|
class="day-item"
|
||||||
|
:class="{ active: selectedDay === day }"
|
||||||
|
@click.stop="selectDay(day)"
|
||||||
|
>
|
||||||
|
<div class="item-content">
|
||||||
|
{{ day }}日
|
||||||
|
<span class="check-icon" v-if="selectedDay === day">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-12>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧数据列表 -->
|
||||||
|
<div class="event-list">
|
||||||
|
<dv-border-box-8>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="searchText"
|
||||||
|
placeholder="请输入"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="event-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="col in columns" :key="col.key">{{ col.title }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(event, index) in filteredEvents" :key="index">
|
||||||
|
<td>{{ event.time }}</td>
|
||||||
|
<td>{{ event.name }}</td>
|
||||||
|
<td>{{ event.algorithm }}</td>
|
||||||
|
<td>{{ event.deviceId }}</td>
|
||||||
|
<td>{{ event.ip }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-tag" :class="event.status">
|
||||||
|
{{ statusText[event.status] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="action-btn view" @click="viewDetail(event)">查看</button>
|
||||||
|
<button class="action-btn delete" @click="deleteEvent(event)">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-8>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
// 时间选择数据
|
||||||
|
const selectedYear = ref('2025')
|
||||||
|
const selectedMonth = ref('7')
|
||||||
|
const selectedDay = ref('10')
|
||||||
|
const years = ref(['2025', '2024', '2023', '2022', '2021', '2020'])
|
||||||
|
const months = ref(['12', '11', '10', '9', '8','7', '6', '5', '4', '3', '2', '1'])
|
||||||
|
const days = ref(['10', '9', '8', '7', '6','10', '9', '8', '7', '6'])
|
||||||
|
|
||||||
|
// 搜索和表格数据
|
||||||
|
const searchText = ref('')
|
||||||
|
const columns = ref([
|
||||||
|
{ key: 'time', title: '时间' },
|
||||||
|
{ key: 'name', title: '事件名称' },
|
||||||
|
{ key: 'algorithm', title: '算法任务' },
|
||||||
|
{ key: 'deviceId', title: '设备ID' },
|
||||||
|
{ key: 'ip', title: 'IP地址' },
|
||||||
|
{ key: 'status', title: '事件状态' },
|
||||||
|
{ key: 'actions', title: '操作' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const events = ref([
|
||||||
|
{
|
||||||
|
time: '14:45:26',
|
||||||
|
name: '目标检测',
|
||||||
|
algorithm: '靠泊、登轮',
|
||||||
|
deviceId: 'VIDEO_214IOISHEK454',
|
||||||
|
ip: '192.268.0.1',
|
||||||
|
status: 'pending'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: '13:30:15',
|
||||||
|
name: '暴力行为识别',
|
||||||
|
algorithm: '异常行为检测',
|
||||||
|
deviceId: 'VIDEO_315IOISHEK455',
|
||||||
|
ip: '192.268.0.2',
|
||||||
|
status: 'processed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
time: '11:20:45',
|
||||||
|
name: '越界检测',
|
||||||
|
algorithm: '区域入侵检测',
|
||||||
|
deviceId: 'VIDEO_516IOISHEK456',
|
||||||
|
ip: '192.268.0.3',
|
||||||
|
status: 'ignored'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusText = {
|
||||||
|
pending: '待处理',
|
||||||
|
processed: '已处理',
|
||||||
|
ignored: '已忽略'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const filteredEvents = computed(() => {
|
||||||
|
return events.value.filter(event =>
|
||||||
|
event.name.includes(searchText.value) ||
|
||||||
|
event.algorithm.includes(searchText.value) ||
|
||||||
|
event.deviceId.includes(searchText.value)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const toggleYear = (year) => {
|
||||||
|
if (selectedYear.value === year) {
|
||||||
|
selectedYear.value = ''
|
||||||
|
selectedMonth.value = ''
|
||||||
|
selectedDay.value = ''
|
||||||
|
} else {
|
||||||
|
selectedYear.value = year
|
||||||
|
selectedMonth.value = ''
|
||||||
|
selectedDay.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMonth = (month) => {
|
||||||
|
if (selectedMonth.value === month) {
|
||||||
|
selectedMonth.value = ''
|
||||||
|
selectedDay.value = ''
|
||||||
|
} else {
|
||||||
|
selectedMonth.value = month
|
||||||
|
selectedDay.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDay = (day) => {
|
||||||
|
selectedDay.value = day
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewDetail = (event) => {
|
||||||
|
console.log('查看事件:', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteEvent = (event) => {
|
||||||
|
console.log('删除事件:', event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.event-center {
|
||||||
|
display: flex;
|
||||||
|
height: 85vh;
|
||||||
|
background: #0a1d3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧年月日导航 */
|
||||||
|
.year-month-nav {
|
||||||
|
width: 300px;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.nav-header {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7cd0ff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 一级菜单 - 年份 */
|
||||||
|
.year-list {
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
.year-item {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
> .item-content {
|
||||||
|
border: 1px solid #09d2ff;
|
||||||
|
padding: 8px 15px;
|
||||||
|
color: rgba(124, 208, 255, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover, .active & {
|
||||||
|
color: white;
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二级菜单 - 月份 */
|
||||||
|
.month-list {
|
||||||
|
margin-left: 20px;
|
||||||
|
/* border-left: 1px solid rgba(124, 208, 255, 0.2); */
|
||||||
|
padding-left: 15px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
.month-item {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
padding: 8px 15px;
|
||||||
|
color: rgba(124, 208, 255, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover, .active & {
|
||||||
|
color: white;
|
||||||
|
/* background: rgba(124, 208, 255, 0.2); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
color: #00ffc0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 三级菜单 - 日期 */
|
||||||
|
.day-list {
|
||||||
|
margin-left: 20px;
|
||||||
|
/* border-left: 1px solid rgba(124, 208, 255, 0.2); */
|
||||||
|
padding-left: 15px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome/Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-item {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
padding: 8px 15px;
|
||||||
|
color: rgba(124, 208, 255, 0.8);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
color: white;
|
||||||
|
background: rgba(124, 208, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
color: #00ffc0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧数据列表 */
|
||||||
|
.event-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
.search-bar {
|
||||||
|
position: relative;
|
||||||
|
padding: 15px;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 40px 8px 12px;
|
||||||
|
background: rgba(16, 42, 87, 0.6);
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(124, 208, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "🔍";
|
||||||
|
position: absolute;
|
||||||
|
right: 25px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #7cd0ff;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-table {
|
||||||
|
width: 98%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid #7cd0ff;
|
||||||
|
font-size: 14px;
|
||||||
|
th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(16, 42, 87, 0.8);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid #7cd0ff;
|
||||||
|
border-right: 1px solid rgba(124, 208, 255, 0.3);
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 1px solid rgba(124, 208, 255, 0.2);
|
||||||
|
border-right: 1px solid rgba(124, 208, 255, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
&:hover {
|
||||||
|
background: rgba(124, 208, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.processed {
|
||||||
|
background: #00ffc0;
|
||||||
|
color: #0a1d3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ignored {
|
||||||
|
background: #888;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: none;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.view {
|
||||||
|
background: linear-gradient(to right, #3a7bd5, #00d2ff);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to right, #2c5fb3, #00b7e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.delete {
|
||||||
|
background: linear-gradient(to right, #ff416c, #ff4b2b);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(to right, #e0355c, #e03d26);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
98
web/src/components/monitor/MonitorDetail.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="monitor-detail">
|
||||||
|
<dv-border-box-11 :title="monitor.location">
|
||||||
|
<div class="detail-content">
|
||||||
|
<div class="video-container">
|
||||||
|
<video autoplay loop muted controls>
|
||||||
|
<source :src="monitor.videoSrc" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-11>
|
||||||
|
<button class="close-btn" @click="close">×</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
monitor: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => ({
|
||||||
|
location: '港口区',
|
||||||
|
videoSrc: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.monitor-detail {
|
||||||
|
width: 60vw;
|
||||||
|
height: 70vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
/* 关键修改:使边框背景不透明 */
|
||||||
|
::v-deep .dv-border-box-11 {
|
||||||
|
background-color: #0a1d3c !important; /* 使用与界面一致的深蓝色 */
|
||||||
|
background-image: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整边框内部内容样式 */
|
||||||
|
::v-deep .border-box-content {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 15px;
|
||||||
|
z-index: 1001;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
width: 96%;
|
||||||
|
height: 88%;
|
||||||
|
background: #000;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 0 7px 7px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
56
web/src/components/monitor/MonitorThumbnail.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<dv-border-box-11 class="monitor-thumbnail" :title="monitor.location">
|
||||||
|
<div class="monitor-content">
|
||||||
|
<div class="video-container">
|
||||||
|
<video autoplay loop muted>
|
||||||
|
<source :src="monitor.videoSrc" type="video/mp4">
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dv-border-box-11>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
monitor: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => ({
|
||||||
|
location: '港口区',
|
||||||
|
videoSrc: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.monitor-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
width: 94%;
|
||||||
|
height: 74%;
|
||||||
|
background: #000;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 0 7px 7px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
16
web/src/main.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import DataV from '@kjgl77/datav-vue3'
|
||||||
|
import '@kjgl77/datav-vue3/dist/style.css'
|
||||||
|
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(DataV);
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.config.globalProperties.$echarts = echarts
|
||||||
|
|
||||||
|
app.mount('#app')
|
79
web/src/style.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
24
web/vite.config.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
find: /^@dataview\/datav-vue3$/,
|
||||||
|
replacement: '@kjgl77/datav-vue3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['@kjgl77/datav-vue3']
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `@use "@/assets/styles/main" as *;`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|