无人机管理后台前端(已迁走)
张含笑
2025-10-29 c41678e94e6c6d1ee4acc2c0307f3de6b01cf58c
feat:渲染数据
3 files modified
508 ■■■■■ changed files
src/views/layerManagement/components/leftList.vue 301 ●●●● patch | view | raw | blame | history
src/views/layerManagement/components/rightEdit.vue 70 ●●●●● patch | view | raw | blame | history
src/views/layerManagement/index.vue 137 ●●●●● patch | view | raw | blame | history
src/views/layerManagement/components/leftList.vue
@@ -17,43 +17,51 @@
      <el-button class="ztzf-button" @click="selectAll">全选</el-button>
    </div>
    <div class="tableListBox">
     <el-tree
  class="ztzf-el-tree"
  ref="treeRef"
  v-model="checkedKeys"
  :data="treeData"
  show-checkbox
  node-key="id"
  default-expand-all
  @check="handleCheck"
  :props="{
    label: 'name',
    children: 'children',
  }"
>
  <!-- 自定义节点内容 -->
  <template #default="{ node }">
    <div class="tree-node">
      <!-- 节点名称 -->
      <span>{{ node.label }}</span>
      <!-- 操作按钮组 -->
      <div class="tree-node-actions">
        <el-button icon="el-icon-location" type="text" @click="handleLocation(node)"></el-button>
        <el-button icon="el-icon-edit" type="text" @click="handleEdit(node)"></el-button>
        <el-button icon="el-icon-delete" type="text" @click="handleDelete(node)"></el-button>
      </div>
    </div>
  </template>
</el-tree>
      <el-tree
        class="ztzf-el-tree"
        ref="treeRef"
        v-model="checkedKeys"
        :data="treeData"
        show-checkbox
        node-key="id"
        default-expand-all
        @check="handleCheck"
        :props="{
          label: 'name',
          children: 'children',
        }"
      >
        <!-- 自定义节点内容 -->
        <template #default="{ node }">
          <div class="tree-node">
            <!-- 节点名称 -->
            <span>{{ node.label }}</span>
            <!-- 操作按钮组 -->
            <div class="tree-node-actions">
              <el-button
                icon="el-icon-location"
                type="text"
                @click="handleLocation(node)"
              ></el-button>
              <el-button icon="el-icon-edit" type="text" @click="handleEdit(node)"></el-button>
              <el-button icon="el-icon-delete" type="text" @click="handleDelete(node)"></el-button>
            </div>
          </div>
        </template>
      </el-tree>
    </div>
  </div>
</template>
<script setup>
import { treeDataApi } from '@/api/layer/index';
import { onMounted } from 'vue';
import { inject, onMounted } from 'vue';
const emit = defineEmits(['update:coverData']);
const coverData = ref();
const layerParams = inject('layerParams');
const checkedKeys = ref([]); // 存储勾选的节点key
const treeRef = ref(null); // 获取tree实例
const checkedNodes = ref([]); // 存储勾选的节点数据
const props = defineProps(['activeName']);
const formData = {
  isPush: undefined,
@@ -70,8 +78,172 @@
// 获取数据
const gettreeDataApi = async () => {
  try {
    const res = await treeDataApi();
    treeAllData.value = res.data.data;
    // const res = await treeDataApi();
    // treeAllData.value = res.data.data;
    treeAllData.value = [
      {
        id: 1,
        name: '电子围栏',
        description: '用于监控特定区域的进入和离开事件',
        area_code: '360103000000',
        create_user: '1831880712850235393',
        create_name: '黄秋雨',
        create_dept: '1916401339951845377',
        create_time: '2025-10-24 11:15:37',
        update_user: '1831880712850235393',
        update_dept: '1916401339951845377',
        update_time: '2025-10-28 11:15:44',
        level: 1,
        children: [
          {
            id: 1,
            category_id: 1,
            name: '厂区周界',
            description: '注意区域',
            event_type: '进入',
            algorithm_name: '高精度围栏算法V2',
            area_code: '360103000000',
            create_user: '1831880712850235393',
            create_name: '黄秋雨',
            create_dept: '1916401339951845377',
            create_time: '2025-10-27 11:14:01',
            update_user: '1831880712850235393',
            update_dept: '1916401339951845377',
            update_time: '2025-10-28 11:13:54',
            level: 2,
            children: [
              {
                id: 1,
                folder_id: 1,
                name: '一号门围栏',
                description: '覆盖工厂一号门入口区域',
                is_enabled: true,
                area: 1250.5,
                geo_data:
                  '{"type":"Polygon","coordinates":[[[114.05,22.54],[114.06,22.54],[114.06,22.55],[114.05,22.55],[114.05,22.54]]]}',
                control_start_time: '2025-10-24 11:14:31',
                control_end_time: '2025-10-28 11:14:41',
                altitude: 120,
                area_code: '360103000000',
                create_user: '1831880712850235393',
                create_name: '黄秋雨',
                create_dept: '1916401339951845377',
                create_time: '2025-10-21 11:14:11',
                update_user: '1831880712850235393',
                update_dept: '1916401339951845377',
                update_time: '2025-10-28 11:14:23',
                level: 3,
              },
              {
                id: 2,
                folder_id: 2,
                name: '二号门围栏',
                description: '覆盖工厂一号门入口区域',
                is_enabled: true,
                area: 1250.5,
                geo_data:
                  '{"type":"Polygon","coordinates":[[[114.05,22.54],[114.06,22.54],[114.06,22.55],[114.05,22.55],[114.05,22.54]]]}',
                control_start_time: '2025-10-24 11:14:31',
                control_end_time: '2025-10-28 11:14:41',
                altitude: 120,
                area_code: '360103000000',
                create_user: '1831880712850235393',
                create_name: '黄秋雨',
                create_dept: '1916401339951845377',
                create_time: '2025-10-21 11:14:11',
                update_user: '1831880712850235393',
                update_dept: '1916401339951845377',
                update_time: '2025-10-28 11:14:23',
                level: 3,
              },
            ],
            total_count: 1,
            total_area: 1250.5,
          },
        ],
      },
      {
        id: 2,
        name: '自定义禁飞区',
        description: '用于设置临时或永久的无人机禁飞区域',
        area_code: '360103000000',
        create_user: '1831880712850235393',
        create_name: '黄秋雨',
        create_dept: '1916401339951845377',
        create_time: '2025-10-19 11:15:40',
        update_user: '1831880712850235393',
        update_dept: '1916401339951845377',
        update_time: '2025-10-28 11:15:47',
        level: 1,
        children: [
          {
            id: 2,
            category_id: 2,
            name: '临时活动区',
            description: '注意区域',
            event_type: '注意',
            algorithm_name: '注意活动',
            area_code: '360103000000',
            create_user: '1831880712850235393',
            create_name: '黄秋雨',
            create_dept: '1916401339951845377',
            create_time: '2025-10-27 11:14:05',
            update_user: '1831880712850235393',
            update_dept: '1916401339951845377',
            update_time: '2025-10-28 11:13:57',
            level: 2,
            children: [
              {
                id: 2,
                folder_id: 2,
                name: '演唱会禁飞区',
                description: '2023年大型演唱会临时禁飞',
                is_enabled: true,
                area: 8800,
                geo_data:
                  '{"type":"Polygon","coordinates":[[[113.27,23.13],[113.28,23.13],[113.28,23.14],[113.27,23.14],[113.27,23.13]]]}',
                control_start_time: '2025-10-16 11:14:37',
                control_end_time: '2025-10-28 11:14:45',
                altitude: 120,
                area_code: '360103000000',
                create_user: '1831880712850235393',
                create_name: '黄秋雨',
                create_dept: '1916401339951845377',
                create_time: '2025-10-22 11:14:19',
                update_user: '1831880712850235393',
                update_dept: '1916401339951845377',
                update_time: '2025-10-28 11:24:47',
                level: 3,
              },
              {
                id: 5,
                folder_id: 2,
                name: '马拉松赛道禁飞',
                description: '城市马拉松赛道沿线禁飞',
                is_enabled: false,
                area: 25000,
                geo_data:
                  '{"type":"LineString","coordinates":[[113.30,23.15],[113.31,23.16],[113.32,23.15]]}',
                control_start_time: '2025-10-28 11:26:43',
                control_end_time: '2025-10-28 11:26:43',
                altitude: 100,
                area_code: '360000000000',
                create_user: '1831880712850235393',
                create_name: '黄秋雨',
                create_dept: '1916401339951845377',
                create_time: '2025-10-28 11:28:38',
                update_user: '1831880712850235393',
                update_dept: '1916401339951845377',
                update_time: '2025-10-28 11:28:38',
                level: 3,
              },
            ],
            total_count: 2,
            total_area: 33800,
          },
        ],
      },
    ];
    console.log('接口获取数据', treeAllData.value);
    setupWatch();
  } catch (error) {
@@ -88,18 +260,11 @@
    { immediate: true }
  );
};
const checkedKeys = ref([]); // 存储勾选的节点key
const treeRef = ref(null); // 获取tree实例
const checkedNodes = ref([]); // 存储勾选的节点数据
const handleCheck = (checkedNodesData, checkedKeysData) => {
  checkedNodes.value = checkedNodesData.checkedNodes;
  coverData.value = checkedKeysData.checkedNodes;
  coverData.value = checkedKeysData.checkedNodes.filter(node => node.level === 3);
  emit('update:coverData', coverData.value);
  // console.log('选中的节点数据:', checkedKeysData);
};
// 全选方法
const selectAll = () => {
@@ -109,7 +274,6 @@
      item.id,
      ...item.children.map(child => child.id),
    ]);
    if (checkedKeys.value.length === allKeys.length) {
      // 如果已经全选,则取消全选
      treeRef.value.setCheckedKeys([]);
@@ -118,38 +282,33 @@
      // 否则全选
      treeRef.value.setCheckedKeys(allKeys);
      // 获取所有节点数据
      checkedNodes.value = treeData.value.flatMap(item => [ ...item.children]);
      checkedNodes.value = treeData.value.flatMap(item => [...item.children]);
      console.log('所有数据', checkedNodes.value);
    }
  }
};
// 定位按钮事件
const handleLocation = (node) => {
const handleLocation = node => {
  console.log('定位操作,节点:', node);
};
// 编辑按钮事件
const handleEdit = (node) => {
const handleEdit = node => {
  console.log('编辑操作,节点:', node.data);
   emit('update:coverData', [node.data]);
  emit('update:coverData', [node.data]);
};
// 删除按钮事件
const handleDelete = (node) => {
const handleDelete = node => {
  console.log('删除操作,节点:', node.data);
};
// 新增围栏
const addFence =()=>{
}
const addFence = () => {
  layerParams.value.addNest = true;
};
// 新增文件夹
const addFolder =()=>{
}
const addFolder = () => {};
onMounted(() => {
  gettreeDataApi();
});
@@ -164,7 +323,6 @@
  height: 95%;
  z-index: 99;
  width: 392px;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.8);
  border-radius: 8px 8px 8px 8px;
@@ -219,24 +377,23 @@
  display: flex;
}
.tableListBox {
.tree-node {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
}
  .tree-node {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
  }
.tree-node-actions {
  display: flex;
  gap: 8px; // 按钮之间的间距
  button {
    padding: 0; // 清除默认内边距
    color: #fff; // 按钮图标颜色
    &:hover {
      color: #409eff; //  hover 时的颜色
  .tree-node-actions {
    display: flex;
    gap: 8px; // 按钮之间的间距
    button {
      padding: 0; // 清除默认内边距
      color: #fff; // 按钮图标颜色
      &:hover {
        color: #409eff; //  hover 时的颜色
      }
    }
  }
}
}
</style>
src/views/layerManagement/components/rightEdit.vue
@@ -5,7 +5,7 @@
      <div class="itemRow">
        <div class="label">文件夹名称</div>
        <div>
          <el-select class="ztzf-layer-search" v-model="value" placeholder="Select">
          <el-select class="ztzf-layer-search" v-model="formData.area" placeholder="Select">
            <el-option
              v-for="item in options"
              :key="item.value"
@@ -18,13 +18,13 @@
      <div class="itemRow">
        <div class="label">围栏名称</div>
        <div>
          <el-input class="ztzf-input inputName" v-model="input" placeholder="围栏名称" />
          <el-input class="ztzf-input inputName" v-model="formData.name" placeholder="围栏名称" />
        </div>
      </div>
      <div class="itemRow">
        <div class="label">围栏备注</div>
        <div>
          <el-input v-model="textarea" :rows="5" type="textarea" placeholder="请输入" />
          <el-input v-model="formData.description" :rows="5" type="textarea" placeholder="请输入" />
        </div>
      </div>
    </div>
@@ -51,13 +51,32 @@
      </div>
    </div>
    <div class="btnGroups">
      <img src="/src/assets/images/layerManagement/savebtn.svg" alt="">
      <img src="/src/assets/images/layerManagement/cancelbtn.svg" alt="">
      <img src="/src/assets/images/layerManagement/savebtn.svg" alt="" @click="submitHandle" />
      <img src="/src/assets/images/layerManagement/cancelbtn.svg" @click="cancelHandel" alt="" />
    </div>
  </div>
</template>
<script setup></script>
<script setup>
const layerParams = inject('layerParams');
const formData = ref({
  folder_id: '',
  name: '',
  description: '',
  is_enabled: false,
  area: '',
  altitude: '',
  control_start_time: '',
  control_end_time: '',
  geo_data: '{"type":"LineString","coordinates":[[113.30,23.15],[113.31,23.16],[113.32,23.15]]}',
});
const cancelHandel = () => {
  layerParams.value.addNest = false;
};
const submitHandle = () => {
  layerParams.value.addNest = false;
};
</script>
<style scoped lang="scss">
.rightContainer {
@@ -72,6 +91,11 @@
  background: rgba(0, 0, 0, 0.8);
  border-radius: 8px 8px 8px 8px;
  padding: 16px 12px;
  :deep(.el-textarea__inner) {
    background: transparent !important;
    color: #ffffff !important;
  }
  .contenttitle {
    font-family: Source Han Sans CN, Source Han Sans CN;
    font-weight: bold;
@@ -102,28 +126,30 @@
      color: #ffffff;
      margin-bottom: 23px;
      .num {
      span{
        font-family: Source Han Sans CN, Source Han Sans CN;
        font-weight: bold;
        font-size: 20px;
        color: #ffffff;}
        span {
          font-family: Source Han Sans CN, Source Han Sans CN;
          font-weight: bold;
          font-size: 20px;
          color: #ffffff;
        }
      }
      .itemData {
      width: 50%;
        width: 50%;
      }
    }
  }
  .btnGroups {
   display: flex;
  justify-content: center;
  position: absolute;
  bottom: 59px;
  left: 0;
  right: 0;
  img{
  width: 125px;
  height: 39px;
  cursor: pointer;}
    display: flex;
    justify-content: center;
    position: absolute;
    bottom: 59px;
    left: 0;
    right: 0;
    img {
      width: 125px;
      height: 39px;
      cursor: pointer;
    }
  }
}
</style>
src/views/layerManagement/index.vue
@@ -10,15 +10,19 @@
      <!-- 地图 -->
      <div class="mapContainer">
        <div id="layMap" class="ztzf-cesium"></div>
        <leftList @update:coverData="handleCoverDataUpdate" :activeName="activeName"></leftList>
        <rightEdit></rightEdit>
        <leftList
          v-if="!layerParams.addNest"
          @update:coverData="handleCoverDataUpdate"
          :activeName="activeName"
        ></leftList>
        <rightEdit v-if="layerParams.addNest"></rightEdit>
      </div>
    </div>
  </basic-container>
</template>
<script setup>
import { flyVisual } from '@/utils/cesium/mapUtil'
import { flyVisual } from '@/utils/cesium/mapUtil';
import rightEdit from '@/views/layerManagement/components/rightEdit.vue';
import leftList from '@/views/layerManagement/components/leftList.vue';
import { DrawPolygon } from '@/views/layerManagement/components/utils';
@@ -26,6 +30,7 @@
import { PublicCesium } from '@/utils/cesium/publicCesium';
import _, { cloneDeep, throttle } from 'lodash';
import { ElButton } from 'element-plus';
import { provide } from 'vue';
const activeName = ref('电子围栏');
const activeType = ref('0');
const tabData = ref([
@@ -42,7 +47,10 @@
    type: '2',
  },
]);
const layerParams = ref({
  addNest: false,
});
provide('layerParams', layerParams);
const handleClick = tab => {
  const clickedTab = tabData.value.find(item => item.type === tab.paneName);
  activeType.value = clickedTab.type;
@@ -51,107 +59,53 @@
// 解析 geo_data 并转换为 DrawPolygon 所需的坐标格式
const parseGeoDataToPositions = (geoDataStr, altitude) => {
  try {
    // 解析 JSON 字符串为 GeoJSON 对象
    const geoData = JSON.parse(geoDataStr);
    // 仅处理 Polygon 类型的地理数据
    if (geoData.type !== 'Polygon') return [];
    // 提取 coordinates(取第一个面,忽略孔洞),并补充高度
    const coordinates = geoData.coordinates[0];
    return coordinates.map(([lng, lat]) => ({
      lng: Number(lng),
      lat: Number(lat),
      height: Number(altitude) || 0, // 高度默认取数据中的 altitude,无则为 0
      height: Number(altitude) || 0,
    }));
  } catch (error) {
    console.error('解析 geo_data 失败:', error);
    return [];
  }
};
let tbJwdList = [];
const selectDataList = ref([]);
// 提取图斑坐标
function polygonWktToTargetArray(wktPolygon, defaultHeight = 140.6) {
  try {
    if (!/^POLYGON\(\([\d\s,.]+\)\)$/.test(wktPolygon)) {
      throw new Error('输入不是有效的 WKT POLYGON 格式');
    }
    const coordStr = wktPolygon
      .replace(/^POLYGON\(\(/, '')
      .replace(/\)\)$/, '')
      .trim();
    const coordPairs = coordStr.split(/,\s*/).filter(pair => pair.trim() !== '');
    const result = coordPairs.map(pair => {
      const [lng, lat] = pair
        .split(/\s+/)
        .map(Number)
        .filter(num => !isNaN(num));
      if (
        lng === undefined ||
        lat === undefined ||
        lng < -180 ||
        lng > 180 ||
        lat < -90 ||
        lat > 90
      ) {
        throw new Error(`无效的经纬度对:${pair}`);
      }
      return {
        lng: lng,
        lat: lat,
        longitude: lng, // 与 lng 一致
        latitude: lat, // 与 lat 一致
      };
    });
    const uniqueResult = Array.from(
      new Map(result.map(item => [`${item.lng}-${item.lat}`, item])).values()
    );
    return uniqueResult;
  } catch (error) {
    return []; // 异常时返回空数组
  }
}
const handleCoverDataUpdate = data => {
  selectDataList.value = data;
console.log('获取的',selectDataList.value);
  console.log('获取的', selectDataList.value);
  if (selectDataList.value.length > 0) {
    loadDataToMap(selectDataList.value);
  } else {
    drawPolygonExample.removeEntities();
    viewer.entities.removeAll();
  }
};
const loadDataToMap = (dataList) => {
  if (!viewer) return;
const loadDataToMap = dataList => {
  if (!viewer) return;
  viewer.entities.removeAll();
    // 存储所有有效坐标,用于后续定位
  const allCoordinates = [];
  // 存储所有有效坐标,用于后续定位
  tbJwdList = [];
  dataList.forEach(item => {
    // 解析 geo_data 为经纬度坐标数组(使用已有的 parseGeoDataToPositions 方法)
    const positions = parseGeoDataToPositions(item.geo_data, item.altitude);
    if (positions.length < 3) {
      console.warn(`数据 ${item.name} 坐标点不足,无法绘制`);
      return;
    }
console.log('positions',positions);
    console.log('positions', positions);
    tbJwdList.push(...positions);
    //  转换为 Cesium 所需的 [lng, lat, lng, lat, ...] 扁平数组格式
    let degreesArray = [];
    positions.forEach(pos => {
      degreesArray.push(pos.lng, pos.lat, pos.height);
    });
      degreesArray.push(pos.lng, pos.lat, pos.height);
    });
    viewer.entities.add({
      id: `polygon_${item.id}`,
      customType: 'fence_polygon',
      id: `polygon_${item.id}`,
      customType: 'fence_polygon',
      customInfo: item,
      polygon: {
        hierarchy: new Cesium.PolygonHierarchy(
@@ -162,9 +116,8 @@
        outlineColor: Cesium.Color.YELLOW, // 边框色
        outlineWidth: 2, // 边框宽度
        clampToGround: true, // 贴地显示
      },
      polyline: {
        positions: Cesium.Cartesian3.fromDegreesArrayHeights(degreesArray),
        width: 2,
@@ -174,15 +127,33 @@
      zIndex: 99, // 层级,确保在其他图层上方显示
    });
  });
  focusOnAllFeatures();
  //  添加点击事件(参考 entitiesAddSpot 的 spotHighlighting 逻辑)
  // 移除已有点击事件,避免重复绑定
  // viewInstance.value?.removeLeftClickEvent('fenceHighlighting');
  // 绑定新的点击事件,用于高亮选中的围栏
  // viewInstance.value?.addLeftClickEvent(null, handleFenceClick, 'fenceHighlighting');
};
//计算所有图斑的包围球并定位
const focusOnAllFeatures = () => {
  if (tbJwdList.length === 0 || !viewer) return;
  // 转换所有经纬度坐标为Cartesian3
  const allPositions = tbJwdList.flatMap(pos =>
    Cesium.Cartesian3.fromDegrees(pos.lng, pos.lat, pos.height || 0)
  );
  if (allPositions.length === 0) return;
  const boundingSphere = Cesium.BoundingSphere.fromPoints(allPositions);
  viewer.camera.flyToBoundingSphere(boundingSphere, {
    duration: 0, // 飞行时间(秒)
    offset: new Cesium.HeadingPitchRange(
      Cesium.Math.toRadians(0), // 方向角
      Cesium.Math.toRadians(-90) // 俯仰角
    ),
  });
};
// 围栏点击处理函数(高亮选中项 + 可联动右侧编辑组件)
const handleFenceClick = (movement) => {
const handleFenceClick = movement => {
  if (!viewer) return;
  // 拾取点击的实体
@@ -195,9 +166,10 @@
    viewer.entities.values.forEach(entity => {
      if (entity.customType === 'fence_polygon') {
        // 恢复默认样式
        entity.polygon.material = entity === selectedEntity
          ? selectedEntity.polygon.material.color.withAlpha(0.8) // 选中时加深透明度
          : selectedEntity.polygon.material.color.withAlpha(0.5); // 未选中恢复默认
        entity.polygon.material =
          entity === selectedEntity
            ? selectedEntity.polygon.material.color.withAlpha(0.8) // 选中时加深透明度
            : selectedEntity.polygon.material.color.withAlpha(0.5); // 未选中恢复默认
      }
    });
@@ -226,7 +198,7 @@
  viewer = publicCesiumInstance.getViewer();
  viewInstance.value = publicCesiumInstance;
  drawPolygonExample.initHandler(viewer);
  // drawPolygonExample.initHandler(viewer);
};
// 处理绘制完成后的多边形坐标(示例)
@@ -245,8 +217,7 @@
const throttleLoadPlanarRoute = throttle(loadPlanarRoute, 200);
drawPolygonExample.subscribe('getPolygonPositions', data => {
console.log('绘制的数据',data);
  console.log('绘制的数据', data);
  throttleLoadPlanarRoute(data);
});