背景描述:系统需要监测每个平面的每个设备的告警信息(如本案例,监测辐射值),如果出现告警,则平面图上的设备要出现告警动画(本案例会有红色扩散波边框),双击设备能显示实时数据,鼠标悬浮设备上能显示提示框。设备默认出现在左上角,左边(0, 0),支持拖拽布置到平面的任意位置,并可保存。
默认进入后,可选择平面,选中平面后,取平面的平面图作为画布的背景,并渲染出设备。class="node"的div是设备,取设备图作为此div的背景,支持拖拽摆放设备在平面图的位置,可以保存。实时取告警信息,出现告警的设备,会有红色边框的动画效果。双击设备可以查看告警详情。
javascript
<template>
<div>
<!-- 画布 -->
<div @drop="prevent" @dragover="prevent" class="container" style="background-repeat:no-repeat;background-size:100% 100%;" :style="{ backgroundImage: `url(${bgUrl})` }">
<div v-if="bfIndex || bfIndex == 0 && isShowDev">
<!-- 每个设备盒子 -->
<!-- 此处假设小于0.2时,类为alarmTypeGreen;大于等于0.2且小于0.5时,类为alarmTypeGreenalarmTypeKelly;大于等于0.5且小于1时,类为alarmTypeOrange,大于等于1时,类为alarmTypeRed -->
<div class="node" :class="{'alarmTypeGreen': node.absorbedDoseRate < 0.2,'alarmTypeKelly': node.absorbedDoseRate >= 0.2 && node.absorbedDoseRate < 0.5,'alarmTypeOrange': node.absorbedDoseRate >= 0.5 && node.absorbedDoseRate < 1,'alarmTypeRed': node.absorbedDoseRate >= 1}" v-for="(node, index) in deviceList" @dblclick.stop="checkDeviceDetail(node, $event)" @dragstart="startDrag(index, $event)" @drag="drag(index, $event)" @dragover="prevent" @dragend="endDrag(index, $event)" @click.stop="clickNode" :draggable="isEdit" :id="`node${index}`" :key="node.id" :style="{ top: node.ordinate + 'px', left: node.abscissa + 'px' }">
<div style="height:100%;width:100%;background-repeat:no-repeat;background-size:100% 100%;" :style="{ backgroundImage: `url(${node.url})`}"></div>
</div>
</div>
<!-- 楼层选择 -->
<div v-if="allPlanList" class="buttons">
<el-tooltip class="item" effect="dark" content="全屏模式下操作更佳,右侧下拉框可选择楼栋楼层, 鼠标悬于设备上方可查看详情, 双击设备可查看设备详情, 更新设备位置后需点击保存(仅保存当前平面)." placement="bottom-end">
<el-button type="warning" circle plain style="padding:5px;margin-right:5px;">💡</el-button>
</el-tooltip>
<!-- 按钮组 -->
<el-button type="warning" plain class="paimage.pngge-back" @click="$router.push('/bigScreen')">大屏</el-button>
<el-button type="success" plain class="paimage.pngge-back" @click="$router.push('/index')">首页</el-button>
<el-button type="primary" plain class="full-screen" @click="switchSize()">{{fullscreenTitle}}</el-button>
<el-select style="display:inline-block;margin-left:10px;" v-model="selectedValue" placeholder="请选择平面" @change="changeSelected">
<el-option v-for="(item, index) in allPlanList" :key="index" :label="`${item.buildFloor}`" :value="item.id">
</el-option>
</el-select>
<span v-show="bfIndex || bfIndex == 0">
<el-button type="success" @click="saveNodesInfos" style="margin-left:10px;">保存</el-button>
<el-dropdown @command="viewDevDetail" style="margin-left:10px;">
<el-button type="primary">
设备详情<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="show">显示</el-dropdown-item>
<el-dropdown-item command="hide">隐藏</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
</div>
</div>
<!-- 查看设备详情 -->
<el-dialog class="deviceDetailInfoDialog" @close="closeDeviceDetailDialog" v-dialogDrag title="设备详情" v-if="isShowDeviceDetailDialog" :visible.sync="isShowDeviceDetailDialog" width="1000px" :append-to-body="true">
<el-descriptions :column="2" border>
<el-descriptions-item>
<template slot="label"><i class="el-icon-s-help"></i>{{ ` 设备名称` }}</template>{{ currentDev.name }}
</el-descriptions-item>
<el-descriptions-item>
<template slot="label"><i class="el-icon-location"></i>{{ ` 所属区域` }}</template>{{ currentDev.buildFloor }}
</el-descriptions-item>
</el-descriptions>
<el-table :data="detailShowData" height="600" border style="width: 100%">
<el-table-column prop="timeString" label="时间" :show-overflow-tooltip="true" align="center" />
<el-table-column prop="data" label="吸收剂量率(μGy/h)" :show-overflow-tooltip="true" align="center" />
<el-table-column label="级别" :show-overflow-tooltip="true" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.data < 0.2" class="alarmTag" color="#05c46b"></el-tag>
<el-tag v-else-if="scope.row.data >= 0.2 && scope.row.data < 0.5" class="alarmTag" color="#badc58"></el-tag>
<el-tag v-else-if="scope.row.data >= 0.5 && scope.row.data < 1" class="alarmTag" color="#ffa801"></el-tag>
<el-tag v-else-if="scope.row.data >= 1" class="alarmTag" color="#ff3f34"></el-tag>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="isShowDeviceDetailDialog = false" size="mini" type="info" plain>关 闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { getPlanDetail, getAllPlan, savePlanDevPos, listPlanManage } from "@/api/rayscan/PlanManage"
import { listDeviceByPlan,updateBatchLocation } from "@/api/rayscan/DeviceManage"
import { getLatestDataByDevice,
getLatestDataByDevices,
getLatest5minDataByDevice,
getLatestDataByPlan,
getLatest5minDataByDevices,
getLatest5minAllDevices
} from "@/api/rayscan/RsRealTimeData"
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
// import 'tippy.js/themes/light.css';
// import 'tippy.js/themes/material.css';
export default {
name: "DataView",
components: {
},
props: {
},
data() {
return {
isShowDev: false, // 是否展示设备
allPlanList: [], // 平面列表
deviceList: {}, // 设备信息
currentPlanDevTips: [], // 当前选中平面的列表的详情tips
fullscreen: false,
fullscreenTitle: "全屏",
bgUrl: undefined, // 当前选中平面对应平面图
bfIndex: undefined, // 当前选中平面的选项索引
selectedValue: undefined, //平面select选择框选中的值
dragging: false, // 是否处于拖拽状态
offset: { x: 0, y: 0 }, // 记录拖拽偏移值
isEdit: true, // 是否可拖拽
isShowDeviceDetailDialog: false, // 平面内设备详情对话框是否可见
currentDev: null, // 当前点击设备
detailShowData: [], // 当前点击设备 详情表格数据
queryDeviceInfoInterval: null,
queryDeviceDetailInfoInterval: null,
};
},
created() {
this.getPlanList();
},
mounted() { },
destroyed() {
this.clearQueryDeviceInfoInterval();
this.clearQueryDeviceDetailInfoInterval();
},
methods: {
// 获取平面列表
getPlanList() {
getAllPlan().then(response => {
this.allPlanList = response.data;
console.log("平面列表:", this.allPlanList);
}).finally(() => { });
},
// 双击查看设备详情
checkDeviceDetail(node, event) {
// console.log(node, event);
/* !!!之后需在此获取该设备详情 */
this.currentDev = node;
// console.log("设备详情对话框 -> 当前选中设备:", this.currentDev);
getLatest5minDataByDevice(this.currentDev.id).then((res)=>{
this.detailShowData = res.data.reverse();
console.log("设备详情对话框 -> 当前设备近5分钟数据:", this.detailShowData);
})
this.queryDeviceDetailInfoInterval = window.setInterval(() => {
getLatestDataByDevice(this.currentDev.id).then((res)=>{
console.log("设备详情对话框 -> 最新数据(每2s获取一次):", res.data);
this.detailShowData.pop();
this.detailShowData.unshift(res.data);
})
}, 2000);
this.isShowDeviceDetailDialog = true;
},
// 关闭设备详情对话框
closeDeviceDetailDialog() {
this.clearQueryDeviceDetailInfoInterval();
},
// 保存平面信息
saveNodesInfos() {
//console.log("保存设备信息 接口调用前: ", this.deviceList);
updateBatchLocation(this.deviceList).then((res) => {
this.$modal.msgSuccess(`设备位置保存成功`);
})
},
// 显示/隐藏提示框
viewDevDetail(type) {
if (type == "show") {
this.currentPlanDevTips.forEach((tip) => {
tip[0].show(100);
})
} else {
this.currentPlanDevTips.forEach((tip) => {
tip[0].hide(100);
})
}
},
// 清除原有tips更新定时器
clearQueryDeviceInfoInterval() {
if (this.queryDeviceInfoInterval) {
// console.log("清除原有tips更新定时器");
window.clearInterval(this.queryDeviceInfoInterval);
}
},
// 清除原有设备详情table更新定时器
clearQueryDeviceDetailInfoInterval() {
if (this.queryDeviceDetailInfoInterval) {
// console.log("清除原有设备详情table更新定时器");
window.clearInterval(this.queryDeviceDetailInfoInterval);
}
},
// 下拉框选项改变时 更换选中平面
changeSelected(id) {
this.isShowDev = false;
this.currentPlanDevTips = [];
this.clearQueryDeviceInfoInterval();
this.clearQueryDeviceDetailInfoInterval();
this.allPlanList.forEach((item, index) => {
if (item.id == id) { // 下拉框选项绑定的value是id
this.isShowDev = true;
listDeviceByPlan(id).then(response => {
this.deviceList = response;
console.log("当前选中平面设备信息:", this.deviceList);
this.bgUrl = item.url; // 切换平面图
this.bfIndex = index;
// 设置设备详情悬浮框
this.$nextTick(() => {
this.deviceList.forEach((subItem, subIndex) => {
this.$set(this.deviceList[subIndex], 'absorbedDoseRate', subItem.absorbedDoseRate);
let tip = tippy(`#node${subIndex}`, {
content: this.createElementForTips({
"设备名称": subItem.name,
"吸收剂量率": `${subItem.absorbedDoseRate ? subItem.absorbedDoseRate : 0} μGy/h`
}), // 内容
showOnCreate: true, // 创建时即展示
arrow: true, // 箭头
delay: [100, 1000], // 延迟 ms [打开,消失]
duration: [275, 1000], // 动画持续 ms [打开,消失]
// theme: 'material',
// theme: 'light',
// background-color: #00a5db,
// color: yellow,
});
this.currentPlanDevTips.push(tip); // 将提示框存储
})
})
return id; // 平面id
}).then((id) => {
this.setPlanDetailTips(id);
}).finally(() => { });
}
})
},
// 设置提示框
setPlanDetailTips(planId) {
this.clearQueryDeviceInfoInterval();
this.clearQueryDeviceDetailInfoInterval();
this.setPlanDetailTipsFun(planId);
this.queryDeviceInfoInterval = setInterval(() => {
// console.log("更新设备信息");
this.setPlanDetailTipsFun(planId);
}, 10000); // 10s
},
// 设置提示框具体方法
setPlanDetailTipsFun(planId) {
getLatestDataByPlan(planId).then(response => {
let devicesRealTimeData = response.data;
console.log(`%c${this.getCurrentTime()} 更新当前选中平面具体信息:`, "color:red;", devicesRealTimeData);
// 更新设备详情悬浮框
this.$nextTick(() => {
devicesRealTimeData.forEach((item, index) => {
this.$set(this.deviceList[index], 'absorbedDoseRate', item.data ? item.data : 0);
this.$set(this.deviceList[index], 'name', item.name);
this.currentPlanDevTips[index][0].setContent(this.createElementForTips({
"设备名称": item.name,
/* !!!之后需更新此吸收剂量率数值 */
"吸收剂量率": `${item.data ? item.data : 0} μGy/h`
}));
})
console.log(`%c更新后的deviceList:`, "color:green;", this.deviceList);
})
})
},
// 生成设备详情悬浮框所需信息列表
createElementForTips(object) {
let arrKey = Object.keys(object);
let arrValue = Object.values(object);
// console.log("arrKey", arrKey);
// console.log("arrValue", arrValue);
let ul = document.createElement('ul');
ul.style.padding = "1px";
ul.style.margin = "1px";
ul.style.listStyleType = "none";
arrKey.forEach((item, index) => {
let li = document.createElement('li');
let text = document.createTextNode(`${arrKey[index]}: ${arrValue[index]}`);
li.appendChild(text);
li.style.padding = "1px";
ul.appendChild(li);
})
// console.log("ul", ul);
return ul;
},
// 点击节点 添加点状边框
clickNode(event) {
// 去除所有节点的node-focus类样式
let nodes = document.querySelectorAll(".node");
nodes.forEach((node) => {
node.classList.remove("node-focus");
});
// 添加或移除node-focus类样式
if (!event.currentTarget.classList.contains("node-focus")) {
event.currentTarget.classList.add("node-focus");
} else {
event.currentTarget.classList.remove("node-focus");
}
},
startDrag(index, event) {
if (this.isEdit) {
// console.log("startDrag");
this.clickNode(event);
this.dragging = true;
this.offset.x = event.pageX - this.deviceList[index].abscissa;
this.offset.y = event.pageY - this.deviceList[index].ordinate;
}
},
drag(index, event) {
if (this.isEdit) {
// console.log("drag");
event.preventDefault();
if (this.dragging) {
this.deviceList[index].abscissa = event.pageX - this.offset.x;
this.deviceList[index].ordinate = event.pageY - this.offset.y;
}
}
},
endDrag(index, event) {
if (this.isEdit) {
// // console.log("endDrag");
this.dragging = false;
}
},
prevent(event) {
event.preventDefault();
},
// 默认进入全屏
defaultSetting() {
let element = document.documentElement;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
// IE11
element.msRequestFullscreen();
}
},
// 切换全屏状态
switchSize() {
let element = document.documentElement;
if (this.fullscreen) {
// this.$message.success("退出全屏模式");
this.fullscreenTitle = "全屏";
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
} else {
// this.$message.success("进入全屏模式");
this.fullscreenTitle = "退出";
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
// IE11
element.msRequestFullscreen();
}
}
this.fullscreen = !this.fullscreen;
},
getCurrentTime() {
const date = new Date();
const year = date.getFullYear().toString();
const month = ('0' + (date.getMonth() + 1)).slice(-2);
const day = ('0' + date.getDate()).slice(-2);
const hour = ('0' + date.getHours()).slice(-2);
const minute = ('0' + date.getMinutes()).slice(-2);
const second = ('0' + date.getSeconds()).slice(-2);
return `${year}年${month}月${day}日 ${hour}时${minute}分${second}秒`;
},
},
};
</script>
<style lang="scss" scoped>
::v-deep.el-dialog {
z-index: 3000;
}
.container {
position: relative;
width: 100%;
height: 950px;
margin-top: 45px;
border-radius: 20px;
background-color: #dadada;
box-shadow: 5px 5px 2px #888888;
opacity: 1;
.bgImg {
position: absolute;
width: 100%;
height: 100%;
}
.buttons {
position: absolute;
margin-top: -40px;
margin-left: 20px;
}
}
.node {
position: absolute;
width: 50px;
height: 50px;
// background-color: silver;
border-radius: 15px;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
overflow: hidden;
opacity: 1;
// border: 2px solid #17a4e6;
border: 2px solid #18dcff;
}
.node-focus {
// border: 4px dotted #00ff00;
border: 4px dotted #18dcff;
}
// 红色告警
.alarmTypeRed {
animation: borderAnimationRed 1s infinite;
}
@keyframes borderAnimationRed {
0% {
border-color: red;
border-width: 4px;
transform: scale(120%);
}
50% {
border-color: red;
border-width: 2px;
transform: scale(100%);
}
100% {
border-color: red;
border-width: 4px;
transform: scale(120%);
}
}
// 橙色预警
.alarmTypeOrange {
animation: borderAnimationOrange 1s infinite;
}
@keyframes borderAnimationOrange {
0% {
border-color: rgb(219, 117, 33);
border-width: 3px;
transform: scale(110%);
}
50% {
border-color: rgb(219, 117, 33);
border-width: 2px;
transform: scale(100%);
}
100% {
border-color: rgb(219, 117, 33);
border-width: 3px;
transform: scale(110%);
}
}
// 黄绿色
.alarmTypeKelly {
border-color: #99ff66;
}
// 绿色
.alarmTypeGreen {
border-color: #00ff00;
}
::v-deep.el-dialog {
z-index: 3000;
}
.alarmTag{
margin-top: 5px;
width: 50px;
height: 24px;
border-radius: 5px;
}
::v-deep.deviceDetailInfoDialog{
.el-dialog{
.el-dialog__body{
padding: 0 20px;
.el-table__cell{
padding: 0;
}
}
}
}
</style>
2D实时告警平面