2D平面,实时查看设备报警信息,可拖拽摆放设备在平面的位置

背景描述:系统需要监测每个平面的每个设备的告警信息(如本案例,监测辐射值),如果出现告警,则平面图上的设备要出现告警动画(本案例会有红色扩散波边框),双击设备能显示实时数据,鼠标悬浮设备上能显示提示框。设备默认出现在左上角,左边(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实时告警平面

相关推荐
前端Hardy7 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891110 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
天天进步20153 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
疯狂的沙粒3 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪3 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背4 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M4 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc4 小时前
学习electron
javascript·学习·electron