添加总览功能
This commit is contained in:
8
klp-ui/src/api/ems/overview.js
Normal file
8
klp-ui/src/api/ems/overview.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getLeftWithDevices() {
|
||||
return request({
|
||||
url: '/ems/location/treeWithDevices',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
BIN
klp-ui/src/assets/images/bird.png
Normal file
BIN
klp-ui/src/assets/images/bird.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -114,7 +114,7 @@
|
||||
<el-table-column label="安装位置" align="center" prop="locationName"></el-table-column>
|
||||
<el-table-column label="设备状态" align="center" prop="status">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.alarm_devide_status" :value="scope.row.status"/>
|
||||
<dict-tag :options="dict.type.alarm_device_status" :value="scope.row.status"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备IP地址" align="center" prop="ipAddress" />
|
||||
@@ -165,14 +165,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="安装位置" prop="locationId">
|
||||
<el-select v-model="form.locationId" placeholder="请选择安装位置">
|
||||
<el-option
|
||||
v-for="item in locationList"
|
||||
:key="item.locationId"
|
||||
:label="item.name"
|
||||
:value="item.locationId"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<treeselect :disable-branch-nodes="true" v-model="form.locationId" :options="locationOptions" :normalizer="normalizer" placeholder="请选择安装位置" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择设备状态">
|
||||
@@ -205,10 +198,15 @@
|
||||
<script>
|
||||
import { listAlarmDevice, getAlarmDevice, delAlarmDevice, addAlarmDevice, updateAlarmDevice } from "@/api/ems/alarmDevice";
|
||||
import { listLocation } from "@/api/ems/location";
|
||||
import Treeselect from "@riophae/vue-treeselect";
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
|
||||
|
||||
export default {
|
||||
name: "AlarmDevice",
|
||||
dicts: ['alarm_device_type', 'alarm_device_status'],
|
||||
components: {
|
||||
Treeselect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 按钮loading
|
||||
@@ -250,17 +248,33 @@ export default {
|
||||
{ required: true, message: "创建时间不能为空", trigger: "blur" }
|
||||
],
|
||||
},
|
||||
locationList: []
|
||||
locationList: [],
|
||||
locationOptions: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getList();
|
||||
this.getLocationList();
|
||||
this.getTreeselect();
|
||||
},
|
||||
methods: {
|
||||
getLocationList() {
|
||||
/** 转换位置数据结构 */
|
||||
normalizer(node) {
|
||||
if (node.children && !node.children.length) {
|
||||
delete node.children;
|
||||
}
|
||||
return {
|
||||
id: node.locationId,
|
||||
label: node.name,
|
||||
children: node.children
|
||||
};
|
||||
},
|
||||
/** 查询位置下拉树结构 */
|
||||
getTreeselect() {
|
||||
listLocation().then(response => {
|
||||
this.locationList = response.data;
|
||||
this.locationOptions = [];
|
||||
const data = { locationId: undefined, name: '顶级节点', children: [] };
|
||||
data.children = this.handleTree(response.data, "locationId", "parentId");
|
||||
this.locationOptions.push(data);
|
||||
});
|
||||
},
|
||||
/** 查询安全警报设备列表 */
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="警报内容" align="center" prop="alarmContent">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="scope.row.alarmContent"></div>
|
||||
<div v-html="scope.row.alarmContent" style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发生时间" align="center" prop="alarmTime" width="180">
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="隐患描述" prop="hazardDesc">
|
||||
<el-input v-model="form.hazardDesc" type="textarea" placeholder="请输入内容" />
|
||||
<el-input v-model="form.hazardDesc" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发现时间" prop="discoveryTime">
|
||||
<el-date-picker clearable
|
||||
|
||||
@@ -62,9 +62,13 @@
|
||||
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
||||
>
|
||||
<el-table-column label="区域名称" prop="name" />
|
||||
<el-table-column label="父级区域" align="center" prop="parentId" />
|
||||
<el-table-column label="区域描述" align="center" prop="description" />
|
||||
<el-table-column label="详细地址" align="center" prop="address" />
|
||||
<el-table-column label="鸟瞰坐标" align="center">
|
||||
<template slot-scope="scope">
|
||||
( {{ scope.row.x }}, {{ scope.row.y }} )
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
@@ -105,6 +109,13 @@
|
||||
<el-form-item label="详细地址" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入详细地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预览坐标">
|
||||
<div style="display: flex; justify-content: space-evenly; gap: 10px;">
|
||||
<el-input-number style="width: 100%;" size="mini" min="0" max="100" step="0.1" v-model="form.x" placeholder="预览坐标X" />
|
||||
<el-input-number style="width: 100%;" size="mini" min="0" max="100" step="0.1" v-model="form.y" placeholder="预览坐标Y" />
|
||||
</div>
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
@@ -211,7 +222,9 @@ export default {
|
||||
createTime: null,
|
||||
updateTime: null,
|
||||
delFlag: null,
|
||||
remark: null
|
||||
remark: null,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
this.resetForm("form");
|
||||
},
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="安装位置" prop="locationId">
|
||||
<treeselect v-model="form.locationId" :options="locationOptions" :normalizer="normalizer" placeholder="请选择安装位置" />
|
||||
<treeselect :disable-branch-nodes="true" v-model="form.locationId" :options="locationOptions" :normalizer="normalizer" placeholder="请选择安装位置" />
|
||||
</el-form-item>
|
||||
<el-form-item label="设备型号" prop="model">
|
||||
<el-input v-model="form.model" placeholder="请输入设备型号" />
|
||||
|
||||
184
klp-ui/src/views/ems/overview/components/Live.vue
Normal file
184
klp-ui/src/views/ems/overview/components/Live.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="left-monitor-card">
|
||||
<h3>实时监测 <span class="refresh-time">(最后更新: {{ lastRefreshTime }})</span></h3>
|
||||
<!-- 消防安全板块 -->
|
||||
<div class="section">
|
||||
<p class="alarm-total">报警设备:<span class="alarm-num">{{ alarmCount }}</span> / {{ totalDevices }}</p>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>位置</th>
|
||||
<th>报警数量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in alarmList" :key="index">
|
||||
<td>{{ item.location }}</td>
|
||||
<td class="red">{{ item.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// 设备总数
|
||||
totalDevices: 424,
|
||||
// 报警设备数量
|
||||
alarmCount: 115,
|
||||
// 报警列表数据
|
||||
alarmList: [
|
||||
{ location: '3#开关站2层', count: 24 },
|
||||
{ location: '3#罩式炉电气室2层', count: 22 },
|
||||
{ location: '3#罩式炉电气室1层', count: 13 }
|
||||
],
|
||||
// 最后刷新时间
|
||||
lastRefreshTime: '',
|
||||
// 定时器ID
|
||||
timer: null,
|
||||
// 刷新间隔(毫秒)
|
||||
refreshInterval: 5000
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化最后刷新时间
|
||||
this.updateRefreshTime();
|
||||
// 启动定时刷新
|
||||
this.startRefresh();
|
||||
},
|
||||
beforeUnmount() {
|
||||
// 组件销毁前清除定时器
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 启动定时刷新
|
||||
startRefresh() {
|
||||
// 清除可能存在的定时器
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
// 设置新的定时器
|
||||
this.timer = setTimeout(() => {
|
||||
// 刷新数据
|
||||
this.refreshData();
|
||||
// 更新刷新时间
|
||||
this.updateRefreshTime();
|
||||
// 递归调用,实现定时循环
|
||||
this.startRefresh();
|
||||
}, this.refreshInterval);
|
||||
},
|
||||
|
||||
// 模拟数据刷新
|
||||
refreshData() {
|
||||
// 随机更新报警设备总数(在一定范围内波动)
|
||||
const minChange = -5;
|
||||
const maxChange = 5;
|
||||
const change = Math.floor(Math.random() * (maxChange - minChange + 1)) + minChange;
|
||||
this.alarmCount = Math.max(0, Math.min(this.totalDevices, this.alarmCount + change));
|
||||
|
||||
// 随机更新每个位置的报警数量
|
||||
this.alarmList = this.alarmList.map(item => {
|
||||
const itemChange = Math.floor(Math.random() * 5) - 2; // -2到2之间的随机变化
|
||||
return {
|
||||
...item,
|
||||
count: Math.max(0, item.count + itemChange)
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 更新最后刷新时间
|
||||
updateRefreshTime() {
|
||||
const now = new Date();
|
||||
this.lastRefreshTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-monitor-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
width: 280px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.refresh-time {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
margin: 12px 0 8px;
|
||||
}
|
||||
|
||||
.alarm-total {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.alarm-num {
|
||||
color: #e53935;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.normal-num {
|
||||
color: #43a047;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.normal-dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #43a047;
|
||||
}
|
||||
</style>
|
||||
85
klp-ui/src/views/ems/overview/components/Record.vue
Normal file
85
klp-ui/src/views/ems/overview/components/Record.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="bottom-records-card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h3>报警记录</h3>
|
||||
<el-button type="text" @click="handleMore">查看更多</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="告警设备" align="center" prop="deviceName" />
|
||||
<el-table-column label="警报类型" align="center" prop="alarmType">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.alarm_record_type" :value="scope.row.alarmType"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="警报级别" align="center" prop="alarmLevel">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.alarm_record_level" :value="scope.row.alarmLevel"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="警报内容" align="center" prop="alarmContent">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="scope.row.alarmContent" style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发生时间" align="center" prop="alarmTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.alarmTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理状态" align="center" prop="handleStatus" />
|
||||
<el-table-column label="处理人" align="center" prop="handleUser" />
|
||||
<el-table-column label="处理时间" align="center" prop="handleTime" width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.handleTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理备注" align="center" prop="handleNotes" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listAlarmRecord } from '@/api/ems/alarmRecord'
|
||||
|
||||
export default {
|
||||
dicts: ['alarm_record_type', 'alarm_record_level'],
|
||||
data() {
|
||||
return {
|
||||
list: [],
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
listAlarmRecord({ pageNum: 1, pageSize: 3 }).then(res => {
|
||||
this.list = res.rows
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleMore() {
|
||||
this.$router.push('/system/env/alarmRecord')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bottom-records-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
59
klp-ui/src/views/ems/overview/components/Statistic.vue
Normal file
59
klp-ui/src/views/ems/overview/components/Statistic.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="right-statistics-card">
|
||||
<h3>今年安全生产天数: 30</h3>
|
||||
<div class="section">
|
||||
<h4>报警统计</h4>
|
||||
<div class="alarm-item green-border">
|
||||
<span>近一月</span>
|
||||
<span>0</span>
|
||||
</div>
|
||||
<div class="alarm-item green-border">
|
||||
<span>近一季</span>
|
||||
<span>0</span>
|
||||
</div>
|
||||
<div class="alarm-item green-border">
|
||||
<span>近一年</span>
|
||||
<span>0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.right-statistics-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.green-border {
|
||||
border: 1px solid #43a047;
|
||||
}
|
||||
|
||||
.red-border {
|
||||
border: 1px solid #e53935;
|
||||
}
|
||||
</style>
|
||||
@@ -1,548 +1,340 @@
|
||||
<template>
|
||||
<div class="app-container ems-overview">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button-group>
|
||||
<el-button size="mini" :type="viewLevel===1?'primary':'default'" @click="setView(1)">1</el-button>
|
||||
<el-button size="mini" :type="viewLevel===2?'primary':'default'" @click="setView(2)">2</el-button>
|
||||
<el-button size="mini" :type="viewLevel===3?'primary':'default'" @click="setView(3)">3</el-button>
|
||||
</el-button-group>
|
||||
<el-input
|
||||
size="mini"
|
||||
v-model="query.keyword"
|
||||
placeholder="据位/设备关键词"
|
||||
clearable
|
||||
class="q-input"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-button size="mini" type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button size="mini" @click="resetQuery">重置查询</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button size="mini" icon="el-icon-zoom-in" @click="zoom(1.1)"/>
|
||||
<el-button size="mini" icon="el-icon-zoom-out" @click="zoom(0.9)"/>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="resetView">重置视图</el-button>
|
||||
<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>
|
||||
</div>
|
||||
<div class="left" v-if="setting.includes('left')">
|
||||
<Live />
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
||||
<!-- 悬停提示框 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-wrapper" ref="wrapper">
|
||||
<svg :viewBox="viewBox" class="overview-svg">
|
||||
<!-- 区域:由矩形拼接组成 -->
|
||||
<g
|
||||
v-for="area in areas"
|
||||
:key="area.id"
|
||||
style="cursor:pointer;"
|
||||
@click="selectArea(area, $event)"
|
||||
>
|
||||
<!-- 拼接矩形 -->
|
||||
<g v-for="(shape, idx) in area.shapes" :key="area.id + '_s_' + idx">
|
||||
<rect
|
||||
:x="shape.x"
|
||||
:y="shape.y"
|
||||
:width="shape.w"
|
||||
:height="shape.h"
|
||||
:fill="area.id===selectedAreaId ? area.fillActive : area.fill"
|
||||
:stroke="area.stroke"
|
||||
:stroke-width="2"
|
||||
:opacity="viewLevel===3 ? 0.8 : 1"
|
||||
rx="4"
|
||||
ry="4"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- 区域标题 -->
|
||||
<text :x="area.labelPos.x" :y="area.labelPos.y" class="area-label">
|
||||
{{ area.name }}
|
||||
</text>
|
||||
|
||||
<!-- 点位格子(方/长方形) -->
|
||||
<g v-if="viewLevel!==1 || area.id===selectedAreaId">
|
||||
<g
|
||||
v-for="pt in area.pointsInfo"
|
||||
:key="pt.id"
|
||||
@click.stop="openPoint(pt, area, $event)"
|
||||
style="cursor:pointer;"
|
||||
>
|
||||
<rect
|
||||
:x="pt.x-pt.w/2"
|
||||
:y="pt.y-pt.h/2"
|
||||
:width="pt.w"
|
||||
:height="pt.h"
|
||||
rx="4"
|
||||
ry="4"
|
||||
:fill="ptFill(pt)"
|
||||
stroke="#444"
|
||||
:stroke-width="selectedPoint && selectedPoint.id===pt.id ? 2 : 1"
|
||||
:opacity="viewLevel===2 ? 0.9 : 0.8"
|
||||
:class="{'pt-highlight': highlightedIds.has(pt.id)}"
|
||||
/>
|
||||
<text :x="pt.x" :y="pt.y-6" class="pt-title">{{ pt.name }}</text>
|
||||
<text :x="pt.x" :y="pt.y+10" class="pt-sub">
|
||||
{{ safeMetric(pt, 'metric1', 'label', '指标1') + ':' + safeMetric(pt, 'metric1', 'value', '--') }}
|
||||
</text>
|
||||
<text :x="pt.x" :y="pt.y+24" class="pt-sub">
|
||||
{{ safeMetric(pt, 'metric2', 'label', '指标2') + ':' + safeMetric(pt, 'metric2', 'value', '--') }}
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- 悬浮信息卡(点击块/点位时显示) -->
|
||||
<div
|
||||
v-if="infoBox.show"
|
||||
class="info-box"
|
||||
:style="{ left: infoBox.x + 'px', top: infoBox.y + 'px' }"
|
||||
>
|
||||
<div class="info-title">{{ infoBox.title }}</div>
|
||||
<div class="info-line" v-for="(line, i) in infoBox.lines" :key="i">{{ line }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 点位详情 -->
|
||||
<el-dialog
|
||||
title="点位详情"
|
||||
:visible.sync="pointDialogOpen"
|
||||
width="420px"
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="selectedPoint">
|
||||
<p>点位名称:{{ selectedPoint.name }}</p>
|
||||
<p>所属区域:{{ selectedArea ? selectedArea.name : '-' }}</p>
|
||||
<p>坐标:({{ selectedPoint.x }}, {{ selectedPoint.y }})</p>
|
||||
<el-divider></el-divider>
|
||||
<p>设备数据:</p>
|
||||
<ul class="point-ul">
|
||||
<li>
|
||||
{{ safeMetric(selectedPoint, 'metric1', 'label', '指标1') }}:
|
||||
{{ safeMetric(selectedPoint, 'metric1', 'value', '--') }}
|
||||
</li>
|
||||
<li>
|
||||
{{ safeMetric(selectedPoint, 'metric2', 'label', '指标2') }}:
|
||||
{{ safeMetric(selectedPoint, 'metric2', 'value', '--') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button size="mini" type="primary" @click="pointDialogOpen=false">关 闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ts-nocheck
|
||||
import Live from './components/Live.vue'
|
||||
import Record from './components/Record.vue'
|
||||
import Statistic from './components/Statistic.vue'
|
||||
|
||||
import { getLeftWithDevices } from '@/api/ems/overview'
|
||||
|
||||
export default {
|
||||
name: 'EmsOverview',
|
||||
components: {
|
||||
Live,
|
||||
Record,
|
||||
Statistic
|
||||
},
|
||||
dicts: ['alarm_device_status'],
|
||||
data() {
|
||||
return {
|
||||
viewLevel: 1, // 1/2/3 切换视图
|
||||
scale: 1,
|
||||
viewBox: '0 0 1200 700',
|
||||
selectedAreaId: null,
|
||||
selectedPoint: null,
|
||||
selectedArea: null,
|
||||
pointDialogOpen: false,
|
||||
|
||||
// 查询
|
||||
query: {
|
||||
keyword: ''
|
||||
},
|
||||
highlightedIds: new Set(),
|
||||
|
||||
infoBox: {
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
title: '',
|
||||
lines: []
|
||||
},
|
||||
|
||||
// 示例数据:使用由正方形/长方形拼接的区域形状
|
||||
areas: [
|
||||
{
|
||||
id: 'A',
|
||||
name: '冷藏区',
|
||||
// 用多个长方形拼接
|
||||
shapes: [
|
||||
{ x: 40, y: 60, w: 560, h: 560 }, // 大矩形
|
||||
{ x: 40, y: 620, w: 560, h: 10 } // 底部细条,强调边界(可选)
|
||||
],
|
||||
labelPos: { x: 60, y: 110 },
|
||||
fill: '#4c6ef5',
|
||||
fillActive: '#3b5bdb',
|
||||
stroke: '#2b2d42',
|
||||
// 点位
|
||||
pointsInfo: [
|
||||
{
|
||||
id: 'A1', name: '点位-01', x: 250, y: 220, w: 110, h: 70,
|
||||
deviceData: { metric1: { label: '温度', value: '4.2℃' }, metric2: { label: '状态', value: '正常' } }
|
||||
},
|
||||
{
|
||||
id: 'A2', name: '点位-02', x: 520, y: 260, w: 110, h: 70,
|
||||
deviceData: { metric1: { label: '温度', value: '5.1℃' }, metric2: { label: '状态', value: '正常' } }
|
||||
},
|
||||
{
|
||||
id: 'A3', name: '点位-03', x: 420, y: 420, w: 110, h: 70,
|
||||
deviceData: { metric1: { label: '温度', value: '7.8℃' }, metric2: { label: '状态', value: '告警' } }
|
||||
}
|
||||
],
|
||||
// 库存示例(点击区域显示)
|
||||
inventory: { title: '库存', count: 31890 }
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
name: '冷冻区',
|
||||
shapes: [
|
||||
{ x: 620, y: 60, w: 520, h: 280 }, // 上部
|
||||
{ x: 620, y: 350, w: 520, h: 270 } // 下部
|
||||
],
|
||||
labelPos: { x: 640, y: 110 },
|
||||
fill: '#4ea8de',
|
||||
fillActive: '#3a86ff',
|
||||
stroke: '#1b263b',
|
||||
pointsInfo: [
|
||||
{
|
||||
id: 'B1', name: '点位-11', x: 780, y: 220, w: 110, h: 70,
|
||||
deviceData: { metric1: { label: '温度', value: '-12.3℃' }, metric2: { label: '状态', value: '正常' } }
|
||||
},
|
||||
{
|
||||
id: 'B2', name: '点位-12', x: 980, y: 460, w: 110, h: 70,
|
||||
deviceData: { metric1: { label: '温度', value: '-10.0℃' }, metric2: { label: '状态', value: '维护' } }
|
||||
}
|
||||
],
|
||||
inventory: { title: '库存', count: 21890 }
|
||||
},
|
||||
// 分拣区:原来是不规则多边形,现改为由多个矩形拼接成“侧边栏”效果
|
||||
{
|
||||
id: 'C',
|
||||
name: '分拣区',
|
||||
shapes: [
|
||||
{ x: 1160, y: 60, w: 20, h: 560 }, // 竖条边框
|
||||
{ x: 1080, y: 60, w: 70, h: 130 },
|
||||
{ x: 1080, y: 200, w: 70, h: 130 },
|
||||
{ x: 1080, y: 340, w: 70, h: 130 },
|
||||
{ x: 1080, y: 480, w: 70, h: 140 }
|
||||
],
|
||||
labelPos: { x: 1090, y: 90 },
|
||||
fill: '#95d5b2',
|
||||
fillActive: '#74c69d',
|
||||
stroke: '#2d6a4f',
|
||||
pointsInfo: [
|
||||
{
|
||||
id: 'C1', name: '能量棒', x: 1115, y: 130, w: 60, h: 60,
|
||||
deviceData: { metric1: { label: '重量', value: '1000g' }, metric2: { label: '状态', value: '良好' } }
|
||||
},
|
||||
{
|
||||
id: 'C2', name: '显卡', x: 1115, y: 270, w: 60, h: 60,
|
||||
deviceData: { metric1: { label: '型号', value: '4001' }, metric2: { label: '状态', value: '正常' } }
|
||||
},
|
||||
{
|
||||
id: 'C3', name: '特种钢', x: 1115, y: 410, w: 60, h: 60,
|
||||
deviceData: { metric1: { label: '数量', value: '999个' }, metric2: { label: '状态', value: '正常' } }
|
||||
},
|
||||
{
|
||||
id: 'C4', name: '能量棒', x: 1115, y: 560, w: 60, h: 60,
|
||||
deviceData: { metric1: { label: '重量', value: '500g' }, metric2: { label: '状态', value: '正常' } }
|
||||
}
|
||||
],
|
||||
inventory: { title: '库存', count: 4080 }
|
||||
}
|
||||
]
|
||||
};
|
||||
// 标点数据补充
|
||||
setting: ['left', 'right'],
|
||||
points: [],
|
||||
activeTooltip: -1 // 控制提示框显示
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getLeftWithDevices()
|
||||
},
|
||||
methods: {
|
||||
setView(n) {
|
||||
this.viewLevel = n;
|
||||
showTooltip(index) {
|
||||
this.activeTooltip = index
|
||||
},
|
||||
zoom(factor) {
|
||||
this.scale = Math.max(0.5, Math.min(2.5, this.scale * factor));
|
||||
const base = { w: 1200, h: 700 };
|
||||
const w = base.w / this.scale;
|
||||
const h = base.h / this.scale;
|
||||
this.viewBox = `0 0 ${w} ${h}`;
|
||||
},
|
||||
resetView() {
|
||||
this.scale = 1;
|
||||
this.viewBox = '0 0 1200 700';
|
||||
this.selectedAreaId = null;
|
||||
this.selectedPoint = null;
|
||||
this.selectedArea = null;
|
||||
this.hideInfo();
|
||||
this.highlightedIds = new Set();
|
||||
hideTooltip() {
|
||||
this.activeTooltip = -1
|
||||
},
|
||||
getLeftWithDevices() {
|
||||
getLeftWithDevices().then(res => {
|
||||
console.log(res.data);
|
||||
|
||||
// 安全读取设备指标
|
||||
safeMetric(obj, mKey, fKey, defVal) {
|
||||
if (!obj || !obj.deviceData) return defVal;
|
||||
const m = obj.deviceData[mKey];
|
||||
return m && m[fKey] != null ? m[fKey] : defVal;
|
||||
},
|
||||
// 1. 树结构转数组,只保留叶子节点(children为空数组的节点)
|
||||
const leafNodes = [];
|
||||
|
||||
// 点位填充色
|
||||
ptFill(pt) {
|
||||
const val = (this.safeMetric(pt, 'metric2', 'value', '') || '').toString();
|
||||
if (val.includes('告警') || val.includes('异常')) return '#ff6b6b';
|
||||
if (val.includes('维护')) return '#ffd166';
|
||||
return '#8ecae6';
|
||||
},
|
||||
// 递归收集叶子节点的函数
|
||||
const collectLeafs = (nodes) => {
|
||||
if (!nodes || !Array.isArray(nodes)) return;
|
||||
|
||||
// 区域选择 + 信息卡显示(库存)
|
||||
selectArea(area, evt) {
|
||||
this.selectedAreaId = area.id;
|
||||
this.selectedArea = area;
|
||||
this.showInfo(evt, area.name, [
|
||||
area.inventory?.title || '库存',
|
||||
`库存数量:${this.formatNumber(area.inventory?.count ?? 0)}`
|
||||
]);
|
||||
},
|
||||
|
||||
// 打开点位(弹框 + 信息卡)
|
||||
openPoint(pt, area, evt) {
|
||||
this.selectedPoint = pt;
|
||||
this.selectedArea = area;
|
||||
this.pointDialogOpen = true;
|
||||
|
||||
const l1 = `${this.safeMetric(pt, 'metric1', 'label', '指标1')}:${this.safeMetric(pt, 'metric1', 'value', '--')}`;
|
||||
const l2 = `${this.safeMetric(pt, 'metric2', 'label', '指标2')}:${this.safeMetric(pt, 'metric2', 'value', '--')}`;
|
||||
this.showInfo(evt, pt.name, [l1, l2]);
|
||||
},
|
||||
|
||||
// 悬浮信息卡
|
||||
showInfo(evt, title, lines) {
|
||||
const wrap = this.$refs.wrapper;
|
||||
if (!wrap) return;
|
||||
const rect = wrap.getBoundingClientRect();
|
||||
const x = evt.clientX - rect.left + 12;
|
||||
const y = evt.clientY - rect.top + 12;
|
||||
this.infoBox = { show: true, x, y, title, lines };
|
||||
},
|
||||
hideInfo() {
|
||||
this.infoBox.show = false;
|
||||
},
|
||||
|
||||
// 查询(据位查询):优先走接口,失败回退本地过滤
|
||||
async handleQuery() {
|
||||
const kw = (this.query.keyword || '').trim();
|
||||
this.highlightedIds = new Set();
|
||||
if (!kw) {
|
||||
this.hideInfo();
|
||||
return;
|
||||
}
|
||||
let usedApi = false;
|
||||
try {
|
||||
// 存在封装的 API 则调用
|
||||
if (this.$api && this.$api.ems && this.$api.ems.queryPoints) {
|
||||
await this.queryPoints({ keyword: kw });
|
||||
usedApi = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略接口错误,回退本地
|
||||
usedApi = false;
|
||||
}
|
||||
|
||||
// 高亮与信息卡(对当前 this.areas 数据集进行)
|
||||
let firstHit = null;
|
||||
this.areas.forEach(area => {
|
||||
(area.pointsInfo || []).forEach(pt => {
|
||||
const text = [
|
||||
pt.name,
|
||||
this.safeMetric(pt, 'metric1', 'label', ''),
|
||||
this.safeMetric(pt, 'metric1', 'value', ''),
|
||||
this.safeMetric(pt, 'metric2', 'label', ''),
|
||||
this.safeMetric(pt, 'metric2', 'value', '')
|
||||
].join(' ');
|
||||
if (text.includes(kw)) {
|
||||
this.highlightedIds.add(pt.id);
|
||||
if (!firstHit) firstHit = { pt, area };
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (firstHit) {
|
||||
this.selectedAreaId = firstHit.area.id;
|
||||
this.selectedArea = firstHit.area;
|
||||
this.selectedPoint = firstHit.pt;
|
||||
const wrap = this.$refs.wrapper;
|
||||
if (wrap) {
|
||||
const rect = wrap.getBoundingClientRect();
|
||||
const x = (firstHit.pt.x / (1200 / rect.width)) + 12;
|
||||
const y = (firstHit.pt.y / (700 / rect.height)) + 12;
|
||||
this.infoBox = {
|
||||
show: true,
|
||||
x: Math.min(rect.width - 220, Math.max(8, x)),
|
||||
y: Math.min(rect.height - 140, Math.max(8, y)),
|
||||
title: firstHit.pt.name,
|
||||
lines: [
|
||||
`${this.safeMetric(firstHit.pt, 'metric1', 'label', '指标1')}:${this.safeMetric(firstHit.pt, 'metric1', 'value', '--')}`,
|
||||
`${this.safeMetric(firstHit.pt, 'metric2', 'label', '指标2')}:${this.safeMetric(firstHit.pt, 'metric2', 'value', '--')}`
|
||||
]
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this.$message.info(usedApi ? '接口无匹配结果' : '未查询到匹配点位/设备数据');
|
||||
this.hideInfo();
|
||||
}
|
||||
},
|
||||
resetQuery() {
|
||||
this.query.keyword = '';
|
||||
this.highlightedIds = new Set();
|
||||
this.hideInfo();
|
||||
},
|
||||
|
||||
// 对接接口:据位查询或区域点位拉取
|
||||
queryPoints(params) {
|
||||
if (!(this.$api && this.$api.ems && this.$api.ems.queryPoints)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this.$api.ems.queryPoints(params).then(res => {
|
||||
const data = (res && (res.data ?? res)) || {};
|
||||
// 结构 A:包含 areas
|
||||
if (Array.isArray(data.areas)) {
|
||||
// 将后端 areas 适配为前端结构,缺省字段从现有同 id 区域继承
|
||||
const existMap = new Map(this.areas.map(a => [a.id, a]));
|
||||
this.areas = data.areas.map(a => {
|
||||
const old = existMap.get(a.id) || {};
|
||||
return {
|
||||
id: a.id,
|
||||
name: a.name ?? old.name ?? '',
|
||||
shapes: Array.isArray(a.shapes) ? a.shapes : (old.shapes || []),
|
||||
labelPos: a.labelPos ?? old.labelPos ?? { x: 60, y: 110 },
|
||||
fill: old.fill || '#4c6ef5',
|
||||
fillActive: old.fillActive || '#3b5bdb',
|
||||
stroke: old.stroke || '#2b2d42',
|
||||
inventory: a.inventory ?? old.inventory ?? null,
|
||||
pointsInfo: Array.isArray(a.points)
|
||||
? a.points.map(p => ({
|
||||
id: p.id, name: p.name,
|
||||
x: p.x, y: p.y, w: p.w, h: p.h,
|
||||
deviceData: p.deviceData || {}
|
||||
}))
|
||||
: (old.pointsInfo || [])
|
||||
};
|
||||
nodes.forEach(node => {
|
||||
// 检查是否为叶子节点:children存在且为空数组
|
||||
if (node.children && Array.isArray(node.children) && node.children.length === 0) {
|
||||
leafNodes.push(node);
|
||||
} else {
|
||||
// 递归处理子节点
|
||||
collectLeafs(node.children);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 结构 B:仅返回 points(需含 areaId)
|
||||
if (Array.isArray(data.points)) {
|
||||
const byArea = {};
|
||||
data.points.forEach(p => {
|
||||
const aid = p.areaId;
|
||||
if (!aid) return;
|
||||
(byArea[aid] = byArea[aid] || []).push({
|
||||
id: p.id, name: p.name,
|
||||
x: p.x, y: p.y, w: p.w, h: p.h,
|
||||
deviceData: p.deviceData || {}
|
||||
});
|
||||
});
|
||||
this.areas = this.areas.map(a => ({
|
||||
...a,
|
||||
pointsInfo: byArea[a.id] ? byArea[a.id] : (a.pointsInfo || [])
|
||||
}));
|
||||
// 可选:data.areasInfo 覆盖库存
|
||||
if (Array.isArray(data.areasInfo)) {
|
||||
const infoMap = new Map(data.areasInfo.map(i => [i.id || i.areaId, i]));
|
||||
this.areas = this.areas.map(a => ({
|
||||
...a,
|
||||
inventory: infoMap.get(a.id)?.inventory ?? a.inventory
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 未识别结构,保持不变
|
||||
}).catch(err => {
|
||||
this.$message.error('查询接口失败');
|
||||
// 继续走本地回退
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
formatNumber(n) {
|
||||
const str = (n ?? 0).toString();
|
||||
return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
// 开始收集叶子节点
|
||||
collectLeafs(res.data);
|
||||
|
||||
// 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字段
|
||||
// 如果有其他需要保留的字段,可以在这里继续添加
|
||||
}));
|
||||
|
||||
// 这里可以使用转换后的数据,例如赋值给组件的状态
|
||||
this.points = transformedData;
|
||||
console.log('转换后的叶子节点数据:', transformedData);
|
||||
}).catch(error => {
|
||||
console.error('获取数据失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ems-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 84px);
|
||||
position: relative;
|
||||
background: url('../../../assets/images/bird.png') no-repeat center center;
|
||||
background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
.toolbar {
|
||||
|
||||
.setting {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.left {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 10px;
|
||||
width: 220px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.right {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 220px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.point-tooltip.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.q-input {
|
||||
width: 220px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.canvas-wrapper {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
height: 720px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.overview-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.area-label {
|
||||
fill: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
||||
}
|
||||
.pt-title, .pt-sub {
|
||||
fill: #ffffff;
|
||||
font-size: 12px;
|
||||
text-anchor: middle;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pt-highlight {
|
||||
stroke: #ffb703 !important;
|
||||
stroke-width: 3 !important;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
position: absolute;
|
||||
min-width: 180px;
|
||||
max-width: 240px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.18);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.info-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.info-line {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.point-ul {
|
||||
.tooltip-header h4 {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.normal {
|
||||
background-color: #43a047;
|
||||
}
|
||||
|
||||
.status-badge.abnormal {
|
||||
background-color: #e53935;
|
||||
}
|
||||
|
||||
.device-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.device-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.device-status.normal {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.device-status.abnormal {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user