- 需求背景
- 解决效果
- 视频效果
- 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): ' ' }}</span><span class="household-item-number">{{ item.number }}</span></div><div v-else class="household-item offline"><span class="household-item-temperature">{{ ' ' }}</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>