2025-10-13 11:40:34 +08:00
|
|
|
|
<template>
|
2025-10-15 16:29:00 +08:00
|
|
|
|
<div class="container">
|
|
|
|
|
|
<div class="setting">
|
|
|
|
|
|
<el-checkbox-group v-model="setting">
|
|
|
|
|
|
<el-checkbox-button label="left">实时监控</el-checkbox-button>
|
|
|
|
|
|
<el-checkbox-button label="right">统计信息</el-checkbox-button>
|
|
|
|
|
|
<el-checkbox-button label="bottom">告警记录</el-checkbox-button>
|
|
|
|
|
|
</el-checkbox-group>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</div>
|
2025-10-15 16:29:00 +08:00
|
|
|
|
<div class="left" v-if="setting.includes('left')">
|
|
|
|
|
|
<Live />
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</div>
|
2025-10-15 16:29:00 +08:00
|
|
|
|
<div class="right" v-if="setting.includes('right')">
|
|
|
|
|
|
<Statistic />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="bottom" v-if="setting.includes('bottom')">
|
|
|
|
|
|
<Record />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="points">
|
|
|
|
|
|
<!-- 标点渲染 -->
|
|
|
|
|
|
<div v-for="(point, index) in points" :key="index" class="point-marker"
|
|
|
|
|
|
:style="{ left: point.x + '%', top: point.y + '%' }"
|
|
|
|
|
|
:class="{ 'normal-status': point.status === 0, 'abnormal-status': point.status === 1 }"
|
|
|
|
|
|
@mouseenter="showTooltip(index)" @mouseleave="hideTooltip(index)">
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="background-color: #00000060; display: flex; align-items: center; justify-content: center; gap: 4px; padding: 4px; color: #fff;">
|
|
|
|
|
|
<!-- 标点图标 -->
|
|
|
|
|
|
<div class="marker-icon">
|
|
|
|
|
|
<i class="fa" :class="point.status === 0 ? 'fa-check-circle' : 'fa-exclamation-circle'"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="point-name">{{ point.name }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-13 11:40:34 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
<!-- 悬停提示框 -->
|
|
|
|
|
|
<div class="point-tooltip" :class="{ 'visible': activeTooltip === index }">
|
|
|
|
|
|
<div class="tooltip-header">
|
|
|
|
|
|
<h4>{{ point.name }}</h4>
|
|
|
|
|
|
<span class="status-badge" :class="point.status === 0 ? 'normal' : 'abnormal'">
|
|
|
|
|
|
{{ point.status === 0 ? '正常' : '异常' }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tooltip-body" v-if="point.children && point.children.length > 0">
|
|
|
|
|
|
<table class="device-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>设备名称</th>
|
|
|
|
|
|
<th>设备IP</th>
|
|
|
|
|
|
<th>状态</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr v-for="(device, devIdx) in point.children" :key="devIdx">
|
|
|
|
|
|
<td>{{ device.deviceName }}</td>
|
|
|
|
|
|
<td>{{ device.ipAddress || '无数据' }}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<!-- <span class="device-status" :class="device.status === 0 ? 'normal' : 'abnormal'">
|
|
|
|
|
|
{{ device.status === 0 ? '正常' : '异常' }}
|
|
|
|
|
|
</span> -->
|
|
|
|
|
|
<dict-tag :options="dict.type.alarm_device_status" :value="device.status"/>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tooltip-body" v-else>
|
|
|
|
|
|
<div class="no-device">暂无设备</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</div>
|
2025-10-15 16:29:00 +08:00
|
|
|
|
</div>
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2025-10-15 16:29:00 +08:00
|
|
|
|
import Live from './components/Live.vue'
|
|
|
|
|
|
import Record from './components/Record.vue'
|
|
|
|
|
|
import Statistic from './components/Statistic.vue'
|
|
|
|
|
|
|
|
|
|
|
|
import { getLeftWithDevices } from '@/api/ems/overview'
|
|
|
|
|
|
|
2025-10-13 11:40:34 +08:00
|
|
|
|
export default {
|
2025-10-15 16:29:00 +08:00
|
|
|
|
components: {
|
|
|
|
|
|
Live,
|
|
|
|
|
|
Record,
|
|
|
|
|
|
Statistic
|
|
|
|
|
|
},
|
|
|
|
|
|
dicts: ['alarm_device_status'],
|
2025-10-13 11:40:34 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2025-10-15 16:29:00 +08:00
|
|
|
|
// 标点数据补充
|
|
|
|
|
|
setting: ['left', 'right'],
|
|
|
|
|
|
points: [],
|
|
|
|
|
|
activeTooltip: -1 // 控制提示框显示
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created() {
|
|
|
|
|
|
this.getLeftWithDevices()
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2025-10-15 16:29:00 +08:00
|
|
|
|
showTooltip(index) {
|
|
|
|
|
|
this.activeTooltip = index
|
2025-10-14 09:40:21 +08:00
|
|
|
|
},
|
2025-10-15 16:29:00 +08:00
|
|
|
|
hideTooltip() {
|
|
|
|
|
|
this.activeTooltip = -1
|
2025-10-13 11:40:34 +08:00
|
|
|
|
},
|
2025-10-15 16:29:00 +08:00
|
|
|
|
getLeftWithDevices() {
|
|
|
|
|
|
getLeftWithDevices().then(res => {
|
|
|
|
|
|
console.log(res.data);
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
// 1. 树结构转数组,只保留叶子节点(children为空数组的节点)
|
|
|
|
|
|
const leafNodes = [];
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
// 递归收集叶子节点的函数
|
|
|
|
|
|
const collectLeafs = (nodes) => {
|
|
|
|
|
|
if (!nodes || !Array.isArray(nodes)) return;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
nodes.forEach(node => {
|
|
|
|
|
|
// 检查是否为叶子节点:children存在且为空数组
|
|
|
|
|
|
if (node.children && Array.isArray(node.children) && node.children.length === 0) {
|
|
|
|
|
|
leafNodes.push(node);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 递归处理子节点
|
|
|
|
|
|
collectLeafs(node.children);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
// 开始收集叶子节点
|
|
|
|
|
|
collectLeafs(res.data);
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
// 2. 转换数组结构
|
|
|
|
|
|
const transformedData = leafNodes.map(node => ({
|
|
|
|
|
|
name: node.path, // 将path转换为name
|
|
|
|
|
|
status: 0, // 状态设为0
|
|
|
|
|
|
children: node.devices, // children使用原来的devices字段
|
|
|
|
|
|
x: node.x, // 保留x字段
|
|
|
|
|
|
y: node.y // 保留y字段
|
|
|
|
|
|
// 如果有其他需要保留的字段,可以在这里继续添加
|
|
|
|
|
|
}));
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
// 这里可以使用转换后的数据,例如赋值给组件的状态
|
|
|
|
|
|
this.points = transformedData;
|
|
|
|
|
|
console.log('转换后的叶子节点数据:', transformedData);
|
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
|
console.error('获取数据失败:', error);
|
2025-10-14 09:40:21 +08:00
|
|
|
|
});
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
}
|
2025-10-13 11:40:34 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
<style>
|
|
|
|
|
|
.container {
|
2025-10-13 11:40:34 +08:00
|
|
|
|
width: 100%;
|
2025-10-15 16:29:00 +08:00
|
|
|
|
height: calc(100vh - 84px);
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
background: url('../../../assets/images/bird.png') no-repeat center center;
|
|
|
|
|
|
background-size: cover;
|
|
|
|
|
|
overflow: hidden;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.setting {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 10px;
|
|
|
|
|
|
z-index: 100;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.left {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 60px;
|
|
|
|
|
|
left: 10px;
|
|
|
|
|
|
width: 220px;
|
|
|
|
|
|
z-index: 10;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.right {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
right: 10px;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
width: 220px;
|
2025-10-15 16:29:00 +08:00
|
|
|
|
z-index: 10;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.bottom {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 10px;
|
|
|
|
|
|
left: 10px;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.points {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 标点样式 */
|
|
|
|
|
|
.point-marker {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.marker-icon {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
border-radius: 50%;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2025-10-15 16:29:00 +08:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.normal-status .marker-icon {
|
|
|
|
|
|
background-color: #43a047;
|
|
|
|
|
|
/* 正常-绿色 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.abnormal-status .marker-icon {
|
|
|
|
|
|
background-color: #e53935;
|
|
|
|
|
|
/* 异常-红色 */
|
|
|
|
|
|
animation: pulse 1.5s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 脉冲动画 */
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
0% {
|
|
|
|
|
|
box-shadow: 0 0 0 0 rgba(229, 57, 53, 0.7);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
70% {
|
|
|
|
|
|
box-shadow: 0 0 0 10px rgba(229, 57, 53, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
100% {
|
|
|
|
|
|
box-shadow: 0 0 0 0 rgba(229, 57, 53, 0);
|
|
|
|
|
|
}
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-14 09:40:21 +08:00
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
.tooltip-body {
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 提示框样式 */
|
|
|
|
|
|
.point-tooltip {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 30px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.95);
|
2025-10-13 11:40:34 +08:00
|
|
|
|
border-radius: 6px;
|
2025-10-15 16:29:00 +08:00
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
z-index: 100;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.point-tooltip.visible {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tooltip-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
|
border-bottom: 1px solid #eee;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.tooltip-header h4 {
|
|
|
|
|
|
margin: 0;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
font-size: 16px;
|
2025-10-15 16:29:00 +08:00
|
|
|
|
color: #333;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.status-badge {
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 12px;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
font-size: 12px;
|
2025-10-15 16:29:00 +08:00
|
|
|
|
color: white;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.status-badge.normal {
|
|
|
|
|
|
background-color: #43a047;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
.status-badge.abnormal {
|
|
|
|
|
|
background-color: #e53935;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
font-size: 13px;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.device-table th {
|
|
|
|
|
|
padding: 6px 4px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-table td {
|
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
2025-10-15 16:29:00 +08:00
|
|
|
|
|
|
|
|
|
|
.device-table tr:last-child td {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-status {
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: 4px;
|
2025-10-14 09:40:21 +08:00
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 16:29:00 +08:00
|
|
|
|
.device-status.normal {
|
|
|
|
|
|
background-color: #e8f5e9;
|
|
|
|
|
|
color: #2e7d32;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.device-status.abnormal {
|
|
|
|
|
|
background-color: #ffebee;
|
|
|
|
|
|
color: #c62828;
|
2025-10-13 11:40:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|