vue3实现2d楼宇模型

  • 需求背景
  • 解决效果
  • 视频效果
  • 2dFloor.vue

需求背景

需要实线一个2d楼宇模型,并按照租户温度渲染颜色

解决效果

在这里插入图片描述

视频效果

2dFloor.vue

<!--/*** @author: liuk* @date: 2023/12/06* @describe: 2d楼宇模型* @CSDN:https://blog.csdn.net/hr_beginner?type=blog*/-->
<template><div class="building-floor-container-bg"></div><div class="building-floor-container"><div class="building-floor-left"><div class="floor-container" ref="floorDiv"><div class="floor-item" v-for="(item, index) in floorList" :key="item.label":style="{height: index === floorList.length - 1 ? '235px' : '297px'}"><span class="floor-item-content">{{ item.label }}</span></div></div></div><div class="building-floor-center"><div class="building-floor-center-unit"><div class="unit-container" ref="unitDiv"><span class="unit-title-item" v-for="(_,index) in householdData" :key="index":style="{width:`${infoModel.building_no_info.maximum *60}px`}">{{ index + 1 }}单元</span></div></div><div class="building-floor-center-household" :style="{ cursor: isDragging ? 'move' : 'default' }"ref="containerDiv"><div class="household-container" ref="householdDiv"><div class="unit-item" v-for="(unitnums,index1) in householdData" :key="index1":style="{width:`${infoModel.building_no_info.maximum * 60}px`}"><div class="floor-item" v-for="(floors,index2) in unitnums" :key="index2"><div class="household-item-box" v-for="item in floors" :key="item.number"@click.stop="clickBox(item.household_id,$event)"><el-tooltip effect="custom-tip-content" placement="bottom-start" :offset="3" :disabled="isDragging":hide-after="0" trigger="hover" :show-arrow="false"><template #content><div style="min-width: 201px;height: 125px;background: #262626 !important;border: 1px solid rgba(84, 84, 84, 1);box-shadow: -10px 0px 22px 0px rgba(0, 0, 0, 0.22);border-radius: 4px;padding: 20px;user-select: none;" v-if="item.household_id"><span style="font-size: 16px">{{props.props.community}}-{{ props.props.building_no }}-{{ item.unitnum }}单元-{{ item.number }}</span><div style="display: flex;justify-content: space-between;margin: 20px 0 30px;"><div><div><span style="font-size: 20px">{{ formatToFixed(item.tt401_value) }}</span><span style="color: #a5a6a6; margin-left: 8px"></span></div><span style="color: #a5a6a6">当前室温</span></div><div><div><span style="font-size: 20px">{{ formatToFixed(item.tt401_value_24hours) }}</span><span style="color: #a5a6a6; margin-left: 8px"></span></div><span style="color: #a5a6a6">24小时住户均温</span></div></div></div></template><div class="household-item" v-if="item.status!=='offline'":style="{background: getColorByTemperature(item.status,item.temperature),opacity:item.isOpacity?'0.3':'1'}"><span class="household-item-temperature">{{item.status !== 'enabled' ? formatToFixed(item.temperature ,1): '&nbsp;' }}</span><span class="household-item-number">{{ item.number }}</span></div><div v-else class="household-item offline"><span class="household-item-temperature">{{ '&nbsp;' }}</span><span class="household-item-number">{{ item.number }}</span></div></el-tooltip></div></div></div></div><div class="household-mask":style="{boxShadow:'inset 0px 10px 10px 10px rgba(16, 16, 16, 0.8)'}"></div></div></div><div class="pagination-bottom"><div class="btn-pagination-bottom" @click="onClickNext"><el-icon size="20" color="#A5A6A6"><ArrowLeft/></el-icon></div><div class="btn-pagination-bottom" @click="onClickPre"><el-icon size="20" color="#A5A6A6"><ArrowRight/></el-icon></div></div><div class="pagination-right"><div class="btn-pagination-right" @click="onClickUp"><el-icon size="20" color="#A5A6A6"><ArrowUp/></el-icon></div><div class="btn-pagination-right" @click="onClickDown"><el-icon size="20" color="#A5A6A6"><ArrowDown/></el-icon></div></div></div>
</template><script lang="ts" setup>
import {reactive, ref, toRefs, watch, getCurrentInstance, onMounted, nextTick} from "vue";
import useModelInfo from "../index"
import {formatToFixed} from "@/utils/dictionary";const infoModel = useModelInfo()
// REfs
const householdDiv = ref(null)
const containerDiv = ref(null)
const unitDiv = ref(null)
const floorDiv = ref(null)// Props
const props = defineProps(['props', 'allHouseData'])
const {proxy} = getCurrentInstance()
const model = reactive({curItem: {number: '',temperature: ''},preItem: {number: '',temperature: ''},householdData: [],isDragging: false,proBoxId: null,//缓存上一个格子对象floorList: [],curCeill: 0,
})const {curItem, isDragging, householdData, floorList, curCeill} = toRefs(model)onMounted(() => {if (!props.allHouseData) returnrenderFloor()
})watch(() => props.allHouseData, () => {if (!props.allHouseData) returnrenderFloor()
})watch(() => infoModel.household_id, (id) => {if (!id) {infoModel.household_id = ''model.householdData.forEach(unitnums => {unitnums.forEach(floors => {floors.forEach(item => item.isOpacity = false)})})proxy.$forceUpdate()} else {model.householdData.forEach(unitnums => {unitnums.forEach(floors => {floors.forEach(item => item.isOpacity = (id === item.household_id ? false : true))})})model.proBoxId = idproxy.$forceUpdate()}
})const renderFloor = () => { // 渲染楼栋const {floor, maximum, unitnum} = infoModel.building_no_info // {floor:"6",maximum:"3",unitnum:"5"}// 楼层 最大户数 单元数model.householdData = new Array(+unitnum).fill([]).map((_, i1) =>new Array(+floor).fill([]).map((_, i2) =>new Array(+maximum).fill({}).map((_, i3) => {const id = `${String(i1 + 1).padStart(2, '0')}_${String(i2 + 1).padStart(2, '0')}${String(i3 + 1).padStart(2, '0')}`const curData = props.allHouseData.find(item => item.household_id.slice(-9,-2) === id) || {}return {id,unitnum: i1 + 1,number: (i2 + 1) + '' + String(i3 + 1).padStart(2, '0'),isOpacity: false,temperature:15 +Math.random()*11,status:'',// temperature: +curData.tt401_value || 0,// status: curData.household_id ? curData.tt401_value ? '' : 'enabled' : 'offline',...curData}})).reverse())model.floorList = new Array(Math.floor(+floor / 5) + 1).fill([]).map((_, i) => ({label: i ? i * 5 + 'F' : '1F',})).reverse()nextTick(() => {doMouseFn()})
}// 点击房间
const clickBox = (id) => {if (model.proBoxId === id) {infoModel.household_id = null} else {infoModel.household_id = id}
}// 住户根据室温渲染单元格样式
const getColorByTemperature = (status: string, temperature: number) => {switch (true) {case status === 'enabled':return '#565656';case temperature > 26:return '#bd0000'case temperature >= 24 && temperature <= 26:return '#e76200'case temperature >= 22 && temperature < 24:return '#eb7926'case temperature >= 20 && temperature < 22:return '#ee914c'case temperature >= 18 && temperature < 20:return '#f2a872'case temperature < 18:return '#2692ff'default:return 'transparent'}
};// 楼层单元平移事件
const doMouseFn = () => {let offsetX: number, offsetY: number;// 水平方向最大偏移量const MaxOffsetWidth = householdDiv.value.getBoundingClientRect().width - containerDiv.value.getBoundingClientRect().width - 10;// 竖直方向最大偏移量const MaxOffsetHeight = householdDiv.value.getBoundingClientRect().height - containerDiv.value.getBoundingClientRect().height;containerDiv.value.addEventListener('mousedown', (event: any) => {isDragging.value = true;offsetX = event.clientX - householdDiv.value.offsetLeft;offsetY = event.clientY - householdDiv.value.offsetTop;});containerDiv.value.addEventListener('mousemove', (event: any) => {if (isDragging.value) {let x = event.clientX - offsetX;let y = event.clientY - offsetY;if (Math.abs(x) > MaxOffsetWidth) {x = -MaxOffsetWidth;} else if (x >= 0) {x = 0;}if (Math.abs(y) > MaxOffsetHeight) {y = -MaxOffsetHeight;} else if (y >= 0) {y = 0;}const bottom = y === -MaxOffsetHeight ? 0 : -(MaxOffsetHeight - Math.abs(y));householdDiv.value.style.left = x + 'px';householdDiv.value.style.bottom = bottom + 'px';unitDiv.value.style.left = x + 'px';floorDiv.value.style.bottom = 15 + bottom + 'px';}});containerDiv.value.addEventListener('mouseup', () => {isDragging.value = false;});document.addEventListener('mouseup', () => {isDragging.value = false;});
}
// 左移按钮响应事件
const onClickPre = () => {const offsetLeft = householdDiv.value.offsetLeft;// 水平方向最大偏移量const MaxOffsetWidth = householdDiv.value.getBoundingClientRect().width - containerDiv.value.getBoundingClientRect().width - 10;if (householdDiv.value) {if (Math.abs(offsetLeft) <= MaxOffsetWidth && offsetLeft <= 0) {const left = Math.abs(offsetLeft - 60) > MaxOffsetWidth ? -MaxOffsetWidth : offsetLeft - 60;householdDiv.value.style.left = left + 'px';unitDiv.value.style.left = left + 'px';}}
};// 右移按钮响应事件
const onClickNext = () => {const offsetLeft = householdDiv.value.offsetLeft;if (householdDiv.value) {if (offsetLeft < 0) {const left = offsetLeft + 60 >= 0 ? 0 : offsetLeft + 60;householdDiv.value.style.left = left + 'px';unitDiv.value.style.left = left + 'px';}}
};// 上移按钮响应事件
const onClickUp = () => {const offsetTop = householdDiv.value.offsetTop;// 竖直方向最大偏移量const MaxOffsetHeight = householdDiv.value.getBoundingClientRect().height - containerDiv.value.getBoundingClientRect().height;if (householdDiv.value) {if (offsetTop < 0) {const top = offsetTop + 60 >= 0 ? 0 : offsetTop + 60;const bottom =top === 0 ? -MaxOffsetHeight : -(MaxOffsetHeight - Math.abs(top));householdDiv.value.style.bottom = bottom + 'px';floorDiv.value.style.bottom = 15 + bottom + 'px';}}
};// 下移按钮响应事件
const onClickDown = () => {const offsetTop = householdDiv.value.offsetTop;// 竖直方向最大偏移量const MaxOffsetHeight = householdDiv.value.getBoundingClientRect().height - containerDiv.value.getBoundingClientRect().height;if (householdDiv.value) {if (Math.abs(offsetTop) <= MaxOffsetHeight && offsetTop <= 0) {const top = Math.abs(offsetTop - 60) > MaxOffsetHeight ? -MaxOffsetHeight : offsetTop - 60;const bottom = top === -MaxOffsetHeight ? 0 : -(MaxOffsetHeight - Math.abs(top));householdDiv.value.style.bottom = bottom + 'px';floorDiv.value.style.bottom = 15 + bottom + 'px';}}
};
</script><style lang="scss" scoped>
.building-floor-container-bg {position: absolute;bottom: 7vh;left: 295px;width: 932px;height: 114px;background: url("@/assets/heatMap/dd6bj.svg") no-repeat center/932px 114px;//background: red;}.building-floor-container {position: absolute;width: 800px; //821px;height: 780px; //775px;// top: 96px;bottom: 10vh;left: 359px;background: url("@/assets/heatMap/zz.svg") no-repeat center;background-size: 105% 102%;.building-floor-center {display: flex;flex-direction: column;position: absolute;top: 0;left: 50%;transform: translateX(-50%);width: 85%;height: 93%;padding: 0 20px 20px;.building-floor-center-unit {position: relative;width: 100%;height: 10%;overflow: hidden;user-select: none;.unit-container {display: flex;align-items: center;position: absolute;left: 0;top: 10px;.unit-title-item {margin-right: 10px;font-size: 16px;text-align: center;}}}.building-floor-center-household {position: relative;width: 100%;height: 90%;overflow: hidden;-moz-user-select: none; /*火狐*/-webkit-user-select: none; /*webkit浏览器*/-ms-user-select: none; /*IE10*/-khtml-user-select: none; /*早期浏览器*/user-select: none;.household-container {display: flex;position: absolute;left: 0;bottom: 0;.unit-item {display: flex;flex-wrap: wrap;margin-right: 10px;.floor-item {display: flex;width: 100%;height: 60px;}.household-item-box {width: 60px;height: 60px;padding: 4px;.household-item {position: relative;width: 100%;height: 100%;display: flex;flex-direction: column;align-items: center;border-radius: 2px;padding: 4px;cursor: pointer;&.offline::after {content: "";position: absolute;top: 0;left: 0;bottom: 0;right: 0;background: url('@/assets/icons/DW.svg') no-repeat center/cover;opacity: 0.5;}&:hover {outline: 1px solid #fff;}.household-item-temperature {font-size: 16px;}.household-item-number {font-size: 12px;}}}}}.household-mask {width: 100%;height: 100%;position: absolute;top: 0;left: 0;pointer-events: none;}}}.building-floor-left {position: absolute;top: 0px;left: -30px;width: 60px;height: 90%;overflow: hidden;user-select: none;.floor-container {position: absolute;bottom: 15px;left: 0;width: 100%;.floor-item {width: 100%;display: flex;align-items: flex-end;.floor-item-content {display: flex;justify-content: center;background: rgba(255, 255, 255, 0.06);border-radius: 20px;color: #a5a6a6;padding: 2px 12px;}}}}.pagination-bottom {display: flex;align-items: center;position: absolute;bottom: 0px;left: 50%;transform: translateX(-50%);.btn-pagination-bottom {display: flex;justify-content: center;align-items: center;width: 32px;height: 32px;border-radius: 50%;border: 0.5px solid #a5a6a6;margin: 0 12px;cursor: pointer;}}.pagination-right {display: flex;flex-direction: column;align-items: center;position: absolute;right: -28px;top: 50%;transform: translateY(-50%);.btn-pagination-right {display: flex;justify-content: center;align-items: center;width: 32px;height: 32px;border-radius: 50%;border: 0.5px solid #a5a6a6;margin: 12px 0;cursor: pointer;}}
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/202492.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

7+衰老+分型+预后模型生信思路,没思路的同学们可参考

今天给同学们分享一篇生信文章“Identification and validation of a novel senescence-related biomarker for thyroid cancer to predict the prognosis and immunotherapy”&#xff0c;这篇文章发表在Front Immunol期刊上&#xff0c;影响因子为7.3。 结果解读&#xff1a;…

用100ask 6ull配合 飞凌 elf1的教程进行学习的记录 - ap3216

100ask板子 不用改 ap3216.c "ap3216creg.h" 添加到drivers/misc 从这抄的: https://gitee.com/flameboyence/linux_driver_example/tree/master/22_i2c #include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #includ…

Qt/C++音视频开发57-切换音视频轨道/切换节目流/分别切换音频视频轨道

一、前言 对各种音视频文件格式的支持&#xff0c;是一个播放器的基础功能。一般的音视频文件只有1路流&#xff0c;比如音频文件只有1路音频流&#xff0c;视频文件只有1路音频1路视频流&#xff0c;实践过程中发现&#xff0c;还有一种ts格式的文件&#xff0c;可能有多路流…

批量免费AI写作工具,批量免费AI写作软件

人工智能&#xff08;AI&#xff09;的应用在各个领域不断创新。面对繁重的写作任务,我们应该怎么完成&#xff1f;本文将专心分享批量免费AI写作的方法、工具以及选择时需要注意的事项。 批量免费AI写作的方法 利用开源AI模型 一种常见的批量免费AI写作方法是利用开源的AI模…

2024年十大最好猫罐头有哪些?2024年10款最好的猫罐头盘点

我发现不少人有这样的困扰&#xff01;买到各种数值都很好的猫罐头后&#xff0c;猫咪一点都不吃。或者是猫咪吃了猫罐头之后&#xff0c;吃了一段时间后就软便身体不舒服。 通过本文&#xff0c;我将与大家盘点2024年10款最好的猫罐头&#xff0c;并提供一些选购猫罐头的小妙招…

第三方组件自定义扫描规则

第三方例如dubbo自定义扫描组件规则方式注入进容器。例如DubboService注解的类注入进容器中&#xff0c;实现ImportBeanDefinitionRegistrar接口&#xff0c;并通过Import注解注入。 Import除了注入ImportBeanDefinitionRegistrar类&#xff0c;还可以注入配置类Configuration和…

虚拟数字人直播软件,是如何提升直播带货效率的?

近年来&#xff0c;随着直播带货的兴起&#xff0c;虚拟数字人直播软件成为了一个备受瞩目的新兴领域。无人直播带货通过虚拟数字人主持直播和推销产品&#xff0c;为商家带来了全新的营销方式。那么&#xff0c;虚拟数字人直播软件是如何进行无人直播带货的&#xff0c;有哪些…

Linux入门指南:Linux环境变量解析

Linux环境变量解析 前言基本概念和作用常见的环境变量设置环境变量Shell脚本和环境变量环境变量的一些小规则 前言 你是不是经常听说Linux环境变量&#xff0c;但又对它一头雾水&#xff1f;别着急&#xff0c;让我们一起来揭开这个神秘的面纱&#xff0c;探索一番吧&#xff…

【网络安全技术】IPsec——AH和ESP

一、IPsec通信 主要是两个协议&#xff0c;认证头AH&#xff08;Authentication Header&#xff09;和封装安全载荷ESP&#xff08;Encapsulate Security Payload&#xff09;。AH提供了认证&#xff08;integrity&#xff0c;抗否认&#xff0c;抗重放&#xff09;&#xff0c…

Python中字符串列表的相互转换详解

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python编程中&#xff0c;经常会遇到需要将字符串列表相互转换的情况。这涉及到将逗号分隔的字符串转换为列表&#xff0c;或者将列表中的元素连接成一个字符串。本文将深入讨论这些情景&#xff0c;并提供丰富…

大模型应用设计的10个思考

技术不是万能的&#xff0c;但没有技术却可能是万万不能的&#xff0c;对于大模型可能也是如此。基于大模型的应用设计需要聚焦于所解决的问题&#xff0c;在自然语言处理领域&#xff0c;大模型本身在一定程度上只是将各种NLP任务统一成了sequence 到 sequence 的模型。利用大…

SAP MM 中的业务伙伴确定配置

这篇博客文章将概述 SAP MM 供应商帐户组中的合作伙伴确定是什么以及如何在 S/4 系统中配置它。 本文将指导您完成分步过程&#xff0c;并为您提供有关在供应商主数据中使用合作伙伴确定的完整想法。 合作伙伴角色 供应商在 SAP 中扮演着不同类型的角色&#xff0c;让我们通…

springboot——自动装配

自动装配 Condition: Condition内置方法&#xff1a;boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)&#xff0c;返回值为布尔型 重写matches方法的类&#xff1a;SpringBootCondition等 SpringBootCondition&#xff1a;springboot自带的实现类…

NodeJS安装

前言&#xff1a; 因为java行业不景气&#xff0c;人才严重过剩&#xff0c;像我我这样的人&#xff0c;只能选择往广度走走&#xff0c;开始学Vue3. Vue3现在默认要NodeJs版本16及以上&#xff0c;所以我这里安装了18.18.0的&#xff0c;目前逛网最新稳定版本是20版本的。 1…

表单修改时取消disabled snippet

前言 有时候开发&#xff0c;表单编辑时有些字段不可更改&#xff0c;比如用户管理的用户名&#xff0c;修改时不可编辑。但是有时候就会有这么一种情况&#xff0c;希望他修改时也可编辑。所以就可以在浏览器–控制台里面写一个snippet&#xff0c;修改时运行。 当然&#xf…

【投稿优惠|检索稳定】2023年信息系统和工程与数字化经济国际会议(ICISEDE 2023)

2024年信息系统和工程与数字化经济国际会议(ICISEDE 2024) 2024 International Conference on Information Systems and Engineering and the Digital Economy(ICISEDE 2024) [会议简介] 2024 年信息系统和工程与数字化经济国际会议(ICISEDE 2024)将于 2024 年 1 月 20 日在厦门…

小型洗衣机什么牌子好又便宜?性价比内衣洗衣机推荐

在内衣洗衣机越来越受欢迎的今天&#xff0c;不少朋友都在犹豫要不要买一台内衣洗衣机&#xff0c;专门来清洗一些贴身衣物&#xff0c;这个问题的答案是很有必要的&#xff0c;因为目前市场上的大型洗衣机只是起到了清洁的作用&#xff0c;并不能有效地清除我们的贴身衣服上细…

Java LeetCode篇-深入了解二叉树经典解法(三种方式实现:获取二叉树的最大深度)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 对称二叉树 1.1 判断对称二叉树实现思路 1.2 代码实现&#xff1a;判断对称二叉树 2.0 二叉树的最大深度 2.1 使用递归实现获取二叉树的最大深度思路 2.2 代码实…

etcd 与 Consul 的一致性读对比

本文分享和对比了 etcd 和 Consul 这两个存储的一致性读的实现。 作者&#xff1a;戴岳兵&#xff0c;爱可生研发中心工程师&#xff0c;负责项目的需求开发与维护工作。 爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。 本…

Python实现FA萤火虫优化算法优化LightGBM回归模型(LGBMRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 萤火虫算法&#xff08;Fire-fly algorithm&#xff0c;FA&#xff09;由剑桥大学Yang于2009年提出 , …