需要实线一个2d楼宇模型,并按照租户温度渲染颜色
vue3实现2d楼宇模型
<!--/**
* @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) return
renderFloor()
})
watch(() => props.allHouseData, () => {
if (!props.allHouseData) return
renderFloor()
})
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 = id
proxy.$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>