forked from drone/command-center-dashboard

罗广辉
2025-04-02 de61feb06c164238302da048c2f05789911e1d17
feat: 事件聚合80%
6 files modified
1 files renamed
378 ■■■■■ changed files
src/api/home/aggregation.js 21 ●●●●● patch | view | raw | blame | history
src/views/Home/Footer.vue 10 ●●●● patch | view | raw | blame | history
src/views/Home/Home.vue 2 ●●● patch | view | raw | blame | history
src/views/Home/useEventOperate/EventPopUpBox.vue 55 ●●●●● patch | view | raw | blame | history
src/views/Home/useEventOperate/useEventOperate.js 1 ●●●● patch | view | raw | blame | history
src/views/Home/useUavHome/DevicePopUpBox.vue patch | view | raw | blame | history
src/views/Home/useUavHome/useUavHome.js 289 ●●●●● patch | view | raw | blame | history
src/api/home/aggregation.js
@@ -1,6 +1,6 @@
import request from '@/axios';
// 机巢聚合
export const getDeviceRegionCount = (params) => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/getDeviceRegionCount',
@@ -9,7 +9,7 @@
  });
};
// 机巢散点
export const getDeviceRegion = (params) => {
  return request({
    url: '/drone-device-core/manage/api/v1/devices/getDeviceRegion',
@@ -17,3 +17,20 @@
    params
  });
};
// 事件聚合 和 事件散点
export const getMapEvents = (data) => {
  return request({
    url: '/drone-device-core/jobEvent/mapEvents',
    method: 'post',
    data
  });
};
// 事件散点详情
export const getEventDetails = (params) => {
  return request({
    url: '/drone-device-core/jobEvent/eventDetails',
    method: 'post',
    params
  });
};
src/views/Home/Footer.vue
@@ -29,9 +29,9 @@
import { useEventOperate } from '@/views/Home/useEventOperate/useEventOperate';
// 机巢聚合
const { init, removeAll } = useUavHome();
const { init, removeAll } = useUavHome('device');
// 事件聚合
const { init: eventInit, removeAll: eventRemove } = useEventOperate();
const { init: eventInit, removeAll: eventRemove } = useUavHome('event');
const list = ref([
  {
@@ -61,16 +61,16 @@
  const fromItem = list.value.find(item => item.active);
  if (fromItem.name === toItem.name) return;
  fromItem?.removeAll?.();
  nextTick(()=>{
  nextTick(() => {
    toItem?.init?.();
  })
  });
  list.value = list.value.map(item => ({ ...item, active: item.name === toItem.name }));
};
// 销毁前钩子
onBeforeUnmount(() => {
  if (!window.$viewer) return;
  removeAll();
  eventRemove()
});
onMounted(() => {
  nextTick(() => {
src/views/Home/Home.vue
@@ -26,7 +26,7 @@
const singleUavHome = computed(() => store.state.home.singleUavHome);
onBeforeUnmount(()=>{
onUnmounted(()=>{
  store.commit('setSingleUavHome',null);
  viewerDestory()
})
src/views/Home/useEventOperate/EventPopUpBox.vue
@@ -1,37 +1,50 @@
<template>
  <div class="mapPopUpBox">
    <div class="title">
      违章建筑
      {{ info.event_name }}
      <el-icon class="header-close" @click="props.removeLabel">
        <Close />
      </el-icon>
    </div>
    <div class="content">
      <img
        class="eventImage"
        src="@/assets/images/home/useEventOperate/eventErr.png"
        alt="Event Image"
      />
      <div class="medium">
        <img v-if="info.media_type === 'PHOTO_TYPE'" class="eventImage" :src="info.url" alt="Event Image" />
        <video v-else :src="info.url" autoplay muted playsinline preload="metadata" />
      </div>
      <div class="details">
        <div class="label">位置:</div>
        <div class="value point">112.12345 23.98726</div>
        <div class="value point">{{ _.round(info.longitude, 3) }},{{ _.round(info.latitude, 3) }}</div>
        <div class="label">时间:</div>
        <div class="value">2025.08.16 22:25</div>
        <div class="value">{{ info.create_time }}</div>
      </div>
    </div>
  </div>
</template>
<script setup>
// src="@/assets/images/home/useEventOperate/eventErr.png"
import { Close } from '@element-plus/icons-vue';
const list = [
  { name: '执行中', value: '1', color: '#FFA768' },
  { name: '在线', value: '20', color: '#8EFFAC' },
  { name: '离线', value: '14', color: '#FFFFFF' },
  { name: '异常', value: '25', color: '#FF6262' },
];
import { getEventDetails } from '@/api/home/aggregation';
import _ from 'lodash';
const props = defineProps(['data', 'removeLabel']);
const loading = ref(true);
const info = ref({
  event_name: '',
  status: 1,
  url: '1',
  longitude: '',
  latitude: '',
  create_time: '04/01 12:41',
  media_type: 'PHOTO_TYPE',
});
onMounted(async () => {
  loading.value = true;
  const res = await getEventDetails({ id: props.data.eventId });
  info.value = res.data.data;
  loading.value = false;
});
</script>
<style scoped lang="scss">
@@ -47,7 +60,7 @@
    font-weight: 400;
    font-size: 18px;
    line-height: 16px;
    background: linear-gradient(180deg, #A8E5FB 0%, #E6F8FF 100%);
    background: linear-gradient(180deg, #a8e5fb 0%, #e6f8ff 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    -moz-background-clip: text;
@@ -77,10 +90,16 @@
    display: flex;
    align-items: center;
    img {
    .medium {
      height: 102px;
      width: 102px;
      width: 120px;
      margin-right: 10px;
      > img,
      video {
        width: 100%;
        height: 100%;
      }
    }
    .details {
src/views/Home/useEventOperate/useEventOperate.js
@@ -1,6 +1,5 @@
import * as Cesium from 'cesium';
import data2 from '@/assets/images/home/homeRight/data2.png';
import data1 from '@/assets/images/home/homeRight/data1.png';
import eventImg from '@/assets/images/home/useEventOperate/event.png';
import jiangxishi from '@/assets/geojson/jiangxishi.json';
import jiangxi from '@/assets/geojson/jiangxi.json';
src/views/Home/useUavHome/DevicePopUpBox.vue
src/views/Home/useUavHome/useUavHome.js
@@ -1,31 +1,51 @@
import * as Cesium from 'cesium';
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 eventImg from '@/assets/images/home/useEventOperate/event.png';
import DevicePopUpBox from '@/views/Home/useUavHome/DevicePopUpBox.vue';
import EventPopUpBox from '@/views/Home/useEventOperate/EventPopUpBox.vue';
import { render } from 'vue';
import { useStore } from 'vuex';
import { getCenterPoint } from '@/utils/cesium/mapUtil';
import cesiumOperation from '@/utils/cesium-tsa';
import { HeadingPitchRange } from 'cesium';
import { getDeviceRegion, getDeviceRegionCount } from '@/api/home/aggregation';
import { getDeviceRegion, getDeviceRegionCount, getEventDetails, getMapEvents } from '@/api/home/aggregation';
/**
 * 机巢聚合功能
 */
export const useUavHome = () => {
export const useUavHome = type => {
  const { flyTo } = cesiumOperation();
  const singleImg = type === 'device' ? uavImg : eventImg;
  const mergeImg = type === 'device' ? aggregationImg : aggregationImg;
  const MapPopUpBox = type === 'device' ? DevicePopUpBox : EventPopUpBox;
  const styleTransform = type === 'device' ? 'translateY(-50%)' : 'translate(-50%,-110%)';
  let scalingJudgment = [
    { 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 },
    { 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;
  let handler = null;
  let positionC3 = null;
  let currentEntity = null;
  const store = useStore();
  const userAreaCode = computed(() => store.state.user.userInfo.detail.areaCode);
  const selectedAreaCode = computed(() => store.state.user.selectedAreaCode);
  const listenerHeight = () => {
    determineScaling();
    viewer.camera.moveEnd.addEventListener(determineScaling);
@@ -44,8 +64,8 @@
        active = item.name;
        removeEntities();
        removeLabel();
        item.gJson ?  aggregation(item) :  splashed(item)
        renderOutline(item)
        item.gJson ? aggregation(item) : splashed(item);
        renderOutline(item);
        break;
      }
    }
@@ -63,100 +83,134 @@
    return [provinceCode, cityCode, code];
  }
  const store = useStore();
  const selectedAreaCode = computed(() => store.state.user.selectedAreaCode);
  function getDeviceCount(areaCode) {
    return getDeviceRegionCount({areaCode}).then(res => {
      return res?.data?.data || [];
    return getDeviceRegionCount({ areaCode }).then(res => {
      return (res?.data?.data || []).map(i=>({...i,type:'deviceAggregation'}));
    });
  }
  function getDeviceList(areaCode) {
    return getDeviceRegion({areaCode}).then(res => {
      return res?.data?.data || [];
    return getDeviceRegion({ areaCode }).then(res => {
      return (res?.data?.data || []).map(i =>({...i,type}));
    });
  }
  let eventList = []
  function processChildren(childrens) {
    return (childrens || []).map(item => {
      const arr = processChildren(item.childrens);
      if (item.data){
        eventList = eventList.concat(item.data)
      }
      const returnObj = {
        total_device_count: item.number,
        region_code: item.id,
        region_name: item.name,
        type:'eventAggregation'
      };
      if (arr.length !== 0) {
        returnObj.childrens = arr;
      }
      return returnObj;
    });
  }
  function getMapEventCount(areaCode) {
    return getMapEvents({ areaCode }).then(res => {
      return processChildren(res?.data?.data?.childrens || []);
    });
  }
  function getEventDetailsFun(areaCode){
    return getEventDetails({areaCode}).then(res => {
      console.log(res.data.data,'getEventDetailsFun');
    });
  }
  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 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`)
  };
  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] }
  }
    return { type: 'FeatureCollection', features: [features] };
  };
  const injectData = (gJson,dataList) => {
  const injectData = (gJson, dataList) => {
    return {
      ...gJson,
      features: gJson.features.map(item => ({ ...item, data: dataList.find(item1 => findFun(item1, item)) })),
    };
  };
  const initMapData = async areaCode => {
    if (!areaCode) return;
    const list = type === 'device' ? await getDeviceCount(areaCode) : await getMapEventCount(areaCode);
    const splashedList = type === 'device'
      ? await getDeviceList(areaCode)
      : eventList.map(i=>({eventId:i.id,latitude:Number(i.latitude),longitude:Number(i.longitude),type:'event'}))
    console.log(list, 'list');
    console.log(splashedList, 'splashedList');
    const hierarchy = convertToHierarchy(areaCode.slice(0, 6));
    const jsonPath = hierarchy.join('/');
    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;
    console.log(hierarchy);
    // 省
    if (hierarchy.length === 1) {
      const gJson1 = await getFiler(`${defaultDir}${jsonPath}/indexDistrict.json`);
      const gJson2 = await getFiler(`${defaultDir}${jsonPath}/index.json`);
      scalingJudgment[1].gJson = {
        ...gJson1,
        features: gJson1.features.map(item => ({
          ...item,
          data: list.flatMap(item => item.childrens || []).find(item1 => findFun(item1, item)),
        })),
      };
      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) {
      scalingJudgment[2].gJson = null;
      scalingJudgment[2].show = false;
      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) {
      scalingJudgment[1].gJson = null;
      scalingJudgment[1].show = false;
      scalingJudgment[2].gJson = null;
      scalingJudgment[2].show = false;
      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));
  };
  watch(
    selectedAreaCode,
    async (newValue, oldValue) => {
      if (newValue) {
        const list = await getDeviceCount(newValue);
        const splashedList = await getDeviceList(newValue);
        console.log(list);
        console.log(splashedList);
        const hierarchy = convertToHierarchy(newValue.slice(0, 6));
        const jsonPath = hierarchy.join('/');
        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) {
          const gJson1 = await getFiler(`${defaultDir}${jsonPath}/indexDistrict.json`)
          const gJson2 = await getFiler(`${defaultDir}${jsonPath}/index.json`)
          scalingJudgment[1].gJson = {
            ...gJson1,
            features: gJson1.features.map(item => ({
              ...item,
              data: list.flatMap(item => item.childrens || []).find(item1 => findFun(item1, item)),
            })),
          };
          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) {
          scalingJudgment[2].gJson = null;
          scalingJudgment[2].show = false;
          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) {
          scalingJudgment[1].gJson = null;
          scalingJudgment[1].show = false;
          scalingJudgment[2].gJson = null;
          scalingJudgment[2].show = false;
          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))
      }
      initMapData(newValue);
    },
    { immediate: true, deep: true }
    { deep: true }
  );
  //散点机巢
@@ -176,7 +230,7 @@
          pixelOffset: new Cesium.Cartesian2(0, -9),
        },
        billboard: {
          image: new Cesium.ConstantProperty(uavImg),
          image: new Cesium.ConstantProperty(singleImg),
          width: 24,
          height: 24,
        },
@@ -190,29 +244,30 @@
  }
  // 渲染轮廓
  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 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, // 贴地显示
            },
          });
        });
      });
    });
  };
  // 聚合机巢
@@ -239,20 +294,20 @@
          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,
          text: feature.data.total_device_count.toString(),
          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(aggregationImg),
          image: new Cesium.ConstantProperty(mergeImg),
          width: 35,
          height: 35,
        },
@@ -276,7 +331,7 @@
          Cesium.Color.YELLOW.withAlpha(0) // 透明填充
        );
        entity.polygon.outline = new Cesium.ConstantProperty(true); // 显示边框
        entity.polygon.outlineColor = new Cesium.ConstantProperty(Cesium.Color.AQUAMARINE );
        entity.polygon.outlineColor = new Cesium.ConstantProperty(Cesium.Color.AQUAMARINE);
      });
      flyTo &&
        viewer.flyTo(dataSource, {
@@ -296,7 +351,7 @@
    const tooltipContainer = document.createElement('div');
    tooltipContainer.id = 'mapPopUpBox';
    tooltipContainer.style.position = 'absolute';
    tooltipContainer.style.transform = 'translateY(-50%)';
    tooltipContainer.style.transform = styleTransform;
    tooltipContainer.style.pointerEvents = 'none';
    document.querySelector('.page-index').append(tooltipContainer);
    render(vNode, tooltipContainer);
@@ -326,13 +381,23 @@
      positionC3 = entity?.position?._value;
      if (!positionC3) return;
      removeLabel();
      const customData = entity.properties.customData._value.data
      console.log(customData);
      if (customData.device_sn){
        store.commit('setSingleUavHome', { id: '123' });
        return
      const customData = entity.properties.customData._value.data;
      switch (customData.type) {
        case 'deviceAggregation':
          viewer.scene.postRender.addEventListener(labelBoxRender);
          break;
        case 'device':
          store.commit('setSingleUavHome', { id: '123' });
          break;
        case 'eventAggregation':
          break;
        case 'event':
          viewer.scene.postRender.addEventListener(labelBoxRender);
          break;
        default:
          break;
      }
      viewer.scene.postRender.addEventListener(labelBoxRender);
    }
  };
@@ -368,11 +433,13 @@
    currentEntity = null;
  };
  const init = () => {
    initMapData(selectedAreaCode.value || userAreaCode.value);
    viewer = window.$viewer;
    handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction(singleMachineEvent, Cesium.ScreenSpaceEventType.LEFT_CLICK);
    listenerHeight();
  };
  onBeforeUnmount(() => {});
  // onMounted(() => {
  //   nextTick(() => {
  //     viewer = window.$viewer;