From 53eab8256b0703f5ab229d06933f3115a744e202 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Wed, 02 Apr 2025 15:39:38 +0800
Subject: [PATCH] feat: 机巢聚合80%

---
 src/assets/images/home/useUavHome/uavImg.png      |    0 
 src/views/Home/useUavHome/useUavHome.js           |  185 ++++++++++++++++++++++--------------
 src/assets/images/home/useUavHome/aggregation.png |    0 
 src/api/home/aggregation.js                       |    6 
 src/views/Home/useUavHome/MapPopUpBox.vue         |   75 +++++++++++----
 5 files changed, 171 insertions(+), 95 deletions(-)

diff --git a/src/api/home/aggregation.js b/src/api/home/aggregation.js
index eb33377..f68ed63 100644
--- a/src/api/home/aggregation.js
+++ b/src/api/home/aggregation.js
@@ -1,17 +1,19 @@
 import request from '@/axios';
 
 
-export const getDeviceRegionCount = () => {
+export const getDeviceRegionCount = (params) => {
   return request({
     url: '/drone-device-core/manage/api/v1/devices/getDeviceRegionCount',
     method: 'get',
+    params
   });
 };
 
 
-export const getDeviceRegion = () => {
+export const getDeviceRegion = (params) => {
   return request({
     url: '/drone-device-core/manage/api/v1/devices/getDeviceRegion',
     method: 'get',
+    params
   });
 };
diff --git a/src/assets/images/home/useUavHome/aggregation.png b/src/assets/images/home/useUavHome/aggregation.png
new file mode 100644
index 0000000..17be30d
--- /dev/null
+++ b/src/assets/images/home/useUavHome/aggregation.png
Binary files differ
diff --git a/src/assets/images/home/useUavHome/uavImg.png b/src/assets/images/home/useUavHome/uavImg.png
new file mode 100644
index 0000000..5b7dcf8
--- /dev/null
+++ b/src/assets/images/home/useUavHome/uavImg.png
Binary files differ
diff --git a/src/views/Home/useUavHome/MapPopUpBox.vue b/src/views/Home/useUavHome/MapPopUpBox.vue
index 6af857b..84afddb 100644
--- a/src/views/Home/useUavHome/MapPopUpBox.vue
+++ b/src/views/Home/useUavHome/MapPopUpBox.vue
@@ -3,41 +3,66 @@
     <div class="header">
       <div class="headerLeft">
         <img class="header-image" src="../../../assets/images/home/mapPopUpBox/titleImg.png" alt="" />
-        <div class="header-title">{{props.data.region_name}}</div>
+        <div class="header-title">{{ dataObj.region_name }}</div>
       </div>
       <el-icon class="header-close" @click="props.removeLabel">
         <Close />
       </el-icon>
     </div>
-    <div class="info">
-      <div class="info-item">
-        机巢数:
-        <div class="num">{{props.data.device_num}}</div>
+    <div class="loading" v-if="loading">加载中...</div>
+    <div class="content" v-else>
+      <div class="info">
+        <div class="info-item">
+          机巢数:
+          <div class="num">{{ dataObj.device_num }}</div>
+        </div>
+        <div class="info-item">
+          任务数:
+          <div class="num">{{ dataObj.jobNum }}</div>
+        </div>
       </div>
-      <div class="info-item">
-        任务数:
-        <div class="num">{{props.data.jobNum}}</div>
+      <div class="status-title">机巢状态</div>
+      <div class="status-list">
+        <div v-for="item in list" :key="item.name" class="status-item">
+          <div class="status-value" :style="{ color: item.color }">{{ item.value }}</div>
+          <div class="status-name">{{ item.name }}</div>
+        </div>
       </div>
     </div>
-    <div class="status-title">机巢状态</div>
-    <div class="status-list">
-      <div v-for="item in list" :key="item.name" class="status-item">
-        <div class="status-value" :style="{ color: item.color }">{{ item.value }}</div>
-        <div class="status-name">{{ item.name }}</div>
-      </div>
-    </div>
+
   </div>
 </template>
 <script setup>
 import { Close } from '@element-plus/icons-vue';
+import { getDeviceInfoNum } from '@/api/home/machineNest';
+import { getTotalJobNum } from '@/api/home';
 
 const props = defineProps(['data', 'removeLabel']);
-const list = [
-  { name: '空闲中', value: props.data.ex_num || 0, color: '#FFA768' },
-  { name: '作业', value:props.data.leisure_num || 0, color: '#8EFFAC' },
-  { name: '离线中', value: props.data.offline_num || 0, color: '#FFFFFF' },
-];
+const list = ref([
+  { name: '空闲中', value: 0, color: '#FFA768' },
+  { name: '作业', value: 0, color: '#8EFFAC' },
+  { name: '离线中', value: 0, color: '#FFFFFF' },
+]);
+const loading = ref(true);
 
+const dataObj = ref({
+  device_num: 0,
+  jobNum: 0,
+  region_name: '',
+});
+onMounted(async () => {
+  loading.value = true;
+  const areaCode = props.data.region_code;
+  dataObj.value.region_name = props.data.region_name;
+  const res = await getDeviceInfoNum({ areaCode });
+  const resJob = await getTotalJobNum({ areaCode });
+  dataObj.value.jobNum = resJob.data.data;
+  dataObj.value.device_num = res.data.data.device_num;
+  list.value[0].value = res.data.data.ex_num;
+  list.value[1].value = res.data.data.leisure_num;
+  list.value[2].value = res.data.data.offline_num;
+  loading.value = false;
+});
 </script>
 <style scoped lang="scss">
 .mapPopUpBox {
@@ -46,6 +71,16 @@
   background: url('@/assets/images/home/mapPopUpBox/mapPopUpBg.png') no-repeat center / 100% 100%;
   padding-left: 98px;
 
+  .loading{
+    display: flex;
+    height: calc(100% - 51px);
+    justify-content: center;
+    align-items: center;
+    font-size: 20px;
+    color: #ffffff;
+    background: rgb(24,50,101,.5);
+  }
+
   .header {
     padding: 0 12px;
     width: 320px;
diff --git a/src/views/Home/useUavHome/useUavHome.js b/src/views/Home/useUavHome/useUavHome.js
index 53a4123..655b589 100644
--- a/src/views/Home/useUavHome/useUavHome.js
+++ b/src/views/Home/useUavHome/useUavHome.js
@@ -1,6 +1,6 @@
 import * as Cesium from 'cesium';
-import data2 from '@/assets/images/home/homeRight/data2.png';
-import data1 from '@/assets/images/home/homeRight/data1.png';
+import aggregationImg from '@/assets/images/home/useUavHome/aggregation.png';
+import uavImg from '@/assets/images/home/useUavHome/uavImg.png';
 import MapPopUpBox from '@/views/Home/useUavHome/MapPopUpBox.vue';
 import { render } from 'vue';
 import { useStore } from 'vuex';
@@ -8,9 +8,7 @@
 import cesiumOperation from '@/utils/cesium-tsa';
 import { HeadingPitchRange } from 'cesium';
 import { getDeviceRegion, getDeviceRegionCount } from '@/api/home/aggregation';
-import _ from 'lodash';
-import { getDeviceInfoNum } from '@/api/home/machineNest';
-import { getTotalJobNum } from '@/api/home';
+
 
 /**
  * 机巢聚合功能
@@ -19,9 +17,9 @@
   const { flyTo } = cesiumOperation();
 
   let scalingJudgment = [
-    { name: '县', splashedList: [], gJson: null, show: false, value: [0, 48651], height: 31753 },
-    { name: '市', splashedList: [], gJson: null, show: false, value: [48651, 314863], height: 257731 },
-    { name: '省', splashedList: [], gJson: null, show: false, value: [314863, 3796280000], height: 1987280 },
+    { name: '县', splashedList: [], gJson: null, show: false,outline:{}, value: [0, 48651], height: 31753 },
+    { name: '市', splashedList: [], gJson: null, show: false,outline:{}, value: [48651, 314863], height: 257731 },
+    { name: '省', splashedList: [], gJson: null, show: false,outline:{}, value: [314863, 3796280000], height: 1987280 },
   ];
   let viewer = null;
   let active = null;
@@ -46,11 +44,8 @@
         active = item.name;
         removeEntities();
         removeLabel();
-        if (item.gJson && item.name !== '县') {
-          aggregation(item);
-        } else {
-          splashed(item);
-        }
+        item.gJson ?  aggregation(item) :  splashed(item)
+        renderOutline(item)
         break;
       }
     }
@@ -71,77 +66,94 @@
   const store = useStore();
   const selectedAreaCode = computed(() => store.state.user.selectedAreaCode);
 
-  function getDeviceCount() {
-    return getDeviceRegionCount().then(res => {
+  function getDeviceCount(areaCode) {
+    return getDeviceRegionCount({areaCode}).then(res => {
       return res?.data?.data || [];
     });
   }
 
-  function getDeviceList() {
-    return getDeviceRegion().then(res => {
+  function getDeviceList(areaCode) {
+    return getDeviceRegion({areaCode}).then(res => {
       return res?.data?.data || [];
     });
   }
+  const findFun = (featItem, numItem) => Number(featItem.region_code.slice(0, 6)) === numItem.properties.adcode;
+  const { VITE_APP_BASE } = import.meta.env;
+  const defaultDir = `${VITE_APP_BASE}public/geoJson/100000/`;
+
+  const getFiler = async (url) => {
+    const gJson = await import(/* @vite-ignore */ url);
+    return JSON.parse(JSON.stringify(gJson));
+  }
+  const getOutLine = async (jsonPathPre,hierarchy) => {
+    const parentGJson = await getFiler(`${defaultDir}${jsonPathPre}/index.json`)
+    let features = parentGJson.features.find(
+      item => item.properties.adcode === Number(hierarchy[hierarchy.length - 1])
+    );
+    return  { type: "FeatureCollection", features: [features] }
+  }
+
+  const injectData = (gJson,dataList) => {
+    return {
+      ...gJson,
+      features: gJson.features.map(item => ({ ...item, data: dataList.find(item1 => findFun(item1, item)) })),
+    }
+  }
+
+
 
   watch(
     selectedAreaCode,
     async (newValue, oldValue) => {
       if (newValue) {
-        const list = await getDeviceCount();
-        const splashedList = await getDeviceList();
+        const list = await getDeviceCount(newValue);
+        const splashedList = await getDeviceList(newValue);
         console.log(list);
         console.log(splashedList);
-        const code = newValue.slice(0, 6);
-        const hierarchy = convertToHierarchy(code);
+        const hierarchy = convertToHierarchy(newValue.slice(0, 6));
         const jsonPath = hierarchy.join('/');
-        const { VITE_APP_BASE } = import.meta.env;
-        const defaultDir = `${VITE_APP_BASE}public/geoJson/100000/`;
+        const jsonPathPre = hierarchy.slice(0, hierarchy.length - 1).join('/');
         scalingJudgment = scalingJudgment.map(item => ({ ...item, show: true }));
+        scalingJudgment[0].gJson = null;
+        scalingJudgment[0].splashedList = splashedList;
+        active = null
+        // 省
         if (hierarchy.length === 1) {
-          scalingJudgment[0].gJson = null;
-          scalingJudgment[0].splashedList = splashedList;
-          const gJson1 = await import(/* @vite-ignore */ `${defaultDir}${jsonPath}/indexDistrict.json`);
-          const gJson1Copy = JSON.parse(JSON.stringify(gJson1));
-          const gJson2 = await import(/* @vite-ignore */ `${defaultDir}${jsonPath}/index.json`);
-          const gJson2Copy = JSON.parse(JSON.stringify(gJson2));
-          const findFun = (item1, item) => Number(item1.region_code.slice(0, 6)) === item.properties.adcode;
+          const gJson1 = await getFiler(`${defaultDir}${jsonPath}/indexDistrict.json`)
+          const gJson2 = await getFiler(`${defaultDir}${jsonPath}/index.json`)
           scalingJudgment[1].gJson = {
-            ...gJson1Copy,
-            features: gJson1Copy.features.map(item => ({
+            ...gJson1,
+            features: gJson1.features.map(item => ({
               ...item,
               data: list.flatMap(item => item.childrens || []).find(item1 => findFun(item1, item)),
             })),
           };
-          scalingJudgment[2].gJson = {
-            ...gJson2Copy,
-            features: gJson2Copy.features.map(item => ({ ...item, data: list.find(item1 => findFun(item1, item)) })),
-          };
-          const { lng, lat } = getCenterPoint(gJson2Copy.features.map(item => item.properties.center));
+          scalingJudgment[2].gJson = injectData(gJson2,list);
+          const { lng, lat } = getCenterPoint(gJson2.features.map(item => item.properties.center));
           flyTo({ longitude: lng, latitude: lat }, 0, scalingJudgment[2].height);
         }
+        // 市
         if (hierarchy.length === 2) {
-          const gJson1 = await import(/* @vite-ignore */ `${defaultDir}${jsonPath}/index.json`);
-          scalingJudgment[0].gJson = null;
-          scalingJudgment[1].gJson = JSON.parse(JSON.stringify(gJson1));
           scalingJudgment[2].gJson = null;
           scalingJudgment[2].show = false;
-          const center = getCenterPoint(scalingJudgment[1].gJson.features.map(item => item.properties.center));
+          const gJson1 = await getFiler(`${defaultDir}${jsonPath}/index.json`)
+          scalingJudgment[1].gJson = injectData(gJson1,list);
+          const center = getCenterPoint(gJson1.features.map(item => item.properties.center));
           flyTo({ longitude: center.lng, latitude: center.lat }, 0, scalingJudgment[1].height);
         }
+        // 区县
         if (hierarchy.length === 3) {
-          const gJson = await import(/* @vite-ignore */ `${defaultDir}${jsonPath.slice(0, -7)}/index.json`);
-          const gJson1 = gJson.features.find(
-            item => item.properties.adcode === Number(hierarchy[hierarchy.length - 1])
-          );
-          // scalingJudgment[0].gJson = { type: "FeatureCollection", features: [gJson1] }
-          scalingJudgment[0].gJson = null;
           scalingJudgment[1].gJson = null;
           scalingJudgment[1].show = false;
           scalingJudgment[2].gJson = null;
           scalingJudgment[2].show = false;
-          const center = gJson1.properties.center;
+          const outlineGJson = await getOutLine(jsonPathPre,hierarchy)
+          const center = outlineGJson.features[0].properties.center;
           flyTo({ longitude: center[0], latitude: center[1] }, 0, scalingJudgment[0].height);
         }
+        // 轮廓
+        const outlineGJson = await getOutLine(jsonPathPre,hierarchy)
+        scalingJudgment.forEach(item => item.show &&(item.outline = outlineGJson))
       }
     },
     { immediate: true, deep: true }
@@ -155,7 +167,7 @@
         label: {
           // 随机整数
           text: item.nickname,
-          font: '14pt monospace',
+          font: '12pt Source Han Sans CN',
           fillColor: Cesium.Color.WHITE,
           outlineColor: Cesium.Color.BLACK,
           outlineWidth: 2,
@@ -164,7 +176,7 @@
           pixelOffset: new Cesium.Cartesian2(0, -9),
         },
         billboard: {
-          image: new Cesium.ConstantProperty(data1),
+          image: new Cesium.ConstantProperty(uavImg),
           width: 24,
           height: 24,
         },
@@ -176,6 +188,32 @@
       });
     });
   }
+
+  // 渲染轮廓
+  const renderOutline = (item) => {
+    item.outline && Cesium.GeoJsonDataSource.load(item.outline).then(dataSource => {
+      viewer.dataSources.add(dataSource);
+      const entities = dataSource.entities.values;
+      entities.forEach(entity => {
+        // 隐藏多边形填充
+        entity.polygon.material = Cesium.Color.TRANSPARENT;
+        entity.polygon.outline = false; // 关闭原生轮廓
+        // 创建独立折线作为轮廓
+        const positions = entity.polygon.hierarchy.getValue().positions;
+        viewer.entities.add({
+          polyline: {
+            positions: positions,
+            width: 5, // 直接设置宽度
+            material: new Cesium.PolylineGlowMaterialProperty({
+              glowPower: 0.5,
+              color: Cesium.Color.AQUA
+            }),
+            clampToGround: true // 贴地显示
+          }
+        });
+      });
+    });
+  };
 
   // 聚合机巢
   const aggregation = (item, flyTo) => {
@@ -191,23 +229,32 @@
       if (!feature.position) return;
       const position = Cesium.Cartesian3.fromDegrees(feature.position[0], feature.position[1]);
       viewer.entities.add({
-        id: feature.id,
         position: position,
         label: {
-          // 随机整数
-          text: feature.name + feature.data.total_device_count,
-          font: '14pt monospace',
+          text: feature.name,
+          font: '14pt Source Han Sans CN',
           fillColor: Cesium.Color.WHITE,
           outlineColor: Cesium.Color.BLACK,
           outlineWidth: 2,
           style: Cesium.LabelStyle.FILL_AND_OUTLINE,
           verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
           pixelOffset: new Cesium.Cartesian2(0, -9),
+        }
+      });
+      viewer.entities.add({
+        id: feature.id,
+        position: position,
+        label: {
+          text: feature.data.total_device_count,
+          font: '12pt Source Han Sans CN',
+          fillColor: Cesium.Color.BLACK,
+          style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+          eyeOffset: new Cesium.Cartesian3(0, 0, -10), // 让label "浮" 在广告牌前面
         },
         billboard: {
-          image: new Cesium.ConstantProperty(data2),
-          width: 24,
-          height: 24,
+          image: new Cesium.ConstantProperty(aggregationImg),
+          width: 35,
+          height: 35,
         },
         properties: {
           id: feature.id,
@@ -217,6 +264,7 @@
         },
       });
     });
+
     // 加载新的 GeoJSON 数据
     Cesium.GeoJsonDataSource.load(item.gJson).then(dataSource => {
       viewer.dataSources.add(dataSource);
@@ -228,7 +276,7 @@
           Cesium.Color.YELLOW.withAlpha(0) // 透明填充
         );
         entity.polygon.outline = new Cesium.ConstantProperty(true); // 显示边框
-        entity.polygon.outlineColor = new Cesium.ConstantProperty(Cesium.Color.YELLOW); // 黑色边框
+        entity.polygon.outlineColor = new Cesium.ConstantProperty(Cesium.Color.AQUAMARINE );
       });
       flyTo &&
         viewer.flyTo(dataSource, {
@@ -256,12 +304,10 @@
   };
 
   // 弹框位置刷新
-  const labelBox = () => {
+  const labelBoxRender = () => {
     let dom = document.querySelector('#mapPopUpBox');
     if (!dom) {
-      const data = { ...currentEntity.requestData, ...currentEntity.customData };
-      console.log(data);
-      dom = getLabelDom(data);
+      dom = getLabelDom(currentEntity.properties.customData._value.data);
     }
     const screenPosition = viewer.scene.cartesianToCanvasCoordinates(positionC3);
     if (screenPosition) {
@@ -283,18 +329,10 @@
       const customData = entity.properties.customData._value.data
       console.log(customData);
       if (customData.device_sn){
-        // todo
+        store.commit('setSingleUavHome', { id: '123' });
         return
       }
-      const areaCode = customData.region_code;
-      const res = await getDeviceInfoNum({ areaCode });
-      const resJob = await getTotalJobNum({ areaCode })
-      currentEntity = {
-        ...entity,
-        requestData: {...res.data.data,jobNum:resJob.data.data},
-        customData: entity.properties.customData._value.data
-      };
-      viewer.scene.postRender.addEventListener(labelBox);
+      viewer.scene.postRender.addEventListener(labelBoxRender);
     }
   };
 
@@ -312,7 +350,7 @@
   };
   // 移除弹框标签
   const removeLabel = () => {
-    viewer.scene.postRender.removeEventListener(labelBox);
+    viewer.scene.postRender.removeEventListener(labelBoxRender);
     removeDom();
   };
 
@@ -327,6 +365,7 @@
     active = null;
     handler = null;
     positionC3 = null;
+    currentEntity = null;
   };
   const init = () => {
     viewer = window.$viewer;

--
Gitblit v1.9.3