vue3实现2d楼宇模型

news2025/1/21 10:13:40

  • 需求背景
  • 解决效果
  • 视频效果
  • 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) 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>

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

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

相关文章

TA-Lib学习研究笔记(九)——Pattern Recognition (5)

TA-Lib学习研究笔记&#xff08;九&#xff09;——Pattern Recognition &#xff08;5&#xff09; 最全面的形态识别的函数的应用&#xff0c;通过使用A股实际的数据&#xff0c;验证形态识别函数&#xff0c;用K线显示出现标志的形态走势&#xff0c;由于入口参数基本上是o…

Element-UI定制化Tree 树形控件

1.复制 说明&#xff1a;复制Tree树形控件。 <script> export default {data() {return {data: [{label: 一级 1,children: [{label: 二级 1-1,children: [{label: 三级 1-1-1}]}]}, {label: 一级 2,children: [{label: 二级 2-1,children: [{label: 三级 2-1-1}]}, {l…

1-3、Java反编译

语雀原文链接 文章目录 1、JD-GUI反编译下载1-1、打开class文件无反应 1、JD-GUI反编译下载 http://java-decompiler.github.io jd-gui-windows-1.6.6.zip 1-1、打开class文件无反应 目前是可以正常打jar包文件&#xff0c;但是在直接打开.class文件时软件会卡住。首先将要…

谷歌发布大模型Gemini,赶超GPT4

迄今为止规模最大&#xff0c;能力最强的谷歌大模型来了。当地时间 12 月 6 日&#xff0c;谷歌 CEO 桑达尔・皮查伊官宣 Gemini 1.0 版正式上线。 这次发布的 Gemini 大模型是原生多模态大模型&#xff0c;是谷歌大模型新时代的第一步&#xff0c;它包括三种量级&#xff1a;…

打破常规思维:Scrapy处理豆瓣视频下载的方式

概述 Scrapy是一个强大的Python爬虫框架&#xff0c;它可以帮助我们快速地开发和部署各种类型的爬虫项目。Scrapy提供了许多方便的功能&#xff0c;例如请求调度、数据提取、数据存储、中间件、管道、信号等&#xff0c;让我们可以专注于业务逻辑&#xff0c;而不用担心底层的…

[报错]记录IDEA远程开发报错:java: Cannot run program.....

报错内容 IDEA在进行远程开发的时候报错&#xff0c;内容如下&#xff1a; java: Cannot run program "/usr/lib/jvm/java-1.8.0-openjdk-amd64/bin/java" (in directory "/home/jim/.cache/JetBrains/RemoteDev-IU/_home_jim_DevCodes_Github_zfile/compile-…

【QED】不想被排除

目录 题目描述输入格式输出格式测试样例温馨提示 思路核心代码 题目描述 给出一个数 n n n 以及 x x x , y y y , z z z &#xff0c;求 1 1 1 到 n n n 中&#xff0c;有多少个数不是 x x x, y y y, z z z 中任意一个数的倍数。 输入格式 第一行输入一个整数 T T…

力扣37. 解数独(java回溯解法)

Problem: 37. 解数独 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 该题可以使用回溯来模拟穷举。回溯问题通常涉及到可选列表&#xff0c;决策阶段&#xff0c;决策路径&#xff0c;而对于本题目我们选择将棋盘的每一个格子作为决策阶段&#xff0c;为此我们应该解…

OrangePi ZERO2 刷机与启动

镜像准备 用读卡器和Win32Diskimager刷写镜像到内存卡&#xff0c;镜像文件见下面百度云链接&#xff1a;https://pan.baidu.com/s/14aKTznc4Jvw4SoFF54JUTg 提取码&#xff1a;1815 刷写完毕后插回香橙派 串口登录 用MobaXterm和USB-TTL进行串口登录&#xff0c;MobaXterm软…

【C】⽂件操作

1. 为什么使⽤⽂件&#xff1f; 如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失了&#xff0c;等再次运⾏程序&#xff0c;是看不到上次程序的数据的&#xff0c;如果要将数据进⾏持久化…

【文件上传系列】No.1 大文件分片、进度图展示(原生前端 + Node 后端 Koa)

分片&#xff08;500MB&#xff09;进度效果展示 效果展示&#xff0c;一个分片是 500MB 的 分片&#xff08;10MB&#xff09;进度效果展示 大文件分片上传效果展示 前端 思路 前端的思路&#xff1a;将大文件切分成多个小文件&#xff0c;然后并发给后端。 页面构建 先在页…

将RK3399的挖掘机开发板在Android10下设置系统默认为24小时制

将RK3399的挖掘机开发板在Android10下设置系统默认为24小时制 2023/12/9 22:07 应该也可以适用于RK3399的Android12系统 --- a/frameworks/base/packages/SettingsProvider/res/values/defaults.xml b/frameworks/base/packages/SettingsProvider/res/values/defaults.xml -2…

智能优化算法应用:基于静电放电算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于静电放电算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于静电放电算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.静电放电算法4.实验参数设定5.算法结果6.参考…

2023年9月13日 Go生态洞察:WASI支持在Go中的实现

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

C++新经典模板与泛型编程:策略类模板

策略类模板 在前面的博文中&#xff0c;策略类SumPolicy和MinPolicy都是普通的类&#xff0c;其中包含的是一个静态成员函数模板algorithm()&#xff0c;该函数模板包含两个类型模板参数。其实&#xff0c;也可以把SumPolicy和MinPolicy类写成类模板—直接把algorithm()中的两…

基于Lucene的全文检索系统的实现与应用

文章目录 一、概念二、引入案例1、数据库搜索2、数据分类3、非结构化数据查询方法1&#xff09; 顺序扫描法(Serial Scanning)2&#xff09;全文检索(Full-text Search) 4、如何实现全文检索 三、Lucene实现全文检索的流程1、索引和搜索流程图2、创建索引1&#xff09;获取原始…

案例057:基于微信小程序的马拉松报名系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

教师需要什么技能?

作为一名老师&#xff0c;需要掌握许多技能&#xff0c;以便能够成功地教育和指导学生。以下是一些关键技能&#xff1a; 1.教学技能&#xff1a;老师需要有深入的学科知识和教学经验&#xff0c;以便能够有效地传授知识。教师应该了解如何设计和执行教学计划&#xff0c;制定课…

点云 ros PointCloud2格式与livox CustomMsg格式介绍

点云 ros PointCloud2格式与livox CustomMsg格式介绍 PointCloud2 点云格式livox CustomMsg 点云格式 PointCloud2 点云格式 PointCloud2 是ros的一种点云格式 具体官方数据 http://docs.ros.org/en/jade/api/sensor_msgs/html/msg/PointCloud2.html std_msgs/Header header…

基于JavaWeb+SSM+Vue居住证申报系统小程序的设计和实现

基于JavaWebSSMVue居住证申报系统小程序的设计和实现 源码获取入口KaiTi 报告Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 KaiTi 报告 1.1题目背景 随着时代的发展&#xff0c;人口流动越来越频繁&#xff0…