vue3实现2d楼宇模型

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

需求背景

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

解决效果

vue3实现2d楼宇模型_第1张图片

视频效果

vue3实现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) 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>

你可能感兴趣的:(#,vue实践,vue.js,javascript,前端)