forked from drone/command-center-dashboard

罗广辉
2025-04-02 53eab8256b0703f5ab229d06933f3115a744e202
feat: 机巢聚合80%
3 files modified
2 files added
266 ■■■■■ changed files
src/api/home/aggregation.js 6 ●●●●● patch | view | raw | blame | history
src/assets/images/home/useUavHome/aggregation.png patch | view | raw | blame | history
src/assets/images/home/useUavHome/uavImg.png patch | view | raw | blame | history
src/views/Home/useUavHome/MapPopUpBox.vue 75 ●●●● patch | view | raw | blame | history
src/views/Home/useUavHome/useUavHome.js 185 ●●●●● patch | view | raw | blame | history
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
  });
};
src/assets/images/home/useUavHome/aggregation.png
src/assets/images/home/useUavHome/uavImg.png
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;
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;