GuLiMmo
2024-03-22 d4d5d48934c3c9647ae73eb2e61cd6bd5a8bba69
update
9 files modified
1 files added
1192 ■■■■ changed files
src/assets/icons/waylinetool/panoramicview.png patch | view | raw | blame | history
src/components/GMap.vue 12 ●●●● patch | view | raw | blame | history
src/components/cesiumMap/cesium.vue 27 ●●●● patch | view | raw | blame | history
src/components/waylinetool/event-edit.vue 104 ●●●●● patch | view | raw | blame | history
src/components/waylinetool/index.vue 922 ●●●●● patch | view | raw | blame | history
src/pages/page-web/projects/components/route-edit/components/setting-point-tip.vue 5 ●●●●● patch | view | raw | blame | history
src/pages/page-web/projects/components/route-edit/index.vue 91 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/wayline.vue 12 ●●●● patch | view | raw | blame | history
src/store/index.ts 12 ●●●● patch | view | raw | blame | history
src/utils/cesium/use-map-draw.ts 7 ●●●● patch | view | raw | blame | history
src/assets/icons/waylinetool/panoramicview.png
src/components/GMap.vue
@@ -848,12 +848,9 @@
          :payloads="osdVisible.payloads">
        </DroneControlPanel>
      </div>
      <!-- <waylineTool /> -->
      <routeProfile />
    </div>
    <div class="event-wrapper" v-if="false">
      <eventEdit />
    </div>
  </div>
</template>
@@ -927,7 +924,6 @@
import Cesium from './cesiumMap/cesium.vue'
import { convertTimestampToDate } from '/@/utils/time'
import { cesiumOperation } from '/@/hooks/use-cesium-tsa'
import waylineTool from './waylinetool/index.vue'
import routeProfile from './waylinetool/route-profile.vue'
export default defineComponent({
  components: {
@@ -954,7 +950,6 @@
    CarryOutOutlined,
    RocketOutlined,
    DesktopOutlined,
    waylineTool,
    routeProfile,
    eventEdit
  },
@@ -970,6 +965,7 @@
      label: string
      more?: any
    }
    // 打开飞行控制
    const openDroneControl = ref(false)
    const root = getRoot()
@@ -1640,7 +1636,7 @@
      aircrafIndex,
      airPortOption,
      controlStatus,
      convertTimestampToDate,
      convertTimestampToDate
    }
  },
})
@@ -1651,10 +1647,6 @@
.map-container {
  height: 100%;
  display: flex;
  .event-wrapper {
    width: 350px;
    height: 100%;
  }
}
.g-map-wrapper {
src/components/cesiumMap/cesium.vue
@@ -1,6 +1,5 @@
<template>
  <div class="height-100 width-100 cesium" id="cesiumContainer"></div>
  <div v-if="centerConfig.type && !centerConfig.latitude" class="pointLongitude">在地图上点击绘制项目中心点</div>
  <div class="search">
    <a-auto-complete v-if="show" v-model:value="searchText" style="width: 200px;height: 100%;" placeholder="请输入地址"
      @select="enter" @change="searchLocation" :options='resultList'>
@@ -9,7 +8,8 @@
      <SearchOutlined style="fontSize:24px; color: blue;" />
    </span>
  </div>
  <!-- 事件编辑 -->
  <actionEdit />
  <div class="zoon">
    <span class="zoon-in" @mousedown="handleMouseDown(zoonIn)" @mouseup="handleMouseUp">
      <PlusOutlined style="fontSize:24px; color: black;" />
@@ -50,6 +50,7 @@
import { SearchOutlined, PlusOutlined, MinusOutlined } from '@ant-design/icons-vue'
import { useMyStore } from '/@/store'
import { requireImg } from '/@/utils/common'
import actionEdit from '../waylinetool/index.vue'
import * as Cesium from 'cesium'
type result = {
  label: string,
@@ -157,20 +158,6 @@
  }
}
.pointLongitude {
  background-color: rgba(20, 20, 20, 0.792);
  position: absolute;
  z-index: 30;
  bottom: 30px;
  color: #fff;
  left: 0;
  right: 0;
  margin: 0 80px;
  padding: 15px 0;
  text-align: center;
  font-size: 16px;
}
.search {
  position: absolute;
  top: 40px;
@@ -192,7 +179,7 @@
.zoon {
  position: absolute;
  right: 20px;
  bottom: 340px;
  bottom: 173px;
  color: #fff;
  font-size: 16px;
  display: flex;
@@ -214,7 +201,7 @@
  position: absolute;
  background-color: #fff;
  z-index: 30;
  bottom: 400px;
  bottom: 205px;
  font-size: 14px;
  right: 20px;
  cursor: pointer;
@@ -235,8 +222,8 @@
  width: 40px;
  height: 40px;
  padding: 3px;
  right: 20px;
  bottom: 120px;
  right: 17px;
  bottom: 70px;
  background: #fff;
  margin-top: 10px;
  border-radius: 50%;
src/components/waylinetool/event-edit.vue
@@ -1,24 +1,114 @@
<template>
    <div class="event-container">
        <div class="edit-box"></div>
  <div class="event-container" v-if="isActionEditShow">
    <div class="edit-box" v-if="currentPointEvent">
      <div class="header">
        <div class="action-name">
          <img :src="currentPointEvent.aciton.icon" alt="icon" />
          <span>{{ currentPointEvent.aciton.name }}</span>
        </div>
        <div class="action-index">{{ currentPointEvent.actionIndex }}</div>
        <div class="action-del">
          <span @click="handleDelAction"><DeleteOutlined /></span>
        </div>
      </div>
      <div class="event-edit">
        <template v-if="currentPointEvent.aciton.key === 'takePhoto'">
          1
        </template>
      </div>
    </div>
        <div class="map-box"></div>
    </div>
</template>
<script lang="ts" scoped>
<script lang="ts" setup>
import { DeleteOutlined } from '@ant-design/icons-vue'
import { useMyStore } from '/@/store'
import { actionList } from '/@/utils/cesium/use-map-draw'
import { template as xmlTemplate } from '/@/utils/cesium/use-kmz-tsa'
const store = useMyStore()
// 事件编辑是否显示
const isActionEditShow = computed(() => store.state.waylineTool.isShow)
// 当前点事件
const selectedAction = computed(() => store.state.waylineTool.selectedAction)
const currentPointEvent = computed(() => {
  const placemarks = xmlTemplate.value.Folder.Placemark
  const pointIndex = store.state.waylineTool.selectedPoint
  if (pointIndex === undefined) return null
  const currentPoint = placemarks[pointIndex]
  const actions = currentPoint.actionGroup.action
  let actionIndex = 1
  if (Array.isArray(actions)) {
    actionIndex = actions.findIndex((item: { actionActuatorFunc: { '#text': string } }) => {
      return item.actionActuatorFunc['#text'] === selectedAction.value.key
    })
  }
  return {
    aciton: selectedAction.value,
    actionIndex: `${pointIndex + 1}-${actionIndex + 1}`,
  }
})
const handleDelAction = () => {}
</script>
<style lang="scss" scoped>
.event-container {
    width: 100%;
  width: 400px;
    height: 100%;
    display: flex;
    flex-direction: column;
    background: rgb(35, 35, 35);
    .edit-box,
    .map-box {
  color: #fff;
  .edit-box {
    width: 400px;
    height: calc(100% - 300px);
    .header {
      padding: 0 15px;
      height: 50px;
      border-bottom: 1px solid rgb(79, 79, 79);
      display: flex;
      align-items: center;
      div {
        flex: 1;
        border: 1px solid red;
        flex-shrink: 0;
      }
      .action-name {
        display: flex;
        align-items: center;
        img {
          width: 15px;
          height: 15px;
          margin-right: 3px;
        }
      }
      .action-index {
        text-align: center;
        font-weight: bold;
      }
      .action-del {
        display: flex;
        justify-content: flex-end;
        span {
          cursor: pointer;
          &:hover {
            color: #409eff;
          }
        }
      }
    }
    .event-edit {
      width: 100%;
      height: calc(100% - 50px);
      padding: 15px;
      overflow: auto;
    }
  }
  .map-box {
    width: 400px;
    height: 300px;
    }
}
</style>
src/components/waylinetool/index.vue
@@ -1,877 +1,115 @@
<template>
  <ul class="btn-list" v-if="waylineAbout.isShow">
    <!-- 参数按钮 -->
    <li class="s-btn" v-for="(item, index) in btnList" :key="index">
      <div class="title">{{ item.title }}</div>
      <a-popover placement="leftTop" trigger="click" arrow-point-at-center class="param-popover">
        <template #title>
          <span>{{ item.title }}</span>
        </template>
        <template #content>
          <div class="popover-content">
            <div v-for="sParam in item.param" :key="sParam.key" class="content-box">
              <div>{{ sParam.title }}</div>
              <a-input v-model:value="sParam.value" placeholder="请输入参数" v-if="!sParam.isRadio" />
              <a-radio-group :options="sParam.options" v-model:value="sParam.value" v-else />
              <div class="annotation" v-if="sParam.annotation">
                注:{{ sParam.annotation }}
  <div class="action-container" v-if="isActionEditShow">
    <div class="more-btn" @click="toggleMoreAction">
      <div class="label">更多</div>
      <div class="icon"><EllipsisOutlined /></div>
              </div>
            </div>
            <div class="btn-tool">
              <a-button @click="resetParamSetting(item)">重置</a-button>
              <a-button type="primary" @click="item.event(item)">确定</a-button>
            </div>
          </div>
        </template>
        <div class="btn" @click="() => saveInitPopoverParam(item.param)"
          :style="{ backgroundColor: item.selected ? '#2D8CF0' : '#323131' }">
          <img :src="item.icon" alt="icon">
        </div>
      </a-popover>
    </li>
    <!-- 确认和取消按钮 -->
    <li class="s-btn">
      <div class="title">取消编辑</div>
      <div class="btn cancel-edit" @click="cencalEdit">
        <CloseOutlined />
      </div>
    </li>
    <li class="s-btn">
      <div class="title">确认编辑</div>
      <div class="btn confirm-edit" @click="confirmEdit">
        <CheckOutlined />
    <ul class="action-list" v-if="isMoreActionShow">
      <li class="action-list_item" v-for="action in actionList" :key="action.key" @click="handleSettingAction(action)">
        <div class="name">{{ action.name }}</div>
        <div class="icon">
          <img :src="action.icon" alt="icon" />
      </div>
    </li>
  </ul>
  </div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useMyStore } from '/@/store'
import axios from 'axios'
import JSZIP from 'jszip'
import { saveAs } from 'file-saver'
import { message } from 'ant-design-vue'
import { cloneDeep } from 'lodash'
import {
  CloseOutlined,
  CheckOutlined
} from '@ant-design/icons-vue'
// 初始化jszip
const JsZip = new JSZIP()
<script lang="ts" setup>
import { EllipsisOutlined } from '@ant-design/icons-vue'
import { actionList } from '/@/utils/cesium/use-map-draw'
import { useMyStore } from '/@/store'
import { message } from 'ant-design-vue'
const store = useMyStore()
const prefix = import.meta.env.VITE_MEDIAPANEL_API_URL
const isMoreActionShow = ref<boolean>(false)
const getResource = (name: string) => {
  return new URL(`/src/assets/icons/${name}`, import.meta.url).href
const isActionEditShow = computed(() => store.state.waylineTool.isShow)
const selectedPoint = computed(() => store.state.waylineTool.selectedPoint)
const toggleMoreAction = () => {
  isMoreActionShow.value = !isMoreActionShow.value
}
interface popoverParamType {
  title: string,
  key: string,
  isRadio?: boolean,
  annotation?: string,
  options?: {
    label: string,
    value: string
  }[],
  value: string
const handleSettingAction = (action: { key: string }) => {
  console.log(selectedPoint.value)
  if (selectedPoint.value === undefined) return message.warn('请选择要编辑的航点!!')
}
// 点击按钮list
interface sBtn {
  key: string,
  icon: string,
  title: string,
  distinguish?: string,
  param: popoverParamType[],
  event: Function,
  selected?: boolean
}
// 保存当前选择的kmz文件
const kmzFileZip = ref()
// 当前选中的位置
const currentPosition = ref()
const curKmzName = ref<string>('')
// 当前kmzpath
const curKmzPath = ref<string>('')
// popover初始参数
const initPopoverParam = ref<popoverParamType[]>([])
const btnList = ref<sBtn[]>([
  // 开始录像
  {
    key: 'startRecord',
    icon: getResource('waylinetool/camera-on.png'),
    title: '开始录像',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '拍摄照片文件后缀',
        key: 'fileSuffix',
        annotation: '为生成媒体文件命名时将额外附带该后缀',
        value: ''
      },
      {
        title: '拍摄照片存储类型',
        key: 'payloadLensIndex',
        isRadio: true,
        options: [
          { label: '存储变焦镜头拍摄照片', value: 'zoom' },
          { label: '存储广角镜头拍摄照片', value: 'wide' },
          { label: '存储红外镜头拍摄照片', value: 'ir' },
          { label: '存储窄带镜头拍摄照片', value: 'narrow_band' },
          { label: '全部使用', value: 'zoom, wide, ir, narrow_band' }
        ],
        value: ''
      },
      {
        title: '是否使用全局存储类型',
        key: 'useGlobalPayloadLensIndex',
        isRadio: true,
        options: [
          { label: '不使用全局设置', value: '0' },
          { label: '使用全局设置', value: '1' }
        ],
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('startRecord', obj)
    }
  },
  // 停止录像
  {
    key: 'stopRecord',
    icon: getResource('waylinetool/camera-off.png'),
    title: '停止录像',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '拍摄照片存储类型',
        key: 'payloadLensIndex',
        isRadio: true,
        options: [
          { label: '存储变焦镜头拍摄照片', value: 'zoom' },
          { label: '存储广角镜头拍摄照片', value: 'wide' },
          { label: '存储红外镜头拍摄照片', value: 'ir' },
          { label: '存储窄带镜头拍摄照片', value: 'narrow_band' },
          { label: '全部使用', value: 'zoom, wide, ir, narrow_band' }
        ],
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('stopRecord', obj)
    }
  },
  // 开始等时间隔拍照
  {
    key: 'time',
    icon: getResource('waylinetool/shoot1.png'),
    title: '开始等时间隔拍照',
    param: [],
    event: (obj: sBtn) => {
      obj.selected = true
      editKmlFile('time')
    }
  },
  // 开始等距间隔拍照
  {
    key: 'distance',
    icon: getResource('waylinetool/shoot2.png'),
    title: '开始等距间隔拍照',
    param: [],
    event: (obj: sBtn) => {
      obj.selected = true
      editKmlFile('distance')
    }
  },
  // 结束间隔拍照
  {
    key: 'deleteShootType',
    icon: getResource('waylinetool/shoot3.png'),
    title: '结束间隔拍照',
    param: [],
    event: (obj: sBtn) => {
      obj.selected = true
      editKmlFile('deleteShootType')
    }
  },
  // 悬停
  {
    key: 'hover',
    icon: getResource('waylinetool/xt.png'),
    title: '悬停',
    param: [
      {
        title: '飞行器悬停等待时间',
        key: 'hoverTime',
        annotation: '参数需大于0, 单位为秒',
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('hover', obj)
    }
  },
  // 飞行器偏航角
  {
    key: 'rotateYaw',
    icon: getResource('waylinetool/droneyaw.png'),
    title: '飞行器偏航角',
    param: [
      {
        title: '飞行器目标偏航角(相对于地理北)',
        key: 'aircraftHeading',
        annotation: '范围为-180°到180°,飞行器旋转至该目标偏航角。0°为正北方向,90°为正东方向,-90°为正西方向,-180°/180°为正南方向。',
        value: ''
      },
      {
        title: '飞行器偏航角转动模式',
        key: 'aircraftPathMode',
        isRadio: true,
        options: [
          { label: '顺时针旋转', value: 'clockwise' },
          { label: '逆时针旋转', value: 'counterClockwise' }
        ],
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('rotateYaw', obj)
    }
  },
  // 云台偏航角
  {
    key: 'gimbalRotate',
    distinguish: 'yaw',
    icon: getResource('waylinetool/holderyaw.png'),
    title: '云台偏航角',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '云台转动模式',
        key: 'gimbalRotateMode',
        isRadio: true,
        options: [
          { label: '绝对角度,相对于正北方的角度', value: 'relativeAngle' },
          { label: '相对角度,相对于飞行器机头的角度', value: 'absoluteAngle' }
        ],
        value: ''
      },
      {
        title: '是否使能云台Yaw转动',
        key: 'gimbalYawRotateEnable',
        isRadio: true,
        options: [
          { label: '不使能', value: '0' },
          { label: '使能', value: '1' }
        ],
        value: ''
      },
      {
        title: '云台Yaw转动角度',
        key: 'gimbalYawRotateAngle',
        annotation: '不同云台可转动范围不同',
        value: ''
      },
      {
        title: '云台完成转动用时',
        key: 'gimbalRotateTime',
        annotation: '单位为秒',
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('gimbalRotate', obj, {
        gimbalHeadingYawBase: 'north',
        gimbalRotateMode: 'relativeAngle',
        // 上下摆动 pitch xyz中心点为基准
        gimbalPitchRotateEnable: 0,
        gimbalPitchRotateAngle: 0,
        // 摆动 roll 已x位基准
        gimbalRollRotateEnable: 0,
        gimbalRollRotateAngle: 0,
        // 左右摆动 yaw xyz中心点为基准
        gimbalRotateTimeEnable: 0,
      })
    }
  },
  // 云台俯仰角
  {
    key: 'gimbalRotate',
    distinguish: 'pitch',
    icon: getResource('waylinetool/holdertilt.png'),
    title: '云台俯仰角',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '云台转动模式',
        key: 'gimbalRotateMode',
        isRadio: true,
        options: [
          { label: '绝对角度,相对于正北方的角度', value: 'relativeAngle' },
          { label: '相对角度,相对于飞行器机头的角度', value: 'absoluteAngle' }
        ],
        value: ''
      },
      {
        title: '是否使能云台Pitch转动',
        key: 'gimbalPitchRotateEnable',
        isRadio: true,
        options: [
          { label: '不使能', value: '0' },
          { label: '使能', value: '1' }
        ],
        value: ''
      },
      {
        title: '云台Pitch转动角度',
        key: 'gimbalPitchRotateAngle',
        annotation: '不同云台可转动范围不同',
        value: ''
      },
      {
        title: '云台完成转动用时',
        key: 'gimbalRotateTime',
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('gimbalRotate', obj, {
        gimbalHeadingYawBase: 'north',
        gimbalRotateMode: 'relativeAngle',
        gimbalRollRotateEnable: 0,
        gimbalRollRotateAngle: 0,
        gimbalYawRotateEnable: 0,
        gimbalYawRotateAngle: 0,
        gimbalRotateTimeEnable: 0,
      })
    }
  },
  // 拍照
  {
    key: 'takePhoto',
    icon: getResource('waylinetool/camera.png'),
    title: '拍照',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '拍摄照片文件后缀',
        key: 'fileSuffix',
        annotation: '为生成媒体文件命名时将额外附带该后缀。',
        value: ''
      },
      {
        title: '拍摄照片存储类型',
        key: 'payloadLensIndex',
        isRadio: true,
        options: [
          { label: '存储变焦镜头拍摄照片', value: 'zoom' },
          { label: '存储广角镜头拍摄照片', value: 'wide' },
          { label: '存储红外镜头拍摄照片', value: 'ir' },
          { label: '存储窄带镜头拍摄照片', value: 'narrow_band' },
          { label: '全部使用', value: 'zoom, wide, ir, narrow_band' }
        ],
        value: ''
      },
      {
        title: '是否使用全局存储类型',
        key: 'useGlobalPayloadLensIndex',
        isRadio: true,
        options: [
          { label: '不使用全局设置', value: '0' },
          { label: '使用全局设置', value: '1' }
        ],
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('takePhoto', obj)
    }
  },
  // 相机变焦
  {
    key: 'zoom',
    icon: getResource('waylinetool/fd.png'),
    title: '相机变焦',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '变焦焦距',
        key: 'focalLength',
        annotation: '单位mm,值>0',
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('zoom', obj)
    }
  },
  // 创建文件夹
  {
    key: 'customDirName',
    icon: getResource('waylinetool/create-file.png'),
    title: '创建文件夹',
    param: [
      {
        title: '负载挂载位置',
        key: 'payloadPositionIndex',
        isRadio: true,
        options: [
          { label: '飞行器1号挂载位置', value: '0' },
          { label: '飞行器2号挂载位置', value: '1' },
          { label: '飞行器3号挂载位置', value: '2' },
        ],
        annotation: '飞行器1号挂载位置。M300 RTK,M350 RTK机型,对应机身左前方。其它机型,对应主云台。飞行器2号挂载位置。M300 RTK,M350 RTK机型,对应机身右前方。飞行器3号挂载位置。M300 RTK,M350 RTK机型,对应机身上方。',
        value: ''
      },
      {
        title: '新文件夹的名称',
        key: 'directoryName',
        value: ''
      }
    ],
    event: (obj: sBtn) => {
      paramPopover('customDirName', obj)
    }
  }
])
const waylineAbout = computed(() => {
  return store.state.waylineTool
})
/**
 * @description: 读取kmz文件转blob
 * @param {*} kmzPath 文件地址
 * @return {*} void
 */
const readKmzFile = (kmzPath: string) => {
  return axios.get(kmzPath, { responseType: 'arraybuffer' })
    .then(fileRes => fileRes.data)
    .then(kmzData => JsZip.loadAsync(kmzData)) // 解压kmz文件
    .then(kmzZip => kmzZip)
    .catch(() => message.error('请求kmz文件失败!!'))
}
watch(() => store.state.waylineTool.kmzPath, (n: string) => {
  curKmzPath.value = n
  const arr = n.split('/')
  curKmzName.value = decodeURIComponent(arr[arr.length - 1])
  readKmzFile(n).then(kmzFile => {
    kmzFileZip.value = kmzFile
  })
}, {
  deep: true
})
watch(() => store.state.waylineTool.position, (position: number) => {
  currentPosition.value = position
  eventLight()
}, {
  deep: true
})
// 事件高亮
const eventLight = () => {
  // 已经添加的事件高亮
  btnList.value.forEach(item => { item.selected = false })
  kmzFileZip.value.file(/\.kml$/i)[0].async('text').then((content: string) => {
    const regx = /<Placemark>([\s\S]*?)<\/Placemark>/g
    const points = content.match(regx) || []
    const funcRegx = /<wpml:actionActuatorFunc>([\s\S]*?)<\/wpml:actionActuatorFunc>/g
    const funcs = (points[currentPosition.value] || '').match(funcRegx)
    const funcNameRegx = /<wpml:actionActuatorFunc>(.*?)<\/wpml:actionActuatorFunc>/
    const shootTypeRegx = /<wpml:shootType>([\s\S]*?)<\/wpml:shootType>/g
    const shootType = (points[currentPosition.value] || '').match(shootTypeRegx)
    const shootTypeValueRegx = /<wpml:shootType>(.*?)<\/wpml:shootType>/
    // 云台正则
    const actionRegx = /<wpml:action>([\s\S]*?)<\/wpml:action>/g
    const actions = (points[currentPosition.value] || '').match(actionRegx)
    const gimbalPitchRotateAngleRegx = /<wpml:gimbalPitchRotateAngle>(.*?)<\/wpml:gimbalPitchRotateAngle>/
    const gimbalYawRotateAngleRegx = /<wpml:gimbalYawRotateAngle>(.*?)<\/wpml:gimbalYawRotateAngle>/
    btnList.value.forEach(sBtn => {
      funcs?.forEach((func: string) => {
        const funcName = func.match(funcNameRegx) || []
        if (funcName[1] === sBtn.key && !sBtn.distinguish) {
          sBtn.selected = true
        }
      })
      shootType?.forEach((shootType: string) => {
        const typeValue = shootType.match(shootTypeValueRegx) || []
        if (typeValue[1] === sBtn.key) {
          sBtn.selected = true
        }
      })
      if (sBtn.distinguish) {
        const action = actions?.find((action: string) => action.includes('<wpml:actionActuatorFunc>gimbalRotate</wpml:actionActuatorFunc>'))
        const pitchAngle = action?.match(gimbalPitchRotateAngleRegx) || []
        const yawAngle = action?.match(gimbalYawRotateAngleRegx) || []
        if (!!Number(pitchAngle[1]) && sBtn.distinguish === 'pitch') {
          sBtn.selected = true
        }
        if (!!Number(yawAngle[1]) && sBtn.distinguish === 'yaw') {
          sBtn.selected = true
        }
      }
    })
    settingDefaultValue(content)
  })
}
// popover设置默认值
const settingDefaultValue = (content: string) => {
  const regx = /<Placemark>([\s\S]*?)<\/Placemark>/g
  const points = content.match(regx) || []
  // 当前选中的点
  const currentPoint = points[currentPosition.value] || ''
  const actionRegx = /<wpml:action>([\s\S]*?)<\/wpml:action>/g
  const actionsKML = currentPoint.match(actionRegx)
  btnList.value.forEach((sBtn: sBtn) => {
    const kml = `<wpml:actionActuatorFunc>${sBtn.key}</wpml:actionActuatorFunc>`
    const action = actionsKML?.find((action: string) => action.includes(kml))
    sBtn.param.forEach((sParam) => {
      const paramRegex = new RegExp(`<wpml:${sParam.key}>([\\s\\S]*?)<\\/wpml:${sParam.key}>`, 'g')
      const actionParam = action?.match(paramRegex) || ['']
      const infoRegx = new RegExp(`<wpml:${sParam.key}>([\\s\\S]*?)<\\/wpml:${sParam.key}>`)
      const value: string[] = actionParam[0]?.match(infoRegx) || ['']
      sParam.value = value[1] || ''
    })
  })
}
/**
 * @description: 创建kml模版
 * @param {*} actionId 事件id(唯一)
 * @param {*} actionType 事件类型
 * @param {*} param 事件参数
 * @return {*} void
 */
const createActionKML = (actionId: string, actionType: string, param: any) => {
  let paramKML = ''
  Object.keys(param).forEach((key: string) => {
    paramKML += `
      <wpml:${key}>${param[key]}</wpml:${key}>
    `
  })
  const template = `
    <wpml:action>
      <wpml:actionId>${actionId}</wpml:actionId>
      <wpml:actionActuatorFunc>${actionType}</wpml:actionActuatorFunc>
      <wpml:actionActuatorFuncParam>
        ${paramKML}
      </wpml:actionActuatorFuncParam>
    </wpml:action>
  `
  return template
}
/**
 * @description: Popover参数编辑
 * @param {*} btnobj: 事件按钮所有参数
 * @param {*} eventType 事件类型
 * @param {*} eventParam 事件参数Param
 * @return {*}
 */
const paramPopover = (eventType: string, btnobj: sBtn, eventParam?: any) => {
  // 判断参数是否为有存在空的
  let isParamNull = false
  btnobj.param.some((sParam: any) => {
    isParamNull = sParam.value === ''
    return isParamNull
  })
  if (isParamNull) return message.warning('确认失败,请检查填写的参数')
  // 参数处理
  const param = { ...eventParam }
  btnobj.param.forEach((sParam: { key: string, value: string | number }) => {
    param[sParam.key] = sParam.value
  })
  editKmlFile(eventType, param)
  btnobj.selected = true
}
const saveInitPopoverParam = (param: popoverParamType[]) => {
  initPopoverParam.value = cloneDeep(param)
}
const resetParamSetting = (btnParam: sBtn) => {
  btnParam.param = cloneDeep(initPopoverParam.value)
}
/**
 * @description: 修改template.kml文件
 * @param {*} eventType 事件类型
 * @param {*} eventParam 事件参数
 * @return {*} void
 */
const editKmlFile = (eventType: string, eventParam?: any) => {
  // kmzZip文件处理
  const kmlFile = kmzFileZip.value.file(/\.kml$/i)[0]
  kmlFile.async('text').then((kmlText: string) => {
    const regx = /<Placemark>([\s\S]*?)<\/Placemark>/g
    const points = kmlText.match(regx) || []
    // 当前要修改的字段
    // const curSelected: string | any = points?.find(item => item.includes(currentPosition.value.join(',')))
    const curSelected: string | any = points[currentPosition.value]
    // 判断是不是间隔拍照任务
    if (eventType === 'time' || eventType === 'distance' || eventType === 'deleteShootType') {
      // 判断当前选中的点是否存在shootType
      const shootTypeRegx = /<wpml:shootType>([\s\S]*?)<\/wpml:shootType>/g
      const kmlResult: string[] = curSelected.match(shootTypeRegx) || []
      const shootTypeKML = kmlResult[0]
      let replacedKML = ''
      // 判断是不是结束间隔拍照任务
      if (eventType === 'deleteShootType') {
        if (!shootTypeKML) {
          message.warning('暂未设置间隔任务,无法结束!!')
        } else {
          replacedKML = curSelected.replace(shootTypeRegx, '')
        }
      } else {
        // 判断是否已经设置间隔拍照任务
        if (shootTypeKML) {
          replacedKML = curSelected.replace(shootTypeRegx, `<wpml:shootType>${eventType}</wpml:shootType>`)
        } else {
          // 判断是否存在actionGroup
          const index = curSelected.indexOf('<wpml:actionGroup>')
          if (index === -1) {
            const placeMarkIdx = curSelected.indexOf('</Placemark>')
            replacedKML = curSelected.slice(0, placeMarkIdx) + `<wpml:shootType>${eventType}</wpml:shootType>` + curSelected.slice(placeMarkIdx)
          } else {
            replacedKML = curSelected.slice(0, index) + `<wpml:shootType>${eventType}</wpml:shootType>` + curSelected.slice(index)
          }
        }
      }
      // 将另外两个间隔任务按钮高亮取消
      btnList.value.forEach((sBtn: sBtn) => {
        if (sBtn.key === 'time' || sBtn.key === 'distance' || sBtn.key === 'deleteShootType') {
          if (sBtn.key !== eventType) {
            sBtn.selected = false
          }
        }
      })
      // 替换原本KML
      const replacedTemplateKML = kmlText.replace(curSelected, replacedKML)
      JsZip.file('wpmz/template.kml', replacedTemplateKML)
      kmzFileZip.value.file('wpmz/template.kml', replacedTemplateKML)
      return
    }
    // 查找actionGroup
    const actionGroupReg = /<wpml:actionGroup>([\s\S]*?)<\/wpml:actionGroup>/g
    const actionGroup = curSelected.match(actionGroupReg) ? curSelected.match(actionGroupReg)[0] : ''
    // 查找action
    const actionReg = /<wpml:action>([\s\S]*?)<\/wpml:action>/g
    const actions = actionGroup.match(actionReg) || []
    // 判断kml文件中是否存在当前事件
    const actionIndex = actions.findIndex((action: string) => action.includes(eventType))
    if (actionIndex === -1) {
      // 创建事件
      const actionKML = createActionKML(actions.length, eventType, eventParam)
      actions.push(actionKML)
    } else {
      // 移除当前存在的事件
      actions.splice(actionIndex, 1)
    }
    let replacedActionsKML = ''
    // 创建actionGroup
    const actionGroupKML = `
      <wpml:actionGroup>
        ${actions.join('')}
      </wpml:actionGroup>
    `
    // 判断是否存在事件组
    if (!actionGroup) {
      const index = curSelected.indexOf('</Placemark>')
      replacedActionsKML = curSelected.slice(0, index) + actionGroupKML + curSelected.slice(index)
    } else {
      // 替换原有的action
      replacedActionsKML = curSelected.replace(actionGroupReg, actionGroupKML)
    }
    // Placemark代替
    const replacedTemplateKML = kmlText.replace(curSelected, replacedActionsKML)
    JsZip.file('wpmz/template.kml', replacedTemplateKML)
    kmzFileZip.value.file('wpmz/template.kml', replacedTemplateKML)
  })
}
const cencalEdit = () => {
  readKmzFile(curKmzPath.value).then(kmzFile => {
    kmzFileZip.value = kmzFile
    eventLight()
  })
}
const confirmEdit = () => {
  JsZip.generateAsync({ type: 'blob' }).then(content => {
    saveAs(content, curKmzName.value)
  })
}
onMounted(() => {
  kmzFileZip.value = null
onUnmounted(() => {
  isMoreActionShow.value = false
})
</script>
<style lang="scss" scoped>
.btn-list {
.action-container {
  .more-btn {
  position: absolute;
  right: 75px;
  top: 50%;
    right: 20px;
  transform: translateY(-50%);
  .s-btn {
    margin-bottom: 15px;
    display: flex;
    align-items: center;
    &:last-child {
      margin: 0;
    }
    .title {
      width: 130px;
      text-align: right;
      margin-right: 10px;
      color: #fff;
      text-shadow: 2px 2px 2px #000;
    .label {
      width: 60px;
      line-height: 34px;
      font-weight: bold;
      text-align: right;
      color: #fff;
      text-shadow: 2px 2px 2px #000000;
    }
    .btn {
    .icon {
      width: 34px;
      height: 34px;
      background-color: #323131;
      color: #fff;
      line-height: 34px;
      text-align: center;
      font-size: 25px;
      margin-left: 7px;
      background-color: rgba(0, 0, 0, .5);
      cursor: pointer;
    }
  }
  .action-list {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    right: 120px;
    padding: 0;
    margin: 0;
    list-style-type: none;
    &_item {
      width: fit-content;
      height: 34px;
      display: flex;
      justify-content: center;
      color: #fff;
      margin-top: 10px;
      &:first-child {
        margin: 0;
      }
      .name {
        width: 120px;
        line-height: 34px;
        text-align: right;
        font-weight: bold;
        text-shadow: 2px 2px 2px #000000;
      }
      .icon {
        width: 34px;
        height: 34px;
        padding: 3px;
        margin-left: 7px;
        background-color: rgba(0, 0, 0, .5);
        display: flex;
      align-items: center;
      img {
        width: 20px;
      }
    }
  }
  .cancel-edit {
    font-size: 20px;
    color: #fff;
    &:hover {
      background-color: #FF4D4F;
    }
  }
  .confirm-edit {
    font-size: 20px;
    color: #fff;
    &:hover {
      background-color: #1777FF;
    }
  }
}
.popover-content {
  .content-box {
    margin-bottom: 10px;
    .annotation {
      font-size: 12px;
      color: rgb(188, 187, 187);
      max-width: 300px;
    }
  }
  .btn-tool {
    display: flex;
    justify-content: center;
    gap: 10px;
    .ant-btn {
      flex: 1;
        cursor: pointer;
        img {
          width: 25px;
          height: 25px;
    }
  }
}
:deep() {
  .ant-radio-wrapper {
    display: block;
    max-width: 300px;
    text-wrap: wrap;
  }
}
</style>
src/pages/page-web/projects/components/route-edit/components/setting-point-tip.vue
@@ -10,6 +10,8 @@
import * as Cesium from 'cesium'
import useKmzTsa, { template as xmlTemplate } from '/@/utils/cesium/use-kmz-tsa'
import useMapDraw, { kmlEntities as globalEntities } from '/@/utils/cesium/use-map-draw'
import { useMyStore } from '/@/store'
const store = useMyStore()
const { appContext }: any = getCurrentInstance()
const global = appContext.config.globalProperties
const kmzUtils = useKmzTsa()
@@ -37,6 +39,9 @@
  const height = 6
  const takeOffPoint = [latitude, longitude, height]
  xmlTemplate.value.missionConfig.takeOffRefPoint['#text'] = takeOffPoint.join(',')
  store.commit('SET_WAYLINE_INFO', {
    isShow: true,
  })
  const xmlStr = kmzUtils.generateXML(xmlTemplate.value)
  mapDraw.drawWayline(globalEntities.value, xmlStr)
}
src/pages/page-web/projects/components/route-edit/index.vue
@@ -59,7 +59,16 @@
        </a-tooltip>
        <contextMenu :menu="[{ title: '删除航点', value: { index, item } }]">
          <div class="graph-right">
            <div v-for="(event, index) in item.eventList" :key="index" class="s-event-icon">
            <div
              v-for="(event, eIndex) in item.eventList"
              :key="eIndex"
              :class="[
                's-event-icon',
                selectedPointAndAction.selectedPoint === index &&
                  selectedPointAndAction.selectedAction === event?.key &&
                  'active-event',
              ]"
              @click="selectionEvent(event, index)">
              <a-tooltip placement="top">
                <template #title>
                  {{ event?.name }}
@@ -177,7 +186,9 @@
        deep: true,
      },
    )
    store.commit('SET_WAYLINE_INFO', {
      isShow: true,
    })
    kmlEntities.value = entities.value
    if (!mouseRightClickEvent) {
      // 添加右键事件
@@ -188,13 +199,7 @@
// 新建航线初始化
const initCreateRoute = () => {
  mapDraw = useMapDraw('', [], '', global)
  const {
    drawWayline,
    waylineDetails: details,
    waylinePointsEvent,
    kmlEntities: entities,
    clearWaylineData
  } = mapDraw
  const { drawWayline, waylineDetails: details, waylinePointsEvent, kmlEntities: entities, clearWaylineData } = mapDraw
  clearWaylineData()
  drawWayline()
  waylineDetails.value = details
@@ -244,17 +249,18 @@
const selectPointIndex = ref<string | number | any>(null)
const pointSelect = (value: tragetPoint, index: number) => {
  removeCesiumChildDom(popupDom)
  if (selectPointIndex.value === index) {
    global.$viewer.entities.remove(prevPointEntity)
    global.$viewer.entities.remove(nextPointEntity)
    kmlEntities.value.forEach((entity: Cesium.Entity | any, i: number) => {
      entity.billboard.image = createBillboard(i + 1, '#61d396')
      entity.label.show = false
    })
    selectPointIndex.value = null
    pointIdx.value = null
    return
  }
  // if (selectPointIndex.value === index) {
  //   global.$viewer.entities.remove(prevPointEntity)
  //   global.$viewer.entities.remove(nextPointEntity)
  //   kmlEntities.value.forEach((entity: Cesium.Entity | any, i: number) => {
  //     entity.billboard.image = createBillboard(i + 1, '#61d396')
  //     entity.label.show = false
  //   })
  //   selectPointIndex.value = null
  //   pointIdx.value = null
  //   store.commit('SET_WAYLINE_INFO', {})
  //   return
  // }
  if (prevPointEntity) {
    global.$viewer.entities.remove(prevPointEntity)
  }
@@ -300,6 +306,15 @@
      entity.label.show = false
    }
  })
  // 默认选中第一个事件
  // if (selectPointIndex.value !== index) {
  //   store.commit('SET_WAYLINE_INFO', {
  //     selectedPoint: index,
  //     // 默认选中第一个
  //     selectedAction: value.eventList[0],
  //   })
  //   selectionEvent(value.eventList[0], index)
  // }
  selectPointIndex.value = index
  pointIdx.value = index
}
@@ -536,6 +551,25 @@
  },
)
// 选中事件
const selectedPointAndAction = reactive<{
  selectedPoint: number | null
  selectedAction: any | null
}>({
  selectedPoint: null,
  selectedAction: null,
})
const selectionEvent = (action: any, index: number) => {
  const params = {
    selectedPoint: index,
    selectedAction: action,
  }
  selectedPointAndAction.selectedPoint = index
  selectedPointAndAction.selectedAction = action.key
  console.log(params)
  store.commit('SET_WAYLINE_INFO', params)
}
onMounted(() => {
  // 清空画布
  clearCesiumMap()
@@ -553,8 +587,6 @@
  clearCesiumMap()
  store.commit('SET_WAYLINE_INFO', {
    isShow: false,
    wayline: {},
    position: null,
  })
})
</script>
@@ -695,16 +727,27 @@
        align-items: center;
        .s-event-icon {
          width: 25px;
          height: 25px;
          width: 20px;
          height: 20px;
          display: flex;
          justify-content: center;
          align-items: center;
          border-radius: 2px;
          margin-right: 3px;
          overflow: hidden;
          &:hover {
            background-color: #409eff;
          }
          img {
            width: 70%;
          }
        }
        .active-event {
          background-color: #409eff;
        }
      }
      &:hover {
src/pages/page-web/projects/wayline.vue
@@ -275,9 +275,7 @@
  removeAllPoint()
  // removeById('clickBox')
  store.commit('SET_WAYLINE_INFO', {
    isShow: false,
    wayline: {},
    position: null,
    isShow: false
  })
})
@@ -353,9 +351,7 @@
    })
  // 清除选中点柱形和隐藏按钮
  store.commit('SET_WAYLINE_INFO', {
    isShow: false,
    wayline: {},
    position: null,
    isShow: false
  })
  // isPointListOpen.value = !isPointListOpen.value
}
@@ -395,9 +391,7 @@
function backPage () {
  isPointListOpen.value = false
  store.commit('SET_WAYLINE_INFO', {
    isShow: false,
    wayline: {},
    position: 0,
    isShow: false
  })
}
src/store/index.ts
@@ -99,8 +99,8 @@
  clientId: '', // mqtt 连接 唯一客户端id,
  waylineTool: {
    isShow: false as boolean,
    wayline: {} as any,
    position: null as any,
    selectedPoint: undefined as number | undefined,
    selectedAction: undefined as any | undefined,
    kmzPath: '' as string
  }
})
@@ -211,10 +211,10 @@
    state.clientId = clientId
  },
  // 设置wayline中的信息
  SET_WAYLINE_INFO (state, { isShow, wayline, position }) {
    state.waylineTool.isShow = isShow
    state.waylineTool.wayline = wayline
    state.waylineTool.position = position
  SET_WAYLINE_INFO (state, { isShow, selectedPoint, selectedAction }) {
    isShow !== undefined && (state.waylineTool.isShow = isShow)
    state.waylineTool.selectedPoint = selectedPoint
    selectedAction !== undefined && (state.waylineTool.selectedAction = selectedAction)
  },
  SET_WAYLINE_KMZPATH (state, kmzPath) {
    state.waylineTool.kmzPath = kmzPath
src/utils/cesium/use-map-draw.ts
@@ -87,6 +87,11 @@
    name: '悬停等待',
    icon: getResource('waylinetool/xt.png'),
  },
  {
    key: 'panoShot',
    name: '全景拍摄',
    icon: getResource('waylinetool/panoramicview.png'),
  }
]
const waylinePointsEvent = ref<tragetPoint[]>([])
@@ -352,5 +357,5 @@
  }
}
export { waylinePointsEvent, waylineDetails, kmlEntities, selectPointIndex }
export { waylinePointsEvent, waylineDetails, kmlEntities, selectPointIndex, actionList }
export default useMapDraw