Pārlūkot izejas kodu

添加组件 无人机飞行管理

彭宇 1 mēnesi atpakaļ
vecāks
revīzija
9f5bf08585
100 mainītis faili ar 28794 papildinājumiem un 0 dzēšanām
  1. 1 0
      package.json
  2. 28 0
      src/components/common-comp-uav-fly-manage/demo/basic.demo.vue
  3. 23 0
      src/components/common-comp-uav-fly-manage/demo/index.demo-entry.md
  4. BIN
      src/components/common-comp-uav-fly-manage/demo/screens/image.png
  5. 5 0
      src/components/common-comp-uav-fly-manage/index.js
  6. 19 0
      src/components/common-comp-uav-fly-manage/package.json
  7. 289 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/airLineHeight/AirLineHeight.vue
  8. 146 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/checkboxControl/CheckboxControl.vue
  9. 103 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/checkboxGroup/CheckboxGroup.vue
  10. 67 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/closeFormItem/CloseFormItem.vue
  11. 92 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/commonTitle/CommonTitle.vue
  12. 154 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/flightNumberList/FlightNumberList.vue
  13. 212 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/FlyActiconCard.vue
  14. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-card-active.png
  15. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-card.png
  16. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-title-active.png
  17. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-title.webp
  18. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/title_maozi.webp
  19. 217 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyResponsiveCard/FlyResponsiveCard.vue
  20. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyResponsiveCard/bg_card_title.webp
  21. BIN
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyResponsiveCard/title_maozi.webp
  22. 132 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/flyToFirstPointType/FlyToFirstPointType.vue
  23. 217 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/intersectionDetectionPop/IntersectionDetectionPop.vue
  24. 91 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/isFollowLine/IsFollowLine.vue
  25. 65 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/mouseTip/MouseTip.vue
  26. 235 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/orthoImageTitle/OrthoImageTitle.vue
  27. 299 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/restrictedFlightZone/RestrictedFlightZone.vue
  28. 262 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/sliderControl/SliderControl.vue
  29. 223 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/stepInput/StepInput.vue
  30. 305 0
      src/components/common-comp-uav-fly-manage/src/baseComponents/warningList/WarningList.vue
  31. 176 0
      src/components/common-comp-uav-fly-manage/src/components/airLineCard/AirLineCard.vue
  32. 2494 0
      src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/AirPointAirLine.vue
  33. 69 0
      src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/Index.vue
  34. 12 0
      src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/ObliqueImage.vue
  35. 2159 0
      src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/OrthoPhoto.vue
  36. 12 0
      src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/SurroundingFlight.vue
  37. 650 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/AirLineMana.vue
  38. 503 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AddAirLine.vue
  39. 170 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AirLineDel.vue
  40. 386 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AirLineFliter.vue
  41. 627 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AirLineitem.vue
  42. 523 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/ImportAirLine.vue
  43. 300 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/RenamLineName.vue
  44. 253 0
      src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/TopArea.vue
  45. 304 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AirLinePointList.vue
  46. 223 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AirPointActions.vue
  47. 275 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AirPointAirLineHeader.vue
  48. 281 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AuxiliaryViewWindow.vue
  49. 1333 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/BottomOptions.vue
  50. 32 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/CurrentPointDelete.vue
  51. 286 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/HeaderActions.vue
  52. 457 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/PointLineOverView.vue
  53. 38 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/PopInfoHeight.vue
  54. 864 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/AccurateRetake.vue
  55. 124 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/Rename.vue
  56. 204 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/RenameFileName.vue
  57. 48 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/Space.vue
  58. 257 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/VirtualSnapshotPreview.vue
  59. 150 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/frustum-flypoints.js
  60. 170 0
      src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/frustum-view.js
  61. 104 0
      src/components/common-comp-uav-fly-manage/src/components/checkboxs/CheckboxInterval.vue
  62. 532 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/DialogFlyAlgorithm.vue
  63. 194 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/DialogFlyEditName.vue
  64. 587 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/DialogFlyMap.vue
  65. 140 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyAttribute.vue
  66. 171 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImg.vue
  67. 68 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgContent.vue
  68. 691 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnail.vue
  69. 207 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnailCard.vue
  70. 110 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnailOne.vue
  71. 287 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnailTab.vue
  72. 406 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyList.vue
  73. 163 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyListCard.vue
  74. 644 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyMap.vue
  75. 172 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyPagination.vue
  76. 47 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyResult.vue
  77. 124 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyResultInfoWindow.vue
  78. 108 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyRightTopTab.vue
  79. 82 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyTab.vue
  80. 317 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyTree.vue
  81. 138 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/MediumViewer.vue
  82. 679 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/DetailLeftPop.vue
  83. 333 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/DialogFrameExtraction.vue
  84. 397 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/DialogFrameExtractionProgress.vue
  85. 155 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/FlyVideo.vue
  86. 88 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/FlyVideoContent.vue
  87. 579 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/FrameExtraction.vue
  88. 633 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoResultList.vue
  89. 855 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoResultOneDetail.vue
  90. 318 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoResultTopLine.vue
  91. 228 0
      src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoShow.vue
  92. 45 0
      src/components/common-comp-uav-fly-manage/src/components/operationPlan/AirLineWindow.vue
  93. 953 0
      src/components/common-comp-uav-fly-manage/src/components/operationPlan/OperationPlan.vue
  94. 56 0
      src/components/common-comp-uav-fly-manage/src/components/operationPlan/OrthoImageWindow.vue
  95. 162 0
      src/components/common-comp-uav-fly-manage/src/components/planMana/PlanCopy.vue
  96. 725 0
      src/components/common-comp-uav-fly-manage/src/components/planMana/PlanMana.vue
  97. 36 0
      src/components/common-comp-uav-fly-manage/src/components/selectTree/Demo.vue
  98. 595 0
      src/components/common-comp-uav-fly-manage/src/components/selectTree/SelectTree.vue
  99. 100 0
      src/components/common-comp-uav-fly-manage/src/components/tabSelect/TabSelect.vue
  100. 0 0
      src/components/common-comp-uav-fly-manage/src/dict/air-line-mana-dict.js

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "webworkify-webpack": "^2.1.5"
   },
   "dependencies": {
+    "xml-js": "^1.6.11",
     "@ct/audio-stream": "^1.0.0",
     "@ct/component-gallery-intelligent-middle": "1.0.8",
     "@ct/component-gallery-theme-chalk": "^1.0.20",

+ 28 - 0
src/components/common-comp-uav-fly-manage/demo/basic.demo.vue

@@ -0,0 +1,28 @@
+<markdown>
+# 飞行管理组件
+</markdown>
+
+<markdown-code>
+<d-uav-fly-manage
+  :mapId="mapId"
+  memoryKey="memoryKey"
+/>
+</markdown-code>
+
+<template>
+  <div>
+    <n-image width="100%" height="100%" :src="url" />
+  </div>
+</template>
+
+<script>
+import picking from './screens/demo.png'
+
+export default {
+  data() {
+    return {
+      url: picking
+    }
+  }
+}
+</script>

+ 23 - 0
src/components/common-comp-uav-fly-manage/demo/index.demo-entry.md

@@ -0,0 +1,23 @@
+# 飞行管理组件
+
+飞行管理组件
+
+## 代码演示
+
+```demo
+basic.vue
+```
+
+## API
+
+### 属性
+
+| 参数        | 类型    | 默认值           | 说明                                   | 版本  |
+| ----------- | ------- | ---------------- | -------------------------------------- | ----- |
+| mapId       | string  | 'mapId'          | 初始化地图的 dom                       | 1.0.0 |
+
+### 事件
+
+- 组件内抛出函数
+  | 事件名称 | 说明 | 回调参数 | 参数说明 |
+  | ----------------------------------------| ---------------------- | -------- | ----------------- |

BIN
src/components/common-comp-uav-fly-manage/demo/screens/image.png


+ 5 - 0
src/components/common-comp-uav-fly-manage/index.js

@@ -0,0 +1,5 @@
+import UavFlyManage from './src/entry/UavFlyManage.vue'
+UavFlyManage.install = (Vue) => {
+  Vue.component(UavFlyManage.name, UavFlyManage)
+}
+export default UavFlyManage

+ 19 - 0
src/components/common-comp-uav-fly-manage/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "@component-gallery/uav-fly-manage",
+  "version": "0.0.1",
+  "description": "飞行管理组件",
+  "dependencies": {
+    "@component-gallery/assets": "workspace:^",
+    "@component-gallery/base-components": "workspace:^",
+    "@component-gallery/build-event-bus-path": "workspace:^",
+    "@component-gallery/map": "workspace:^",
+    "@component-gallery/theme-chalk": "workspace:^",
+    "@component-gallery/utils": "workspace:^",
+    "@turf/turf": "^7.1.0",
+    "jszip": "^3.10.1",
+    "screenfull": "4.2.0",
+    "uuid": "^10.0.0",
+    "vuedraggable": "2.24.3",
+    "xml-js": "^1.6.11"
+  }
+}

+ 289 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/airLineHeight/AirLineHeight.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="height_cont">
+    <div class="header">
+      <div
+        :class="['header_item', item.value == currentIndex && 'active']"
+        v-for="(item, index) in header"
+        :key="index + 'header'"
+        @click="handleItemClick(item.value)"
+      >
+        {{ item.name }}
+      </div>
+    </div>
+    <div class="pic_des">
+      <img :src="picMap[currentIndex]" alt="" />
+    </div>
+
+    <styled-dialog
+      industryClass="common-iw-s fly_height_change_dialog"
+      :visible="dialogShow"
+      title="高度模式切换"
+      @close="close"
+      :modal="true"
+      :canClose="true"
+      :appendToBody="true"
+      width="6rem"
+    >
+      <div class="fly_height_change_dialog_body">
+        <template v-if="clickIndex == 'AGLP' || preIndex == 'AGLP'">
+          <div class="title">{{ heightChangeTips['AGLP'].title }}</div>
+          <div class="cont" v-html="heightChangeTips['AGLP'].content"></div>
+        </template>
+        <template v-if="clickIndex != 'AGLP' && preIndex != 'AGLP'">
+          <div class="title">{{ heightChangeTips[clickIndex].title }}</div>
+          <div class="cont" v-html="heightChangeTips[clickIndex].content"></div>
+        </template>
+        <div class="footer">
+          <template v-if="clickIndex == 'AGLP' || preIndex == 'AGLP'">
+            <base-button
+              type="primary"
+              :height="32"
+              :width="108"
+              class
+              @click="handleSubmit('keep')"
+            >
+              保持不变
+            </base-button>
+            <base-button
+              type="primary"
+              :height="32"
+              :width="108"
+              @click="handleSubmit('changeHeight')"
+            >
+              重新计算
+            </base-button>
+          </template>
+          <template v-if="clickIndex != 'AGLP' && preIndex != 'AGLP'">
+            <base-button
+              type="primary"
+              :height="32"
+              :width="108"
+              @click="handleSubmit('sure')"
+            >
+              确定
+            </base-button>
+          </template>
+          <base-button :height="32" :width="108" @click="close">
+            取消
+          </base-button>
+        </div>
+      </div>
+    </styled-dialog>
+  </div>
+</template>
+
+<script>
+import jdgd from '../../img/airLine/height_jdgd.webp'
+import xdqfd from '../../img/airLine/height_xdqfd.png'
+import xddm from '../../img/airLine/height_dimian.webp'
+
+import StyledDialog from '@component-gallery/base-components/styled-dialog/StyledDialog.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+
+const heightChangeTips = {
+  ASLP: {
+    title: '航线将转换至“绝对高度”模式',
+    content: `已规划的各个航点将按照“相已规划的各个航点将按照“绝对高度”模式重新计算。<br/>各航点的绝对高度=各个航点的相对起飞点高度+设置起飞点的绝对高度。`
+  },
+  ALTP: {
+    title: '航线将转换至“相对起飞点高度”模式',
+    content: `已规划的各个航点将按照“相对起飞点高度”模式重新计算。<br/>各航点的相对起飞点高度=各个航点的绝对高度-设置起飞点的绝对高度。`
+  },
+  AGLP: {
+    title: '是否重新计算航点空间位置?',
+    content: `保持不变:航点空间位置保持不变,航点高度值显示新高度模式下的高度值。<br/>重新计算:根据已设置的航点高度,重新计算航点在新高度模式下的空间位置。`
+  }
+}
+export default {
+  name: 'lineHeight',
+  components: {
+    StyledDialog,
+    BaseButton
+  },
+  /**
+   * 获取指定位置的其他高度模式的高度值
+   * @param {*} positions [{lng, lat}, {lng, lat}]
+   * @param {*} nestHeight 飞点高度 (相对起飞点高度)
+   * @param {*} heightMode 高度模式:ALTP-相对起飞点高度,AGLP-相对地面高度,ASLP-相对海平面(海拔),ASLT-相对椭球(高程)
+   * @param {*} heightVal 高度值
+   * @return {ASLT:椭球高/高程, ASLP:海拔高度, ALTP:相对起飞点高度, AGLP:相对地面(地形)高度}
+   * @des 获取指定位置的特定高度模式的高度值。异步函数
+   */
+  data() {
+    return {
+      header: [
+        {
+          name: '绝对高度',
+          value: 'ASLP'
+        },
+        {
+          name: '相对起飞点',
+          value: 'ALTP'
+        },
+        {
+          name: '相对地面',
+          value: 'AGLP'
+        }
+      ],
+      currentIndex: 'ALTP',
+      clickIndex: 'ALTP',
+      preIndex: null,
+      picMap: {
+        ASLP: jdgd,
+        ALTP: xdqfd,
+        AGLP: xddm
+      },
+      dialogShow: false,
+      heightChangeTips
+    }
+  },
+  props: {
+    value: {
+      type: [String, Number],
+      default: ''
+    },
+    noDialog: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.currentIndex = val || 'ASLP'
+        this.preIndex = val || 'ASLP'
+      }
+    }
+  },
+  methods: {
+    handleItemClick(index) {
+      this.clickIndex = index
+      if (this.noDialog) {
+        this.handleSubmit()
+      } else {
+        if (this.clickIndex != this.currentIndex) {
+          this.dialogShow = true
+        }
+      }
+    },
+    close() {
+      this.dialogShow = false
+    },
+    handleSubmit(type) {
+      console.log('this.preIndex', this.preIndex)
+      this.currentIndex = this.clickIndex
+      this.dialogShow = false
+      this.$emit('input', this.currentIndex)
+      this.$emit('change', this.currentIndex)
+      this.$emit('lineHeightSubmit', this.currentIndex, type, this.preIndex)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin textShadow {
+  text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+}
+.height_cont {
+  width: 100%;
+  .header {
+    width: 100%;
+    height: px-to-rem(48);
+    line-height: px-to-rem(48);
+    position: relative;
+    @include flex;
+    &::after {
+      content: '';
+      position: absolute;
+      width: 100%;
+      height: px-to-rem(1);
+      bottom: 0;
+      background: linear-gradient(
+        270deg,
+        rgba(176, 212, 255, 0) 0%,
+        #b0d4ff 52%,
+        rgba(176, 212, 255, 0) 100%
+      );
+      opacity: 0.35;
+      z-index: 1;
+    }
+    .header_item {
+      width: 33.33%;
+      text-align: center;
+      font-size: px-to-rem(16);
+      color: rgba(232, 243, 254, 0.7);
+      position: relative;
+      cursor: pointer;
+      &.active {
+        color: #e8f3fe;
+        @include textShadow;
+        &::after {
+          content: '';
+          position: absolute;
+          width: 100%;
+          height: px-to-rem(11);
+          bottom: 0;
+          left: 0;
+          z-index: 1;
+          background: url('../../img/airLine/bg_title_guang_33.webp') no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+    }
+  }
+  .pic_des {
+    margin-top: px-to-rem(12);
+    width: 100%;
+    height: px-to-rem(106);
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+.fly_height_change_dialog_body {
+  padding: px-to-rem(12);
+  .title,
+  .cont {
+    font-size: px-to-rem(16);
+    color: #e8f3fe;
+  }
+  .footer {
+    text-align: center;
+    margin-top: px-to-rem(12);
+    ::v-deep .base-button {
+      .base-button_content {
+        font-size: px-to-rem(16);
+        color: #e8f3fe;
+      }
+    }
+    .base-button + .base-button {
+      margin-left: px-to-rem(12);
+    }
+  }
+}
+.fly_height_change_dialog {
+  ::v-deep .el-dialog {
+    margin-top: px-to-rem(350) !important;
+  }
+}
+</style>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.fly_height_change_dialog {
+  .el-dialog {
+    margin-top: 30vh !important;
+    .el-dialog__header span {
+      font-size: px-to-rem(18);
+      font-weight: 600;
+    }
+  }
+}
+</style>

+ 146 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/checkboxControl/CheckboxControl.vue

@@ -0,0 +1,146 @@
+<template>
+  <div class="checkbox-control">
+    <div class="option-container">
+      <div
+        class="checkbox-label"
+        :class="{
+          active: selectedValue.includes(item.value),
+          norml: !selectedValue.includes(item.value),
+          disabled:
+            disabled ||
+            (selectedValue.includes(item.value) && selectedValue.length === 1)
+        }"
+        v-for="(item, index) in options"
+        :key="index"
+        @click="handleCheck(item.value)"
+      >
+        <div
+          :class="{ checkboxActive: selectedValue.includes(item.value) }"
+        ></div>
+        {{ item.label }}</div
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'checkboxControl',
+  data() {
+    return {
+      selectedValue: []
+    }
+  },
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    },
+    options: {
+      type: Array,
+      default: () => []
+    },
+    disabled: Boolean
+  },
+  model: {
+    prop: 'value',
+    event: 'input'
+  },
+  watch: {
+    value: {
+      immediate: true,
+      deep: true,
+      handler(val) {
+        console.log(val, 'watch监听')
+        this.selectedValue = val
+      }
+    }
+  },
+  methods: {
+    handleCheck(value) {
+      if (this.disabled) {
+        return
+      }
+      const index = this.selectedValue.indexOf(value)
+      if (index === -1) {
+        this.selectedValue.push(value)
+      } else {
+        if (this.selectedValue?.length !== 1) {
+          this.selectedValue.splice(index, 1)
+        }
+      }
+      this.$emit('input', this.selectedValue)
+      this.$emit('change', this.selectedValue)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.checkbox-control {
+  .option-container {
+    display: flex;
+    align-items: center;
+  }
+  .checkboxActive {
+    left: 0;
+    top: 0;
+    width: px-to-rem(82);
+    height: px-to-rem(32);
+    position: absolute;
+    z-index: -1;
+    background: linear-gradient(
+        287deg,
+        rgba(19, 115, 230, 0.11) 0%,
+        rgba(19, 115, 230, 0.11) 0%,
+        rgba(19, 115, 230, 0.3) 100%
+      ),
+      radial-gradient(
+        206% 82% at 13% 50%,
+        rgba(19, 115, 230, 0.33) 0%,
+        rgba(19, 115, 230, 0.01) 69%,
+        rgba(19, 115, 230, 0) 100%
+      );
+    border-radius: px-to-rem(6);
+  }
+  .checkbox-label {
+    width: px-to-rem(82);
+    height: px-to-rem(32);
+    background: url('../../img/checkbox-control-bg.png') no-repeat;
+    background-size: 100% 100%;
+    position: relative;
+    line-height: px-to-rem(32);
+    padding-left: px-to-rem(28);
+    cursor: pointer;
+    z-index: 0;
+    & + .checkbox-label {
+      margin-left: px-to-rem(12);
+    }
+    &.active::after {
+      content: '';
+      height: px-to-rem(24);
+      width: px-to-rem(24);
+      position: absolute;
+      top: px-to-rem(6);
+      left: px-to-rem(2);
+      background: url('../../img/check-status.png') no-repeat;
+      background-size: 100% 100%;
+    }
+    // &.norml::after {
+    //   content: '';
+    //   height: px-to-rem(16);
+    //   width: px-to-rem(16);
+    //   position: absolute;
+    //   left: px-to-rem(4);
+    //   top: px-to-rem(8);
+    //   background: url('../../img/check-status_nor.png') no-repeat;
+    //   background-size: 100% 100%;
+    // }
+    &.disabled {
+      opacity: 0.7;
+      cursor: not-allowed;
+    }
+  }
+}
+</style>

+ 103 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/checkboxGroup/CheckboxGroup.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="checkbox-control">
+    <div class="option-container">
+      <div
+        class="checkbox-label"
+        :class="{
+          active: selectedValue.includes(item[optionKeys.value])
+        }"
+        v-for="(item, index) in options"
+        :key="index"
+        @click="handleCheck(item[optionKeys.value])"
+        >{{ item[optionKeys.label] }}</div
+      >
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'checkboxControl',
+  data() {
+    return {
+      selectedValue: []
+    }
+  },
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    },
+    options: {
+      type: Array,
+      default: () => []
+    },
+    optionKeys: {
+      type: Object,
+      default: () => {
+        return {
+          label: 'label',
+          value: 'value'
+        }
+      }
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      deep: true,
+      handler(val) {
+        this.selectedValue = val
+      }
+    }
+  },
+  methods: {
+    handleCheck(value) {
+      const index = this.selectedValue.indexOf(value)
+      if (index === -1) {
+        this.selectedValue.push(value)
+      } else {
+        if (this.selectedValue?.length !== 1) {
+          this.selectedValue.splice(index, 1)
+        }
+      }
+      this.$emit('input', this.selectedValue)
+      this.$emit('change', this.selectedValue)
+      this.$emit('update:value', this.selectedValue)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.checkbox-control {
+  .option-container {
+    display: flex;
+    align-items: center;
+  }
+  .checkbox-label {
+    width: px-to-rem(82);
+    height: px-to-rem(32);
+    background: url('../../img/checkbox-control-bg.png') no-repeat;
+    background-size: 100% 100%;
+    position: relative;
+    line-height: px-to-rem(32);
+    padding-left: px-to-rem(28);
+    cursor: pointer;
+    & + .checkbox-label {
+      margin-left: px-to-rem(12);
+    }
+    &.active::after {
+      content: '';
+      height: px-to-rem(24);
+      width: px-to-rem(24);
+      position: absolute;
+      top: px-to-rem(6);
+      left: 0;
+      background: url('../../img/check-status.png') no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+}
+</style>

+ 67 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/closeFormItem/CloseFormItem.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="close-form-item">
+    <div class="left-cont">
+      <div class="icon-wrapper">
+        <slot></slot>
+      </div>
+      <div class="title">{{ title }}</div>
+    </div>
+    <span
+      @click="$emit('close')"
+      class="iconfont_tools icon-linye_icon_shanchu close-icon"
+    ></span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CloseFormItem',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.close-form-item {
+  padding: 0 px-to-rem(12);
+  display: flex;
+  align-items: center;
+  width: px-to-rem(346);
+  height: px-to-rem(32);
+  justify-content: space-between;
+  background: url('../../img/close-form-item-bg.png') no-repeat;
+  background-size: 100% 100%;
+  .left-cont {
+    display: flex;
+    align-items: center;
+  }
+  .icon-wrapper {
+    height: px-to-rem(20);
+    width: px-to-rem(20);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: px-to-rem(6);
+  }
+  .title {
+    font-size: px-to-rem(16);
+    color: #e8f3fe;
+    font-weight: 600;
+    text-shadow: 0px 0px px-to-rem(10) rgba(74, 141, 254, 0.7);
+  }
+  .iconfont_tools {
+    font-size: px-to-rem(20);
+    color: #e8f3fe;
+  }
+  .close-icon {
+    cursor: pointer;
+  }
+}
+</style>

+ 92 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/commonTitle/CommonTitle.vue

@@ -0,0 +1,92 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <div class="common-title">
+    <div class="title-content" :class="noBg && 'no-bg'">
+      <div class="left-title">
+        <div>{{ title }}</div>
+        <el-tooltip
+          popper-class="tooltip-class"
+          v-if="showTooltip"
+          :content="content"
+          :placement="placement"
+        >
+          <div v-if="$slots.content" slot="content">
+            <slot name="content"></slot>
+          </div>
+          <i class="iconfont_tools icon-tongyong_icon_wenhao icon-wenhao"></i>
+        </el-tooltip>
+      </div>
+      <div v-if="$slots.extra">
+        <slot name="extra"></slot>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'commonTitle',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    content: {
+      type: String,
+      default: ''
+    },
+    placement: {
+      type: String,
+      default: 'right-start'
+    },
+    showTooltip: {
+      type: Boolean,
+      default: false
+    },
+    noBg: {
+      type: Boolean,
+      default: false
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.common-title {
+  margin-top: px-to-rem(12);
+  .title-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    font-size: px-to-rem(16);
+    color: #e8f3fe;
+    height: px-to-rem(38);
+    line-height: px-to-rem(38);
+    width: 100%;
+    padding: 0 0 0 px-to-rem(12);
+    text-shadow: 0px 0px px-to-rem(2) rgba(74, 141, 254, 0.7);
+    background: url('../../img/flight-number-list-header-bg.png') no-repeat;
+    background-size: 100% 100%;
+    &.no-bg {
+      background: none;
+    }
+    .left-title {
+      display: flex;
+      align-items: center;
+    }
+  }
+  .icon-wenhao {
+    color: #ffeeb1;
+    font-size: px-to-rem(20);
+    margin-left: px-to-rem(6);
+  }
+}
+</style>
+<style lang="scss">
+@import '../../style/common-title.scss';
+</style>

+ 154 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/flightNumberList/FlightNumberList.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="flight-number-list">
+    <div class="flight-header">
+      <div>飞行架次总计</div>
+      <div class="flight-number">{{ flightList?.length || '-' }}</div>
+    </div>
+    <div class="flight-list-header">
+      <p class="width-80">架次</p>
+      <p class="width-120">执行时间</p>
+      <p class="width-146">航线长度(m)</p>
+    </div>
+    <div class="flight-list-wrapper">
+      <el-scrollbar class="scrollbar" v-if="flightList?.length">
+        <div
+          class="flight-list-item"
+          v-for="(item, index) in flightList"
+          :key="index"
+        >
+          <p class="width-80">{{ item.label }}</p>
+          <p class="width-120">{{ item.time }}</p>
+          <p class="width-146">{{ item.length }}</p>
+        </div>
+      </el-scrollbar>
+      <div class="no-data" v-else></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { formatTimeFromSeconds } from '../../dict/plan-map'
+export default {
+  name: 'FlightNumberList',
+  props: {
+    flightNumberList: {
+      type: Array,
+      default: () => []
+    }
+  },
+  computed: {
+    flightList() {
+      return this.flightNumberList.map((item, index) => {
+        return {
+          ...item,
+          label: '架次' + (index + 1),
+          time: formatTimeFromSeconds(item.time * 60),
+          length: item.predictMiles.toFixed(1)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+@mixin textEllipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.flight-number-list {
+  @include themeify(false) {
+    margin-top: px-to-rem(12);
+    .width-80 {
+      width: px-to-rem(80);
+    }
+    .width-120 {
+      width: px-to-rem(120);
+    }
+    .width-146 {
+      width: px-to-rem(146);
+    }
+    .flight-list-wrapper {
+      height: px-to-rem(160);
+      background: url('../../img/shadow-bg.png') no-repeat;
+      background-size: 100% 100%;
+    }
+    ::v-deep .scrollbar {
+      height: px-to-rem(160);
+      margin-top: px-to-rem(0);
+      .el-scrollbar__wrap {
+        padding-right: 0;
+      }
+    }
+    .flight-header {
+      display: flex;
+      align-items: center;
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      height: px-to-rem(38);
+      line-height: px-to-rem(38);
+      width: 100%;
+      padding: 0 px-to-rem(12);
+      text-shadow: 0px 0px px-to-rem(2) rgba(74, 141, 254, 0.7);
+      background: url('../../img/flight-number-list-header-bg.png') no-repeat;
+      background-size: 100% 100%;
+
+      .flight-number {
+        color: #4f9fff;
+        margin-left: px-to-rem(6);
+      }
+    }
+    .flight-list-header {
+      display: flex;
+      height: px-to-rem(32);
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      background: rgba(79, 159, 255, 0.2);
+      border-bottom: px-to-rem(1) solid rgba(232, 243, 254, 0.2);
+
+      p {
+        padding: 0 px-to-rem(12);
+        line-height: px-to-rem(32);
+      }
+    }
+    .flight-list-item {
+      display: flex;
+      border-bottom: px-to-rem(1) solid rgba(232, 243, 254, 0.2);
+      &:hover {
+        background: rgba(79, 159, 255, 0.2);
+      }
+
+      p {
+        padding: 0 px-to-rem(12);
+        line-height: px-to-rem(32);
+        @include textEllipsis;
+      }
+    }
+    .no-data {
+      height: px-to-rem(160);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-direction: column;
+      &:before {
+        content: url('~@component-gallery/assets/image/ar-label/noData.png');
+        text-align: center;
+        display: block;
+      }
+      &:after {
+        content: '暂无数据';
+        font-size: px-to-rem(16);
+        color: #e8f3fe;
+        display: block;
+        text-align: center;
+      }
+    }
+  }
+}
+</style>

+ 212 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/FlyActiconCard.vue

@@ -0,0 +1,212 @@
+<!--
+  定位容器。
+  这个容器用于锚定于某一个页面元素固定位置的容器。
+  这个容器不可拖拽、视为父元素的子元素。
+-->
+<template>
+  <div
+    :class="[
+      bemClass.container,
+      industryClass,
+      'FlyResponsiveCard',
+      isActive && 'active'
+    ]"
+    ref="FlyResponsiveCard"
+    @mousedown="onMouseDown"
+  >
+    <div v-if="canClose" :class="[bemClass.close]" @click="close" />
+
+    <div v-if="headTitle" :class="[bemClass.header, leftTitle && 'left-title']">
+      <div :class="[bemClass.headertitle]">
+        <!-- <slot name="titleIcon" /> -->
+        <ct-icon :name="titleIcon" class="title_icon" :size="pxToRem(20)" />
+        <span v-if="!$slots.headTitle">{{ headTitle }}</span>
+      </div>
+      <slot name="extra" />
+      <i
+        v-if="leftTitle && canCollapse"
+        :class="['iconfont_tools title-pointer icon-linye_icon_shanchu']"
+        @click="onDeleteHeader"
+      />
+    </div>
+    <div
+      :class="[bemClass.body]"
+      :style="{
+        padding: hasBodyPadding ? `${pxToRem(12)} 0` : '0',
+        overflowX: 'hidden'
+      }"
+    >
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+import { createNameSpace } from '@component-gallery/utils/bem/create'
+
+const bem = createNameSpace('fly_point_card')
+
+export default {
+  name: 'FlyResponsiveCard',
+  props: {
+    industryClass: {
+      type: String,
+      default: 'fly_point_card'
+    },
+    headTitle: {
+      // 标题。如果不填写的话,整个header区域不会出现。
+      type: String,
+      default: ''
+    },
+    // 铁塔字体图标库图标name
+    titleIcon: {
+      type: String,
+      default: 'aircraft-yaw'
+    },
+    canClose: {
+      // 是否展示关闭按钮,关闭按钮会触发close事件。
+      type: Boolean,
+      default: false
+    },
+    canCollapse: {
+      // 是否有折叠事件
+      type: Boolean,
+      default: true
+    },
+    leftTitle: {
+      type: Boolean,
+      default: true
+    },
+    isActive: {
+      type: Boolean,
+      default: false
+    },
+    isOpen: {
+      // 是否展开默认展开
+      type: Boolean,
+      default: true
+    },
+    hasBodyPadding: {
+      type: Boolean,
+      default: true
+    }
+  },
+  computed: {
+    bemClass() {
+      return {
+        container: bem.b(''),
+        header: bem.b('header'),
+        body: bem.b('body', 'rscardbody'),
+        close: bem.b('close'),
+        headertitle: bem.be('header', 'title')
+      }
+    }
+  },
+  data() {
+    return {
+      cardOpen: true
+    }
+  },
+  methods: {
+    onMouseDown(event) {
+      event.stopPropagation()
+    },
+    onDeleteHeader() {
+      this.$emit('delete')
+    },
+    close() {
+      this.$emit('close')
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin textShadow {
+  text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+}
+.FlyResponsiveCard {
+  width: 100%;
+  cursor: pointer;
+  .fly_point_card-header {
+    width: 100%;
+    height: px-to-rem(33);
+    padding-left: px-to-rem(12);
+    @include flex;
+    justify-content: space-between;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 600;
+    font-size: px-to-rem(18);
+    color: #e8f3fe;
+    background: url('./bg-title.webp') no-repeat;
+    background-size: 100% 100%;
+    position: relative;
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: px-to-rem(44);
+      pointer-events: none;
+      z-index: 1;
+      background: url('./title_maozi.webp') no-repeat;
+      background-size: 100% 100%;
+    }
+    .title-pointer {
+      font-size: px-to-rem(20);
+      margin-right: px-to-rem(12);
+      cursor: pointer;
+    }
+  }
+  .fly_point_card-header__title {
+    @include flex;
+    .title_icon {
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+      margin-right: px-to-rem(6);
+      line-height: px-to-rem(20);
+      ::v-deep .ct-icon {
+        .icon-ctw {
+          color: #e8f3fe !important;
+          font-size: px-to-rem(20);
+        }
+      }
+    }
+  }
+  .fly_point_card-body {
+    padding: px-to-rem(12) 0;
+    position: relative;
+    &::before {
+      content: '';
+      width: 100%;
+      height: px-to-rem(8);
+      position: absolute;
+      bottom: 0;
+      z-index: 1;
+      pointer-events: none;
+      background: url('./bg-card.png') no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+  &.active {
+    background: radial-gradient(
+      200% 50% ellipse at 50% 10%,
+      rgba(0, 74, 166, 0.4) 0%,
+      rgba(0, 62, 137, 0.4) 200%
+    );
+    .fly_point_card-header {
+      background-image: url('./bg-title-active.png');
+    }
+    .fly_point_card-body {
+      &::before {
+        background-image: url('./bg-card-active.png');
+      }
+    }
+  }
+}
+</style>

BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-card-active.png


BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-card.png


BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-title-active.png


BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/bg-title.webp


BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyActiconCard/title_maozi.webp


+ 217 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/flyResponsiveCard/FlyResponsiveCard.vue

@@ -0,0 +1,217 @@
+<!--
+  定位容器。
+  这个容器用于锚定于某一个页面元素固定位置的容器。
+  这个容器不可拖拽、视为父元素的子元素。
+-->
+<template>
+  <div
+    :class="[
+      bemClass.container,
+      industryClass,
+      'FlyResponsiveCard',
+      isHeightSetting && 'isHeightSetting',
+      cardOpen && 'isOpen'
+    ]"
+    ref="FlyResponsiveCard"
+    @mousedown="onMouseDown"
+  >
+    <div v-if="canClose" :class="[bemClass.close]" @click="close" />
+
+    <div v-if="headTitle" :class="[bemClass.header, leftTitle && 'left-title']">
+      <div :class="[bemClass.headertitle]">
+        <slot name="titleIcon" />
+        <span v-if="!$slots.headTitle">{{ headTitle }}</span>
+      </div>
+      <slot name="extra" />
+      <i
+        v-if="leftTitle && canCollapse"
+        :class="[
+          'iconfont_tools title-pointer icon-linye_icon_biaotizhankai_you',
+          cardOpen && 'isOpen'
+        ]"
+        @click="onToggleHeader"
+      />
+    </div>
+    <div :class="[bemClass.body, !cardOpen && 'hidebody']" :style="style">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+import { createNameSpace } from '@component-gallery/utils/bem/create'
+const bem = createNameSpace('fly_point_card')
+
+export default {
+  name: 'FlyResponsiveCard',
+  props: {
+    industryClass: {
+      type: String,
+      default: 'fly_point_card'
+    },
+    headTitle: {
+      // 标题。如果不填写的话,整个header区域不会出现。
+      type: String,
+      default: ''
+    },
+    titleIcon: {
+      type: String,
+      default: ''
+    },
+    canClose: {
+      // 是否展示关闭按钮,关闭按钮会触发close事件。
+      type: Boolean,
+      default: false
+    },
+    canCollapse: {
+      // 是否有折叠事件
+      type: Boolean,
+      default: true
+    },
+    leftTitle: {
+      type: Boolean,
+      default: true
+    },
+    // 是否高级设置
+    isHeightSetting: {
+      type: Boolean,
+      default: false
+    },
+    isOpen: {
+      // 是否展开默认展开
+      type: Boolean,
+      default: true
+    },
+    isFlexScroll: Boolean
+  },
+  watch: {
+    isOpen() {
+      if (this.isOpen !== undefined) {
+        this.cardOpen = this.isOpen
+      }
+    }
+  },
+  computed: {
+    bemClass() {
+      return {
+        container: bem.b(''),
+        header: bem.b('header'),
+        body: bem.b('body', 'rscardbody'),
+        close: bem.b('close'),
+        headertitle: bem.be('header', 'title')
+      }
+    },
+    style() {
+      const style = {}
+      if (this.isFlexScroll) {
+        style.flex = this.cardOpen ? '1' : '0'
+        style.overflowY = 'hidden'
+      } else {
+        style.height = this.cardOpen ? 'auto' : '0'
+      }
+      return style
+    }
+  },
+  data() {
+    return {
+      cardOpen: true
+    }
+  },
+  mounted() {
+    if (this.isOpen !== undefined) {
+      this.cardOpen = this.isOpen
+    }
+  },
+  methods: {
+    onMouseDown(event) {
+      event.stopPropagation()
+    },
+    onToggleHeader() {
+      if (this.canCollapse) {
+        this.cardOpen = !this.cardOpen
+        this.$emit('toggle', this.cardOpen)
+      }
+    },
+    close() {
+      this.$emit('close')
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin textShadow {
+  text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+}
+.FlyResponsiveCard {
+  width: 100%;
+  .fly_point_card-header {
+    width: 100%;
+    height: px-to-rem(33);
+    padding-left: px-to-rem(6);
+    @include flex;
+    justify-content: space-between;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 600;
+    font-size: px-to-rem(18);
+    color: #e8f3fe;
+    background: url('./bg_card_title.webp') no-repeat;
+    background-size: 100% 100%;
+    position: relative;
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: px-to-rem(44);
+      pointer-events: none;
+      z-index: 1;
+      background: url('./title_maozi.webp') no-repeat;
+      background-size: 100% 100%;
+    }
+    .title-pointer {
+      font-size: px-to-rem(20);
+      transform: rotate(90deg);
+      margin-right: px-to-rem(12);
+      cursor: pointer;
+      &.isOpen {
+        transform: rotate(-90deg);
+      }
+    }
+    span {
+      font-weight: 600;
+    }
+  }
+  &.isHeightSetting {
+    .fly_point_card-header {
+      padding-left: px-to-rem(12);
+      background: url('../../img/flight-number-list-header-bg.png') no-repeat;
+      background-size: 100% 100%;
+    }
+    &.isOpen {
+      border-top-left-radius: px-to-rem(1);
+      background: radial-gradient(
+        400% 29% at 18% -1%,
+        rgba(13, 97, 198, 0.34) 0%,
+        rgba(13, 97, 198, 0.14) 34%,
+        rgba(0, 14, 31, 0) 100%
+      );
+    }
+  }
+  .fly_point_card-header__title {
+    @include flex;
+    img {
+      width: px-to-rem(28);
+      height: px-to-rem(28);
+    }
+  }
+  .hidebody {
+    overflow: hidden;
+  }
+}
+</style>

BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyResponsiveCard/bg_card_title.webp


BIN
src/components/common-comp-uav-fly-manage/src/baseComponents/flyResponsiveCard/title_maozi.webp


+ 132 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/flyToFirstPointType/FlyToFirstPointType.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="height_cont">
+    <div class="header">
+      <div
+        :class="['header_item', item.value == currentIndex && 'active']"
+        v-for="item in header"
+        :key="item.value + 'header'"
+        @click="handleItemClick(item.value)"
+      >
+        {{ item.name }}
+      </div>
+    </div>
+    <div class="pic_des">
+      <img :src="picMap[currentIndex]" alt="" />
+    </div>
+  </div>
+</template>
+
+<script>
+import pashengfs from '../../img/airLine/pashengfs.webp'
+export default {
+  name: 'lineHeight',
+  data() {
+    return {
+      header: [
+        {
+          name: '垂直爬升',
+          value: 'safely'
+        },
+        {
+          name: '倾斜爬升',
+          value: 'pointToPoint'
+        }
+      ],
+      currentIndex: 'safely',
+      picMap: {
+        safely: pashengfs,
+        pointToPoint: pashengfs
+      }
+    }
+  },
+  props: {
+    value: {
+      type: [String, Number],
+      default: ''
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        this.currentIndex = val || 'safely'
+      }
+    }
+  },
+  methods: {
+    handleItemClick(index) {
+      this.currentIndex = index
+      this.$emit('input', this.currentIndex)
+      this.$emit('change', this.currentIndex)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin textShadow {
+  text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+}
+.height_cont {
+  width: 100%;
+  .header {
+    width: 100%;
+    height: px-to-rem(48);
+    line-height: px-to-rem(48);
+    position: relative;
+    @include flex;
+    &::after {
+      content: '';
+      position: absolute;
+      width: 100%;
+      height: px-to-rem(1);
+      bottom: 0;
+      background: linear-gradient(
+        270deg,
+        rgba(176, 212, 255, 0) 0%,
+        #b0d4ff 52%,
+        rgba(176, 212, 255, 0) 100%
+      );
+      opacity: 0.35;
+      z-index: 1;
+    }
+    .header_item {
+      width: 50%;
+      text-align: center;
+      font-size: px-to-rem(16);
+      color: rgba(232, 243, 254, 0.7);
+      position: relative;
+      cursor: pointer;
+      &.active {
+        color: #e8f3fe;
+        @include textShadow;
+        &::after {
+          content: '';
+          position: absolute;
+          width: 100%;
+          height: px-to-rem(11);
+          bottom: 0;
+          left: 0;
+          z-index: 1;
+          background: url('../../img/airLine/bg_title_guang_50.webp') no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+    }
+  }
+  .pic_des {
+    margin-top: px-to-rem(12);
+    width: 100%;
+    height: px-to-rem(106);
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 217 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/intersectionDetectionPop/IntersectionDetectionPop.vue

@@ -0,0 +1,217 @@
+<template>
+  <div class="intersection-detection-pop" v-drag>
+    <!--    <el-dialog-->
+    <!--      v-if="detectionPopStatus"-->
+    <!--      custom-class="mask-dialog"-->
+    <!--      :close-on-click-modal="false"-->
+    <!--      :close-on-press-escape="false"-->
+    <!--      :append-to-body="true"-->
+    <!--      :visible="detectionPopStatus"-->
+    <!--    >-->
+    <absolute-container
+      class="intersection-detection-pop-wrapper"
+      can-close
+      title="碰撞测试"
+      :left="0"
+      :top="0"
+      :width="412"
+      :height="482"
+      @close="handleCancel"
+    >
+      <div class="intersection-detection-pop-content" v-loading="loading">
+        <common-title title="地形列表"></common-title>
+        <div class="terrain-list">
+          <el-radio v-model="form.radio" label="1">基础地形数据</el-radio>
+        </div>
+        <common-title title="警告范围"></common-title>
+        <slider-control
+          :max="50"
+          :min="minWranValue"
+          :step="1"
+          :precision="0"
+          suffixText="m"
+          v-model="form.warningRange"
+          reverse
+        ></slider-control>
+        <common-title title="危险范围"></common-title>
+        <slider-control
+          :max="20"
+          :min="1"
+          :step="1"
+          :precision="0"
+          suffixText="m"
+          v-model="form.dangerRange"
+          reverse
+        ></slider-control>
+      </div>
+      <div class="footer-btn">
+        <base-button
+          type="primary"
+          :height="32"
+          :width="108"
+          class="submit-btn"
+          @click="handleSubmit"
+        >
+          确定
+        </base-button>
+        <base-button :height="32" :width="108" @click="handleCancel">
+          取消
+        </base-button>
+      </div>
+    </absolute-container>
+    <!--    </el-dialog>-->
+  </div>
+</template>
+
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import CommonTitle from '../commonTitle/CommonTitle'
+import SliderControl from '../sliderControl/SliderControl'
+import { dragBind } from '@component-gallery/utils/funCommon/common'
+
+export default {
+  name: 'IntersectionDetectionPop',
+  components: {
+    AbsoluteContainer,
+    CommonTitle,
+    SliderControl,
+    BaseButton
+  },
+  props: {
+    isSuccess: {
+      type: Boolean,
+      default: false
+    },
+    detectionPopStatus: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      form: {
+        radio: '1',
+        warningRange: 20,
+        dangerRange: 8
+      }
+    }
+  },
+  directives: {
+    drag: {
+      bind: (el) => {
+        dragBind(el)
+      }
+    }
+  },
+  computed: {
+    minWranValue() {
+      const min = 2
+      if (this.form.dangerRange + 1 >= min) {
+        return Number(this.form.dangerRange) + 1
+      }
+      return min
+    },
+    offsetLeft() {
+      console.log(window.innerWidth / 2 - 206, 'offsetLeft')
+      return window.innerWidth / 2 - 206
+    }
+  },
+  beforeDestroy() {
+    this.loading = false
+  },
+
+  methods: {
+    handleSubmit() {
+      this.form.isSuccess = this.isSuccess
+      this.$emit('submit', JSON.parse(JSON.stringify(this.form)))
+      if (this.isSuccess) {
+        this.loading = true
+      }
+    },
+    handleCancel() {
+      this.$emit('update:detectionPopStatus', false)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.intersection-detection-pop {
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  pointer-events: auto;
+  width: fit-content;
+  height: fit-content;
+  position: absolute;
+}
+.intersection-detection-pop-wrapper {
+  left: 50% !important;
+  top: 50% !important;
+  transform: translate(-50%, -50%) !important;
+  .intersection-detection-pop-content {
+    padding: 0 px-to-rem(12);
+  }
+  .terrain-list {
+    padding: 0 px-to-rem(12);
+    height: px-to-rem(160);
+    width: 100%;
+    background: url('../../img/shadow-bg.png') no-repeat;
+    background-size: 100% 100%;
+    ::v-deep .el-radio {
+      display: block;
+      padding: px-to-rem(8) 0;
+      border-bottom: 1px solid rgba(232, 243, 254, 0.2);
+      .el-radio__label {
+        font-size: px-to-rem(16);
+        padding-left: px-to-rem(6);
+      }
+      .el-radio__input.is-checked + .el-radio__label {
+        color: #4f9fff;
+      }
+    }
+  }
+  .footer-btn {
+    padding: px-to-rem(12) 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .submit-btn {
+      margin-right: px-to-rem(12);
+    }
+    ::v-deep .base-button span {
+      line-height: initial !important;
+      font-size: px-to-rem(16);
+    }
+  }
+}
+</style>
+<style lang="scss">
+.mask-dialog.el-dialog {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  margin-top: 0 !important;
+  width: fit-content;
+  height: fit-content;
+  background: transparent !important;
+  border: none !important;
+  transform: translate(-50%, -50%);
+
+  .el-dialog__header {
+    display: none;
+  }
+
+  .el-dialog__body {
+    padding: 0;
+    width: fit-content;
+    height: fit-content;
+    background: transparent;
+  }
+}
+</style>

+ 91 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/isFollowLine/IsFollowLine.vue

@@ -0,0 +1,91 @@
+<!-- 跟随航线 -->
+<template>
+  <div :class="['is_follow_line', disabled && 'disabled']">
+    <div
+      :class="['my_radio', checked && 'checked']"
+      @click="handleChecked"
+    ></div>
+    <div class="des">跟随航线</div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'IsFollowLine',
+  data() {
+    return {
+      checked: true
+    }
+  },
+  props: {
+    value: {
+      type: String,
+      default: '1'
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(val) {
+        if (val == '1') {
+          this.checked = true
+        } else {
+          this.checked = false
+        }
+      }
+    }
+  },
+  methods: {
+    handleChecked() {
+      this.checked = !this.checked
+      let value = this.checked == true ? '1' : '0'
+      this.$emit('input', value)
+      this.$emit('change', value)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.is_follow_line {
+  display: flex;
+  align-items: center;
+  &.disabled {
+    pointer-events: none !important;
+  }
+  .my_radio {
+    margin-right: px-to-rem(6);
+    width: px-to-rem(16);
+    height: px-to-rem(16);
+    border-radius: 50%;
+    border: px-to-rem(1) solid #1373e6;
+    cursor: pointer;
+    &.checked {
+      background: #1373e6;
+      position: relative;
+      &::after {
+        position: absolute;
+        content: '';
+        z-index: 1;
+        width: px-to-rem(6);
+        height: px-to-rem(6);
+        background: #ffffff;
+        border-radius: 50%;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+      }
+    }
+  }
+  .des {
+    font-size: px-to-rem(16);
+    color: #e8f2fe;
+    font-weight: 400;
+  }
+}
+</style>

+ 65 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/mouseTip/MouseTip.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="mouse-tip" v-if="inTarget">{{ title }}</div>
+</template>
+
+<script>
+export default {
+  name: 'MouseTip',
+  data() {
+    return {
+      inTarget: true
+    }
+  },
+  props: {
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  mounted() {
+    document.addEventListener('mousemove', this.handleMouseMove)
+  },
+  methods: {
+    handleMouseMove(event) {
+      try {
+        this.inTarget =
+          event.target.parentElement.classList.contains('cesium-widget')
+        if (!this.inTarget || !this.$el?.style) {
+          return
+        }
+        const { offsetX, offsetY } = event
+        const { innerWidth, innerHeight } = window
+        const x = innerWidth
+        const y = innerHeight
+        this.$el.style.transform = `translateX(${offsetX + 10}px) translateY(${
+          offsetY + 10
+        }px)`
+      } catch (e) {
+        console.error(e)
+      }
+    }
+  },
+  beforeDestroy() {
+    document.removeEventListener('mousemove', this.handleMouseMove)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.mouse-tip {
+  position: fixed;
+  z-index: 9999;
+  left: 0;
+  top: 0;
+  padding: 0 px-to-rem(12);
+  height: px-to-rem(24);
+  line-height: px-to-rem(24);
+  background: #172537;
+  border-radius: px-to-rem(4);
+  font-size: px-to-rem(14);
+  color: #e8f3fe;
+}
+</style>

+ 235 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/orthoImageTitle/OrthoImageTitle.vue

@@ -0,0 +1,235 @@
+<template>
+  <div class="ortho-image-title">
+    <div class="herder">
+      <span>正射影像</span>
+      <el-tooltip popper-class="tooltip-class" placement="right-start">
+        <template v-slot:content>
+          <div class="tooltip-content">
+            <p>在地图上绘制飞行区域,根据绘制区域自动生成航线。</p>
+            <p>在设置中调整无人机飞行参数。</p>
+          </div>
+        </template>
+        <i class="iconfont_tools icon-tongyong_icon_wenhao icon-wenhao"></i>
+      </el-tooltip>
+    </div>
+    <div class="line_name_wrap">
+      <img class="icon_img" src="../../img/icon_air-ortho.png" alt="" />
+      <el-tooltip popper-class="tooltip-class" placement="right-start">
+        <template v-slot:content>
+          <div class="tooltip-content">
+            <p>{{ airLineName }}</p>
+          </div>
+        </template>
+        <span class="line_name">{{ airLineName }}</span>
+      </el-tooltip>
+    </div>
+    <div class="hangxain_zonglan">
+      <div class="zl_left">
+        <div class="zl_left_top">
+          <span class="zl_title">航线长度(m)</span>
+          <span class="zl_des five-words" :c-tip="airLineLength">{{
+            airLineLength
+          }}</span>
+        </div>
+        <div class="zl_left_top">
+          <span class="zl_title">面积(㎡)</span>
+          <span class="zl_des five-words" :c-tip="airArea">{{ airArea }}</span>
+        </div>
+      </div>
+      <div class="middle"></div>
+      <div class="zl_right">
+        <div class="zl_left_bottom">
+          <span class="zl_title min_width">预计执行时间</span>
+          <span class="zl_des six-words other" :c-tip="formatTime">{{
+            formatTime
+          }}</span>
+        </div>
+        <div class="zl_left_bottom">
+          <span class="zl_title">预计拍照数</span>
+          <span class="zl_des six-words" :c-tip="photoNum">{{ photoNum }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { formatTimeFromSeconds } from '../../dict/plan-map'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+export default {
+  name: 'OrthoImageTitle',
+  props: {
+    airLineInfo: {
+      type: Object,
+      default: () => ({})
+    },
+    airLineName: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    airArea() {
+      if (!this.airLineInfo?.airArea) {
+        return '-'
+      }
+      return this.airLineInfo?.airArea.toFixed(2)
+    },
+    formatTime() {
+      if (!this.airLineInfo?.totalTime) {
+        return '-'
+      }
+      return formatTimeFromSeconds(this.airLineInfo?.totalTime?.toFixed(0))
+    },
+    airLineLength() {
+      if (!this.airLineInfo?.airLineLength) {
+        return '-'
+      }
+      return this.airLineInfo?.airLineLength.toFixed(1)
+    },
+    photoNum() {
+      if (!this.airLineInfo?.photoCountList) {
+        return '-'
+      }
+      return this.airLineInfo.photoCountList.reduce((acc, cur) => acc + cur, 0)
+    }
+  },
+  mounted() {
+    setupCTips()
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin textShadow {
+  text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+}
+.ortho-image-title {
+  padding-right: px-to-rem(6);
+  .herder {
+    width: 100%;
+    height: px-to-rem(20);
+    background: url('../../img/airLine/bg_title.webp') no-repeat 100% 100%;
+    background-size: 100% 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    span {
+      text-align: center;
+      font-weight: 600;
+      font-size: px-to-rem(18);
+      color: #e8f3fe;
+      line-height: 1.2;
+      @include textShadow;
+    }
+    i {
+      margin-left: px-to-rem(6);
+      color: #ffeeb1;
+      font-size: px-to-rem(20);
+    }
+  }
+  .line_name_wrap {
+    margin-top: px-to-rem(12);
+    height: px-to-rem(32);
+    @include flex;
+    .icon_img {
+      width: px-to-rem(28);
+      height: px-to-rem(28);
+      margin-right: px-to-rem(6);
+    }
+    .line_name {
+      flex: 1;
+      line-height: px-to-rem(32);
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      font-weight: 500;
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      @include textShadow;
+    }
+  }
+  .hangxain_zonglan {
+    margin-top: px-to-rem(12);
+    padding: px-to-rem(8) px-to-rem(8) px-to-rem(12);
+    width: 100%;
+    height: px-to-rem(68);
+    background: url('../../img/airLine/card_hdhx_tj.webp') no-repeat 100% 100%;
+    background-size: 100% 100%;
+    @include flex;
+    .middle {
+      min-width: px-to-rem(1);
+      width: px-to-rem(1);
+      height: px-to-rem(64);
+      background: linear-gradient(
+        to bottom,
+        rgba(79, 159, 255, 0) 0%,
+        #4f9fff 50%,
+        rgba(79, 159, 255, 0) 100%
+      );
+    }
+    .zl_left {
+      flex: 1;
+      @include flex;
+      justify-content: space-between;
+      flex-direction: column;
+      .zl_left_top,
+      .zl_left_bottom {
+        padding-right: px-to-rem(12);
+      }
+    }
+    .zl_right {
+      flex: 1;
+      @include flex;
+      justify-content: space-between;
+      flex-direction: column;
+      .zl_left_top,
+      .zl_left_bottom {
+        padding-left: px-to-rem(12);
+      }
+    }
+    .zl_left_top,
+    .zl_left_bottom {
+      @include flex;
+      width: 100%;
+      justify-content: space-between;
+      height: px-to-rem(24);
+      line-height: px-to-rem(24);
+    }
+    .zl_left_bottom {
+      margin-top: px-to-rem(4);
+    }
+    .zl_title {
+      font-weight: 400;
+      color: #e8f3fe;
+      white-space: nowrap;
+    }
+    .zl_des {
+      font-weight: 600;
+      @include textShadow;
+      &.five-words {
+        max-width: px-to-rem(80);
+      }
+      &.six-words {
+        max-width: px-to-rem(64);
+      }
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .min_width {
+      min-width: px-to-rem(85);
+    }
+  }
+}
+</style>
+<style lang="scss">
+@import '../../style/common-title.scss';
+</style>

+ 299 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/restrictedFlightZone/RestrictedFlightZone.vue

@@ -0,0 +1,299 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/12/6
+ * @Description: 限飞区
+ -->
+<template>
+  <absolute-container
+    :right="right"
+    :bottom="bottom"
+    :left="left"
+    :top="top"
+    :canClose="false"
+    industryClass="restricted-flight-zone"
+  >
+    <div class="restricted-flight-zone-contnet">
+      <div
+        @click="switchChange"
+        class="restricted-flight-zone-button"
+        :class="[isOpen && 'restricted-flight-zone-button-active']"
+      >
+        <ct-icon name="display-restricted-flight-zone"></ct-icon>
+      </div>
+      <div class="position" :class="position" v-if="isOpen" @click.stop>
+        <div class="length">
+          <div
+            class="length-item"
+            :key="item.code"
+            v-for="item in restrictedFlightListComputed"
+          >
+            <div
+              class="length-item-box"
+              :style="{
+                background: item.background,
+                borderColor: item.borderColor
+              }"
+            >
+            </div>
+            <div class="length-item-title">
+              {{ item.title }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </absolute-container>
+</template>
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import { restrictedFlightList } from '../../dict/fly-result-map'
+import { getUserMemoryInfo, getDictType } from '../../service'
+import CTMapOl from '@ct/ct_map_ol'
+import { uptUserMemoryInfo } from '@component-gallery/utils/request'
+import eventPath from '@component-gallery/build-event-bus-path'
+
+export default {
+  components: { AbsoluteContainer },
+  inject: ['mapRef'],
+  props: {
+    bottom: {
+      type: Number
+    },
+    top: {
+      type: Number
+    },
+    left: {
+      type: Number
+    },
+    right: {
+      type: Number
+    },
+    mapId: {
+      type: String
+    },
+    resolveEventName: {
+      // 初始化事件名
+      type: [String, Boolean],
+      default: false
+    }
+  },
+  data() {
+    return {
+      isOpen: false,
+      wmsLayerObj: {},
+      wmsLayerUrl: '', //限飞区图层ip 端口
+      position: 'right'
+    }
+  },
+  computed: {
+    //限飞区图层数组
+    restrictedFlightListComputed() {
+      return restrictedFlightList() || []
+    },
+    /**
+     * 用户记忆 key
+     * @returns {string}
+     */
+    memoryTypeListComputed() {
+      return 'restrictedFlightZoneStatus'
+    }
+  },
+  mounted() {
+    this.position =
+      this.$el.offsetLeft < window.innerWidth / 2 ? 'left' : 'right'
+    this.initEvent()
+  },
+  methods: {
+    /**
+     * 初始化事件
+     */
+    initEvent() {
+      if (this.resolveEventName) {
+        // 地图初始化完成后,初始化用户记忆, 并添加限飞区图层
+        this.$globalEventBus.$on(this.resolveEventName, () =>
+          this.initUserMemoryInfoApi()
+        )
+      } else {
+        this.initUserMemoryInfoApi()
+      }
+    },
+    /**
+     * 获取图层字典
+     * @returns {Promise<string>}
+     */
+    async getDictTypeApi() {
+      if (this.wmsLayerUrl !== '') {
+        return this.wmsLayerUrl
+      }
+      const res = await getDictType('restricted_flight_zone')
+      console.log('限飞区res====>', res)
+      if (res.code === 200 && res.data.length > 0) {
+        console.log('res.data', res.data)
+        this.wmsLayerUrl = res.data[0]?.dictValue
+      }
+      return this.wmsLayerUrl || null
+    },
+    /**
+     * 读取用户记忆
+     */
+    initUserMemoryInfoApi() {
+      getUserMemoryInfo({ memoryTypeList: [this.memoryTypeListComputed] }).then(
+        (res) => {
+          if (res.code === 200 && res.data.length > 0) {
+            this.isOpen = res.data[0].memoryValue === '0'
+            if (this.isOpen) {
+              this.addWmsLayer()
+            }
+          }
+        }
+      )
+    },
+    /**
+     * 设置用户记忆
+     */
+    setUserMemoryInfoApi() {
+      uptUserMemoryInfo({
+        memoryType: this.memoryTypeListComputed,
+        memoryValue: this.isOpen ? '0' : '1'
+      })
+    },
+    /**
+     * 开关限飞区
+     */
+    switchChange() {
+      this.isOpen = !this.isOpen
+      if (this.isOpen) {
+        this.addWmsLayer()
+      } else {
+        this.removeWmsLayer()
+      }
+      this.setUserMemoryInfoApi()
+    },
+    /**
+     * 添加图层
+     * @returns {Promise<void>}
+     */
+    async addWmsLayer() {
+      this.removeWmsLayer()
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      for (let i = 0; i < this.restrictedFlightListComputed.length; i++) {
+        const item = this.restrictedFlightListComputed[i]
+        let options = {
+          opacity: 0.4
+        }
+        const url =
+          ((await this.getDictTypeApi()) || window.location.origin) +
+          item.wmsUrl
+        //const url = item.wmsUrl
+        this.wmsLayerObj[item.code] = new CTMapOl.LayerControl.lib.WMSLayer(
+          {
+            mapRef,
+            url: url,
+            params: {
+              FORMAT: 'image/png',
+              VERSION: '1.1.0',
+              LAYERS: 'guotu:sys_ban_fly_sub_areas',
+              STYLES: 'alarmEvent_style',
+              ENV: 'opacity1: 0.5',
+              HEIGHT: 256
+            }
+          },
+          options
+        )
+        this.wmsLayerObj[item.code]?.init()
+
+        await this.wmsLayerObj[item.code]?.mount(mapRef)
+      }
+    },
+    /**
+     * 移除图层
+     */
+    removeWmsLayer() {
+      if (this.wmsLayerObj) {
+        Object.keys(this.wmsLayerObj).forEach((key) => {
+          if (this.wmsLayerObj[key]) {
+            this.wmsLayerObj[key]?.destroy()
+            this.wmsLayerObj[key] = null
+          }
+        })
+      }
+      this.wmsLayerObj = {}
+    }
+  },
+  destroyed() {
+    this.removeWmsLayer()
+    this.$globalEventBus.$off(this.resolveEventName, () =>
+      this.initUserMemoryInfoApi()
+    )
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$_widht: px-to-rem(180);
+.restricted-flight-zone {
+  pointer-events: auto;
+  z-index: 1;
+  ::v-deep .ct-icon {
+    width: auto !important;
+    display: flex;
+    .icon-ctw {
+      color: inherit !important;
+      font-size: px-to-rem(22) !important;
+      vertical-align: initial !important;
+    }
+  }
+  &-contnet {
+    position: relative;
+    .restricted-flight-zone-button {
+      width: px-to-rem(30);
+      height: px-to-rem(30);
+      box-shadow: 0 px-to-rem(2) px-to-rem(5) 0 rgba(168, 175, 184, 0.54);
+      border-radius: px-to-rem(4);
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: rgba(23, 37, 55, 1);
+      background: #ffffff;
+      cursor: pointer;
+      &.restricted-flight-zone-button-active {
+        background: #1373e6;
+        color: rgba(232, 243, 254, 1);
+      }
+    }
+    .position {
+      &.right {
+        right: calc(px-to-rem(30) + px-to-rem(6));
+      }
+      &.left {
+        left: calc(px-to-rem(30) + px-to-rem(6));
+      }
+      position: absolute;
+      bottom: 0;
+      width: px-to-rem(148);
+      padding: px-to-rem(12);
+      background: rgba(23, 37, 55, 0.9);
+      border-radius: px-to-rem(8);
+      .length {
+        &-item {
+          display: flex;
+          align-items: center;
+          height: px-to-rem(16);
+          line-height: px-to-rem(16);
+          &-box {
+            width: px-to-rem(12);
+            height: px-to-rem(12);
+            border-style: solid;
+            border-width: px-to-rem(1);
+          }
+          &-title {
+            margin-left: px-to-rem(12);
+            font-size: px-to-rem(16);
+            color: #e8f3fe;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 262 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/sliderControl/SliderControl.vue

@@ -0,0 +1,262 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <div class="slider-control" :class="reverse ? 'is-reverse' : ''">
+    <el-input
+      class="slider-input"
+      type="number"
+      v-model.number="numValue"
+      :disabled="disabled"
+      @blur="blur(numValue)"
+      @mousewheel.native.prevent
+    >
+      <template slot="append">
+        <div class="slider-suffix">{{ suffixText }}</div>
+      </template>
+    </el-input>
+    <div class="slider-model">
+      <div
+        class="slider-button"
+        :disabled="value <= min || disabled"
+        @click="minusValue"
+      >
+        <i class="el-icon-minus"></i>
+      </div>
+      <el-slider
+        v-model.number="sliderValue"
+        :step="step"
+        :max="max"
+        :min="min"
+        :disabled="disabled"
+        @change="blur(numValue)"
+        @click.stop
+        @mousemove.stop
+        @mouseup.stop
+        @mousedown.stop
+        tooltip-class="tooltip-class"
+      ></el-slider>
+      <div
+        class="slider-button"
+        :disabled="value >= max || disabled"
+        @click="addValue"
+      >
+        <i class="el-icon-plus"></i>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SliderControl',
+  props: {
+    value: {
+      type: Number,
+      default: 0
+    },
+    max: {
+      type: Number,
+      default: 100
+    },
+    min: {
+      type: Number,
+      default: 0
+    },
+    step: {
+      type: Number,
+      default: 1
+    },
+    suffixText: {
+      type: String,
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    reverse: {
+      type: Boolean,
+      default: false
+    },
+    precision: {
+      type: Number,
+      default: 0
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data() {
+    return {
+      numValue: this.value
+    }
+  },
+  computed: {
+    sliderValue: {
+      get() {
+        return this.value
+      },
+      set(value) {
+        this.$emit('change', value)
+      }
+    }
+  },
+  methods: {
+    blur(value) {
+      let val = Number(value).toFixed(this.precision)
+      if (val > this.max) {
+        val = this.max
+      } else if (val < this.min) {
+        val = this.min
+      }
+      if (isNaN(val)) {
+        val = this.min
+      }
+      this.numValue = Number(val)
+      this.$emit('change', Number(val))
+      this.$emit('clickChange', Number(val))
+    },
+    minusValue() {
+      if (this.disabled || this.value <= this.min) {
+        return
+      }
+      this.blur(this.value - this.step)
+    },
+    addValue() {
+      if (this.disabled || this.value >= this.max) {
+        return
+      }
+      this.blur(this.value + this.step)
+    }
+  },
+  mounted() {
+    this.$watch('value', (val) => (this.numValue = val))
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.slider-control {
+  display: flex;
+  align-items: center;
+  &.is-reverse {
+    flex-direction: row-reverse;
+    .slider-input {
+      margin-right: 0;
+    }
+    .slider-model {
+      margin-right: px-to-rem(6);
+    }
+  }
+  .slider-input {
+    width: px-to-rem(68);
+    min-width: px-to-rem(68);
+    margin-right: px-to-rem(6);
+    background: rgba(79, 159, 255, 0.2);
+    border-radius: px-to-rem(4);
+    ::v-deep input {
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+      background: transparent;
+      border-radius: px-to-rem(4);
+      height: px-to-rem(32);
+      border: px-to-rem(1) solid transparent;
+      padding: 0px px-to-rem(8);
+      &:hover {
+        border: px-to-rem(1) solid rgba(79, 159, 255, 0.6);
+      }
+      &:focus {
+        border: px-to-rem(1) solid rgba(79, 159, 255, 0.8);
+      }
+    }
+    ::v-deep input[type='number']::-webkit-outer-spin-button,
+    ::v-deep input[type='number']::-webkit-inner-spin-button {
+      -webkit-appearance: none;
+      margin: 0;
+    }
+  }
+  .slider-model {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+  }
+  .slider-button {
+    &[disabled] {
+      opacity: 0.7;
+      cursor: not-allowed;
+    }
+    width: px-to-rem(32);
+    height: px-to-rem(32);
+    background: rgba(79, 159, 255, 0.2);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    border-radius: px-to-rem(4);
+    i {
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+    }
+  }
+  ::v-deep .el-input-group__append {
+    height: 0;
+    width: 0;
+    padding: 0;
+    color: #e8f3fe;
+    border: none;
+    position: relative;
+  }
+  .slider-suffix {
+    width: fit-content;
+    height: px-to-rem(32);
+    line-height: px-to-rem(32);
+    position: absolute;
+    right: px-to-rem(6);
+    top: 0;
+    color: #e8f3fe;
+    font-size: px-to-rem(16);
+  }
+  ::v-deep .el-slider {
+    padding: 0 px-to-rem(6);
+    flex: 1;
+    background: linear-gradient(
+      to right,
+      rgba(79, 159, 255, 0.05) 0%,
+      rgba(79, 159, 255, 0.2) 30%,
+      rgba(79, 159, 255, 0) 100%
+    );
+    .el-slider__button-wrapper {
+      height: fit-content;
+      width: fit-content;
+    }
+    .el-slider__bar {
+      height: px-to-rem(2);
+      background: linear-gradient(270deg, #a5ff8e 0%, #0074ff 100%) !important;
+      box-shadow: 0px 0px px-to-rem(5) 0px rgba(19, 115, 230, 0.7);
+    }
+    .el-slider__button {
+      position: absolute;
+      display: block;
+      width: px-to-rem(6);
+      height: px-to-rem(6);
+      background: #ffffff;
+      box-shadow: 0px 0px px-to-rem(5) 0px #1373e6;
+      border: none;
+    }
+
+    .el-slider__button-wrapper,
+    .el-slider__button {
+      top: 50%;
+      transform: translateY(-50%);
+    }
+    .el-slider__runway {
+      background: rgba(19, 115, 230, 0.3);
+      height: px-to-rem(2);
+    }
+  }
+}
+</style>

+ 223 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/stepInput/StepInput.vue

@@ -0,0 +1,223 @@
+<template>
+  <div class="step-input" :class="reverse ? 'is-reverse' : ''">
+    <div class="text-content" v-if="$slots.default">
+      <slot></slot>
+    </div>
+    <div class="step-input-content">
+      <div
+        class="step-button prev"
+        :disabled="numValue <= min || disabled"
+        @click="minusValue"
+      >
+        <i class="el-icon-minus"></i>
+        <span class="step-text">{{ step }}</span>
+      </div>
+      <div class="step-input-container">
+        <el-input
+          type="number"
+          class="input-number next"
+          v-model.number="numValue"
+          :max="max"
+          :min="min"
+          :disabled="disabled"
+          @mousewheel.native.prevent
+          @blur="handleBlur"
+        ></el-input>
+        <div class="suffix-text">{{ suffixText }}</div>
+      </div>
+      <div
+        class="step-button next"
+        :disabled="numValue >= max || disabled"
+        @click="addValue"
+      >
+        <i class="el-icon-plus"></i>
+        <span class="step-text">{{ step }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'StepInput',
+  props: {
+    value: {
+      type: Number,
+      default: 0
+    },
+    max: {
+      type: Number,
+      default: Infinity
+    },
+    min: {
+      type: Number,
+      default: 0
+    },
+    step: {
+      type: Number,
+      default: 1
+    },
+    suffixText: {
+      type: String,
+      default: ''
+    },
+    precision: {
+      type: Number,
+      default: 1
+    },
+    reverse: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  data() {
+    return {
+      numValue: this.value
+    }
+  },
+  watch: {
+    value(val) {
+      this.numValue = val
+    }
+  },
+  methods: {
+    minusValue() {
+      if (this.disabled || this.numValue <= this.min) {
+        return
+      }
+      this.numValue = this.numValue - this.step
+      this.handleBlur()
+    },
+    addValue() {
+      if (this.disabled || this.numValue >= this.max) {
+        return
+      }
+      this.numValue = this.numValue + this.step
+      this.handleBlur()
+    },
+    handleBlur() {
+      let val = Number(this.numValue)
+      if (val < this.min) {
+        val = this.min
+      } else if (val > this.max) {
+        val = this.max
+      }
+
+      const str = val.toFixed(this.precision)
+      this.numValue = parseFloat(str)
+      this.$emit('change', this.numValue)
+      this.$emit('blur', this.numValue)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.step-input {
+  display: flex;
+  align-items: center;
+  &.is-reverse {
+    flex-direction: row-reverse;
+    .slider-input {
+      margin-right: 0;
+    }
+    .slider-model {
+      margin-right: px-to-rem(6);
+    }
+  }
+  .text-content {
+    min-width: fit-content;
+    padding-right: px-to-rem(12);
+  }
+
+  .step-input-content {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: relative;
+    flex: 1;
+  }
+  .step-input-container {
+    flex: 1;
+    display: flex;
+    position: relative;
+    .suffix-text {
+      position: absolute;
+      right: px-to-rem(12);
+      height: px-to-rem(32);
+      line-height: px-to-rem(32);
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+    }
+  }
+  ::v-deep .input-number {
+    flex: 1;
+    width: unset;
+
+    .el-input-number__increase,
+    .el-input-number__decrease {
+      display: none;
+    }
+    .el-input__inner {
+      background: rgba(79, 159, 255, 0.1);
+      height: px-to-rem(32);
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+      border: none;
+      outline: none;
+      border-radius: 0;
+      text-align: center;
+    }
+  }
+  ::v-deep input[type='number']::-webkit-outer-spin-button,
+  ::v-deep input[type='number']::-webkit-inner-spin-button {
+    -webkit-appearance: none !important;
+    margin: 0 !important;
+  }
+  .step-button {
+    width: fit-content;
+    min-width: px-to-rem(54);
+    text-align: center;
+    height: px-to-rem(32);
+    background: rgba(79, 159, 255, 0.2);
+    display: flex;
+    padding: 0 px-to-rem(6);
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    &[disabled] {
+      opacity: 0.7;
+      cursor: not-allowed;
+    }
+    &.prev {
+      border-radius: px-to-rem(4) 0 0 px-to-rem(4);
+    }
+    &.next {
+      border-radius: 0 px-to-rem(4) px-to-rem(4) 0;
+    }
+    .step-text {
+      margin-left: px-to-rem(3);
+      font-size: px-to-rem(16);
+    }
+    i {
+      color: #e8f3fe;
+      font-size: px-to-rem(14);
+    }
+  }
+  ::v-deep .el-input-number .el-input {
+    line-height: px-to-rem(32);
+    text-align: center;
+  }
+}
+</style>

+ 305 - 0
src/components/common-comp-uav-fly-manage/src/baseComponents/warningList/WarningList.vue

@@ -0,0 +1,305 @@
+<template>
+  <div class="warning-list">
+    <div class="warning-list-title" v-if="showWarningList"> </div>
+    <div
+      class="warning-title"
+      :class="showWarningList && 'unfold'"
+      @click="toggleWarningList"
+    >
+      <div class="warning-type">
+        <ct-icon
+          color="#fff"
+          class="warning-level-icon"
+          name="warning-level"
+        ></ct-icon>
+        <div class="warning-type-text">{{
+          tipText[riskList[0].key].typeText
+        }}</div>
+      </div>
+      <el-tooltip
+        placement="top"
+        popper-class="tooltip-class"
+        :content="
+          (riskList[0].index || '') +
+          tipText[riskList[0].key][riskList[0].type](
+            riskList[0][riskList[0].type]
+          )
+        "
+      >
+        <div class="warning-text title-content"
+          >{{ riskList[0].index || ''
+          }}{{
+            tipText[riskList[0].key][riskList[0].type](
+              riskList[0][riskList[0].type]
+            )
+          }}</div
+        >
+      </el-tooltip>
+    </div>
+    <div class="warning-content" v-if="showWarningList">
+      <el-scrollbar class="scroller">
+        <div
+          class="warning-list-item"
+          :class="{
+            warningFirstItem: index === 0
+          }"
+          v-for="(item, index) in riskList"
+          :key="index"
+        >
+          <ct-icon
+            :color="item.type === 'danger' ? ' #ED5158' : '#FB913C'"
+            class="warning-level-icon"
+            name="warning-level"
+          ></ct-icon>
+          <el-tooltip
+            placement="top"
+            popper-class="tooltip-class"
+            :content="
+              (riskList[index].index || '') +
+              tipText[item.key][item.type](item[item.type])
+            "
+          >
+            <p class="warning-text"
+              >【{{ tipText[riskList[index].key].typeText }}】{{
+                riskList[index].index || ''
+              }}{{ tipText[item.key][item.type](item[item.type]) }}</p
+            >
+          </el-tooltip>
+        </div>
+      </el-scrollbar>
+    </div>
+  </div>
+</template>
+
+<script>
+const tipText = {
+  enduranceRisk: {
+    typeText: '续航风险',
+    warning: () => '预计飞行时间较长,可能无法一次完成全部任务。',
+    sort: 1
+  },
+  collisionRisk: {
+    typeText: '碰撞风险',
+    danger: (num) => `距离障碍物过近(小于${num}m),请注意飞行安全。`,
+    warning: (num) => `距离障碍物过近(小于${num}m),请注意飞行安全。`,
+    sort: 5
+  },
+  heightRisk: {
+    typeText: '碰撞风险',
+    warning: () => `高度距离参考地形小于20 m,请注意飞行安全。`,
+    sort: 6
+  },
+  pointHeightRisk: {
+    typeText: '碰撞风险',
+    warning: (num) => `航点#${num}高度距离参考地形小于20 m,请注意飞行安全。`,
+    sort: 5
+  },
+  notDetectionRisk: {
+    typeText: '碰撞风险',
+    warning: () => '未进行碰撞检测。',
+    sort: 10
+  },
+  startLineAirspaceRisk: {
+    typeText: '空域风险',
+    warning: () => '起航段 航线经过禁飞区,请确认禁飞区是否解禁。',
+    sort: 2
+  },
+  backLineAirspaceRisk: {
+    typeText: '空域风险',
+    warning: () => '返航段 航线经过禁飞区,请确认禁飞区是否解禁。',
+    sort: 2
+  },
+  airLineAirspaceRisk: {
+    typeText: '空域风险',
+    warning: () => `航线经过禁飞区,请确认禁飞区是否解禁。`,
+    sort: 2
+  },
+  airPointAirspaceRisk: {
+    typeText: '空域风险',
+    warning: () => `经过禁飞区,请确认禁飞区是否解禁。`,
+    sort: 2
+  }
+}
+export default {
+  name: 'WarningList',
+  data() {
+    return {
+      showWarningList: false
+    }
+  },
+  computed: {
+    tipText() {
+      return tipText
+    },
+    riskList() {
+      const riskList = this.warningList.map((item) => {
+        return {
+          ...item,
+          sort: tipText[item.key].sort
+        }
+      })
+      const dangers = riskList.filter((item) => item.type === 'danger')
+      const warnings = riskList.filter((item) => item.type === 'warning')
+      // 分别排序
+      const dangerList = dangers.sort((a, b) => a.sort - b.sort)
+      const warningList = warnings.sort((a, b) => a.sort - b.sort)
+      // 合并数组
+      return [...dangerList, ...warningList]
+    }
+  },
+  props: {
+    warningList: {
+      type: Array,
+      default: () => []
+    }
+  },
+  methods: {
+    toggleWarningList() {
+      this.showWarningList = !this.showWarningList
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.warning-list {
+  position: absolute;
+  left: px-to-rem(382);
+  top: px-to-rem(94);
+  z-index: 20;
+  pointer-events: auto;
+  width: px-to-rem(432);
+  height: px-to-rem(34);
+  .close-icon {
+    display: block;
+    position: absolute;
+    right: px-to-rem(-19);
+    top: px-to-rem(-19);
+    width: px-to-rem(38);
+    height: px-to-rem(38);
+    background: url('../../img/close.png') no-repeat;
+    background-size: 100% 100%;
+    cursor: pointer;
+  }
+  ::v-deep .scroller {
+    height: 100%;
+    width: 100%;
+    .el-scrollbar__wrap {
+      max-height: px-to-rem(160);
+      padding: 0 px-to-rem(12);
+    }
+  }
+  .warning-level-icon {
+    height: px-to-rem(16);
+    width: px-to-rem(16);
+    display: flex;
+    align-items: center;
+    margin-top: px-to-rem(-4);
+    ::v-deep span {
+      height: px-to-rem(20);
+    }
+  }
+  ::v-deep .ct-icon .icon {
+    font-size: px-to-rem(16) !important;
+  }
+  .warning-type-text {
+    padding-left: px-to-rem(6);
+  }
+  .warning-text {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    &.title-content {
+      margin-left: px-to-rem(-16);
+      padding-right: px-to-rem(32);
+    }
+  }
+  .warning-list-title {
+    position: absolute;
+    z-index: 10;
+    top: px-to-rem(1);
+    left: px-to-rem(1);
+    width: px-to-rem(430);
+    height: px-to-rem(34);
+    border-radius: px-to-rem(8);
+    background: rgba(23, 37, 55, 0.9);
+  }
+  .warning-title {
+    display: flex;
+    z-index: 11;
+    align-items: center;
+    height: 100%;
+    position: relative;
+    cursor: pointer;
+    background: url('../../img/warning-list-bg.png') no-repeat;
+    background-size: 100% 100%;
+    user-select: none;
+    &::after {
+      content: '';
+      font-size: px-to-rem(20);
+      font-family: 'iconfont_tools';
+      content: '\ec16';
+      position: absolute;
+      right: 0;
+      top: 0;
+      line-height: px-to-rem(32);
+      color: #e8f3fe;
+      transform-origin: center center;
+      transition: all 0.3s ease-in-out;
+    }
+    &.unfold::after {
+      transform: rotate(180deg);
+    }
+    .warning-type {
+      padding-left: px-to-rem(12);
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      display: flex;
+      align-items: center;
+      z-index: 11;
+      background: url('../../img/warning-list-red.png') no-repeat;
+      background-size: 100% 100%;
+      width: px-to-rem(140);
+      min-width: px-to-rem(140);
+      height: 100%;
+      &.danger {
+      }
+      &.warning {
+      }
+    }
+  }
+  .warning-content {
+    left: px-to-rem(1);
+    display: flex;
+    z-index: 1;
+    top: px-to-rem(1);
+    position: absolute;
+    width: px-to-rem(432);
+    max-height: px-to-rem(160);
+    background: rgba(23, 37, 55, 0.9);
+    border-radius: px-to-rem(8);
+    min-height: px-to-rem(32);
+  }
+  .warning-list-item {
+    display: flex;
+    align-items: center;
+    height: px-to-rem(32);
+    border-bottom: px-to-rem(1) solid rgba(232, 243, 254, 0.2);
+    &:last-child {
+      border-bottom: none;
+    }
+  }
+  .warningFirstItem {
+    height: px-to-rem(64);
+    .warning-level-icon {
+      margin-top: px-to-rem(28);
+    }
+    .warning-text {
+      margin-top: px-to-rem(32);
+    }
+  }
+}
+</style>

+ 176 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineCard/AirLineCard.vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="air-line-card" :class="type === '1' ? 'air-line' : 'ortho-image'">
+    <div
+      class="air-line-item"
+      v-for="(item, index) in airLineMapInfos"
+      :key="index"
+    >
+      <div class="air-info-label">{{ item.label }}</div>
+      <el-tooltip
+        :content="item.value + ''"
+        placement="start-top"
+        popper-class="tooltip-popper"
+      >
+        <div class="air-info-value" :c-tip="item.value">{{ item.value }}</div>
+      </el-tooltip>
+    </div>
+  </div>
+</template>
+
+<script>
+import { airLineMapInfos, orthoMapInfos } from '../../dict/dict'
+import { formatTimeFromSeconds } from '../../dict/plan-map'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+
+export default {
+  name: 'airLineCard',
+  props: {
+    type: {
+      type: String,
+      default: '1'
+    },
+    lineInfo: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  computed: {
+    airLineMapInfos() {
+      const airLineMap = airLineMapInfos.map((item) => {
+        const temp = { ...item, value: this.lineInfo[item.key] }
+        if (item.key === 'planTime') {
+          temp.value = formatTimeFromSeconds(temp.value)
+        }
+        if (item.key === 'airLineDis') {
+          const airLineDis = temp?.value ? Number(temp?.value).toFixed(0) : ''
+          temp.value = airLineDis + 'm'
+        }
+        return temp
+      })
+      const orthoMap = orthoMapInfos.map((item) => {
+        const temp = { ...item, value: this.lineInfo[item.key] }
+        if (item.key === 'planTime') {
+          temp.value = formatTimeFromSeconds(temp.value)
+        }
+        if (item.key === 'area') {
+          const area_ = temp?.value ? Number(temp?.value).toFixed(2) : ''
+          temp.value = area_ + 'm²'
+        }
+        if (item.key === 'routeDistance') {
+          const routeDistance = temp?.value
+            ? Number(temp?.value).toFixed(2)
+            : ''
+          temp.value = routeDistance + 'm'
+        }
+        return temp
+      })
+      return this.type === '1' ? airLineMap : orthoMap
+    }
+  },
+  mounted() {
+    setupCTips()
+  },
+  data() {
+    return {}
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.air-line-card {
+  width: px-to-rem(603);
+  position: fixed;
+  top: px-to-rem(48);
+  left: 50%;
+  transform: translateX(-50%);
+  height: px-to-rem(96);
+  display: flex;
+  align-items: center;
+  &.air-line {
+    background: url('../../img/air-line-card-bg.png') no-repeat;
+    background-size: 100% 100%;
+  }
+  &.ortho-image {
+    background: url('../../img/ortho-card-bg.png') no-repeat;
+    background-size: 100% 100%;
+  }
+
+  .air-line-item {
+    width: 25%;
+    height: 100%;
+    position: relative;
+    .air-info-label {
+      position: absolute;
+      top: px-to-rem(26);
+      left: 0;
+      width: 100%;
+      height: px-to-rem(20);
+      line-height: px-to-rem(20);
+      padding-left: px-to-rem(50);
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .air-info-value {
+      position: absolute;
+      bottom: px-to-rem(24);
+      left: 0;
+      width: 100%;
+      line-height: px-to-rem(18);
+      padding-left: px-to-rem(50);
+      font-size: px-to-rem(18);
+      color: #e8f3fe;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+</style>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.tooltip-popper {
+  font-size: px-to-rem(14) !important;
+
+  @include themeify(false) {
+    @if $theme-name == 'theme-aquamarine' {
+      background: #00221b !important;
+      color: #fff !important;
+
+      .popper__arrow {
+        border-top-color: #00221b !important;
+
+        &::after {
+          border-top-color: #00221b !important;
+        }
+      }
+    }
+
+    @if $theme-name == 'theme-terracotta' {
+      background: rgb(22 18 9 / 95%) !important;
+      color: #e4e7c1 !important;
+
+      .popper__arrow::after {
+        border-top-color: #00221b !important;
+      }
+    }
+
+    @if $theme-name == 'theme-wiseblue' {
+      background: #0f1926 !important;
+      color: #e8f3fe !important;
+
+      .popper__arrow::after {
+        border-top-color: #0f1926 !important;
+      }
+    }
+  }
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2494 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/AirPointAirLine.vue


+ 69 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/Index.vue

@@ -0,0 +1,69 @@
+<!--4类航线新增编辑详情页面 -->
+<template>
+  <div class="air_line_warp_page">
+    <air-point-air-line
+      @close="close"
+      v-if="pageType == '1'"
+      :pageParams="pageParams"
+    ></air-point-air-line>
+    <ortho-photo
+      @close="close"
+      :mapId="mapId"
+      v-if="pageType == '2'"
+      :pageParams="pageParams"
+    ></ortho-photo>
+    <oblique-image v-if="pageType == '3'"></oblique-image>
+    <surrounding-flight v-if="pageType == '4'"></surrounding-flight>
+  </div>
+</template>
+
+<script>
+// 1: 航点航线 2:正射影像 3: 倾斜影像 4: 环绕飞行
+// todo 采用component is 形式,不再采用v-if形式
+import AirPointAirLine from './AirPointAirLine.vue'
+import OrthoPhoto from './OrthoPhoto.vue'
+import ObliqueImage from './ObliqueImage.vue'
+import SurroundingFlight from './SurroundingFlight.vue'
+
+export default {
+  name: 'AirLineEditPage',
+  components: {
+    AirPointAirLine,
+    OrthoPhoto,
+    ObliqueImage,
+    SurroundingFlight
+  },
+  data() {
+    return {}
+  },
+  props: {
+    pageParams: {
+      type: Object,
+      default: () => ({})
+    },
+    mapId: {
+      type: String,
+      default: ''
+    }
+  },
+  computed: {
+    pageType() {
+      return this.pageParams.type || '1'
+    }
+  },
+  methods: {
+    close() {
+      this.$emit('close')
+    }
+  }
+}
+</script>
+
+<style>
+.air_line_warp_page {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  z-index: 31;
+}
+</style>

+ 12 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/ObliqueImage.vue

@@ -0,0 +1,12 @@
+<!-- 倾斜影像页面 -->
+<template>
+  <div class="obliquephoto_page"> 倾斜影像页面 </div>
+</template>
+
+<script>
+export default {
+  name: 'obliquePhotoPage'
+}
+</script>
+
+<style></style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2159 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/OrthoPhoto.vue


+ 12 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineEditPage/SurroundingFlight.vue

@@ -0,0 +1,12 @@
+<!-- 环绕飞行页面 -->
+<template>
+  <div class="surrounding_flight_page"> 环绕飞行页面 </div>
+</template>
+
+<script>
+export default {
+  name: 'SurroundingFlightPage'
+}
+</script>
+
+<style></style>

+ 650 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/AirLineMana.vue

@@ -0,0 +1,650 @@
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div>
+    <div class="air-line-bg">
+      <top-area
+        @showAddAirLine="showAddAirLine"
+        @showFliter="showFliter"
+        @search="searchAirLineList"
+        @showCollect="showCollection"
+        @showImportAirLine="showImportAirLine"
+        ref="topAreaRef"
+      ></top-area>
+      <div class="air-line-list">
+        <el-scrollbar
+          class="scrollbar"
+          v-if="airLineList.length > 0 && !loading"
+        >
+          <div v-for="(item, index) in airLineList" :key="index">
+            <air-lineitem
+              :item="item"
+              :index="index"
+              :isFromcollect="isShowCollection"
+              @deleteCollect="deleteCollect"
+              @rename="renameLineItem"
+              @copy="copyLineItem"
+              @check="checkLineItem"
+              @delete="showDelLine"
+              @edit="editItem"
+              @checkDetail="checkFlightDetail(item, index)"
+              :class="[item.isSelect ? 'active' : 'normal']"
+            ></air-lineitem>
+          </div>
+        </el-scrollbar>
+        <div style="text-align: right" v-if="airLineList.length > 0">
+          <fly-pagination
+            :currentPage.sync="params.pageNum"
+            :total="total"
+            :pageSize="params.pageSize"
+            @changePagination="planCurrentChange"
+          />
+        </div>
+        <div class="fly_list_content">
+          <div
+            class="loading-datas empty-text"
+            v-if="airLineList.length <= 0 && !loading"
+          >
+            <span>暂无数据</span>
+          </div>
+          <div v-if="loading" class="loading-datas">
+            <i class="el-icon-loading"></i>
+            <span>加载中</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    <add-air-line
+      v-show="isShowAddAirLine"
+      @closeAddAirLine="closeAddAirLine"
+    ></add-air-line>
+    <air-line-fliter
+      ref="airLineFliterRef"
+      v-show="isShowFliter"
+      @filterSubmit="fliterSubmit"
+    ></air-line-fliter>
+    <renam-line-name
+      v-if="isShowEditLineName"
+      :item="editLineItem"
+      :titlename="titlename"
+      :type="alterType"
+      @closeRenameAirLineName="closeRenameAirLineName"
+    ></renam-line-name>
+    <air-line-del
+      v-if="isShowDelLine"
+      :item="editLineItem"
+      @closeAirLineDel="closeAirLineDel"
+    ></air-line-del>
+    <import-air-line
+      v-if="isShowImportAirLine"
+      @closeImportAirLine="closeImportAirLine"
+    >
+    </import-air-line>
+    <air-line-card
+      :type="airLineType"
+      v-if="isShowAirLineCard"
+      :lineInfo="lineInfo"
+    ></air-line-card>
+    <!-- 禁飞区图层 -->
+    <!--    <restricted-flight-zone
+      :right="20"
+      :bottom="140"
+      :map-id="mapId"
+    ></restricted-flight-zone>-->
+  </div>
+</template>
+
+<script>
+import topArea from './components/TopArea.vue'
+import airLineitem from './components/AirLineitem.vue'
+import addAirLine from './components/AddAirLine.vue'
+import airLineFliter from './components/AirLineFliter.vue'
+import renamLineName from './components/RenamLineName.vue'
+import airLineDel from './components/AirLineDel.vue'
+import importAirLine from './components/ImportAirLine.vue'
+import AirLineCard from '../../components/airLineCard/AirLineCard'
+import eventPath from '@component-gallery/build-event-bus-path'
+import FlyPagination from '../flyResult/FlyPagination.vue'
+import {
+  getAirLineList,
+  queryAirLineCollectList,
+  getAirLineInfo
+} from '../../service'
+import {
+  setAirLineAndPoint,
+  setOrthoImage,
+  handleAirLineAndPointData,
+  handleOrthophotoData,
+  getHeights,
+  getLinkLines,
+  planMapFocusEntity,
+  planMapFocusArea
+} from '../../dict/plan-map'
+import { findDeviceNodeByCode } from '../../dict/plan-map'
+
+export default {
+  props: {
+    mapId: {
+      type: String,
+      default: ''
+    }
+  },
+  name: 'airLineMana',
+  components: {
+    topArea,
+    airLineitem,
+    addAirLine,
+    airLineFliter,
+    renamLineName,
+    airLineDel,
+    importAirLine,
+    AirLineCard,
+    FlyPagination
+  },
+  inject: ['mapRef'],
+  data() {
+    return {
+      isShowAddAirLine: false,
+      isShowFliter: false,
+      airLineList: [],
+      isShowCollection: false,
+      isShowEditLineName: false,
+      isShowDelLine: false,
+      isShowImportAirLine: false,
+      isShowAirLineCard: false,
+      editLineItem: {},
+      loading: false,
+      params: {
+        flightRouteName: '',
+        createBeginTime: '',
+        createEndTime: '',
+        deviceCodes: '',
+        types: '',
+        pageNum: 1,
+        pageSize: 9
+      },
+      total: null,
+      titlename: '重命名航线',
+      // 类型 0 重命名航线 1 复制航线
+      alterType: 0,
+      airLineAndPointMap: {},
+      orthoImageMap: {},
+      lineInfo: {},
+      currentLine: {},
+      // 地图航线详情卡片是否显示 -1 关闭 0 显示
+      isShowAirLineCardStatus: -1
+    }
+  },
+  created() {
+    this.getAirLineListData()
+    this.isShowAirLineCard = false
+    this.isShowAirLineCardStatus = -1
+    this.$globalEventBus.$on(`airPointAirLineDidCancel`, (res) => {
+      this.airLineDetailsDidCancel()
+    })
+    this.$globalEventBus.$on(`orthophotoDidCancel`, (res) => {
+      this.airLineDetailsDidCancel()
+    })
+  },
+  beforeDestroy() {
+    let mapRef = this.mapRef.getMapRef(this.mapId)
+    mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = false
+  },
+  destroyed() {
+    if (this.isShowAirLineCard) {
+      this.clearAirLineMap('1')
+      this.clearAirLineMap('2')
+    }
+    this.$globalEventBus.$off(`airPointAirLineDidCancel`)
+    this.$globalEventBus.$off(`orthophotoDidCancel`)
+  },
+  methods: {
+    // 航线详情取消显示
+    airLineDetailsDidCancel() {
+      this.isShowAirLineCard = this.isShowAirLineCardStatus >= 0
+    },
+    // 重命名航线
+    renameLineItem(item) {
+      this.titlename = '重命名航线'
+      this.alterType = 0
+      this.editLineItem = item
+      this.isShowEditLineName = true
+      this.isShowDelLine = false
+      this.isShowAddAirLine = false
+    },
+    // 复制航线
+    copyLineItem(item) {
+      this.titlename = '复制航线'
+      this.alterType = 1
+      console.log('复制航线', item)
+      // 原始航线名称超过18位 截取前18位
+      const oriFlightRouteName =
+        item.flightRouteName.length > 18
+          ? item.flightRouteName.substring(0, 18)
+          : item.flightRouteName
+      const flightRouteName = oriFlightRouteName + '复制'
+      this.editLineItem = JSON.parse(JSON.stringify(item))
+      this.editLineItem.flightRouteName = flightRouteName
+      this.isShowEditLineName = true
+      this.isShowDelLine = false
+      this.isShowAddAirLine = false
+    },
+    // 删除航线弹窗
+    showDelLine(item) {
+      this.editLineItem = item
+      this.isShowDelLine = true
+      this.isShowEditLineName = false
+      this.isShowAddAirLine = false
+    },
+    // 关闭删除航线弹窗
+    closeAirLineDel(status) {
+      this.isShowDelLine = false
+      if (!status) {
+        return
+      }
+      if (this.currentLine.flightRouteId === this.editLineItem.flightRouteId) {
+        this.isShowAirLineCard = false
+        this.clearAirLineMap('1')
+        this.clearAirLineMap('2')
+      }
+      this.getAirLineListData()
+    },
+    // 关闭编辑航线名称
+    closeRenameAirLineName(status) {
+      this.isShowEditLineName = false
+      if (!status) {
+        return
+      }
+      this.getAirLineListData()
+    },
+    // 删除收藏
+    deleteCollect() {
+      this.getAirLineListData()
+    },
+    // 筛选框提交
+    fliterSubmit(param) {
+      console.log('筛选框数据', param)
+      this.$refs.topAreaRef.isShowFliter = false
+      this.isShowFliter = false
+      const oriSearhValue = this.params.flightRouteName
+      this.params = param
+      this.params.pageNum = 1
+      this.params.pageSize = 9
+      this.params.flightRouteName = oriSearhValue
+      this.$refs.topAreaRef.fliterActive = false
+      if (
+        this.params.createBeginTime != '' ||
+        this.params.deviceCodes != '' ||
+        this.params.types != ''
+      ) {
+        this.$refs.topAreaRef.fliterActive = true
+      }
+      this.getAirLineListData()
+    },
+    // 搜索
+    searchAirLineList(name) {
+      this.params.pageNum = 1
+      this.params.flightRouteName = name
+      this.getAirLineListData()
+    },
+    // 分页
+    planCurrentChange() {
+      this.getAirLineListData()
+    },
+    //TODO //增加注释 //抽取方法
+    // 航线列表数据
+    // 合并后的方法,用于获取航线列表或收藏列表数据
+    getAirLineListData() {
+      this.loading = true
+      let action = this.isShowCollection
+        ? queryAirLineCollectList
+        : getAirLineList
+      action(this.params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.airLineList = res.rows
+            this.total = res.total
+            if (this.params.pageNum > 1 && this.airLineList.length <= 0) {
+              this.params.pageNum = 1
+              this.getAirLineListData()
+            }
+          }
+        })
+        .catch((err) => {
+          console.error(err)
+          this.airLineList = []
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    // 显示收藏
+    showCollection(status) {
+      this.params.pageNum = 1
+      this.isShowCollection = status
+      console.log('showCollection', status)
+      this.getAirLineListData()
+    },
+    // 显示添加航线弹窗
+    showAddAirLine() {
+      this.isShowAddAirLine = true
+      this.isShowEditLineName = false
+      this.isShowDelLine = false
+    },
+    // 关闭添加航线弹窗
+    closeAddAirLine(params) {
+      this.isShowAddAirLine = false
+      if (!params) {
+        return
+      }
+      console.log('添加航线参数', params)
+      this.handleJumpPage(params)
+    },
+    // 编辑航线
+    editItem(item) {
+      const uavTreeData = this.$refs.airLineFliterRef.uavTreeData
+      const currentNode = findDeviceNodeByCode(uavTreeData, item.deviceCode)
+      item.currentNode = currentNode
+      item.pageType = 'edit'
+      console.log('编辑航线参数', item)
+      this.handleJumpPage(item)
+    },
+    //显示导入航线
+    showImportAirLine() {
+      this.isShowImportAirLine = true
+    },
+    // 关闭导入航线弹窗
+    closeImportAirLine(item) {
+      this.isShowImportAirLine = false
+      if (!item) {
+        return
+      }
+      const uavTreeData = this.$refs.airLineFliterRef.uavTreeData
+      const currentNode = findDeviceNodeByCode(uavTreeData, item.deviceCode)
+      item.currentNode = currentNode
+      console.log('导入航线完成', item)
+      this.getAirLineListData()
+      this.handleJumpPage(item)
+    },
+
+    // 显示筛选框
+    showFliter(status) {
+      this.isShowFliter = status
+      this.$refs.topAreaRef.fliterActive = status
+    },
+    // 跳转页面
+    handleJumpPage(item) {
+      this.editLineItem = item
+      this.clearAirLineMap('1')
+      this.clearAirLineMap('2')
+      console.log('handleJumpPage', item)
+      this.isShowAirLineCard = false
+      this.$globalEventBus.$emit(
+        `${eventPath.commonCompUavFlyManage}__open-airline-page`,
+        item
+      )
+    },
+    // 地图显示航线详情
+    checkFlightDetail(item, currIndex) {
+      console.log('checkFlightDetail', item)
+      this.airLineList.forEach((item, index) => {
+        item.isSelect = currIndex == index
+      })
+      this.$forceUpdate()
+      this.clearAirLineMap('1')
+      this.clearAirLineMap('2')
+      this.getAirLineInfo(item)
+    },
+    // 获取航线详情
+    async getAirLineInfo(item) {
+      const params = {
+        flightRouteId: item.flightRouteId,
+        uavNestCode: item.deviceCode,
+        uavCode: item.deviceCode,
+        snapshotId: item.snapshotId
+      }
+      // todo 优化代码行数、不超过1000行
+      // 调用获取航线详情的接口
+      const res = await getAirLineInfo(params)
+      if (res.code !== 200) {
+        return
+      }
+      console.log('获取航线详情', res)
+      const uavNestPoint = [
+        +res.data.uavNestLongitude,
+        +res.data.uavNestLatitude
+      ]
+      const currentLine = res.data
+      this.currentLine = currentLine
+      // 获取uavTreeData
+      const uavTreeData = this.$refs.airLineFliterRef.uavTreeData
+      // 根据设备编码在uavTreeData中查找设备节点
+      const currentNode = findDeviceNodeByCode(
+        uavTreeData,
+        currentLine.deviceCode
+      )
+      console.log('currentNode:', currentNode)
+      const uavPoint = [+res.data.uavLongitude, +res.data.uavLatitude]
+      // 根据currentLine.uavNestLongitude是否存在来选择uavNestPoint或uavPoint
+      let orthoUavPoint = currentLine.uavNestLongitude ? uavNestPoint : uavPoint
+      orthoUavPoint.push(+currentNode.altitude)
+      const type = currentLine.type
+      this.airLineType = type
+      // type 为 1 航点航线  type 为 2 正射影像
+      if (!currentLine?.kmzJson) {
+        this.isShowAirLineCardStatus = -1
+        this.isShowAirLineCard = false
+        return
+      }
+      // 获取地图实例
+      let mapRef = this.mapRef.getMapRef(this.mapId)
+      // 绘制的线会穿透地形,需要关闭地形深度检测,就会把多余的航线隐藏,需要在页面
+      //beforeDestroy()中重置
+      mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = true
+      if (type === '1') {
+        // 处理航线和点线的数据
+        this.handleAirLineAndPointLineData(
+          currentLine,
+          currentNode,
+          orthoUavPoint,
+          mapRef
+        )
+      }
+      if (type === '2') {
+        // 处理正射影像线的数据
+        this.handleOrthophotoLineData(
+          currentLine,
+          currentNode,
+          orthoUavPoint,
+          mapRef
+        )
+      }
+      this.isShowAirLineCardStatus = 0
+      this.isShowAirLineCard = true
+    },
+    /**
+     * 处理航点航线的数据
+     * @param currentLine 当前航线数据
+     * @param currentNode 当前设备数据
+     * @param orthoUavPoint 机巢/无人机坐标
+     * @param mapRef 地图实例
+     */
+    async handleAirLineAndPointLineData(
+      currentLine,
+      currentNode,
+      orthoUavPoint,
+      mapRef
+    ) {
+      // 处理航点航线的数据
+      const { linePoints, lineInfo } = handleAirLineAndPointData(currentLine)
+      // 将线点坐标转换为经纬度格式
+      const positions = linePoints.map((item) => {
+        return { lng: Number(item[0]), lat: Number(item[1]) }
+      })
+      // 获取第一个线点的高度值
+      const heightVal = linePoints[0][2]
+      // 将kmzJson格式的字符串转换为json对象
+      const airLineJson = JSON.parse(currentLine.kmzJson)
+      // 获取高程信息
+      const { heightRes } = await getHeights(
+        positions,
+        airLineJson,
+        mapRef,
+        heightVal,
+        currentNode.altitude
+      )
+      // 根据高度信息对线点进行处理
+      let newLinePoints = linePoints.map((item, index) => {
+        let linePoint_ = heightRes[index]
+        return [Number(item[0]), Number(item[1]), Number(linePoint_['ASLT'])]
+      })
+      // 更新线信息
+      this.lineInfo = lineInfo
+      // 清除飞行线图层
+      this.clearAirLineMap('1')
+      // 创建线点数组
+      let points = [orthoUavPoint, ...newLinePoints, orthoUavPoint]
+      // 起飞点到首航点坐标
+      const firstPoints = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[0],
+        heightRes
+      )
+      // 返航点坐标到起飞点坐标
+      const lastPoints = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[positions.length - 1],
+        heightRes
+      )
+      const linkLines = firstPoints.concat(lastPoints)
+      // 绘制航线航点
+      this.airLineAndPointMap = setAirLineAndPoint(
+        mapRef,
+        points,
+        this.lineInfo,
+        linkLines
+      )
+      // 创建聚焦区域点数组
+      let focusAreaPonits = linePoints
+      focusAreaPonits.push([
+        orthoUavPoint[0],
+        orthoUavPoint[1],
+        orthoUavPoint[2]
+      ])
+      // 设置聚焦区域
+      planMapFocusArea(focusAreaPonits, mapRef)
+    },
+    /**
+     * 处理正射影响的数据
+     * @param currentLine 当前航线数据
+     * @param currentNode 当前设备数据
+     * @param orthoUavPoint 机巢/无人机坐标
+     * @param mapRef 地图实例
+     */
+    async handleOrthophotoLineData(
+      currentLine,
+      currentNode,
+      orthoUavPoint,
+      mapRef
+    ) {
+      const { linePoints, polygonPoints, lineInfo } =
+        // 处理航迹数据
+        handleOrthophotoData(currentLine)
+      const positions = linePoints.map((item) => {
+        // 生成经纬度数组
+        return { lng: Number(item[0]), lat: Number(item[1]) }
+      })
+      // 获取高度值
+      const heightVal = linePoints[0][2]
+      // 解析当前航迹为JSON对象
+      const airLineJson = JSON.parse(currentLine.kmzJson)
+      // 获取高度信息
+      const { heightRes } = await getHeights(
+        positions,
+        airLineJson,
+        mapRef,
+        heightVal,
+        currentNode.altitude
+      )
+      // 生成新的航迹点数组
+      let newLinePoints = linePoints.map((item, index) => {
+        // 获取对应位置的高度信息
+        let linePoint_ = heightRes[index]
+        return [Number(item[0]), Number(item[1]), Number(linePoint_['ASLT'])]
+      })
+      // 获取连接线数组
+      const linkLines = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[0],
+        heightRes
+      )
+      // 更新航迹信息
+      this.lineInfo = lineInfo
+      // 清除地图上的航迹
+      this.clearAirLineMap('2')
+      // 设置正射影像地图
+      this.orthoImageMap = setOrthoImage(
+        mapRef,
+        newLinePoints,
+        [polygonPoints],
+        orthoUavPoint,
+        this.lineInfo,
+        linkLines
+      )
+      let entity =
+        this.orthoImageMap.orthoAirLineInstance0._dataSource._entityCollection
+      // 规划地图聚焦区域
+      planMapFocusEntity(entity, mapRef)
+    },
+    // 清除航线和航点 clearType: 1-清除航点航线,2-清除正射影像
+    clearAirLineMap(clearType) {
+      const clearKey =
+        clearType === '1' ? 'airLineAndPointMap' : 'orthoImageMap'
+      // todo 清除逻辑整合
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      // todo 改为Object.keys
+      const keys = Object.keys(this[clearKey])
+      keys.forEach((key) => {
+        if (this[clearKey][key]) {
+          try {
+            this[clearKey][key].customRemove(mapRef, this[clearKey][key])
+          } catch (e) {
+            console.error(e)
+          }
+          this[clearKey][key] = null
+        }
+      })
+    },
+    // 航线详情
+    checkLineItem(item) {
+      const uavTreeData = this.$refs.airLineFliterRef.uavTreeData
+      const currentNode = findDeviceNodeByCode(uavTreeData, item.deviceCode)
+      item.currentNode = currentNode
+      item.pageType = 'view'
+      console.log('checkLineItem', item)
+      item.isEdit = true
+      this.handleJumpPage(item)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '../../style/air-line-mana.scss';
+
+.active {
+  background-image: url('../../img/air_line_list_sel_bg.svg');
+  background-size: 100% 100%;
+}
+
+.normal {
+  background-image: url('../../img/air_line_list_nor_bg.svg');
+  background-size: 100% 100%;
+}
+
+.innercomp-abcontainer {
+  position: fixed;
+}
+</style>

+ 503 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AddAirLine.vue

@@ -0,0 +1,503 @@
+<template>
+  <div class="add-air-line-box">
+    <absolute-container
+      :title="title"
+      :width="412"
+      @close="handleClose"
+      canClose
+      industryClass="AddAirLine air_line_info common-iw-s"
+    >
+      <div class="content-info">
+        <div class="addItem">
+          <img
+            src="../../../img/add_line_type_title_bg.svg"
+            alt=""
+            class="bg_img"
+          />
+          <span class="addItem_star">*</span>
+          <span class="addItem_text">航线类型</span>
+        </div>
+        <div class="airLineType">
+          <div
+            class="line_type_item"
+            v-for="(item, index) in airLineTypeTab"
+            :key="index + 'list'"
+            @click="clickItem(index)"
+          >
+            <img
+              class="icon_bg_img"
+              :src="item.isSel ? item.bgActive : item.bgIcon"
+              alt=""
+            />
+            <img
+              class="sel_icon_img"
+              src="../../../img/add_line_type_sel.svg"
+              alt=""
+              v-if="item.isSel"
+            />
+            <img class="icon_img" :src="item.active" alt="" />
+            <div class="des">{{ item.name }}</div>
+          </div>
+        </div>
+        <div class="addItem">
+          <img
+            src="../../../img/add_line_type_title_bg.svg"
+            alt=""
+            class="bg_img"
+          />
+          <span class="addItem_star">*</span>
+          <span class="addItem_text">执行设备</span>
+        </div>
+        <div class="select-air">
+          <select-tree
+            expandOnClickNode
+            onlyLeafSelect
+            :defaultProps="{ label: 'name', children: 'list' }"
+            nodeKey="code"
+            filterable
+            v-model="uavValue"
+            :dataSource="uavTreeData"
+            industryClass="common-iw-s select-air-select-tree"
+            @refresh="getDeviceAreaTreeData"
+          ></select-tree>
+        </div>
+        <div class="addItem">
+          <img
+            src="../../../img/add_line_type_title_bg.svg"
+            alt=""
+            class="bg_img"
+          />
+          <span class="addItem_star">*</span>
+          <span class="addItem_text">航线名称</span>
+        </div>
+        <div class="input-bg">
+          <el-input
+            placeholder="输入航线名称"
+            size="small"
+            v-model.trim="flightRouteName"
+            class="search-input"
+            :clearable="false"
+            maxlength="20"
+            @input="handleSearchValueChange"
+          >
+            <template #suffix>
+              <i
+                v-show="flightRouteName !== ''"
+                :style="{
+                  color: 'rgb(232 242 254 / 50%)',
+                  cursor: 'pointer',
+                  fontSize: pxToRem(16),
+                  lineHeight: `${pxToRem(32)} !important`
+                }"
+                @click="clearSearch"
+                class="el-input__icon el-icon-error"
+              ></i>
+            </template>
+          </el-input>
+        </div>
+        <div class="botBtn">
+          <base-button type="primary" class="submitBtn" @click="submit">
+            确定
+          </base-button>
+          <base-button type="tiny" class="cancelBtn" @click="close">
+            取消
+          </base-button>
+        </div>
+      </div>
+    </absolute-container>
+  </div>
+</template>
+
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import { airLineTypeTab } from '../../../entry/data.js'
+import SelectTree from '../../selectTree/SelectTree.vue'
+import {
+  getDeviceAreaTree,
+  queryDefultAirLineName,
+  checkAirLineName
+} from '../../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { findDeviceNodeByCode } from '../../../dict/plan-map'
+
+export default {
+  name: 'addAirLine',
+  components: {
+    AbsoluteContainer,
+    BaseButton,
+    SelectTree
+  },
+  data() {
+    return {
+      airLineTypeTab: JSON.parse(JSON.stringify(airLineTypeTab)),
+      title: '新增航线',
+      flightRouteName: '',
+      uavValue: '',
+      uavTreeData: [],
+      currSelIndex: -1
+    }
+  },
+  created() {
+    this.getDeviceAreaTreeData()
+  },
+  methods: {
+    handleClose() {
+      this.$emit('closeAddAirLine')
+      this.resetData()
+    },
+    /**
+     * 提交
+     */
+    submit() {
+      if (this.currSelIndex === -1) {
+        return CommonMessage.error('请选择航线类型')
+      }
+      if (this.uavValue === '') {
+        return CommonMessage.error('请选择执行设备')
+      }
+      if (this.flightRouteName === '') {
+        return CommonMessage.error('请填写航线名称')
+      }
+      const params = {
+        flightRouteName: this.flightRouteName
+      }
+      // 检查航线名称是否重复
+      checkAirLineName(params)
+        .then((res) => {
+          console.log(res)
+          if (res.code === 200) {
+            let submitParams = {
+              type: this.airLineTypeTab[this.currSelIndex].type,
+              deviceCode: this.uavValue,
+              flightRouteName: this.flightRouteName,
+              pageType: 'add'
+            }
+            const currentNode = findDeviceNodeByCode(
+              this.uavTreeData,
+              this.uavValue
+            )
+            submitParams.currentNode = currentNode
+            this.$emit('closeAddAirLine', submitParams)
+            this.resetData()
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err.msg)
+          console.error(err)
+        })
+    },
+    /**
+     * 关闭弹窗
+     */
+    close() {
+      this.$emit('closeAddAirLine')
+      this.resetData()
+    },
+    // 重置数据
+    resetData() {
+      this.flightRouteName = ''
+      this.uavValue = ''
+      this.currSelIndex = -1
+      this.airLineTypeTab.forEach((item, index) => {
+        item.isSel = false
+      })
+    },
+    /**
+     * 清除关键字搜索
+     */
+    clearSearch() {
+      this.flightRouteName = ''
+    },
+    // 获取无人机机场数据
+    getDeviceAreaTreeData(fn) {
+      const params = {
+        needDevice: true,
+        orderStatus: '1',
+        queryType: 1
+      }
+      getDeviceAreaTree(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.uavTreeData = res.data
+            this.$nextTick(() => {
+              fn && fn()
+            })
+          }
+        })
+        .catch((err) => {
+          console.error(err)
+          this.uavTreeData = []
+        })
+    },
+    //输入框内容变化
+    handleSearchValueChange() {
+      console.log(this.flightRouteName)
+    },
+    // 点击航线类型
+    clickItem(currIndex) {
+      if (currIndex == 2 || currIndex == 3) {
+        return
+      }
+      this.currSelIndex = currIndex
+      this.airLineTypeTab.forEach((item, index) => {
+        item.isSel = currIndex == index
+      })
+      const params = {
+        type: this.airLineTypeTab[currIndex].type
+      }
+      // 获取默认航线名称
+      queryDefultAirLineName(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.flightRouteName = res.data
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err.msg)
+          console.error(err)
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.add-air-line-box {
+  width: 100vw;
+  height: 100vh;
+  z-index: 100;
+  position: absolute;
+  top: 0;
+  left: 0;
+  align-items: center;
+  justify-content: center;
+  display: flex;
+  .content-info {
+    height: px-to-rem(352);
+    width: px-to-rem(412);
+    .submitBtn {
+      margin-right: px-to-rem(12);
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: px-to-rem(16);
+    }
+
+    .cancelBtn {
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      background: rgba(79, 159, 255, 0.2);
+      font-size: px-to-rem(16) !important;
+    }
+  }
+  .botBtn {
+    display: flex;
+    justify-content: center;
+
+    .el-button {
+      padding: 0;
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: var(--font-size);
+
+      + .el-button {
+        margin-left: px-to-rem(12);
+      }
+    }
+  }
+  .addItem {
+    display: flex;
+    position: relative;
+    flex-direction: row;
+    align-items: center;
+    width: px-to-rem(346);
+    height: px-to-rem(38);
+    margin-left: px-to-rem(12);
+    margin-top: px-to-rem(12);
+    &_star {
+      align-self: center;
+      padding-left: px-to-rem(12);
+      color: #ed5158;
+      font-size: px-to-rem(14);
+    }
+    &_text {
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      line-height: px-to-rem(20);
+      text-shadow: 0px 0px px-to-rem(2) rgba(74, 141, 254, 0.7);
+    }
+  }
+  .bg_img {
+    position: absolute;
+    width: px-to-rem(346);
+    height: px-to-rem(38);
+  }
+  .airLineType {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    .line_type_item {
+      z-index: 2;
+      margin-left: px-to-rem(12);
+      display: flex;
+      position: relative;
+      flex-direction: column;
+      align-items: center;
+      width: px-to-rem(88);
+      height: px-to-rem(82);
+      cursor: pointer;
+      .icon_bg_img {
+        width: px-to-rem(88);
+        height: px-to-rem(82);
+        position: absolute;
+      }
+      .sel_icon_img {
+        z-index: 2;
+        left: px-to-rem(2);
+        top: px-to-rem(4);
+        width: px-to-rem(24);
+        height: px-to-rem(24);
+        position: absolute;
+      }
+    }
+    .icon_img {
+      z-index: 2;
+      margin-top: px-to-rem(12);
+      margin-bottom: px-to-rem(9);
+      width: px-to-rem(32);
+      height: px-to-rem(32);
+    }
+    .des {
+      font-size: px-to-rem(16);
+      z-index: 2;
+    }
+  }
+}
+.add-air-line-box .air_line_info {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.AddAirLine {
+  ::v-deep .el-dialog {
+    .el-dialog__header {
+      padding-left: px-to-rem(12);
+    }
+  }
+  ::v-deep .innercomp-abcontainer-header {
+    width: px-to-rem(412);
+    font-size: px-to-rem(18);
+    .innercomp-abcontainer-header__title span {
+      color: #e8f2fe !important;
+    }
+  }
+}
+.el-menu-demo .el-submenu {
+  border: px-to-rem(1) solid transparent;
+  border-radius: px-to-rem(4);
+}
+
+.el-menu-demo .el-submenu.is-active,
+.el-menu-demo .el-submenu.is-hover,
+.el-menu-demo .el-submenu.is-opened,
+.el-menu-demo .el-submenu:hover {
+  border: px-to-rem(1) solid rgb(79 159 255 / 60%);
+  border-radius: px-to-rem(4);
+}
+
+.select-air {
+  margin-left: px-to-rem(12);
+  margin-right: px-to-rem(12);
+  ::v-deep .el-menu {
+    border-bottom: none;
+    background: transparent;
+    .el-submenu {
+      width: 100%;
+
+      &__title {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 0 px-to-rem(6) 0 px-to-rem(10);
+        height: px-to-rem(32);
+        border-radius: px-to-rem(4);
+        font-size: px-to-rem(14);
+        color: rgba(232, 243, 254, 0.7);
+
+        &:hover {
+          background: transparent;
+        }
+
+        i {
+          font-family: 'iconfont_tools' !important;
+          font-weight: bold;
+          transition: transform 0.3s;
+          color: #e8f3fe;
+          font-size: px-to-rem(20);
+          cursor: pointer;
+          &::before {
+            content: '\ea83';
+          }
+        }
+
+        span {
+          line-height: px-to-rem(32);
+          margin-top: px-to-rem(1.2);
+        }
+      }
+    }
+  }
+  ::v-deep .el-select .el-input__inner {
+    background: rgba(23, 43, 68, 0.2);
+    border-radius: px-to-rem(4);
+    border: px-to-rem(1) solid rgba(79, 159, 255, 0);
+    font-size: px-to-rem(16) !important;
+    padding-left: px-to-rem(12);
+    color: #e8f3fe;
+  }
+}
+.input-bg {
+  margin: 0 px-to-rem(12) px-to-rem(12) px-to-rem(12);
+  height: px-to-rem(32);
+  background: rgba(79, 159, 255, 0.2);
+  border-radius: px-to-rem(4);
+  border: px-to-rem(1) solid rgba(79, 159, 255, 0);
+}
+// 搜索
+.input-bg .search-input ::v-deep .el-input__inner {
+  color: #e8f3fe;
+  font-size: px-to-rem(16);
+  text-align: left;
+  border: px-to-rem(1) solid transparent;
+  line-height: px-to-rem(30);
+  background: transparent;
+
+  &::placeholder {
+    color: rgba(232, 243, 254, 0.7);
+  }
+}
+.input-bg .search-input ::v-deep .el-input__suffix {
+  .el-input__suffix-inner :before {
+    color: #e8f3fe;
+    font-size: px-to-rem(20);
+    font-family: 'iconfont_tools';
+    content: '\ec12';
+  }
+}
+</style>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.select-air-select-tree {
+  width: px-to-rem(390) !important;
+}
+.select-air-tree {
+  width: px-to-rem(320) !important;
+}
+</style>

+ 170 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AirLineDel.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="airdel-line-box">
+    <absolute-container
+      :title="title"
+      :width="412"
+      @close="handleClose"
+      canClose
+      industryClass="airLineDel  common-iw-s"
+    >
+      <div class="content-info">
+        <div class="del-info"> 是否确认删除该航线? </div>
+        <div class="botBtn">
+          <base-button type="primary" class="submitBtn" @click="submit">
+            确定
+          </base-button>
+          <base-button type="tiny" class="cancelBtn" @click="close">
+            取消
+          </base-button>
+        </div>
+      </div>
+    </absolute-container>
+  </div>
+</template>
+
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import { delAirLine } from '../../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+
+export default {
+  props: {
+    item: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  name: 'airLineDel',
+  components: {
+    AbsoluteContainer,
+    BaseButton
+  },
+  data() {
+    return {
+      title: '删除航线'
+    }
+  },
+  methods: {
+    // 关闭弹窗
+    handleClose() {
+      const isNeedQueryData = false
+      this.$emit('closeAirLineDel', isNeedQueryData)
+    },
+    /**
+     * 提交
+     */
+    submit() {
+      const params = {
+        flightRouteId: this.item.flightRouteId
+      }
+      delAirLine(params)
+        .then((res) => {
+          console.log(res)
+          if (res.code === 200) {
+            const isNeedQueryData = true
+            this.$emit('closeAirLineDel', isNeedQueryData)
+            return
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err.msg)
+          console.log(err)
+        })
+    },
+    /**
+     * 关闭弹窗
+     */
+    close() {
+      const isNeedQueryData = false
+      this.$emit('closeAirLineDel', isNeedQueryData)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.airdel-line-box {
+  width: 100vw;
+  height: 100vh;
+  z-index: 100;
+  position: absolute;
+  top: 0;
+  left: 0;
+  align-items: center;
+  justify-content: center;
+  display: flex;
+  background: rgba(0, 0, 0, 0.7);
+  .content-info {
+    height: px-to-rem(110);
+    width: px-to-rem(412);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    .del-info {
+      align-self: center;
+      margin-top: px-to-rem(24);
+      font-size: px-to-rem(16);
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      color: #e8f3fe;
+      line-height: px-to-rem(20);
+    }
+    .submitBtn {
+      margin-right: px-to-rem(12);
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: px-to-rem(16);
+    }
+
+    .cancelBtn {
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      background: rgba(79, 159, 255, 0.2);
+      font-size: px-to-rem(16) !important;
+    }
+  }
+  .botBtn {
+    display: flex;
+    margin-top: px-to-rem(24);
+    margin-bottom: px-to-rem(24);
+    justify-content: center;
+
+    .el-button {
+      padding: 0;
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: var(--font-size);
+
+      + .el-button {
+        margin-left: px-to-rem(12);
+      }
+    }
+  }
+}
+.add-air-line-box .airLineDel {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.airLineDel {
+  ::v-deep .el-dialog {
+    .el-dialog__header {
+      padding-left: px-to-rem(12);
+    }
+  }
+  ::v-deep .innercomp-abcontainer-header {
+    width: px-to-rem(412);
+    font-size: px-to-rem(18);
+    .innercomp-abcontainer-header__title span {
+      color: #e8f2fe !important;
+    }
+  }
+}
+</style>

+ 386 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AirLineFliter.vue

@@ -0,0 +1,386 @@
+<template>
+  <div class="fliter-content-box">
+    <div class="content-info">
+      <span class="title-text">创建时间</span>
+      <el-date-picker
+        ref="alarmDataPicker"
+        v-model="creatTiem"
+        :append-to-body="false"
+        value-format="yyyy-MM-dd"
+        type="daterange"
+        format="yyyy-MM-dd"
+        popper-class="common-iw-s operation-plan-daterange"
+        class="date-picker-box"
+        prefix-icon="iconfont_tools icon-tongyong-shaixuanriqi alarmfilte"
+        range-separator="至"
+        start-placeholder="开始时间"
+        end-placeholder="结束时间"
+        :picker-options="pickerOptions"
+        :editable="false"
+        @change="changeTime"
+      />
+      <span class="title-text">执行设备</span>
+      <select-tree
+        ref="selectTreeRef"
+        :defaultProps="{ label: 'name', children: 'list' }"
+        nodeKey="code"
+        v-model="uavValue"
+        multiple
+        filterable
+        industryClass="common-iw-s select-air-tree"
+        :dataSource="uavTreeData"
+        @refresh="getDeviceAreaTreeData"
+        class="uav-select-tree"
+      ></select-tree>
+      <span class="title-text">航线类型</span>
+      <div class="airLineType">
+        <div
+          class="line_type_item"
+          v-for="(item, index) in airLineTypeTab"
+          :key="index + 'list'"
+          @click="clickItem(index)"
+          :class="item.isSel ? 'active' : ''"
+        >
+          <img
+            class="icon_img"
+            :src="item.isSel ? item.active : item.icon"
+            alt=""
+          />
+          <div class="des" :class="item.isSel ? 'activeDes' : ''">{{
+            item.name
+          }}</div>
+        </div>
+      </div>
+      <div class="botBtn">
+        <base-button type="primary" class="submitBtn" @click="submit">
+          确定
+        </base-button>
+        <base-button type="tiny" class="cancelBtn" @click="reset">
+          重置
+        </base-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import { airLineTypeTab } from '../../../entry/data.js'
+import SelectTree from '../../selectTree/SelectTree.vue'
+import { getDeviceAreaTree } from '../../../service'
+export default {
+  name: 'airLineFliter',
+  components: {
+    BaseButton,
+    SelectTree
+  },
+  data() {
+    return {
+      airLineTypeTab: JSON.parse(JSON.stringify(airLineTypeTab)),
+      creatTiem: '',
+      uavValue: '',
+      uavTreeData: [],
+      pickerOptions: {
+        shortcuts: [
+          {
+            text: '今日',
+            onClick(picker) {
+              const end = new Date()
+              const start = new Date()
+              start.setTime(start.getTime() - 0)
+              picker.$emit('pick', [start, end])
+            }
+          },
+          {
+            text: '近三天',
+            onClick(picker) {
+              const end = new Date()
+              const start = new Date()
+              start.setTime(start.getTime() - 3600 * 1000 * 24 * 2)
+              picker.$emit('pick', [start, end])
+            }
+          },
+          {
+            text: '近七天',
+            onClick(picker) {
+              const end = new Date()
+              const start = new Date()
+              start.setTime(start.getTime() - 3600 * 1000 * 24 * 6)
+              picker.$emit('pick', [start, end])
+            }
+          },
+          {
+            text: '近三十天',
+            onClick(picker) {
+              const end = new Date()
+              const start = new Date()
+              start.setTime(start.getTime() - 3600 * 1000 * 24 * 29)
+              picker.$emit('pick', [start, end])
+            }
+          }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getDeviceAreaTreeData()
+  },
+  methods: {
+    changeTime(time) {
+      console.log(time)
+    },
+    reset() {
+      console.log('reset')
+      this.creatTiem = ''
+      this.uavValue = ''
+      this.$refs.selectTreeRef.handleClear()
+      this.airLineTypeTab.forEach((item, index) => {
+        item.isSel = false
+      })
+    },
+    submit() {
+      // 筛选条件
+      let param = {
+        // 创建开始时间
+        createBeginTime: this.creatTiem[0] || '',
+        // 创建结束时间
+        createEndTime: this.creatTiem[1] || '',
+        // 无人机参数
+        deviceCodes: this.uavValue.length > 0 ? this.uavValue.join(',') : ''
+      }
+      const types = this.airLineTypeTab
+        .filter((item) => item.isSel)
+        .map((item) => item.type)
+      param.types = types.join(',')
+      this.$globalEventBus.$emit('notiFfliterSubmit')
+      this.$emit('filterSubmit', param)
+      console.log('submit', param)
+    },
+    // 点击航线类型
+    clickItem(index) {
+      this.airLineTypeTab[index].isSel = !this.airLineTypeTab[index].isSel
+    },
+    // 获取无人机机场数据
+    getDeviceAreaTreeData(fn) {
+      const params = {
+        needDevice: true,
+        orderStatus: '1',
+        queryType: 1
+      }
+      getDeviceAreaTree(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.uavTreeData = res.data
+            this.$nextTick(() => {
+              fn && fn()
+            })
+          }
+        })
+        .catch((err) => {
+          console.error(err)
+          this.uavTreeData = []
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.fliter-content-box {
+  display: flex;
+  flex-direction: column;
+  z-index: 111;
+  align-items: center;
+  top: px-to-rem(82);
+  position: absolute;
+  width: px-to-rem(370);
+  .submitBtn {
+    margin-right: px-to-rem(12);
+    width: px-to-rem(108);
+    height: px-to-rem(32);
+    font-size: px-to-rem(16);
+  }
+
+  .cancelBtn {
+    width: px-to-rem(108);
+    height: px-to-rem(32);
+    background: rgba(79, 159, 255, 0.2);
+    font-size: px-to-rem(16) !important;
+  }
+
+  .botBtn {
+    margin-top: px-to-rem(6);
+    margin-bottom: px-to-rem(12);
+    display: flex;
+    justify-content: center;
+
+    .el-button {
+      padding: 0;
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: var(--font-size);
+
+      + .el-button {
+        margin-left: px-to-rem(12);
+      }
+    }
+  }
+  .content-info {
+    display: flex;
+    width: px-to-rem(346);
+    background: #172537;
+    border-radius: px-to-rem(4);
+    flex-direction: column;
+  }
+  .title-text {
+    margin: px-to-rem(12) 0 px-to-rem(12) px-to-rem(12);
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    font-size: px-to-rem(18);
+    color: #e8f3fe;
+    line-height: px-to-rem(26);
+    text-shadow: 0px 0px px-to-rem(10) rgba(74, 141, 254, 0.7);
+  }
+  ::v-deep .el-date-editor {
+    border-color: transparent;
+    i.iconfont.icon-tongyong_icon_riqi {
+      float: none;
+      margin-left: px-to-rem(5);
+      color: #e8f3fe;
+      font-size: px-to-rem(20);
+      line-height: px-to-rem(26);
+    }
+
+    .el-range-input {
+      background-color: transparent;
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+    }
+
+    .el-range-separator {
+      color: #e8f3fe;
+    }
+
+    .el-range-separator {
+      height: auto;
+      color: #e8f3fe;
+      line-height: normal !important;
+    }
+
+    .el-input__icon.el-range__close-icon.el-icon-circle-close {
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      &::before {
+        color: #e8f3fe;
+        font-size: px-to-rem(20);
+        font-family: 'iconfont_tools';
+        content: '\ec12';
+      }
+    }
+    .el-input__inner,
+    &.el-input__inner {
+      background-color: rgb(79 159 255 / 20%);
+      height: px-to-rem(32);
+    }
+
+    &:not(.is-disabled) {
+      .el-input__inner,
+      &.el-input__inner {
+        &:hover:not(.is-active) {
+          border-color: rgb(79 159 255 / 60%);
+        }
+
+        &.is-active,
+        &:focus,
+        &:active {
+          border-color: rgb(79 159 255 / 100%);
+
+          &:hover {
+            border-color: rgb(79 159 255 / 100%);
+          }
+        }
+      }
+    }
+  }
+  .airLineType {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    flex-wrap: wrap;
+    margin-left: px-to-rem(12);
+    width: px-to-rem(320);
+    justify-content: space-between;
+    .line_type_item {
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+      align-items: center;
+      margin-bottom: px-to-rem(6);
+      border-radius: px-to-rem(4);
+      border: px-to-rem(1) solid rgba(232, 243, 254, 0.2);
+      width: px-to-rem(158);
+      height: px-to-rem(32);
+      cursor: pointer;
+    }
+    .icon_img {
+      margin-top: px-to-rem(12);
+      margin-bottom: px-to-rem(12);
+      width: px-to-rem(16);
+      height: px-to-rem(16);
+    }
+    .des {
+      margin-left: px-to-rem(4);
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+    }
+    .activeDes {
+      color: #4f9fff;
+    }
+    .active {
+      border: px-to-rem(1) solid #4f9fff;
+    }
+  }
+  .uav-select-tree {
+    margin-left: px-to-rem(12);
+    width: px-to-rem(320) !important;
+    ::v-deep .el-select__input {
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+      text-align: left;
+      height: px-to-rem(32);
+      border: px-to-rem(1) solid transparent;
+      line-height: px-to-rem(30);
+      background: transparent;
+
+      &::placeholder {
+        color: rgba(232, 243, 254, 0.7);
+      }
+    }
+  }
+  ::v-deep .el-select .el-input__inner {
+    background: rgba(23, 43, 68, 0.2);
+    border-radius: px-to-rem(4);
+    border: px-to-rem(1) solid rgba(79, 159, 255, 0);
+    font-size: px-to-rem(16) !important;
+    padding-left: px-to-rem(12);
+    color: #e8f3fe;
+  }
+  .date-picker-box {
+    margin-left: px-to-rem(12);
+    width: px-to-rem(320) !important;
+    ::v-deep .alarmfilte {
+      height: auto;
+      color: #e8f3fe;
+      font-size: px-to-rem(20);
+      line-height: normal;
+      margin-left: 0;
+    }
+  }
+}
+</style>

+ 627 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/AirLineitem.vue

@@ -0,0 +1,627 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <div style="position: relative">
+    <div class="conten-info" @click="checkDetail($event)">
+      <div class="topInfo">
+        <img
+          class="line_type_bg"
+          src="../../../img/icon_line_type_bg.png"
+          alt=""
+        />
+        <div class="leftText">
+          <img class="line-type-icon" :src="typeIconPath" alt="" />
+          <span class="lineType" :c-tip="routeTypeName">{{
+            routeTypeName
+          }}</span>
+          <span class="lineName" :c-tip="item.flightRouteName">
+            {{ item.flightRouteName }}
+          </span>
+        </div>
+      </div>
+      <div class="bottomInfo">
+        <div class="img_bg">
+          <img
+            class="icon_img"
+            src="../../../img/air_line_uav_nest.png"
+            alt=""
+            v-if="item.uavNestName"
+          />
+          <img
+            class="icon_img"
+            src="../../../img/air_line_uav.png"
+            alt=""
+            v-else
+          />
+        </div>
+        <div
+          class="bottomText"
+          :c-tip="item.uavNestName ? item.uavNestName : item.deviceName"
+          >{{ item.uavNestName ? item.uavNestName : item.deviceName }}</div
+        >
+      </div>
+    </div>
+    <div class="rightMoreIcon">
+      <el-dropdown
+        trigger="click"
+        class="dropdownMenu"
+        @command="handleCommand"
+        @visible-change="dropdownChangeHandler"
+      >
+        <span class="el-dropdown-link">
+          <div class="moreBg">
+            <i
+              class="iconfont iconfont_tools icon-tongyong_icon_gengduo searchTypeIcon"
+              v-if="!isShowMore"
+            ></i>
+            <ct-icon
+              class="ct-icon-style actvie"
+              name="more-active"
+              :size="pxToRem(25)"
+              v-else
+            />
+          </div>
+        </span>
+        <el-dropdown-menu slot="dropdown">
+          <el-dropdown-item class="dropdown-rename" command="0">
+            <div class="dropdown-item">
+              <ct-icon
+                class="ct-icon-style menu-icon"
+                name="rename"
+                :size="pxToRem(17)"
+              />
+              <span class="dropdown-item-title">重命名</span>
+            </div>
+          </el-dropdown-item>
+          <el-dropdown-item class="dropdown-rename" command="1">
+            <div class="dropdown-item">
+              <ct-icon
+                class="ct-icon-style menu-icon"
+                name="check"
+                :size="pxToRem(17)"
+              />
+              <span class="dropdown-item-title">查看</span>
+            </div>
+          </el-dropdown-item>
+          <el-dropdown-item class="" command="2">
+            <div class="dropdown-item">
+              <ct-icon
+                class="ct-icon-style menu-icon"
+                name="edit"
+                :size="pxToRem(17)"
+              />
+              <span class="dropdown-item-title">编辑</span>
+            </div>
+          </el-dropdown-item>
+          <el-dropdown-item class="" command="3">
+            <div class="dropdown-item">
+              <ct-icon
+                class="ct-icon-style menu-icon"
+                name="table-delete"
+                :size="pxToRem(17)"
+              />
+              <span class="dropdown-item-title">删除</span>
+            </div>
+          </el-dropdown-item>
+          <el-dropdown-item class="dropdown-copy" command="4">
+            <div class="dropdown-item">
+              <ct-icon
+                class="ct-icon-style menu-icon"
+                name="copy"
+                :size="pxToRem(17)"
+              />
+              <span class="dropdown-item-title">复制</span>
+            </div>
+          </el-dropdown-item>
+          <el-dropdown-item class="dropdown-rename" command="5">
+            <div class="dropdown-item">
+              <ct-icon
+                class="ct-icon-style menu-icon"
+                name="card-download"
+                :size="pxToRem(17)"
+              />
+              <span class="dropdown-item-title">下载</span>
+            </div>
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </el-dropdown>
+      <div @click.stop="onCollect" class="collectBg">
+        <i
+          class="iconfont iconfont_tools icon-AR-gaojingxiangqing-yishoucang searchTypeIcon collectActive collect"
+          v-if="collectStatus === '0'"
+        ></i>
+        <i
+          class="iconfont iconfont_tools icon-AR-gaojingxiangqing-shoucang searchTypeIcon collect"
+          v-else
+        ></i>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import AirLineListHangdian from '../../../img/icon_list_hangdian.svg'
+import AirLineListZhengxiang from '../../../img/icon_list_zhengxiang.svg'
+import AirLineListQingxie from '../../../img/icon_list_qingxie.svg'
+import AirLineListHuanrao from '../../../img/icon_list_huanrao.svg'
+import {
+  addAirLineCollect,
+  deleteAirLineCollect,
+  checkAirLineBind
+} from '../../../service'
+import { downloadKmzUrlsApi } from '../../../dict/air-line-mana-dict'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+
+export default {
+  name: 'airLineitem',
+  props: {
+    item: {
+      type: Object,
+      default: () => ({})
+    },
+    index: {
+      type: Number,
+      default: 0
+    },
+    isFromcollect: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {},
+  data() {
+    return {
+      isShowMore: false,
+      typeIconPath: '',
+      routeTypeName: '',
+      collectStatus: 0
+    }
+  },
+  mounted() {
+    setupCTips()
+  },
+  created() {
+    console.log('created')
+    switch (this.item.type) {
+      case '1':
+        this.typeIconPath = AirLineListHangdian
+        this.routeTypeName = '航点航线'
+        break
+      case '2':
+        this.typeIconPath = AirLineListZhengxiang
+        this.routeTypeName = '正射影像'
+        break
+      case '3':
+        this.typeIconPath = AirLineListQingxie
+        this.routeTypeName = '倾斜摄影'
+        break
+      default:
+        this.typeIconPath = AirLineListHuanrao
+        this.routeTypeName = '环绕飞行'
+        break
+    }
+  },
+  watch: {
+    item: {
+      handler(newVal) {
+        if (newVal) {
+          this.collectStatus = newVal.colStatus
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  methods: {
+    //更多按钮点击事件
+    handleCommand(type) {
+      console.log('更多按钮', type)
+      // TODO// 改成map
+      const actionMap = new Map([
+        [
+          '0',
+          () => {
+            console.log('重命名')
+            this.checkFlightRouteStatus('3')
+          }
+        ],
+        [
+          '1',
+          () => {
+            console.log('查看')
+            this.$emit('check', this.item)
+          }
+        ],
+        [
+          '2',
+          () => {
+            console.log('编辑')
+            this.checkFlightRouteStatus('1')
+          }
+        ],
+        [
+          '3',
+          () => {
+            console.log('删除')
+            this.checkFlightRouteStatus('2')
+          }
+        ],
+        [
+          '4',
+          () => {
+            console.log('复制')
+            this.$emit('copy', this.item)
+          }
+        ],
+        [
+          '5',
+          () => {
+            console.log('下载')
+            this.downLoadAirLine()
+          }
+        ]
+      ])
+      // 调用对应的处理方法
+      actionMap.get(type)?.()
+    },
+    //下载航线文件 kmz
+    async downLoadAirLine() {
+      // 下载航线
+      console.log('下载航线')
+      const params = {
+        fileName: this.item.flightRouteName + '.kmz',
+        kmzUrl: this.item.kmzAccessUrl
+      }
+      await downloadKmzUrlsApi(params)
+    },
+    //校验航线是否被关联
+    checkFlightRouteStatus(type) {
+      let params = {
+        flightRouteId: this.item.flightRouteId
+      }
+      if (type == '1') {
+        this.$emit('edit', this.item)
+        return
+      }
+      checkAirLineBind(params)
+        .then((res) => {
+          if (res.code === 200) {
+            const data = res.data
+            if (data.length > 0) {
+              let alterContent =
+                type === '2'
+                  ? '当前航线已有飞行计划,不可进行删除。'
+                  : '当前航线已有飞行计划,不可进行重命名,请删除飞行计划后再试。'
+              CommonMessage.warning(alterContent)
+              return
+            }
+            this.$emit(type === '2' ? 'delete' : 'rename', this.item)
+            // //TODO // 改成三目
+            // if (type === '3') {
+            //   this.$emit('rename', this.item)
+            // } else {
+            // }
+            // if (type === '1') {
+            //   alterContent = '当前航线正在执行,不可进行编辑。'
+            // } else if (type === '2') {
+            //   alterContent = '当前航线已有飞行计划,不可进行删除。'
+            // } else {
+            //   alterContent =
+            //     '当前航线已有飞行计划,不可进行重命名,请删除飞行计划后再试。'
+            // }
+          }
+        })
+        .catch((res) => {
+          CommonMessage.error(res.msg)
+          console.error(res)
+        })
+    },
+    //更多
+    onMore() {
+      console.log('更多')
+      this.isShowMore = !this.isShowMore
+    },
+    //收藏
+    onCollect() {
+      console.log('去收藏')
+      const params = {
+        flightRouteId: this.item.flightRouteId
+      }
+      //TODO// 方法抽取
+      const action =
+        this.collectStatus === '1' ? addAirLineCollect : deleteAirLineCollect
+      const successMessage =
+        this.collectStatus === '1' ? '收藏成功' : '取消收藏成功'
+      // 执行收藏或取消收藏请求
+      action(params)
+        .then((res) => {
+          if (res.code === 200) {
+            CommonMessage.success(successMessage)
+            // 更新收藏状态
+            this.collectStatus = this.collectStatus === '1' ? '0' : '1'
+            // 如果是从收藏列表中取消收藏
+            if (this.collectStatus === '1' && this.isFromcollect) {
+              this.$emit('deleteCollect', this.index)
+            }
+          }
+        })
+        .catch((res) => {
+          CommonMessage.error(res.msg)
+          console.error(res)
+        })
+    },
+    //下拉菜单显示隐藏
+    dropdownChangeHandler(status) {
+      this.isShowMore = status
+    },
+    //点击航线名称
+    checkDetail(event) {
+      // 检查点击事件是否来自 el-dropdown 或其子元素
+      if (this.isDropdownOrChild(event.target)) {
+        // 如果是,则不执行任何操作
+        return
+      }
+      this.$emit('checkDetail')
+    },
+    //检查点击事件是否来自 el-dropdown 或其子元素
+    isDropdownOrChild(target) {
+      // 检查 target 是否是 el-dropdown 或其子元素
+      if (
+        target.className === 'el-dropdown' ||
+        target.parentNode.className === 'el-dropdown'
+      ) {
+        return true
+      }
+      return false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.conten-info {
+  position: relative;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-content: center;
+  height: px-to-rem(92);
+  margin-bottom: px-to-rem(12);
+}
+
+.topInfo {
+  z-index: 100;
+  display: flex;
+  flex-direction: row;
+  align-content: center;
+  justify-content: space-between;
+  height: px-to-rem(32);
+  margin-top: px-to-rem(7);
+
+  .line_type_bg {
+    position: absolute;
+    margin-top: px-to-rem(7);
+    margin-left: px-to-rem(12);
+    width: px-to-rem(88);
+    height: px-to-rem(20);
+  }
+
+  .leftText {
+    // height: px-to-rem(20);
+    margin-left: px-to-rem(12);
+    align-items: center;
+    display: flex;
+    z-index: 2;
+    flex-direction: row;
+
+    .line-type-icon {
+      display: flex;
+      align-items: center;
+      align-self: center;
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+    }
+
+    .lineType {
+      cursor: default;
+      margin-left: px-to-rem(6);
+      margin-top: px-to-rem(3);
+      align-self: center;
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 500;
+      width: px-to-rem(56);
+      font-size: px-to-rem(14);
+      color: #c6e5ff;
+      line-height: px-to-rem(14);
+    }
+
+    .lineName {
+      // cursor: pointer;
+      cursor: default;
+      align-self: center;
+      margin-top: px-to-rem(3);
+      margin-left: px-to-rem(12);
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 500;
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      line-height: px-to-rem(16);
+      width: px-to-rem(182);
+      text-shadow: 0px 0px px-to-rem(2) rgba(74, 141, 254, 0.7);
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+}
+
+.rightMoreIcon {
+  z-index: 100;
+  float: right;
+  right: 0;
+  top: px-to-rem(5);
+  line-height: px-to-rem(32);
+  display: flex;
+  position: absolute;
+  align-items: center;
+  cursor: pointer;
+
+  .searchTypeIcon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    font-size: px-to-rem(20);
+  }
+
+  .moreBg {
+    width: px-to-rem(32);
+  }
+
+  .collectBg {
+    width: px-to-rem(32);
+
+    .collectActive {
+      color: #4f9fff !important;
+    }
+
+    .collect {
+      font-size: px-to-rem(16) !important;
+    }
+  }
+
+  .actvie {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #4f9fff !important;
+
+    ::v-deep .icon {
+      color: #4f9fff !important;
+      font-size: px-to-rem(18) !important;
+    }
+  }
+}
+
+.bottomInfo {
+  z-index: 100;
+  display: flex;
+  height: px-to-rem(32);
+  margin-top: px-to-rem(12);
+  margin-left: px-to-rem(12);
+  flex-direction: row;
+  align-content: center;
+
+  .img_bg {
+    display: flex;
+    justify-content: center;
+    align-content: center;
+    height: px-to-rem(32);
+    width: px-to-rem(20);
+  }
+
+  .icon_img {
+    align-self: center;
+    width: px-to-rem(20);
+    height: px-to-rem(20);
+  }
+
+  .bottomText {
+    z-index: 100;
+    cursor: default;
+    margin-left: px-to-rem(6);
+    align-self: center;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    font-size: px-to-rem(16);
+    color: #e8f3fe;
+    line-height: px-to-rem(16);
+    width: px-to-rem(308);
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+}
+
+.el-dropdown-menu {
+  background: #0f1926;
+  border-radius: px-to-rem(4);
+  border: none;
+  padding: 0 !important;
+  width: px-to-rem(96);
+
+  .el-dropdown-menu__item {
+    display: flex;
+    position: relative;
+    justify-content: center !important;
+    align-content: center !important;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    height: px-to-rem(32);
+    font-size: px-to-rem(14);
+    color: #e8f3fe;
+    line-height: px-to-rem(20);
+
+    &:hover {
+      background: rgba(79, 159, 255, 0.4);
+      color: #4f9fff;
+    }
+  }
+
+  .dropdown-rename {
+    border-radius: px-to-rem(4) px-to-rem(4) 0 0;
+    border: none;
+  }
+
+  .dropdown-item {
+    height: 100%;
+    display: flex;
+    align-items: center;
+
+    &:hover .dropdown-item-title {
+      color: #4f9fff;
+    }
+  }
+
+  .dropdown-copy {
+    border-radius: 0 0 px-to-rem(4) px-to-rem(4);
+    border: none;
+  }
+
+  ::v-deep .popper__arrow {
+    display: none;
+  }
+
+  .dropdown-item-title {
+    display: flex;
+    width: px-to-rem(50) !important;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    font-size: px-to-rem(16);
+    color: #e8f3fe;
+    line-height: px-to-rem(20);
+  }
+
+  .dropdown-item-icon {
+    color: #fff;
+    width: px-to-rem(20);
+    height: px-to-rem(20);
+  }
+
+  .menu-icon {
+    ::v-deep .icon {
+      color: #fff !important;
+      font-size: px-to-rem(17) !important;
+    }
+  }
+}
+
+.el-popper {
+  margin-top: 0;
+  left: px-to-rem(382) !important;
+}
+</style>

+ 523 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/ImportAirLine.vue

@@ -0,0 +1,523 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+
+<template>
+  <div class="import-air-line-box">
+    <absolute-container
+      :title="title"
+      :width="480"
+      @close="handleClose"
+      canClose
+      industryClass="ImportAirLine air_line_info common-iw-s"
+    >
+      <div class="content-info">
+        <el-form :label-width="pxToRem(123)">
+          <el-form-item label="" prop="flightRouteName">
+            <span slot="label" class="">
+              <span class="addItem_star">*</span>
+              <span class="addItem_text">航线名称</span>
+            </span>
+            <el-input
+              placeholder="请输入"
+              size="small"
+              v-model="queryParams.flightRouteName"
+              class="c-input"
+              maxlength="20"
+              clearable
+            >
+            </el-input>
+          </el-form-item>
+          <el-form-item label="" prop="type">
+            <span slot="label" class="">
+              <span class="addItem_star">*</span>
+              <span class="addItem_text">航线类型</span>
+            </span>
+            <el-select
+              v-model.trim="queryParams.type"
+              class="c-select"
+              popper-class="c-select-dropdown air-line-type-select"
+              placeholder="请选择"
+              :popper-append-to-body="false"
+            >
+              <el-checkbox-group v-model="queryParams.type">
+                <el-option
+                  v-for="item in airLineTypeTab"
+                  :key="item.type"
+                  :label="item.name"
+                  :value="item.type"
+                  :disabled="item.disabled"
+                >
+                  <div>
+                    {{ item.name }}
+                  </div>
+                </el-option>
+              </el-checkbox-group>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="" prop="type">
+            <span slot="label" class="">
+              <span class="addItem_star">*</span>
+              <span class="addItem_text">执行设备</span>
+            </span>
+            <select-tree
+              expandOnClickNode
+              onlyLeafSelect
+              :defaultProps="{ label: 'name', children: 'list' }"
+              nodeKey="code"
+              filterable
+              v-model="queryParams.uavValue"
+              :dataSource="uavTreeData"
+              industryClass="common-iw-s select-air-tree"
+              @refresh="getDeviceAreaTreeData"
+            ></select-tree>
+          </el-form-item>
+          <el-form-item label="" prop="" class="import-el-item">
+            <span slot="label" class="form-item-label">
+              <span class="addItem_star">*</span>
+              <span class="addItem_text">导入文件</span>
+              <el-tooltip
+                popper-class="c-tooltip-2"
+                effect="dark"
+                placement="top"
+              >
+                <div slot="content" class="import-tip-info">
+                  导入航线说明:<br />请选择KMZ文件,KMZ中需包含template.kml及<br />waylines.wpml文件,可上传不超过50M的文件。
+                </div>
+                <i class="iconfont icon-tishi wenhao-css"></i>
+              </el-tooltip>
+            </span>
+            <div class="upload-box">
+              <el-upload
+                ref="upload"
+                class="upload-demo"
+                :headers="headers"
+                :action="uploadFileAction"
+                :accept="'.kmz'"
+                :limit="1"
+                :on-remove="handleRemove"
+                :on-error="handleUploadError"
+                :on-change="handleChange"
+                :file-list="fileList"
+                :disabled="isUpload"
+                :auto-upload="false"
+                :show-file-list="false"
+              >
+                <base-button
+                  type="tiny"
+                  class="uploadBtn"
+                  :class="isUpload ? 'disableUploadBtn' : ''"
+                >
+                  <span
+                    class="icon iconfont icon-PC_icon_shangchuan menu-icon upload-icon"
+                  ></span
+                  >点击上传</base-button
+                >
+              </el-upload>
+            </div>
+          </el-form-item>
+          <el-form-item label="" prop="type" v-if="isUpload">
+            <div class="up-file-list">
+              <div class="left-name">
+                <ct-icon
+                  class="ct-icon-style menu-icon link-icon"
+                  name="link"
+                  :size="pxToRem(25)"
+                />
+                <span class="upload-name" :c-tip="queryParams.upLoadFileName">{{
+                  queryParams.upLoadFileName
+                }}</span>
+              </div>
+              <span
+                class="icon iconfont icon-tongyong_icon_xiaoxitishi_chenggong menu-icon green"
+                @click="deleteFile"
+              >
+              </span>
+            </div>
+          </el-form-item>
+        </el-form>
+        <div class="botBtn">
+          <base-button type="primary" class="submitBtn" @click="submit">
+            确定
+          </base-button>
+          <base-button type="tiny" class="cancelBtn" @click="close">
+            取消
+          </base-button>
+        </div>
+      </div>
+    </absolute-container>
+  </div>
+</template>
+
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import { airLineTypeTab } from '../../../entry/data.js'
+import SelectTree from '../../selectTree/SelectTree.vue'
+import {
+  getDeviceAreaTree,
+  queryDefultAirLineName,
+  checkAirLineName,
+  getAirDeviceInfo
+} from '../../../service'
+import { turnCameraList } from '../../../dict/camera-util'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+import { unKMZ, getLineType } from '../../../dict/kmz-util'
+import { handleKMZVerifyRule } from '../../../dict/kmz-verify-rule'
+import validatorFactory from '../../../dict/validator'
+import { findDeviceNodeByCode } from '../../../dict/plan-map'
+export default {
+  name: 'importAirLine',
+  components: {
+    AbsoluteContainer,
+    BaseButton,
+    SelectTree
+  },
+  data() {
+    return {
+      airLineTypeTab: JSON.parse(JSON.stringify(airLineTypeTab)),
+      uploadFileAction: '/problem/docApi/uploadFile', // 其实上传是自定义的没用到这个,但是不加action vue会有warn
+      title: '导入航线',
+      uavTreeData: [],
+      queryParams: {
+        flightRouteName: '',
+        uavValue: '',
+        type: '1',
+        upLoadFileName: '',
+        kmzJson: ''
+      },
+      fileList: [],
+      isUpload: false,
+      headers: {
+        Authorization: `Bearer ${sessionStorage.getItem('token')}`
+      },
+      //waypoint 航点航线 mapping2d 正射影像
+      lineType: '',
+      // 新增正射影像需要的参数
+      uosInfo: {
+        multiOrthoList: [],
+        orthoBoundary: '', // 测区
+        orthoDeltaTime: '', // 单次续航时间
+        orthoSurveyDatumAlt: '' // 测绘基准面高度
+      }
+    }
+  },
+
+  watch: {
+    'queryParams.type': {
+      handler: function (val) {
+        this.getNorFlightRouteName()
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  created() {
+    this.airLineTypeTab.forEach((item, index) => {
+      item.disabled = index === 2 || index === 3
+    })
+    this.getDeviceAreaTreeData()
+    this.getNorFlightRouteName()
+  },
+  mounted() {
+    setupCTips()
+  },
+  methods: {
+    handleClose() {
+      this.$emit('closeImportAirLine')
+      this.resetData()
+    },
+    /**
+     * 提交
+     */
+    async submit() {
+      if (this.queryParams.flightRouteName === '') {
+        return CommonMessage.error('请输入航线名称')
+      }
+      if (this.queryParams.type === -1) {
+        return CommonMessage.error('请选择航线类型')
+      }
+      if (this.queryParams.uavValue === '') {
+        return CommonMessage.error('请选择执行设备')
+      }
+      if (this.queryParams.upLoadFileName === '') {
+        return CommonMessage.error('请上传文件')
+      }
+      if (this.queryParams.type === '1' && this.lineType != 'waypoint') {
+        return CommonMessage.error('航线类型选择错误。')
+      }
+      if (this.queryParams.type === '2' && this.lineType != 'mapping2d') {
+        return CommonMessage.error('航线类型选择错误。')
+      }
+      const currentNode = findDeviceNodeByCode(
+        this.uavTreeData,
+        this.queryParams.uavValue
+      )
+      console.log('选择的设备: ', currentNode)
+      const params_ = {
+        deviceCode: currentNode.deviceCode
+      }
+      const res = await getAirDeviceInfo(params_)
+      if (res.code !== 200) {
+        return
+      }
+      const droneEnumValue = res.data.baseInfo.type
+      const droneSubEnumValue = res.data.baseInfo.subType
+      turnCameraList(res.data.cameraModelVOList)
+      const currentCamera = this.findCurrentCamera(
+        res.data.baseInfo,
+        res.data.cameraModelVOList
+      )
+      const payloadEnumValue = currentCamera.type
+      const fliePath = this.fileList[0]
+      let kmzJson = await unKMZ(fliePath.raw, true)
+      console.log('kmzJson: ', kmzJson)
+      const Document = kmzJson['template.xml'].kml['Document'] || {}
+      const missionConfig = Document['wpml:missionConfig'] || {}
+      const droneInfo = missionConfig['wpml:droneInfo'] || {}
+      const payloadInfo = missionConfig['wpml:payloadInfo'] || {}
+      const payloadEnumValue_ = payloadInfo['wpml:payloadEnumValue']
+      const droneEnumValue_ = droneInfo['wpml:droneEnumValue']
+      const droneSubEnumValue_ = droneInfo['wpml:droneSubEnumValue']
+      const validatorSuccess = droneSubEnumValue == droneSubEnumValue_
+      droneEnumValue == droneEnumValue_
+      payloadEnumValue == payloadEnumValue_
+
+      console.log('droneSubEnumValue_: ', droneSubEnumValue_)
+      console.log('droneEnumValue_: ', droneEnumValue_)
+      console.log('payloadEnumValue_ ', payloadEnumValue_)
+
+      console.log('droneSubEnumValue: ', droneSubEnumValue)
+      console.log('droneEnumValue: ', droneEnumValue)
+      console.log('payloadEnumValue ', payloadEnumValue)
+      if (!validatorSuccess) {
+        CommonMessage.error(
+          '无人机型号或相机型号与所选设备不一致,暂不支持导入'
+        )
+        return
+      }
+      const params = {
+        flightRouteName: this.queryParams.flightRouteName
+      }
+      // 检查航线名称是否重复
+      checkAirLineName(params)
+        .then((res) => {
+          console.log(res)
+          if (res.code === 200) {
+            this.upLoadKmzFile()
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err.msg)
+          console.error(err)
+        })
+    },
+    findCurrentCamera(baseInfo, cameraModelVOList) {
+      // 从 baseInfo 中获取 cameraModelCode
+      const cameraModelCode = baseInfo.cameraModelCode
+
+      // 从 cameraModelVOList 查找匹配的元素
+      const matchingModel = cameraModelVOList.find(
+        (model) => model.modelCode === cameraModelCode
+      )
+      return matchingModel
+    },
+    async upLoadKmzFile() {
+      const currentNode = findDeviceNodeByCode(
+        this.uavTreeData,
+        this.queryParams.uavValue
+      )
+      const fliePath = this.fileList[0]
+      let kmzJson = await unKMZ(fliePath.raw, true)
+      if (this.lineType == 'mapping2d') {
+        this.uosInfo.multiOrthoList.push({
+          wholeKmz: true,
+          takeOffLongitude: currentNode.longitude,
+          takeOffLatitude: currentNode.latitude,
+          takeOffSecurityHeight: this.uosInfo.takeOffSecurityHeight
+        })
+        kmzJson.uosInfo = this.uosInfo
+      }
+      const item = {
+        flightRouteName: this.queryParams.flightRouteName,
+        deviceCode: this.queryParams.uavValue,
+        type: this.queryParams.type,
+        pageType: 'export',
+        kmzJson: kmzJson
+      }
+      this.$emit('closeImportAirLine', item)
+      this.resetData()
+    },
+    // 上传之前,验证文件
+    beforeAvatarUpload(file) {
+      if (!this.checkFileType(file)) {
+        CommonMessage.error('文件格式错误,请重新上传。')
+        return false
+      }
+      return true
+    },
+    // 删除文件
+    deleteFile() {
+      this.$refs.upload.clearFiles()
+      this.isUpload = false
+      this.queryParams.upLoadFileName = ''
+    },
+    handleUploadError(err) {
+      console.log('handleUploadError', err)
+      CommonMessage.error('文件格式错误,请重新上传。')
+    },
+    handleRemove(file, fileList) {
+      this.fileList = fileList
+    },
+    handleChange(file, fileList) {
+      console.log('handleChange')
+      this.fileList = fileList
+      let stringlength = file.name.length
+      let type = file.name.substring(stringlength - 3, stringlength)
+      let maxSize = 50 * 1024 * 1024 // KMZ航线最大64M
+      if (type == 'kmz' && file.size <= maxSize) {
+        this.fileType = type
+        this.upkmzFile()
+        return false
+      }
+      if (type != 'kmz') {
+        CommonMessage.error('上传文件不符合规则,请重新上传。')
+      } else if (file.size > maxSize) {
+        CommonMessage.error('暂不支持导入大于50M的kmz文件。')
+      }
+      this.$refs.upload.clearFiles()
+      this.fileList = []
+      return true
+    },
+    async upkmzFile() {
+      try {
+        const fliePath = this.fileList[0]
+        let kmzJson = await unKMZ(fliePath.raw)
+        this.validatorKmzJson(kmzJson)
+      } catch (error) {
+        this.$refs.upload.clearFiles()
+        this.fileList = []
+        console.error('error: ', error)
+        CommonMessage.error('上传文件不符合规则,请重新上传。')
+      }
+    },
+    // 验证kmzjson文件格式
+    async validatorKmzJson(kmzJson) {
+      let json = {
+        tempJson: kmzJson.template,
+        wampJson: kmzJson.waylines,
+        imageList: kmzJson.imageList
+      }
+      let kmzTelDesc = handleKMZVerifyRule(json.tempJson, false)
+      await validatorFactory(kmzTelDesc, json.tempJson)
+        .then(async () => {
+          this.validatorFinsh()
+        })
+        .catch(({ errors, fields }) => {
+          this.$refs.upload.clearFiles()
+          this.fileList = []
+          CommonMessage.error('上传文件不符合规则,请重新上传。')
+          console.log('promise Catch', errors, fields)
+        })
+    },
+    //验证kmzjson完成
+    async validatorFinsh() {
+      const fliePath = this.fileList[0]
+      this.queryParams.upLoadFileName = fliePath.name
+      this.queryParams.kmzJson = await unKMZ(fliePath.raw)
+      this.lineType = getLineType(this.queryParams.kmzJson)
+      console.log('kmz文件的航线类型:', this.lineType)
+      if (this.lineType == 'mapping2d') {
+        const { waylines } = this.queryParams.kmzJson
+        const kml = waylines?.kml || {}
+        const Document = kml?.Document || {}
+        const zk_missionConfig = Document.zk_missionConfig || {}
+        this.uosInfo.speed = zk_missionConfig.zk_globalTransitionalSpeed?._text
+        this.uosInfo.takeOffSecurityHeight =
+          zk_missionConfig.zk_takeOffSecurityHeight?._text
+        this.uosInfo.rthHeight = zk_missionConfig.zk_globalRTHHeight?._text
+        this.uosInfo.disconnectionStrategy =
+          zk_missionConfig.zk_exitOnRCLost?._text
+        const Folder = Document?.Folder || {}
+        this.uosInfo.heightMode = Folder.zk_executeHeightMode?._text
+        console.log('获取到的正射影像参数:', this.uosInfo)
+      }
+      this.isUpload = true
+      CommonMessage.success('上传成功。')
+      console.log('解析后KMZ的文件:', this.queryParams.kmzJson)
+    },
+    // 检查附件类型
+    checkFileType(file) {
+      const { name } = file
+      this.queryParams.upLoadFileName = name
+      const nameLowerCase = name?.toLowerCase()
+      return /(kmz)/g.test(nameLowerCase)
+    },
+    /**
+     * 关闭弹窗
+     */
+    close() {
+      this.$emit('closeImportAirLine')
+      this.resetData()
+    },
+    // 重置数据
+    resetData() {
+      this.queryParams.flightRouteName = ''
+      this.queryParams.uavValue = ''
+      this.queryParams.type = 1
+      this.queryParams.upLoadFileName = ''
+    },
+    /**
+     * 清除关键字搜索
+     */
+    clearSearch() {
+      this.queryParams.flightRouteName = ''
+    },
+    // 获取无人机机场数据
+    getDeviceAreaTreeData(fn) {
+      const params = {
+        needDevice: true,
+        orderStatus: '1',
+        queryType: 1
+      }
+      getDeviceAreaTree(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.uavTreeData = res.data
+            this.$nextTick(() => {
+              fn && fn()
+            })
+          }
+        })
+        .catch((err) => {
+          console.error(err)
+          this.uavTreeData = []
+        })
+    },
+    //输入框内容变化
+    handleSearchValueChange() {
+      console.log(this.queryParams.flightRouteName)
+    },
+    // 获取默认航线名称
+    getNorFlightRouteName() {
+      const params = {
+        type: this.queryParams.type
+      }
+      // 获取默认航线名称
+      queryDefultAirLineName(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.queryParams.flightRouteName = res.data
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err.msg)
+          console.error(err)
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '../../../style/air-line-mana-import.scss';
+</style>

+ 300 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/RenamLineName.vue

@@ -0,0 +1,300 @@
+<template>
+  <div class="rename-line-box">
+    <absolute-container
+      :title="titlename"
+      :width="480"
+      @close="handleClose"
+      canClose
+      industryClass="renameAirLineName  common-iw-s"
+    >
+      <div class="content-info">
+        <div class="top-info">
+          <div class="addItem">
+            <span class="addItem_star">*</span>
+            <span class="addItem_text">航线名称</span>
+          </div>
+          <div class="input-bg">
+            <el-input
+              placeholder="输入航线名称"
+              ref="inputRef"
+              size="small"
+              v-model.trim="flightRouteName"
+              class="search-input"
+              maxlength="20"
+              :clearable="false"
+              @input="handleSearchValueChange"
+            >
+              <template #suffix>
+                <i
+                  v-show="flightRouteName !== ''"
+                  :style="{
+                    color: 'rgb(232 242 254 / 50%)',
+                    cursor: 'pointer',
+                    fontSize: pxToRem(14),
+                    lineHeight: `${pxToRem(32)} !important`
+                  }"
+                  @click="clearSearch"
+                  class="el-input__icon el-icon-error"
+                ></i>
+              </template>
+            </el-input>
+          </div>
+        </div>
+        <div class="botBtn">
+          <base-button type="primary" class="submitBtn" @click="submit">
+            确定
+          </base-button>
+          <base-button type="tiny" class="cancelBtn" @click="close">
+            取消
+          </base-button>
+        </div>
+      </div>
+    </absolute-container>
+  </div>
+</template>
+
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import {
+  checkAirLineName,
+  editAirLineName,
+  copyAirLine
+} from '../../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+
+export default {
+  props: {
+    item: {
+      type: Object,
+      default: () => ({})
+    },
+    titlename: {
+      type: String,
+      default: '重命名航线'
+    },
+    // 类型 0 重命名航线 1 复制航线
+    type: {
+      type: Number,
+      default: 0
+    }
+  },
+  name: 'renamLineName',
+  components: {
+    AbsoluteContainer,
+    BaseButton
+  },
+  data() {
+    return {
+      flightRouteName: ''
+    }
+  },
+  mounted() {
+    this.flightRouteName = this.item.flightRouteName
+    if (this.type === 0) {
+      this.$nextTick(() => {
+        this.$refs.inputRef.select()
+      })
+    }
+  },
+  methods: {
+    // 关闭弹窗
+    handleClose() {
+      const isNeedQueryData = false
+      this.$emit('closeRenameAirLineName', isNeedQueryData)
+    },
+    /**
+     * 提交
+     */
+    submit() {
+      let params = {
+        flightRouteName: this.flightRouteName
+      }
+      if (this.type === 0) {
+        params.flightRouteId = this.item.flightRouteId
+      }
+      checkAirLineName(params)
+        .then((res) => {
+          console.log(res)
+          if (res.code === 200) {
+            this.upDateEditAirLineName()
+            return
+          }
+        })
+        .catch((res) => {
+          CommonMessage.error(res.msg)
+          console.error(res)
+        })
+    },
+    // 编辑航线名称
+    upDateEditAirLineName() {
+      const params = {
+        flightRouteId: this.item.flightRouteId,
+        flightRouteName: this.flightRouteName
+      }
+      //TODO
+      // 增加类型参数 方法抽取
+      let action = this.type === 0 ? editAirLineName : copyAirLine
+      action(params)
+        .then((res) => {
+          console.log(res)
+          if (res.code === 200) {
+            const isNeedQueryData = true
+            this.$emit('closeRenameAirLineName', isNeedQueryData)
+          }
+        })
+        .catch((res) => {
+          CommonMessage.error(res.msg)
+          console.error(res)
+        })
+    },
+    /**
+     * 关闭弹窗
+     */
+    close() {
+      const isNeedQueryData = false
+      this.$emit('closeRenameAirLineName', isNeedQueryData)
+    },
+    // 输入框内容变化
+    handleSearchValueChange(value) {
+      this.flightRouteName = value
+    },
+    /**
+     * 清除关键字搜索
+     */
+    clearSearch() {
+      this.flightRouteName = ''
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.rename-line-box {
+  width: 100vw;
+  height: 100vh;
+  z-index: 100;
+  position: absolute;
+  top: 0;
+  left: 0;
+  align-items: center;
+  justify-content: center;
+  display: flex;
+  background: rgba(0, 0, 0, 0.7);
+  .content-info {
+    width: px-to-rem(480);
+    flex-direction: column;
+    .top-info {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
+    .addItem {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      margin-left: px-to-rem(12);
+      margin-top: px-to-rem(12);
+      &_star {
+        color: #ed5158;
+        font-size: px-to-rem(14);
+      }
+      &_text {
+        font-family: PingFangSC, PingFang SC;
+        font-weight: 400;
+        font-size: px-to-rem(16);
+        color: #e8f3fe;
+        line-height: px-to-rem(20);
+        text-shadow: 0px 0px px-to-rem(2) rgba(74, 141, 254, 0.7);
+      }
+    }
+    .submitBtn {
+      margin-right: px-to-rem(12);
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: px-to-rem(16);
+    }
+
+    .cancelBtn {
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      background: rgba(79, 159, 255, 0.2);
+      font-size: px-to-rem(16) !important;
+    }
+  }
+  .botBtn {
+    display: flex;
+    margin-top: px-to-rem(18);
+    margin-bottom: px-to-rem(12);
+    justify-content: center;
+
+    .el-button {
+      padding: 0;
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: var(--font-size);
+
+      + .el-button {
+        margin-left: px-to-rem(12);
+      }
+    }
+  }
+}
+.add-air-line-box .renameAirLineName {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.renameAirLineName {
+  ::v-deep .el-dialog {
+    .el-dialog__header {
+      padding-left: px-to-rem(12);
+    }
+  }
+  ::v-deep .innercomp-abcontainer-header {
+    width: px-to-rem(480);
+    font-size: px-to-rem(18);
+    .innercomp-abcontainer-header__title span {
+      color: #e8f2fe !important;
+    }
+  }
+}
+
+.input-bg {
+  display: flex;
+  width: px-to-rem(371);
+  margin: px-to-rem(24) 0 px-to-rem(12) px-to-rem(12);
+  height: px-to-rem(32);
+  background: rgba(79, 159, 255, 0.2);
+  border-radius: px-to-rem(4);
+  border: px-to-rem(1) solid rgba(79, 159, 255, 0);
+}
+// 搜索
+.input-bg .search-input ::v-deep .el-input__inner {
+  color: #e8f3fe;
+  font-size: px-to-rem(16);
+  text-align: left;
+  border: px-to-rem(1) solid transparent;
+  line-height: px-to-rem(30);
+  background: transparent;
+  width: px-to-rem(371);
+  overflow: hidden;
+  padding: 0 px-to-rem(30) 0 px-to-rem(12);
+  &::placeholder {
+    color: rgba(232, 243, 254, 0.7);
+  }
+}
+.input-bg .search-input ::v-deep .el-input__suffix {
+  .el-input__suffix-inner :before {
+    color: #e8f3fe;
+    font-size: px-to-rem(20);
+    font-family: 'iconfont_tools';
+    content: '\ec12';
+  }
+}
+</style>

+ 253 - 0
src/components/common-comp-uav-fly-manage/src/components/airLineMana/components/TopArea.vue

@@ -0,0 +1,253 @@
+<template>
+  <div class="top-area">
+    <div class="left-bg">
+      <div class="input-bg">
+        <el-input
+          placeholder="输入航线名称"
+          size="small"
+          v-model.trim="keywordValue"
+          class="search-input"
+          :clearable="false"
+          @keyup.enter="onSearch"
+          @input="handleSearchValueChange"
+          :class="keywordValue.length > 0 ? 'search-input-active' : ''"
+        >
+          <template #suffix>
+            <i
+              v-show="keywordValue !== ''"
+              :style="{
+                color: 'rgb(232 242 254 / 50%)',
+                cursor: 'pointer',
+                fontSize: pxToRem(14),
+                lineHeight: `${pxToRem(32)} !important`
+              }"
+              @click="clearSearch"
+              class="el-input__icon el-icon-error"
+            ></i>
+          </template>
+        </el-input>
+      </div>
+      <div class="searchTypeDiv">
+        <div @click="onSearch" class="seachBg">
+          <i
+            class="iconfont iconfont_tools icon-linye_icon_sousuo searchTypeIcon"
+          ></i
+        ></div>
+        <div @click="onFliter" class="fliteBg">
+          <div class="line"></div>
+          <i
+            class="iconfont iconfont_tools icon-pc_shaixuan_s searchTypeIcon actvie"
+            v-if="fliterActive"
+          ></i>
+          <i
+            class="iconfont iconfont_tools icon-pc_shaixuan_n searchTypeIcon"
+            v-else
+          ></i>
+        </div>
+        <div @click="onCollect" class="collectBg">
+          <div class="line"></div>
+          <i
+            class="iconfont iconfont_tools icon-AR-yishoucang icon-AR-gaojingxiangqing-yishoucang actvie collect"
+            v-if="isShowCollect"
+          ></i>
+          <i
+            class="iconfont iconfont_tools icon-AR-gaojingxiangqing-shoucang searchTypeIcon collect"
+            v-else
+          ></i>
+        </div>
+      </div>
+    </div>
+    <base-button
+      class="addBtn"
+      type="tiny"
+      :style="{ fontSize: pxToRem(16) }"
+      @click="addAirLine()"
+      >新增</base-button
+    >
+    <base-button
+      class="addBtn"
+      :style="{ fontSize: pxToRem(16) }"
+      @click="importAirLine()"
+      >导入</base-button
+    >
+  </div>
+</template>
+
+<script>
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+export default {
+  name: 'airLineMana',
+  components: {
+    BaseButton
+  },
+  data() {
+    return {
+      //搜索关键字
+      keywordValue: '',
+      isShowFliter: false,
+      isShowCollect: false,
+      fliterActive: false
+    }
+  },
+  created() {
+    this.$globalEventBus.$on('notiFfliterSubmit', () => {
+      this.isShowCollect = false
+    })
+  },
+  methods: {
+    // 新增航线
+    addAirLine() {
+      this.$emit('showAddAirLine')
+      console.log('新增航线')
+    },
+    // 导入航线
+    importAirLine() {
+      this.$emit('showImportAirLine')
+      console.log('导入航线')
+    },
+    /**
+     * 查询关键字搜索
+     */
+    onSearch() {
+      this.$emit('search', this.keywordValue)
+      console.log('搜索')
+    },
+    /**
+     * 筛选
+     */
+    onFliter() {
+      console.log('筛选')
+      this.isShowFliter = !this.isShowFliter
+      this.$emit('showFliter', this.isShowFliter)
+    },
+    /**
+     * 收藏
+     */
+    onCollect() {
+      console.log('收藏')
+      this.isShowCollect = !this.isShowCollect
+      this.$emit('showCollect', this.isShowCollect)
+    },
+    /**
+     * 清除关键字搜索
+     */
+    clearSearch() {
+      this.keywordValue = ''
+      this.$emit('search', this.keywordValue)
+    },
+    handleSearchValueChange() {
+      this.$emit('search', this.keywordValue)
+      console.log('handleSearchValueChange')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.top-area {
+  display: flex;
+  justify-content: space-between;
+  padding: px-to-rem(44) px-to-rem(12) px-to-rem(12) px-to-rem(12);
+  flex-direction: row;
+}
+.left-bg {
+  display: flex;
+  flex-direction: row;
+  width: 100%;
+  height: px-to-rem(32);
+  background: rgba(79, 159, 255, 0.2);
+  justify-content: space-between;
+  border-radius: px-to-rem(4);
+  width: px-to-rem(214);
+  border: px-to-rem(1) solid rgba(79, 159, 255, 0);
+  .searchTypeDiv {
+    float: left;
+    line-height: px-to-rem(32);
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+  }
+
+  .searchTypeIcon {
+    color: #fff;
+    font-size: px-to-rem(20);
+  }
+  .seachBg {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: px-to-rem(32);
+  }
+  .fliteBg {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: px-to-rem(32);
+  }
+  .line {
+    position: absolute;
+    left: 0;
+    height: px-to-rem(10);
+    width: px-to-rem(1);
+    background: rgba(232, 243, 254, 0.2);
+  }
+  .collectBg {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: px-to-rem(32);
+    .collect {
+      font-size: px-to-rem(16) !important;
+    }
+  }
+  .actvie {
+    color: #4f9fff;
+  }
+}
+.addBtn {
+  height: px-to-rem(32);
+  margin-left: px-to-rem(6);
+  width: px-to-rem(60);
+}
+.input-bg {
+  height: px-to-rem(32);
+  width: 100%;
+}
+// 搜索
+.input-bg .search-input ::v-deep .el-input__inner {
+  color: #e8f3fe;
+  font-size: px-to-rem(16);
+  text-align: left;
+  height: px-to-rem(32);
+  padding-left: px-to-rem(10) !important;
+  border: px-to-rem(1) solid transparent;
+  padding-right: px-to-rem(10);
+  background: transparent;
+  &::placeholder {
+    color: rgba(232, 243, 254, 0.7);
+  }
+}
+.input-bg .search-input-active ::v-deep .el-input__inner {
+  padding-right: px-to-rem(20) !important;
+}
+.input-bg .search-input ::v-deep .el-input__suffix {
+  right: px-to-rem(-5) !important;
+  .el-input__suffix-inner {
+    width: px-to-rem(10);
+    i {
+      font-size: inherit !important;
+    }
+  }
+  .el-input__suffix-inner :before {
+    color: #e8f3fe;
+    font-size: px-to-rem(20);
+    font-family: 'iconfont_tools';
+    content: '\ec12';
+  }
+}
+</style>

+ 304 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AirLinePointList.vue

@@ -0,0 +1,304 @@
+<!-- 航线航点列表内容 -->
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <fly-responsive-card class="mt-12" headTitle="航线航点">
+    <template v-slot:titleIcon>
+      <img src="../../img/airLine/icon_line_point_action.png" alt="" />
+    </template>
+
+    <div class="card_cont mt-12">
+      <div class="airline-point-list">
+        <div
+          :class="['list_item', currentPointIndex === index && 'active']"
+          v-for="(item, index) in flyPointsList"
+          :key="index + 'id'"
+          @click="operate(item, 1)"
+        >
+          <div class="list_item_left">
+            <div class="title" :class="[item.alarmStatus && 'active']">{{
+              index + 1
+            }}</div>
+            <div
+              class="icons"
+              :style="{
+                width: pxToRem(hoverId ? 215 : 176)
+              }"
+            >
+              <draggable
+                class="draggable_cont"
+                v-model="item.actions"
+                :group="`action-${item.id}`"
+              >
+                <transition-group>
+                  <div
+                    v-for="(element, i) in item.actions"
+                    :key="`draggable-${index}-${i}`"
+                    :class="[
+                      'draggable-item',
+                      hoverId === element.id && 'hover'
+                    ]"
+                    @mouseover="onMouseover(element.id, i)"
+                    @mouseleave="onMouseleave"
+                  >
+                    <el-tooltip
+                      :content="element.name"
+                      placement="top"
+                      :open-delay="500"
+                      popper-class="action-tooltip-popper"
+                    >
+                      <div @click.stop="actionClick(item, element)">
+                        <ct-icon
+                          :name="
+                            currentActionId === element.id
+                              ? `${element.activeIcon}`
+                              : element.icon
+                          "
+                          :color="
+                            currentActionId === element.id
+                              ? '#4F9FFF'
+                              : '#e8f3fe'
+                          "
+                        ></ct-icon>
+                      </div>
+                    </el-tooltip>
+                    <ct-icon
+                      class="tag-delete"
+                      name="tag-delete"
+                      color="#e8f3fe"
+                      @click.native.stop="deleteAction(element.id)"
+                    ></ct-icon>
+                  </div>
+                </transition-group>
+              </draggable>
+            </div>
+          </div>
+          <div class="opt_btns" v-if="pageType != 'view'">
+            <ct-icon
+              name="edit"
+              color="#e8f3fe"
+              :size="18"
+              @click.native.stop="operate(item, 2, index)"
+            ></ct-icon>
+            <ct-icon
+              name="table-delete"
+              color="#e8f3fe"
+              :size="18"
+              @click.native.stop="operate(item, 3)"
+            ></ct-icon>
+          </div>
+        </div>
+      </div>
+    </div>
+  </fly-responsive-card>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+import FlyResponsiveCard from '../../baseComponents/flyResponsiveCard/FlyResponsiveCard.vue'
+import { actionsArr } from '../../dict/dict'
+export default {
+  name: 'AirlinePointList',
+  components: {
+    draggable,
+    FlyResponsiveCard
+  },
+  props: {
+    alarmPoints: {
+      type: Array,
+      default: () => []
+    },
+    flyPoints: {
+      type: Array,
+      default: () => []
+    },
+    currentPointIndex: {
+      type: Number,
+      default: -1
+    },
+    pageType: {
+      type: String,
+      default: 'add'
+    },
+    currentActionId: String
+  },
+  data() {
+    return {
+      hoverId: null,
+      hoverIndex: null,
+      actionsArr
+    }
+  },
+  computed: {
+    flyPointsList() {
+      return this.flyPoints.map((item, index) => {
+        return {
+          ...item,
+          alarmStatus: this.alarmPoints.includes(item.id),
+          actions: item.actions.map((action) => {
+            const actionObj = actionsArr.find(
+              (item) => item.type === action.type
+            )
+            return {
+              ...action,
+              ...actionObj
+            }
+          })
+        }
+      })
+    }
+  },
+  methods: {
+    deleteAction(id) {
+      this.$emit('deleteAction', id)
+    },
+    async operate(item, type, editIndex) {
+      if (type === 1) {
+        this.$emit('edit', { id: item.id })
+      } else if (type === 2) {
+        this.$emit('edit', { id: item.id, editIndex })
+      } else {
+        this.$emit('delete', item)
+      }
+    },
+    actionClick(item, action) {
+      this.$emit('edit', { id: item.id, actionId: action.id })
+    },
+    onMouseleave() {
+      if (this.pageType === 'view') return
+      this.hoverId = null
+      this.hoverIndex = null
+    },
+    onMouseover(id, i) {
+      if (this.pageType === 'view') return
+      this.hoverIndex = i
+      this.hoverId = id
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin ctIconStyle {
+  cursor: pointer;
+  ::v-deep .ct-icon {
+    width: auto !important;
+    height: px-to-rem(20) !important;
+    line-height: px-to-rem(20);
+    .icon-ctw {
+      font-size: px-to-rem(18) !important;
+    }
+    .icon-ctw-tag-delete {
+      font-size: px-to-rem(14) !important;
+    }
+  }
+}
+.mt-12 {
+  margin-top: px-to-rem(12);
+}
+.airline-point-list {
+  width: 100%;
+  .list_item {
+    cursor: pointer;
+    @include flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: px-to-rem(12);
+    padding: px-to-rem(6) 0;
+    border-bottom: px-to-rem(1) solid rgba(232, 243, 254, 0.2);
+    padding-right: px-to-rem(14);
+    &.active {
+      background: url('../../img/airLine/list_action_choosed.webp') no-repeat;
+      background-size: 100% 100%;
+    }
+    .list_item_left {
+      display: flex;
+      align-items: center;
+      gap: px-to-rem(12);
+    }
+    .title {
+      width: px-to-rem(56);
+      height: px-to-rem(20);
+      padding-right: px-to-rem(12);
+      text-align: right;
+      color: #e8f3fe;
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 500;
+      font-size: px-to-rem(14);
+      text-shadow: 0 px-to-rem(2) px-to-rem(2) rgba(0, 101, 255, 0.32);
+      background-image: url('../../img/airLine/tag_line_title_1.webp');
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      &.active {
+        background-image: url('../../img/airLine/tag_line_title_2.webp');
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+    .icons {
+      width: px-to-rem(176);
+      gap: px-to-rem(8);
+
+      .draggable_cont {
+        width: 100%;
+        span {
+          display: flex;
+          width: 100%;
+          flex-wrap: wrap;
+          gap: px-to-rem(6);
+        }
+        .draggable-item {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          gap: px-to-rem(4);
+          width: px-to-rem(20);
+          height: px-to-rem(20);
+          .tag-delete {
+            display: none;
+          }
+          .ct-icon__inline-block {
+            @include ctIconStyle;
+          }
+        }
+        .hover {
+          width: px-to-rem(58);
+          background: radial-gradient(
+              1685% 185% at 1% 0%,
+              rgb(17 125 255 / 20%) 0%,
+              rgba(0, 14, 31, 0) 100%
+            ),
+            radial-gradient(
+              20% 121% at 50% 116%,
+              rgb(17 84 168 / 20%) 0%,
+              rgb(42 135 248 / 10%) 72%,
+              rgba(42, 137, 251, 0) 100%
+            );
+          border-radius: 10px;
+          border: 1px solid;
+          border-image: radial-gradient(
+              circle,
+              rgba(71, 131, 255, 0.3),
+              rgba(25, 122, 255, 0)
+            )
+            1 1;
+          .tag-delete {
+            display: block;
+          }
+        }
+      }
+    }
+    .opt_btns {
+      @include flex;
+      gap: px-to-rem(8);
+      .ct-icon__inline-block {
+        @include ctIconStyle;
+      }
+    }
+  }
+}
+</style>

+ 223 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AirPointActions.vue

@@ -0,0 +1,223 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <fly-responsive-card
+    class="mt-12 air-point-actions"
+    headTitle="航点动作"
+    isFlexScroll
+    :isOpen="isOpen"
+    @toggle="setToggle"
+  >
+    <template v-slot:titleIcon>
+      <img src="../../img/airLine/icon_point_config_action.webp" alt="" />
+    </template>
+    <el-scrollbar class="scrollbar">
+      <div class="action-item" v-for="(item, i) in point?.actions" :key="i">
+        <fly-acticon-card
+          :titleIcon="item.icon"
+          :headTitle="`${item.name} ${index + 1}-${i + 1}`"
+          :hasBodyPadding="!['2', '5', '12'].includes(item.type)"
+          :isActive="currentActionId === item.id"
+          :canCollapse="!pageEditStatus"
+          @click.native="actionClick(item)"
+          @delete="handleDelete(item.id)"
+        >
+          <div class="action-item-content">
+            <!-- 命名 -->
+            <rename
+              v-model="item.rename"
+              v-if="['0', '1', '10'].includes(item.type)"
+              :action="item"
+            ></rename>
+            <!-- 间隔 -->
+            <step-input
+              :disabled="pageEditStatus"
+              v-model="item.space"
+              :min="1"
+              :step="item.type === '3' ? 1 : 10"
+              :max="ranges[i].max"
+              :precision="item.type === '4' ? 1 : 0"
+              :suffix-text="item.type === '4' ? 'm' : 's'"
+              v-if="['3', '4', '6'].includes(item.type)"
+            >
+              {{ item.type === '6' ? '悬停时间' : '间隔' }}
+            </step-input>
+            <!-- 跟随 -->
+            <is-follow-line
+              :disabled="pageEditStatus"
+              v-model="item.flow"
+              @change="handleFollowChange(item)"
+              v-if="['0', '1', '3', '4', '10'].includes(item.type)"
+            ></is-follow-line>
+            <!-- 选择相机 -->
+            <checkbox-control
+              class="mt-12"
+              v-model="item.camera"
+              :options="currentCamera.list"
+              :disabled="item.flow === '1' || pageEditStatus"
+              v-if="['0', '1', '3', '4', '10'].includes(item.type)"
+            ></checkbox-control>
+            <!-- 航向角度 -->
+            <slider-control
+              v-model="item.rotate"
+              :min="ranges[i].min"
+              :max="ranges[i].max"
+              :step="0.1"
+              :precision="1"
+              suffixText="°"
+              @change="drawFrustum(item)"
+              v-if="['7', '8', '9'].includes(item.type)"
+              :disabled="pageEditStatus"
+            />
+            <!-- 变焦 -->
+            <slider-control
+              v-model="item.rotate"
+              :min="ranges[i].min"
+              :max="ranges[i].max"
+              :step="1"
+              suffixText="X"
+              @change="drawFrustum(item)"
+              :disabled="pageEditStatus"
+              v-if="['11'].includes(item.type)"
+            />
+            <!-- 虚拟快照 -->
+            <accurate-retake
+              v-if="item.type === '0'"
+              :curActionData="item"
+              :retakeFlag="retakeFlag"
+              @change="(params) => handleRetakeChange(params, item)"
+              :disabled="pageEditStatus"
+              :point="point"
+            />
+          </div>
+        </fly-acticon-card>
+      </div>
+    </el-scrollbar>
+  </fly-responsive-card>
+</template>
+<script>
+import IsFollowLine from '../../baseComponents/isFollowLine/IsFollowLine.vue'
+import FlyResponsiveCard from '../../baseComponents/flyResponsiveCard/FlyResponsiveCard.vue'
+import FlyActiconCard from '../../baseComponents/flyActiconCard/FlyActiconCard.vue'
+import Rename from './actions/Rename.vue'
+import CheckboxControl from '../../baseComponents/checkboxControl/CheckboxControl.vue'
+import SliderControl from '../../baseComponents/sliderControl/SliderControl.vue'
+import StepInput from '../../baseComponents/stepInput/StepInput.vue'
+import AccurateRetake from './actions/AccurateRetake.vue'
+import { frustumFlyPoints } from '../airLineEditPage/AirPointAirLine.vue'
+import { throttle } from '../../dict/util'
+export default {
+  props: {
+    point: Object,
+    index: Number,
+    currentCamera: Object,
+    grobalForm: Object,
+    retakeFlag: String,
+    pageType: String,
+    cameraInfo: Object,
+    currentActionId: String
+  },
+  components: {
+    IsFollowLine,
+    FlyResponsiveCard,
+    FlyActiconCard,
+    Rename,
+    CheckboxControl,
+    SliderControl,
+    StepInput,
+    AccurateRetake
+  },
+  data() {
+    return {
+      isOpen: true
+    }
+  },
+  computed: {
+    ranges() {
+      const range = {}
+      const camera = this.currentCamera
+      // TODO
+      this.point.actions.forEach((item, index) => {
+        const { type } = item
+        if (['3', '4', '6'].includes(type)) {
+          range[index] = { max: type === '6' ? 900 : type === '3' ? 30 : 100 }
+        } else if (type === '7') {
+          range[index] = {
+            min: -180,
+            max: 180
+          }
+        } else if (type === '8') {
+          range[index] = {
+            min: camera.rangeOfYawMin,
+            max: camera.rangeOfYawMax
+          }
+        } else if (type === '9') {
+          range[index] = {
+            min: camera.rangeOfPanMin,
+            max: camera.rangeOfPanMax
+          }
+        } else if (type === '11') {
+          range[index] = {
+            min: camera.cameraZoomMin,
+            max: camera.cameraZoomMax
+          }
+        }
+      })
+      return range
+    },
+    pageEditStatus() {
+      return this.pageType == 'view'
+    }
+  },
+  methods: {
+    handleDelete(id) {
+      this.$emit('deleteAction', id)
+    },
+    handleFollowChange(item) {
+      if (item.flow === '1') {
+        item.camera = [...this.grobalForm.imageFormat]
+      }
+    },
+    drawFrustum: throttle(function (item) {
+      this.updateFlyPoint(item)
+    }, 200),
+    handleRetakeChange(position, item) {
+      item.position = position
+      this.updateFlyPoint(item)
+    },
+    updateFlyPoint(item) {
+      const point = frustumFlyPoints.update(item)
+      this.$emit('updateFlyPoint', point)
+    },
+    setToggle(val) {
+      this.isOpen = val
+    },
+    actionClick(item) {
+      this.$emit('update:currentActionId', item.id)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.air-point-actions {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  .fly_point_card-body {
+    flex: 1;
+    .scrollbar {
+      height: 100%;
+      .action-item {
+        margin-top: px-to-rem(12);
+        .action-item-content {
+          display: flex;
+          flex-direction: column;
+          gap: px-to-rem(12);
+          padding-right: px-to-rem(12);
+        }
+      }
+    }
+  }
+}
+</style>

+ 275 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AirPointAirLineHeader.vue

@@ -0,0 +1,275 @@
+<template>
+  <div
+    :class="[
+      'top',
+      rightShow && 'top_has_right',
+      pageEditStatus && 'view_model'
+    ]"
+  >
+    <div class="top_center_wrap">
+      <div class="top_center">
+        <div class="top_center_item haiba">
+          <div class="top_icon"></div>
+          <span class="des">机场海拔</span>
+          <span class="value">{{ pageParams.currentNode?.altitude }}m</span>
+        </div>
+        <div class="top_center_item uav">
+          <div class="top_icon"></div>
+          <el-tooltip
+            popper-class="tooltip-class"
+            :content="pageParams.currentNode.deviceAlias"
+            placement="top"
+          >
+            <div class="device_name">{{
+              pageParams.currentNode.deviceAlias
+            }}</div>
+          </el-tooltip>
+        </div>
+        <div class="top_center_item camera">
+          <div class="top_icon"></div>
+          <div class="camera_name">
+            <el-select
+              class="c-select"
+              popper-class="c-select-dropdown"
+              placeholder="请选择"
+              :disabled="disabled"
+              :value="form.camera"
+              @change="cameraChange"
+            >
+              <el-option
+                v-for="(item, index) in cameraList || []"
+                :key="index + 'cameraCode'"
+                :label="item.modelName"
+                :value="item.modelCode"
+              ></el-option>
+            </el-select>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="top_right_opt">
+      <el-tooltip
+        v-for="item in ariLineType"
+        :content="item.tooltip"
+        :key="item.icon"
+        popper-class="tooltip-class"
+        placement="top"
+      >
+        <img
+          :disabled="item.type === '1' ? disabled : false"
+          class="top_right_opt_item"
+          :src="item.icon"
+          @click="topRightOptClick(item)"
+        />
+      </el-tooltip>
+
+      <!-- 
+      <el-select
+        ref="need_hidden_select"
+        class="c-select need_hidden"
+        popper-class="c-select-dropdown"
+        placeholder="请选择"
+        v-model="type"
+        @change="cameraModeChange"
+      >
+        <el-option
+          v-for="(item, index) in types"
+          :key="index + 'cameraMode'"
+          :label="item.label + '拍照范围'"
+          :value="index"
+        >
+        </el-option>
+      </el-select>
+       -->
+    </div>
+  </div>
+</template>
+
+<script>
+import { airLineIcon2 } from '../../dict/dict'
+export default {
+  name: 'AirPointAirLineHeader',
+  props: {
+    cameraList: {
+      type: Array,
+      default: () => []
+    },
+    types: {
+      type: Array,
+      default: () => []
+    },
+    pageParams: Object,
+    form: {
+      type: Object,
+      required: true
+    },
+    rightShow: {
+      type: Boolean,
+      default: () => true
+    },
+    disabled: {
+      type: Boolean,
+      default: () => false
+    },
+    pageType: {
+      type: String,
+      default: 'add'
+    }
+  },
+  computed: {
+    pageEditStatus() {
+      return this.pageType == 'view'
+    }
+  },
+  data() {
+    return {
+      type: 0,
+      ariLineType: airLineIcon2
+    }
+  },
+  methods: {
+    cameraChange(value) {
+      const cur = this.cameraList.find((item) => item.modelCode === value)
+      Object.assign(this.form, { camera: value })
+      this.$emit('cameraChange', cur)
+    },
+    cameraModeChange(value) {
+      this.$emit('cameraModeChange', this.types[value])
+    },
+    topRightOptClick(item) {
+      if (item.type === '1') {
+        if (this.disabled || this.pageEditStatus) {
+          return
+        }
+        this.$emit('clearMap')
+      } else if (item.type === '2') {
+        this.$emit('boundingBoxView')
+      } else {
+        this.$refs.need_hidden_select.visible = true
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.top {
+  pointer-events: auto;
+  position: absolute;
+  top: px-to-rem(27);
+  left: px-to-rem(382);
+  right: px-to-rem(157);
+  height: px-to-rem(56);
+  background: url('../../img/airLine/bg_uav_point_line_top_opt_long.webp')
+    no-repeat;
+  background-size: 100% 100%;
+  &.top_has_right {
+    right: px-to-rem(515);
+    background: url('../../img/airLine/bg_uav_point_line_top_opt_shot.webp')
+      no-repeat;
+    background-size: 100% 100%;
+  }
+  &.view_model {
+    right: px-to-rem(390);
+  }
+  .top_center_wrap {
+    position: absolute;
+    width: calc(100vw - px-to-rem(764));
+    text-align: center;
+  }
+  .top_center {
+    display: inline-flex;
+    align-items: center;
+    width: fit-content;
+    .top_center_item {
+      width: px-to-rem(210);
+      height: px-to-rem(56);
+      line-height: px-to-rem(56);
+      padding: 0 px-to-rem(12);
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      display: flex;
+      align-items: center;
+      position: relative;
+      &::after {
+        position: absolute;
+        content: '';
+        top: px-to-rem(12);
+        right: 0;
+        width: px-to-rem(1);
+        height: px-to-rem(32);
+        z-index: 1;
+        background: linear-gradient(
+          180deg,
+          rgba(79, 159, 255, 0) 0%,
+          #4f9fff 49%,
+          rgba(79, 159, 255, 0) 100%
+        );
+      }
+      &:last-child {
+        &::after {
+          display: none;
+        }
+      }
+      .top_icon {
+        margin-right: px-to-rem(12);
+        width: px-to-rem(24);
+        height: px-to-rem(24);
+        background: url('../../img/airLine/icon_nest_haiba.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      .des {
+        font-weight: 400;
+        margin-right: px-to-rem(12);
+      }
+      .value {
+        font-weight: 600;
+      }
+      &.uav {
+        .top_icon {
+          background: url('../../img/airLine/icon_uav_top.png') no-repeat;
+          background-size: 100% 100%;
+        }
+        .device_name {
+          text-align: left;
+          flex: 1;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          font-weight: 400;
+        }
+      }
+      &.camera {
+        .top_icon {
+          background: url('../../img/airLine/icon_camera_top.png') no-repeat;
+          background-size: 100% 100%;
+        }
+        .camera_name {
+          flex: 1;
+        }
+      }
+    }
+  }
+  .top_right_opt {
+    display: flex;
+    align-items: center;
+    width: fit-content;
+    position: absolute;
+    right: px-to-rem(12);
+    top: 0;
+    height: px-to-rem(56);
+    .top_right_opt_item {
+      cursor: pointer;
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+      &[disabled] {
+        cursor: not-allowed;
+        opacity: 0.7;
+      }
+    }
+    .top_right_opt_item + .top_right_opt_item {
+      margin-left: px-to-rem(12);
+    }
+  }
+}
+</style>

+ 281 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/AuxiliaryViewWindow.vue

@@ -0,0 +1,281 @@
+<template>
+  <div class="right_bottom" v-show="rightShow">
+    <div class="mapContain" v-loading="loading">
+      <div
+        v-show="switchType !== 'ir'"
+        class="zoom-frame warning zoom-frame-main"
+        ref="zoomFrame"
+      >
+        <div class="top left corner"></div>
+        <div class="top right corner"></div>
+        <div class="bottom left corner"></div>
+        <div class="bottom right corner"></div>
+        <div class="b-center"></div>
+        <div class="distance" v-if="distance">{{ distance.toFixed(1) }}m</div>
+        <div class="scale" v-if="zoomVal">{{ zoomVal }}X</div>
+      </div>
+      <div
+        v-show="switchType === 'ir'"
+        class="zoom-frame warning zoom-frame-main"
+        ref="irFrame"
+      >
+        <div class="top left redBorder corner"></div>
+        <div class="top right redBorder corner"></div>
+        <div class="bottom left redBorder corner"></div>
+        <div class="bottom right redBorder corner"></div>
+        <div class="b-center"></div>
+      </div>
+      <div id="firstViewMap" ref="firstViewMap" class="firstViewMap"></div>
+    </div>
+    <div class="camera_type">
+      <div
+        class="camera_type_item"
+        :class="{ active: switchIndex === index }"
+        v-for="(item, index) in currentCamera.types"
+        @click="switchAngle(item, index)"
+        :key="index"
+      >
+        <div class="camera_type_item_name">{{ item.label }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+export let frustumView
+import FrustumView from './frustum-view'
+export default {
+  name: 'AuxiliaryViewWindow',
+  props: {
+    rightShow: Boolean,
+    flyPoint: Object,
+    currentCamera: Object,
+    cameraInfo: Object
+  },
+  data() {
+    return {
+      // setView
+      view: false,
+      loading: false,
+      scale: 0,
+      distance: 0,
+      zoomVal: 1,
+      switchType: 'wide',
+      switchIndex: 0
+    }
+  },
+  methods: {
+    switchAngle(item, index) {
+      this.switchIndex = index
+      frustumView.setCameraMode(item.value)
+    },
+    async takePicture() {
+      this.loading = true
+      let params
+      if (!frustumView) return
+      try {
+        params = await frustumView.takePicture()
+      } catch (e) {
+        console.error(e)
+      }
+      this.loading = false
+      return params
+    },
+    updateView(point) {
+      if (!frustumView) return
+      frustumView.setView(point)
+    },
+    // 更新当前参数
+    updateParam(params, update) {
+      if (!params) return
+      const { cameraMode, distance, zoomVal } = params
+      if (cameraMode) {
+        this.switchType = cameraMode
+      }
+      if (distance) {
+        this.distance = distance
+      }
+      if (zoomVal) {
+        this.zoomVal = zoomVal
+      }
+      if (update) {
+        this.$emit('updateFlyPoint', params)
+      }
+    },
+    // 初始化地图
+    initMap() {
+      if (!this.view) {
+        this.switchType = this.currentCamera.types[0].value
+        const _this = this
+        frustumView = new FrustumView({
+          domId: 'firstViewMap',
+          point: this.flyPoint,
+          // 更新视锥体
+          updateFlyPoint(params) {
+            _this.updateParam(params, true)
+          },
+          currentCamera: this.currentCamera,
+          cameraInfo: this.cameraInfo
+        })
+        frustumView.create({
+          zoomEl: this.$refs.zoomFrame,
+          irEl: this.$refs.irFrame,
+          switchType: this.switchType
+        })
+        frustumView.firstView.updateZoomFrame(1)
+        this.view = true
+      }
+    }
+  },
+  mounted() {
+    this.$watch('cameraInfo', (info) => {
+      if (!this.view) return
+      frustumView.cameraInfo = info
+    })
+    this.$watch('flyPoint', (point) => {
+      this.initMap()
+      frustumView.setView(point)
+      this.updateParam(point, false)
+    })
+    this.$watch('rightShow', (val) => {
+      if (val) {
+        this.initMap()
+      }
+    })
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.right_bottom {
+  width: px-to-rem(370);
+  height: px-to-rem(196);
+  position: absolute;
+  right: 0;
+  bottom: px-to-rem(36);
+  .mapContain {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+    .zoom-frame {
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      position: absolute;
+      z-index: 9;
+      pointer-events: none;
+    }
+
+    .warning .corner {
+      width: 0.5rem;
+      height: 0.5rem;
+      max-width: 40%;
+      max-height: 40%;
+      border: 0.03rem solid #00ee8b;
+      position: absolute;
+    }
+    .redBorder {
+      position: relative;
+      color: #ef2100;
+      border-color: #ef2100 !important;
+      span {
+        position: absolute;
+        top: -0.2rem;
+        right: -0.05rem;
+        font-weight: 700;
+        font-size: 0.16rem;
+        text-shadow: 0 0 0.1rem #ef2100;
+      }
+    }
+    .zoom-frame .corner.left {
+      left: 0;
+      border-right: none;
+    }
+
+    .zoom-frame .corner.right {
+      right: 0;
+      border-left: none;
+    }
+
+    .zoom-frame .corner.top {
+      top: 0;
+      border-bottom: none;
+    }
+
+    .zoom-frame .corner.bottom {
+      bottom: 0;
+      border-top: none;
+    }
+
+    .b-center {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      width: 0.4rem;
+      height: 0.4rem;
+      background-image: url('../../img//airLine/point_center.png');
+      background-size: auto 100%;
+      max-width: 100%;
+      max-height: 100%;
+      background-repeat: no-repeat;
+      background-position: center;
+    }
+    .distance {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-200%, -50%);
+      text-shadow: 0 0 0.1rem #000;
+    }
+    .scale {
+      position: absolute;
+      left: 50%;
+      bottom: 50%;
+      transform: translate(100%, 200%);
+      font-weight: bold;
+      color: #00ee8b;
+    }
+  }
+  .firstViewMap {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+  }
+  .camera_type {
+    display: inline-flex;
+    align-items: center;
+    width: fit-content;
+    height: px-to-rem(32);
+    line-height: px-to-rem(32);
+    border-radius: px-to-rem(16);
+    background: rgba(23, 37, 55, 0.7);
+    position: absolute;
+    left: px-to-rem(12);
+    top: px-to-rem(12);
+    z-index: 10;
+    &_item {
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      text-shadow: 0 px-to-rem(2) px-to-rem(4) #1373e6;
+      padding: 0 px-to-rem(12);
+      cursor: pointer;
+      &.active {
+        border-radius: px-to-rem(16);
+        background: linear-gradient(
+            287deg,
+            rgba(19, 115, 230, 0.11) 0%,
+            rgba(19, 115, 230, 0.11) 0%,
+            rgba(19, 115, 230, 0.3) 100%
+          ),
+          radial-gradient(
+            206% 82% at 13% 50%,
+            rgba(19, 115, 230, 0.33) 0%,
+            rgba(19, 115, 230, 0.01) 69%,
+            rgba(19, 115, 230, 0) 100%
+          );
+      }
+    }
+  }
+}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1333 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/BottomOptions.vue


+ 32 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/CurrentPointDelete.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="PopInfo">
+    <div class="delete" @click="handleDelete">删除</div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PopInfoDelete',
+  methods: {
+    handleDelete() {
+      this.$emit('delete')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.PopInfo {
+  height: px-to-rem(24);
+  background: #172537;
+  border-radius: px-to-rem(4);
+  display: flex;
+  align-items: center;
+  padding: 0 px-to-rem(12);
+  font-size: px-to-rem(14);
+  color: #e8f3fe;
+  cursor: pointer;
+}
+</style>

+ 286 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/HeaderActions.vue

@@ -0,0 +1,286 @@
+<template>
+  <div
+    :class="['right_action', rightShow && 'rightShow']"
+    v-if="!pageEditStatus"
+  >
+    <div
+      :class="['right_action_item', !isReadyStart && 'disabled']"
+      id="addPointButton"
+      @click="handleAddNewPoint"
+    >
+      <span class="act_name">新增航点</span>
+      <div class="right_action_icon"></div>
+    </div>
+    <template v-if="rightShow">
+      <div
+        class="right_action_item"
+        @click="handleActionClick(actionsSources[0])"
+      >
+        <span class="act_name">定向拍照</span>
+        <div class="right_action_icon paizhao"></div>
+      </div>
+      <div
+        class="right_action_item"
+        @click="isShowMoreAction = !isShowMoreAction"
+      >
+        <span class="act_name">更多动作</span>
+        <div
+          class="right_action_icon more"
+          :class="[isShowMoreAction && 'more_active']"
+        ></div>
+      </div>
+    </template>
+    <div class="more_actions_cont" v-show="isShowMoreAction && rightShow">
+      <div
+        class="more_actions_cont_item"
+        :class="[item.disabled && 'disabled']"
+        v-for="(item, index) in actionsSources.slice(1)"
+        :key="index"
+        @click="handleActionClick(item)"
+      >
+        <el-tooltip
+          :content="item.desc"
+          placement="top"
+          v-if="item.desc"
+          popper-class="action-tooltip-popper"
+        >
+          <div class="more_action">
+            <div class="more_action_name">{{ item.name }}</div>
+            <div class="more_action_iocn">
+              <ct-icon :name="item.icon"></ct-icon>
+            </div>
+          </div>
+        </el-tooltip>
+        <template v-else>
+          <div class="more_action_name">
+            {{ item.name }}
+          </div>
+          <div class="more_action_iocn">
+            <ct-icon :name="item.icon"></ct-icon>
+          </div>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+let controlIsActive = false
+let altIsActive = false
+export default {
+  name: 'HeaderActions',
+  props: {
+    rightShow: Boolean,
+    currentCamera: Object,
+    pageType: String,
+    actionsSources: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  computed: {
+    pageEditStatus() {
+      return this.pageType == 'view'
+    }
+  },
+  watch: {
+    rightShow(val) {
+      if (!val) {
+        this.isShowMoreAction = false
+      }
+    }
+  },
+  data() {
+    return {
+      isShowMoreAction: false,
+      isReadyStart: false
+    }
+  },
+  methods: {
+    handleActionClick(item) {
+      if (item.disabled) return
+      this.$emit('actionClick', item)
+    },
+    // 键盘按下事件
+    handleKeyDownEvent(e) {
+      let keyCode = e.keyCode
+      // console.log('keyCode', keyCode)
+      if (keyCode == 17) {
+        controlIsActive = true
+      }
+      if (keyCode == 18) {
+        altIsActive = true
+      }
+    },
+    handleKeyUpEvent(e) {
+      controlIsActive = false
+      altIsActive = false
+    },
+    // 点击添加新航点
+    handleAddNewPoint() {
+      if (!controlIsActive && !altIsActive) {
+        this.isReadyStart = !this.isReadyStart
+        this.$emit('isReadyStart', this.isReadyStart)
+      }
+      if (altIsActive && this.isReadyStart) {
+        console.log('altIsActive+click')
+        this.$emit('addPoint', 'after')
+      }
+    },
+    initEvent() {
+      let this_ = this
+      document.addEventListener('keydown', this.handleKeyDownEvent)
+      document.addEventListener('keyup', this.handleKeyUpEvent)
+
+      const element = document.getElementById('addPointButton')
+      element.addEventListener('mousedown', function (e) {
+        console.log('mousedown', e)
+        if (e.ctrlKey && e.button === 0 && this_.isReadyStart) {
+          // 阻止Mac上的默认右键菜单行为
+          e.preventDefault()
+          // 在这里添加你的自定义交互逻辑
+          // console.log('Ctrl + 鼠标左键被触发')
+          this_.$emit('addPoint', 'before')
+        }
+      })
+      element.addEventListener('contextmenu', function (e) {
+        if (e.ctrlKey && e.button === 0) {
+          e.preventDefault()
+        }
+      })
+    }
+  },
+  mounted() {
+    this.initEvent()
+  },
+  beforeDestroy() {
+    document.removeEventListener('keydown', this.handleKeyDownEvent)
+    document.removeEventListener('keyup', this.handleKeyUpEvent)
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.right_action {
+  width: px-to-rem(122);
+  position: absolute;
+  right: px-to-rem(24);
+  top: px-to-rem(32);
+  &.rightShow {
+    right: px-to-rem(382);
+  }
+  .right_action_item {
+    width: 100%;
+    height: px-to-rem(24);
+    margin-top: px-to-rem(12);
+    padding-right: px-to-rem(12);
+    background: url('../../img/airLine/actions_bg_right.webp') no-repeat;
+    background-size: 100% 100%;
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    .act_name {
+      flex: 1;
+      text-align: right;
+      font-size: px-to-rem(16);
+    }
+    .right_action_icon {
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+      margin-left: px-to-rem(12);
+      background: url('../../img/airLine/actions_bg_right_addpoint.webp')
+        no-repeat;
+      background-size: 100% 100%;
+      &.paizhao {
+        background: url('../../img/airLine/actions_bg_right_paizhao.webp')
+          no-repeat;
+        background-size: 100% 100%;
+      }
+      &.more {
+        background: url('../../img/airLine/actions_bg_right_more.webp')
+          no-repeat;
+        background-size: 100% 100%;
+      }
+      &.more_active {
+        transform: rotate(180deg);
+      }
+    }
+    &.disabled {
+      background: url('../../img/airLine/disable_bg.png') no-repeat;
+      background-size: 100% 100%;
+      .right_action_icon {
+        background: url('../../img/airLine/icon_add_disabled.png') no-repeat;
+        background-size: 100% 100%;
+      }
+      .act_name {
+        color: rgba(232, 243, 254, 0.4);
+      }
+    }
+  }
+  .more_actions_cont {
+    margin-top: px-to-rem(6);
+    right: 0;
+    z-index: 1;
+    position: absolute;
+    width: px-to-rem(196);
+    height: auto;
+    background: url('../../img/airLine/bg_actions_color.webp') no-repeat;
+    background-size: 100% 100%;
+    padding-right: px-to-rem(12);
+    padding-bottom: px-to-rem(6);
+    .more_actions_cont_item {
+      height: px-to-rem(24);
+      line-height: px-to-rem(24);
+      margin-top: px-to-rem(5);
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      width: 100%;
+      &.disabled {
+        opacity: 0.4;
+        pointer-events: none;
+      }
+      .more_action {
+        width: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+      }
+      .more_action_name {
+        flex: 1;
+        text-align: right;
+        margin-right: px-to-rem(6);
+      }
+      .more_action_iocn {
+        width: px-to-rem(20);
+        height: px-to-rem(20);
+        line-height: px-to-rem(19);
+        .ct-icon__inline-block {
+          ::v-deep .ct-icon {
+            width: auto !important;
+            height: auto !important;
+            .icon-ctw {
+              font-size: px-to-rem(20) !important;
+              color: #e8f3fe !important;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.action-tooltip-popper {
+  line-height: px-to-rem(24) !important;
+  padding: px-to-rem(6) px-to-rem(12);
+  background: #0f1926 !important;
+  max-width: px-to-rem(312) !important;
+  font-size: px-to-rem(16) !important;
+  .popper__arrow::after {
+    border-top-color: #0f1926 !important;
+  }
+}
+</style>

+ 457 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/PointLineOverView.vue

@@ -0,0 +1,457 @@
+<template>
+  <div class="">
+    <div class="herder_with_line">
+      <span>航点航线</span>
+      <el-tooltip popper-class="tooltip-class" placement="right-start">
+        <template v-slot:content>
+          <div class="tooltip-content">
+            <p class="tooltip-content-title">起飞点:</p>
+            <p>默认为无人机当前位置。</p>
+            <p class="tooltip-content-title mt-12">航点编辑: </p>
+            <p
+              >修改航点位置:<span class="content-text"
+                >选中航点,点击飞行数据展示及操作区编辑按钮,输入经纬度改</span
+              ></p
+            >
+            <p class="content-text"
+              >变航点位置或鼠标左键选中航点后拖拽改变航点位置。</p
+            >
+            <p class="mt-12"
+              >删除航点:<span class="content-text"
+                >右键选中航点,删除航点。</span
+              ></p
+            >
+            <p class="tooltip-content-title mt-12">航点动作编辑:</p>
+            <p
+              >航点动作调整:<span class="content-text"
+                >鼠标悬浮航点动作,点击编辑按钮,调整航点动作。</span
+              ></p
+            >
+            <p
+              >删除航点动作:<span class="content-text"
+                >鼠标悬浮航点动作点击删除或在动作编辑器中删除航点动作。</span
+              ></p
+            >
+          </div>
+        </template>
+        <i class="iconfont_tools icon-tongyong_icon_wenhao icon-wenhao"></i>
+      </el-tooltip>
+    </div>
+    <div class="line_name_wrap">
+      <img class="icon_img" src="../../img/icon_hangdian.svg" alt="" />
+      <span class="line_name" :c-tip="pageParams?.flightRouteName">{{
+        pageParams?.flightRouteName
+      }}</span>
+    </div>
+    <div class="hangxain_zonglan">
+      <div class="zl_left">
+        <div class="zl_left_top">
+          <span class="zl_title">航线长度(m)</span>
+          <span class="zl_des" :c-tip="distance">{{ distance }}</span>
+        </div>
+        <div class="zl_left_bottom" :class="{ pr_0: timeUse.length > 6 }">
+          <span class="zl_title max_width">预计执行时间</span>
+          <span class="zl_des max_time" :c-tip="timeUse">{{ timeUse }}</span>
+        </div>
+      </div>
+      <div class="middle"></div>
+      <div class="zl_right">
+        <div class="zl_left_top">
+          <span class="zl_title">航点数量</span>
+          <span class="zl_des" :c-tip="count.pointNum">{{
+            count.pointNum
+          }}</span>
+        </div>
+        <div class="zl_left_bottom">
+          <span class="zl_title">预计拍照数</span>
+          <span class="zl_des" :c-tip="count.takePhotoNum">{{
+            count.takePhotoNum
+          }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+// 根据秒数,返回XhXmXs格式的时间
+function formatTimeFromSeconds(seconds = 0) {
+  let hours = Math.floor(seconds / 3600)
+  let minutes = Math.floor((seconds % 3600) / 60)
+  let remainingSeconds = Math.floor(seconds % 60)
+  if (seconds == 0) {
+    return 0
+  }
+  if (hours > 0) {
+    return `${hours.toString()}h${minutes.toString()}m${remainingSeconds.toString()}s`
+  } else if (minutes > 0) {
+    return `${minutes.toString()}m${remainingSeconds.toString()}s`
+  } else {
+    return `${remainingSeconds.toString()}s`
+  }
+}
+export default {
+  name: 'PointLineOverView',
+  data() {
+    return {
+      distance: 0,
+      count: {
+        pointNum: 0,
+        distance: 0,
+        timeUse: 0,
+        takePhotoNum: 0
+      }
+    }
+  },
+  props: {
+    pageParams: {
+      type: Object,
+      required: true
+    },
+    form: {
+      required: true,
+      type: Object,
+      default: () => ({})
+    },
+    flyPoints: {
+      required: true,
+      type: Array
+    },
+    startLineDistance: {
+      type: [Number, String],
+      default: 0
+    },
+    endLineDistance: {
+      type: [Number, String],
+      default: 0
+    },
+    cameraModeType: {
+      type: Array,
+      default: () => []
+    }
+  },
+  computed: {
+    startLineTime() {
+      return (+this.startLineDistance / this.form.autoFlightSpeed).toFixed(0)
+    },
+    endLineTime() {
+      return (+this.endLineDistance / this.form.autoFlightSpeed).toFixed(0)
+    },
+    lensNum() {
+      return this.cameraModeType.length
+    },
+    timeUse() {
+      let time_ = +this.startLineTime + +this.endLineTime + +this.count.timeUse
+      return formatTimeFromSeconds(time_)
+    }
+  },
+  watch: {
+    flyPoints: {
+      deep: true,
+      handler(val) {
+        // console.log('flyPoints', val)
+        this.count.pointNum = this.flyPoints.length
+        this.getDidstance()
+        // this.getPointsTime()
+        this.getCount()
+      }
+    }
+  },
+  methods: {
+    // 重新计算
+    getCount() {
+      const count = {
+        distance: 0,
+        timeUse: 0,
+        takePhotoNum: 0,
+        pointNum: this.flyPoints.length
+      }
+
+      // 计算所有时间和距离
+      const actions = this.flyPoints.reduce((pre, cur, index) => {
+        const next = this.flyPoints[index + 1]
+        if (next) {
+          count.distance += next.pointDistance
+          count.timeUse += next.pointDistance / cur.waypointSpeed
+        }
+        cur.actions.forEach((action) => (action.pointIndex = index))
+        return pre.concat(cur.actions)
+      }, [])
+
+      // 判断是否有间隔拍照,如果没有结束,补上
+      const lastSpaceIndex = actions.findLastIndex((action) =>
+        ['3', '4'].includes(action.type)
+      )
+      if (lastSpaceIndex !== -1) {
+        const endSpaceAction = actions
+          .slice(lastSpaceIndex)
+          .find((action) => action.type === '5')
+        if (!endSpaceAction) {
+          actions.push({
+            type: '5',
+            pointIndex: this.flyPoints.length - 1
+          })
+        }
+      }
+
+      // 计算间隔拍照数量
+      let startAction
+      actions.forEach((action) => {
+        if (action.type === '6') {
+          count.timeUse += action.space
+        } // 定向拍照,拍照
+        else if (['0', '10', '12'].includes(action.type)) {
+          count.takePhotoNum++
+        } else if (['3', '4'].includes(action.type)) {
+          startAction = action
+        } else if (action.type === '5') {
+          if (startAction) {
+            for (let i = startAction.pointIndex; i < action.pointIndex; i++) {
+              const next = this.flyPoints[i + 1]
+              if (next) {
+                const distance = next.pointDistance
+                const timeUse = distance / this.flyPoints[i].waypointSpeed
+                if (startAction.type === '3') {
+                  count.takePhotoNum += Math.round(timeUse / startAction.space)
+                } else {
+                  count.takePhotoNum += Math.round(distance / startAction.space)
+                }
+              }
+            }
+            startAction = null
+          }
+        }
+      })
+      this.count = count
+      // this.$emit('lineDistance', count.distance)
+      this.$emit('expectedTime', count.timeUse)
+      this.$emit('takePhotoNum', count.takePhotoNum)
+    },
+
+    // 获取航线长度
+    getDidstance() {
+      let disArr = this.flyPoints.map((item) => {
+        return item.pointDistance ? item.pointDistance : 0
+      })
+      let lineDistance = disArr.reduce((acc, curr) => acc + curr, 0).toFixed()
+      this.distance = (
+        +lineDistance +
+        +this.startLineDistance +
+        +this.endLineDistance
+      ).toFixed()
+      this.$emit('lineDistance', lineDistance)
+    },
+    // 就算航点动作时间
+    getPointsTime() {
+      let localCompPoints = []
+      this.flyPoints.forEach((pointItem, index) => {
+        let speed = pointItem.waypointSpeed
+        localCompPoints.push({
+          id: pointItem.id,
+          doActionTime: 0,
+          flyTime:
+            index == 0 ? 0 : +(pointItem.pointDistance / speed).toFixed(0),
+          takePhoneNumNoRate: 0,
+          takePhoneNumRate: 0
+        })
+        pointItem.actions.forEach((actionItme) => {
+          if (actionItme.type == '6') {
+            // 悬停
+            localCompPoints[index].doActionTime += actionItme.space
+          }
+
+          //  todo 计算预计拍照数量 (计算方式不对需要重新计算 建议单独方法拆出去)
+          if (actionItme.tye == '12') {
+            // 全景拍照
+            localCompPoints[index].takePhoneNumNoRate += 1
+          }
+          if (actionItme.type == '10' || actionItme.type == '0') {
+            // 拍照 定向拍照
+            localCompPoints[index].takePhoneNumRate += 1
+          }
+          if (actionItme.type == '3' || actionItme.type == '4') {
+            // 开始等时间隔拍照 // 开始等距间隔拍照
+            if (index == 0) {
+              localCompPoints[index].takePhoneNumRate += 1
+            }
+            if (index > 0) {
+              localCompPoints[index].takePhoneNumRate += Math.ceil(
+                localCompPoints[index].flyTime / actionItme.space
+              )
+            }
+          }
+        })
+      })
+      // console.log('localCompPoints', localCompPoints)
+      let actTime = localCompPoints
+        .map((item) => item.doActionTime)
+        .reduce((acc, curr) => acc + curr, 0)
+        .toFixed(0)
+      let flyTime = localCompPoints
+        .map((item) => item.flyTime)
+        .reduce((acc, curr) => acc + curr, 0)
+        .toFixed(0)
+      let expectedTime_ =
+        +this.startLineTime + +this.endLineTime + +actTime + +flyTime
+      this.expectedTime = formatTimeFromSeconds(expectedTime_)
+      this.$emit('expectedTime', expectedTime_)
+      let photoNoRateNum = localCompPoints
+        .map((item) => item.takePhoneNumNoRate)
+        .reduce((acc, curr) => acc + curr, 0)
+        .toFixed(0)
+      let photoPateNum = localCompPoints
+        .map((item) => item.takePhoneNumRate)
+        .reduce((acc, curr) => acc + curr, 0)
+        .toFixed(0)
+      this.takePhoneNum = +photoNoRateNum + +photoPateNum * this.lensNum
+      this.$emit('takePhoneNum', this.takePhoneNum)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin textShadow {
+  text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+}
+@mixin bgColor {
+  background: linear-gradient(
+    to right,
+    rgb(24, 37, 57) 0%,
+    rgba(24, 37, 57, 0.9) 40%,
+    rgba(24, 37, 57, 0.8) 100%
+  );
+}
+.herder_with_line {
+  text-align: center;
+  font-weight: 600;
+  font-size: px-to-rem(18);
+  color: #e8f3fe;
+  line-height: 1.2;
+  @include flex;
+  justify-content: center;
+  @include textShadow;
+  width: 100%;
+  height: px-to-rem(20);
+  background: url('../../img/airLine/bg_title.webp') no-repeat;
+  background-size: 100% 100%;
+  .arrow_icon {
+    width: px-to-rem(24);
+    height: px-to-rem(24);
+    cursor: pointer;
+    &.left_arrow {
+      margin-right: px-to-rem(6);
+      background: url('../../img/airLine/icon_action_left.webp') no-repeat;
+      background-size: 100% 100%;
+    }
+    &.right_arrow {
+      margin-left: px-to-rem(6);
+      background: url('../../img/airLine/icon_action_right.webp') no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+  .icon-wenhao {
+    color: #ffeeb1;
+    font-size: px-to-rem(20);
+    margin-left: px-to-rem(6);
+  }
+  span {
+    font-weight: 600;
+  }
+}
+.line_name_wrap {
+  margin-top: px-to-rem(12);
+  height: px-to-rem(32);
+  @include flex;
+  .icon_img {
+    width: px-to-rem(20);
+    height: px-to-rem(20);
+    margin-right: px-to-rem(6);
+  }
+  .line_name {
+    flex: 1;
+    line-height: px-to-rem(32);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    font-weight: 600;
+    font-size: px-to-rem(18);
+    color: #e8f3fe;
+    @include textShadow;
+  }
+}
+.hangxain_zonglan {
+  margin-top: px-to-rem(12);
+  padding: px-to-rem(8) px-to-rem(8) px-to-rem(12);
+  width: 100%;
+  height: px-to-rem(68);
+  background: url('../../img/airLine/card_hdhx_tj.webp') no-repeat 100% 100%;
+  background-size: 100% 100%;
+  @include flex;
+  .middle {
+    width: px-to-rem(4);
+    height: px-to-rem(64);
+    background: radial-gradient(
+      25% 40% at 50% 46%,
+      #4f9fff 0%,
+      rgba(79, 159, 255, 0) 100%
+    );
+  }
+  .zl_left {
+    flex: 1;
+    @include flex;
+    justify-content: space-between;
+    flex-direction: column;
+    .zl_left_top,
+    .zl_left_bottom {
+      padding-right: px-to-rem(12);
+    }
+  }
+  .zl_right {
+    flex: 1;
+    @include flex;
+    justify-content: space-between;
+    flex-direction: column;
+    .zl_left_top,
+    .zl_left_bottom {
+      padding-left: px-to-rem(12);
+    }
+  }
+  .zl_left_top,
+  .zl_left_bottom {
+    @include flex;
+    width: 100%;
+    justify-content: space-between;
+    height: px-to-rem(24);
+    line-height: px-to-rem(24);
+  }
+  .zl_left_bottom {
+    margin-top: px-to-rem(4);
+  }
+  .zl_title {
+    font-weight: 400;
+    color: #e8f3fe;
+  }
+  .zl_des {
+    font-weight: 600;
+    @include textShadow;
+  }
+  .pr_0 {
+    padding-right: 0;
+  }
+  .max_width {
+    white-space: nowrap;
+  }
+  .max_time {
+    max-width: px-to-rem(70);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+</style>

+ 38 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/PopInfoHeight.vue

@@ -0,0 +1,38 @@
+<template>
+  <div class="PopInfo">
+    <p>ASL:{{ info.height }}</p>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'PopInfo',
+  props: {
+    info: {
+      type: Object,
+      default: () => ({})
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.PopInfo {
+  height: px-to-rem(24);
+  background: #172537;
+  border-radius: px-to-rem(4);
+  display: flex;
+  align-items: center;
+  padding: 0 px-to-rem(12);
+  p {
+    margin-right: px-to-rem(12);
+  }
+  p,
+  i {
+    font-size: px-to-rem(14);
+    color: #e8f3fe;
+  }
+}
+</style>

+ 864 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/AccurateRetake.vue

@@ -0,0 +1,864 @@
+<template>
+  <div class="orient_container">
+    <div class="header">
+      <span class="label">虚拟快照预览</span>
+      <div v-if="retakeFlag === '1'">
+        <el-button size="mini" type="primary" @click="accurateRephoto"
+          >精准复拍</el-button
+        >
+      </div>
+      <div v-else>
+        <el-tooltip
+          content="航线飞行执行过飞行任务后才可以使用此功能。"
+          placement="top"
+        >
+          <el-button size="mini" type="primary" :disabled="true"
+            >精准复拍</el-button
+          >
+        </el-tooltip>
+      </div>
+    </div>
+    <div
+      class="interactive_container"
+      v-if="showPicUrl?.length"
+      ref="container"
+    >
+      <div class="img_container">
+        <img
+          :src="showPicUrl"
+          :style="{
+            width: picWidth + 'px',
+            height: picHeight + 'px',
+            top: imgTop + 'px',
+            left: imgLeft + 'px'
+          }"
+        />
+      </div>
+      <div
+        v-if="allowInte"
+        class="target_rect"
+        ref="target_rect"
+        :class="{ gray_rect: accurateFrameValid != '1' }"
+        :style="{
+          width: rectWidth + 'px',
+          height: rectHeight + 'px',
+          top: rectTop + 'px',
+          left: rectLeft + 'px'
+        }"
+        @mousedown="dragMousedown"
+      >
+        <div
+          class="box-left-top"
+          @mousedown="scaleMousedown($event, 'lt')"
+        ></div>
+        <div
+          class="box-right-top"
+          @mousedown="scaleMousedown($event, 'rt')"
+        ></div>
+        <div
+          class="box-right-bottom"
+          @mousedown="scaleMousedown($event, 'rb')"
+        ></div>
+        <div
+          class="box-left-bottom"
+          @mousedown="scaleMousedown($event, 'lb')"
+        ></div>
+        <div class="center"></div>
+      </div>
+    </div>
+    <div class="footer">
+      <div class="label mt-8">{{ curActionData.params?.dateTime }}</div>
+      <div class="icon">
+        <ct-icon
+          name="reset"
+          color="#E8F3FE"
+          @click="reset"
+          v-if="!initialStatus"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { frustumView } from '../AuxiliaryViewWindow'
+export default {
+  props: {
+    curActionData: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    ratioRange: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    },
+    point: Object,
+    payloadEnumValue: {
+      type: Number,
+      default: 81
+    },
+    actionIndex: {
+      type: Number,
+      default: 0
+    },
+    retakeFlag: {
+      type: String,
+      default: '0'
+    },
+    disabled: Boolean
+  },
+  inject: ['currentCamera'],
+  data() {
+    return {
+      virtualPhotoUrl: null,
+      photoUrl: null,
+      // photoFile: '',
+      // orientedFilePath: null,
+      lensInfo: null,
+      originalDate: null,
+      picWidth: 0,
+      picHeight: 0,
+      rectLeft: 0,
+      rectTop: 0,
+      rectWidth: 0,
+      rectHeight: 0,
+      maxRectScalewidth: 0, // 缩放过程中目标框最大宽度
+      maxRectScaleHeight: 0, // 缩放过程中目标框最大高度
+      increaseRatioFactor: null, // 移动过程中缩放比例
+      preRectWidth: 0,
+      preRectHeight: 0,
+      rectDefaultWidthRem: 1.72,
+      rectDefaultHeightRem: 1.29,
+      inteContainerDefaultWidthRem: 3.52,
+      inteContainerDefaultHeightRem: 2.145,
+      // rectDefaultWidthRem: 0.8,
+      // rectDefaultHeightRem: 0.6,
+      preRectLeft: 0,
+      preRectTop: 0,
+      imgLeft: 0,
+      imgTop: 0,
+      accurateFrameValid: 0,
+      mouseRectOffsetX: 0, // 鼠标相对于目标框的x轴距离
+      mouseRectOffsetY: 0, // 鼠标相对于目标框的y轴距离
+      pxEachRem: 100,
+      ratio: 2,
+      initialData: {}, // 记录初始数据,用于重置
+      isScaleRect: false, // 是否在缩放
+      isDragRect: false, // 是否在拖拽
+      disableAccurateRetake: true, // 是否禁用开启精准复拍按钮
+      // beforeScaleRectPosInfo: [], // 缩放前目标框四个角的位置
+      // scaleMousedownPos: [], // 缩放前鼠标落点
+      // allowInteCameraType: ['42-0', '43-0', '52-0', '53-0', '61-1', '80-0', '81-0'], // 允许交互的相机类型
+      allowInteCameraType: [52, 53, 61, 80, 81], // 允许交互的相机类型
+      rectPosition: '', // 拖拽的目标框位置,lt 左上;rt 右上;lb 左下;rb 右下
+      isFirstMove: true,
+      isFirstInit: true,
+      interAreaPosInfo: null,
+      // imgPosInfo: null,
+      initialStatus: true, // 初始状态
+      recordInitRatio: 0 //记录初始的变焦倍率
+    }
+  },
+  computed: {
+    cameraFocalLength() {
+      // 可见光动态获取相机镜头最小焦距
+      return frustumView?.firstView.cameraEquivalentFocalLength || 24
+    },
+    allowInte() {
+      return (
+        true ||
+        this.allowInteCameraType.includes(Number(this.currentCamera?.type))
+      )
+    },
+    showPicUrl() {
+      // 来源:kmz包、业务数据真实/虚拟照片地址
+      return this.virtualPhotoUrl || this.photoUrl
+    }
+  },
+  watch: {
+    curActionData: {
+      handler(val) {
+        this.getparams()
+      },
+      deep: true
+    },
+    ratio(val) {
+      val && frustumView.firstView?.updateZoomFrame(val)
+    }
+  },
+  mounted() {
+    this.getparams()
+    this.lensInfo = frustumView.firstView?.lensInfo
+    if (!this.disabled) {
+      document.addEventListener('mousemove', this.mousemove)
+      document.addEventListener('mouseup', this.mouseup)
+    }
+  },
+  beforeDestroy() {
+    document.removeEventListener('mousemove', this.mousemove)
+    document.removeEventListener('mouseup', this.mouseup)
+  },
+  methods: {
+    // 修改动作组该航点对应动作的精准复拍状态
+    changeAccurateValidState() {
+      if (this.curActionData.disabled) return
+      this.accurateFrameValid == 0
+        ? (this.accurateFrameValid = 1)
+        : (this.accurateFrameValid = 0)
+      // 选中精准复拍,框大于图片要恢复、超出图片边界挪进图片里
+      if (this.accurateFrameValid) {
+        this.rectWidth > this.picWidth && (this.rectWidth = this.picWidth)
+        this.rectHeight > this.picHeight && (this.rectHeight = this.picHeight)
+        this.rectLeft < this.imgLeft
+          ? (this.imgLeft = this.rectLeft)
+          : this.rectLeft + this.rectWidth > this.imgLeft + this.picWidth
+          ? (this.imgLeft = this.rectLeft + this.rectWidth - this.picWidth)
+          : ''
+        this.rectTop < this.imgTop
+          ? (this.imgTop = this.rectTop)
+          : this.rectTop + this.rectHeight > this.imgTop + this.picHeight
+          ? (this.imgTop = this.rectTop + this.rectHeight - this.picHeight)
+          : ''
+      }
+      this.saveData()
+    },
+    // 目标框中心点距图片中心点偏移量
+    getCenterDistance() {
+      const _this = this
+      const { fixedHeadingPitch } = this.curActionData.params
+      const { zoomRatio } = fixedHeadingPitch
+      const xDis =
+        this.rectLeft + this.rectWidth / 2 - (this.imgLeft + this.picWidth / 2)
+      const yDis =
+        this.rectTop + this.rectHeight / 2 - (this.imgTop + this.picHeight / 2)
+
+      frustumView.firstView.computeAngleByCoordinate(
+        xDis,
+        yDis,
+        zoomRatio,
+        {
+          width: this.picWidth,
+          height: this.picHeight
+        },
+        fixedHeadingPitch,
+        (params) => {
+          const { heading, pitch } = params
+          _this.$emit('change', { heading, pitch })
+        }
+      )
+
+      // console.log("🚀 ~ centerDistance ~ xDis, yDis:", xDis, yDis);
+      // return [xDis, yDis];
+    },
+    // 获取虚拟快照本地路径。调用 gis 方法
+    async accurateRephoto() {
+      const { params } = this.curActionData
+      this.accurateFrameValid = 1
+      params.accurateFrameValid = 1
+    },
+    // 初始化获取动作数据
+    getparams() {
+      // console.log('getparams', this.curActionData.params)
+      if (this.curActionData.params) {
+        this.pxEachRem = sessionStorage.getItem('pxEachRem') || 100
+        const {
+          focusRegionWidth,
+          focusRegionHeight,
+          imageWidth,
+          imageHeight,
+          focusX,
+          focusY,
+          focalLength,
+          accurateFrameValid,
+          virtualPhotoUrl,
+          photoUrl,
+          // photoFile,
+          orientedFilePath,
+          rectWidth,
+          rectHeight,
+          rectLeft,
+          rectTop,
+          imgLeft,
+          imgTop,
+          picWidth,
+          picHeight,
+          originalDate
+        } = this.curActionData.params
+        this.accurateFrameValid = accurateFrameValid || 0
+        this.virtualPhotoUrl = virtualPhotoUrl
+        this.originalDate = originalDate
+        if (!virtualPhotoUrl) {
+          this.photoUrl = photoUrl
+        }
+        // this.photoFile = photoFile;
+        this.orientedFilePath = orientedFilePath
+        this.ratio = focalLength / this.cameraFocalLength
+        if (this.isFirstInit) {
+          this.recordInitRatio = focalLength / this.cameraFocalLength
+        }
+        let increaseRatioFactor
+        // 编辑时用保存的展示信息,保存前会删除这些信息
+        if (rectWidth) {
+          this.rectWidth = rectWidth
+          this.rectHeight = rectHeight
+          this.rectLeft = rectLeft
+          this.rectTop = rectTop
+          this.imgLeft = imgLeft
+          this.imgTop = imgTop
+          this.picWidth = picWidth
+          this.picHeight = picHeight
+          return
+        }
+        // 计算目标框宽高、位置
+        if (+imageWidth) {
+          // 参照司空规则,用目标框长边 * .75和短边对比,使用更大的去计算变焦缩放系数
+          if (focusRegionWidth * 0.75 > focusRegionHeight) {
+            // 目标框重置宽高计算规则:长边 = 容器长边 * 0.6
+            const curRectWidth =
+              this.inteContainerDefaultWidthRem * 0.6 * this.pxEachRem
+            increaseRatioFactor = curRectWidth / focusRegionWidth
+            this.rectHeight = focusRegionHeight * increaseRatioFactor
+            this.rectWidth = curRectWidth
+          } else {
+            const curRectHeight =
+              this.inteContainerDefaultHeightRem * 0.6 * this.pxEachRem
+            increaseRatioFactor = curRectHeight / focusRegionHeight
+            this.rectWidth = focusRegionWidth * increaseRatioFactor
+            this.rectHeight = curRectHeight
+          }
+
+          this.rectLeft =
+            (this.inteContainerDefaultWidthRem * this.pxEachRem -
+              this.rectWidth) /
+            2
+          this.rectTop =
+            (this.inteContainerDefaultHeightRem * this.pxEachRem -
+              this.rectHeight) /
+            2
+          // 计算图片初始大小,单位px
+          this.picWidth = imageWidth * increaseRatioFactor
+          this.picHeight = imageHeight * increaseRatioFactor
+          // 计算图片相对位置:目标框左上角 + 目标框到图片左上角
+          this.imgLeft =
+            this.rectLeft +
+            (focusRegionWidth / 2 - focusX) * increaseRatioFactor
+          this.imgTop =
+            this.rectTop +
+            (focusRegionHeight / 2 - focusY) * increaseRatioFactor
+          this.getDafaultInfo()
+        } else {
+          this.rectWidth = this.rectDefaultWidthRem * this.pxEachRem
+          this.rectHeight = this.rectDefaultHeightRem * this.pxEachRem
+          this.rectLeft =
+            ((this.inteContainerDefaultWidthRem - this.rectDefaultWidthRem) /
+              2) *
+            this.pxEachRem
+          this.rectTop =
+            ((this.inteContainerDefaultHeightRem - this.rectDefaultHeightRem) /
+              2) *
+            this.pxEachRem
+          this.picWidth = this.inteContainerDefaultWidthRem * this.pxEachRem
+          this.picHeight = this.inteContainerDefaultHeightRem * this.pxEachRem
+          this.imgLeft = 0
+          this.imgTop = 0
+          this.getDafaultInfo()
+          this.$nextTick(() => {
+            this.saveData()
+          })
+        }
+      }
+    },
+    getDafaultInfo() {
+      // 获取初始数据,用于重置
+      if (this.isFirstInit) {
+        this.initialData = {
+          rectWidth: this.rectWidth,
+          rectHeight: this.rectHeight,
+          rectLeft: this.rectLeft,
+          rectTop: this.rectTop,
+          picWidth: this.picWidth,
+          picHeight: this.picHeight,
+          imgLeft: this.imgLeft,
+          imgTop: this.imgTop,
+          ratio: this.ratio,
+          accurateFrameValid: this.accurateFrameValid
+        }
+        this.isFirstInit = false
+      }
+    },
+    dragMousedown(e) {
+      console.log('dragMousedown', this.disabled)
+      if (this.disabled) return
+      if (!this.isScaleRect && this.allowInte) {
+        // 记录鼠标相对于目标框的位置
+        const rectEle = this.$el?.querySelector('.target_rect')
+        this.mouseRectOffsetX = e.clientX - rectEle.getBoundingClientRect().left
+        this.mouseRectOffsetY = e.clientY - rectEle.getBoundingClientRect().top
+        this.isDragRect = true
+        this.preRectLeft = this.rectLeft
+        this.preRectTop = this.rectTop
+
+        // 更新第一视角范围
+        // this.curActionIndex != this.actionIndex
+
+        // frustumView.firstView?.updateZoomFrame(
+        //   this.curActionData.params.focalLength / this.cameraFocalLength
+        // )
+      }
+    },
+    scaleMousedown(e, pos) {
+      if (this.disabled) return
+      if (this.allowInte) {
+        this.isScaleRect = true
+        this.preRectWidth = this.rectWidth
+        this.preRectHeight = this.rectHeight
+        this.rectPosition = pos
+        // 记录鼠标落下时目标框上下左右四角坐标、鼠标坐标
+        // 不一定所有动作都有框,导入的没飞过的定向拍照生成前没有框,通过父级获取
+        const beforeScaleRectPosInfo = this.$el
+          ?.querySelector('.target_rect')
+          .getBoundingClientRect()
+        // this.scaleMousedownPos = [e.clientX, e.clientY];
+        this.interAreaPosInfo = this.$el
+          ?.querySelector('.interactive_container')
+          .getBoundingClientRect()
+        const imgPosInfo = this.$el
+          ?.querySelector('.img_container img')
+          .getBoundingClientRect()
+        const maxLeft = Math.max(this.interAreaPosInfo.left, imgPosInfo.left)
+        const minRight = Math.min(this.interAreaPosInfo.right, imgPosInfo.right)
+        const maxTop = Math.max(this.interAreaPosInfo.top, imgPosInfo.top)
+        const minBottom = Math.min(
+          this.interAreaPosInfo.bottom,
+          imgPosInfo.bottom
+        )
+        this.maxRectScalewidth =
+          Math.abs(
+            Math.min(
+              beforeScaleRectPosInfo.left - maxLeft,
+              minRight - beforeScaleRectPosInfo.right
+            )
+          ) *
+            2 +
+          this.rectWidth
+        this.maxRectScaleHeight =
+          Math.abs(
+            Math.min(
+              beforeScaleRectPosInfo.top - maxTop,
+              minBottom - beforeScaleRectPosInfo.bottom
+            )
+          ) *
+            2 +
+          this.rectHeight
+      }
+    },
+    mouseup(e) {
+      // 缩放,计算框大小;图片大小、位置
+      if (
+        this.isScaleRect &&
+        this.ratio <= this.lensInfo.maxZoomRatio &&
+        this.ratio >= this.lensInfo.minZoomRatio
+      ) {
+        let increaseRatioFactor
+        if (this.rectWidth * 0.75 > this.rectHeight) {
+          const curRectWidth =
+            this.inteContainerDefaultWidthRem * 0.6 * this.pxEachRem
+          increaseRatioFactor = curRectWidth / this.rectWidth
+          this.rectHeight = this.rectHeight * increaseRatioFactor
+          this.rectWidth = curRectWidth
+        } else {
+          const curRectHeight =
+            this.inteContainerDefaultHeightRem * 0.6 * this.pxEachRem
+          increaseRatioFactor = curRectHeight / this.rectHeight
+          this.rectWidth = this.rectWidth * increaseRatioFactor
+          this.rectHeight = curRectHeight
+        }
+        if (this.isScaleRect) {
+          this.picWidth = this.picWidth * increaseRatioFactor
+          this.picHeight = this.picHeight * increaseRatioFactor
+          // 缩放时图片以框中心点为中心缩放
+          const centerRect = [
+            (this.inteContainerDefaultWidthRem * this.pxEachRem) / 2,
+            (this.inteContainerDefaultHeightRem * this.pxEachRem) / 2
+          ]
+          // 缩放前框中心点离图片左上角距离
+          const rawX = centerRect[0] - this.imgLeft
+          const rawY = centerRect[1] - this.imgTop
+          this.imgLeft -= (increaseRatioFactor - 1) * rawX
+          this.imgTop -= (increaseRatioFactor - 1) * rawY
+        }
+        this.getRectPos()
+        this.saveData()
+        this.isScaleRect = false
+      }
+
+      // 拖拽,计算图片位置
+      if (this.isDragRect) {
+        // 计算图片左上角边距:原左上角边距 - 新增移动距离(和目标框移动方向相反)
+        this.imgLeft -= this.rectLeft - this.preRectLeft
+        this.imgTop -= this.rectTop - this.preRectTop
+        this.getRectPos()
+        this.saveData()
+        this.getCenterDistance()
+        this.isDragRect = false
+      }
+    },
+    getRectPos() {
+      // 缩放和拖拽公用。目标框居中,计算框相对容器位置
+      this.rectLeft =
+        (this.inteContainerDefaultWidthRem * this.pxEachRem - this.rectWidth) /
+        2
+      this.rectTop =
+        (this.inteContainerDefaultHeightRem * this.pxEachRem -
+          this.rectHeight) /
+        2
+    },
+    mousemove(e) {
+      const increaseRatio =
+        (this.rectPosition == 'lt' && e.movementX >= 0 && e.movementY >= 0) ||
+        (this.rectPosition == 'lb' && e.movementX >= 0 && e.movementY <= 0) ||
+        (this.rectPosition == 'rt' && e.movementX <= 0 && e.movementY >= 0) ||
+        (this.rectPosition == 'rb' && e.movementX <= 0 && e.movementY <= 0)
+      // 移动。判断到焦距最值时是否还可拖拽
+      if (
+        this.isScaleRect &&
+        (increaseRatio
+          ? this.ratio < this.lensInfo.maxZoomRatio
+          : this.ratio > this.lensInfo.minZoomRatio)
+      ) {
+        // 目标框中心点在屏幕上的坐标
+        const rectCenterX =
+          this.interAreaPosInfo.x +
+          (this.inteContainerDefaultWidthRem / 2) * this.pxEachRem
+        const rectCenterY =
+          this.interAreaPosInfo.y +
+          (this.inteContainerDefaultHeightRem / 2) * this.pxEachRem
+        // 计算缩放时目标框的宽高:abs(鼠标坐标 - 中心点坐标) * 2
+        let curRectWidth = Math.min(
+          Math.max(Math.abs(e.clientX - rectCenterX) * 2, 24),
+          this.maxRectScalewidth
+        )
+        const curRectHeight = Math.min(
+          Math.max(Math.abs(e.clientY - rectCenterY) * 2, 24),
+          this.maxRectScaleHeight
+        )
+        // 计算变焦倍率(使用长边)、图片宽高
+        this.increaseRatioFactor =
+          curRectWidth * 0.75 > curRectHeight
+            ? curRectWidth / this.rectWidth
+            : curRectHeight / this.rectHeight
+        const curRatio = (this.ratio / this.increaseRatioFactor).toFixed(1)
+        this.ratio =
+          curRatio < this.lensInfo.minZoomRatio
+            ? this.lensInfo.minZoomRatio
+            : curRatio > this.lensInfo.maxZoomRatio
+            ? this.lensInfo.maxZoomRatio
+            : curRatio
+        this.rectWidth = curRectWidth
+        this.rectHeight = curRectHeight
+        // 计算缩放时左上相对定位:目标框左上角坐标 - 容器左上角坐标
+        this.rectLeft =
+          rectCenterX - this.rectWidth / 2 - this.interAreaPosInfo.x
+        this.rectTop =
+          rectCenterY - this.rectHeight / 2 - this.interAreaPosInfo.y
+      }
+      // 拖拽
+      if (this.isDragRect) {
+        // 计算框相对图片的位置
+        const container = this.$el?.querySelector('.img_container img')
+        let newX =
+          e.clientX -
+          this.mouseRectOffsetX -
+          container.getBoundingClientRect().left
+        let newY =
+          e.clientY -
+          this.mouseRectOffsetY -
+          container.getBoundingClientRect().top
+        // 限制框在图片范围内
+        if (this.accurateFrameValid == 1) {
+          // 有精准复拍照片时,框左上角在相对图片 [0, 图片宽/高 - 框宽/高] 范围内
+          newX = Math.max(0, Math.min(this.picWidth - this.rectWidth, newX))
+          newY = Math.max(0, Math.min(this.picHeight - this.rectHeight, newY))
+        } else {
+          // 没有精准复拍照片时,框左上角在相对图片 [-0.5框宽高, 图片宽/高 - 0.5框宽/高] 范围内
+          newX = Math.max(
+            this.rectWidth * -0.5,
+            Math.min(container.clientWidth - this.rectWidth * 0.5, newX)
+          )
+          newY = Math.max(
+            this.rectHeight * -0.5,
+            Math.min(container.clientHeight - this.rectHeight * 0.5, newY)
+          )
+        }
+
+        // 设置框的新位置
+        this.rectLeft = this.imgLeft + newX
+        this.rectTop = this.imgTop + newY
+      }
+    },
+    // 交互框恢复初始状态
+    reset() {
+      Object.keys(this.initialData)?.forEach((key) => {
+        this[key] = this.initialData[key]
+      })
+      this.saveData()
+      this.initialStatus = true
+    },
+    // 保存当前动作数据到vuex,主要在鼠标抬起时触发
+    saveData() {
+      const imgPosInfo = this.$el
+        ?.querySelector('.img_container img')
+        ?.getBoundingClientRect()
+      const factor = 960 / imgPosInfo.width
+      const focusRegionWidth = factor * this.rectWidth
+      const focusRegionHeight = factor * this.rectHeight
+      // 目标框中心点离照片左上角:目标框中心点(目标框左上角 + 1/2框宽高) - 图片左上角坐标
+      const focusX =
+        factor * (this.rectLeft + this.rectWidth / 2 - this.imgLeft)
+      const focusY = factor * (this.rectTop + this.rectHeight / 2 - this.imgTop)
+
+      const newActionData = {
+        ...this.curActionData.params,
+        accurateFrameValid: this.accurateFrameValid,
+        focalLength: this.ratio * this.cameraFocalLength,
+        focusRegionHeight: focusRegionHeight.toFixed(0), // 目标框宽高、中心点到照片左上角距离是整型
+        focusRegionWidth: focusRegionWidth.toFixed(0),
+        focusX: focusX.toFixed(0),
+        focusY: focusY.toFixed(0),
+        imageHeight: 720, // 导入虚拟照片后修改,原来没值,需要加上。确认是否所有情况导出的图片宽高都是固定值?
+        imageWidth: 960,
+        targetAngle: 0,
+        gimbalRollRotateAngle: 0,
+        rectWidth: this.rectWidth,
+        rectHeight: this.rectHeight,
+        rectLeft: this.rectLeft,
+        rectTop: this.rectTop,
+        imgLeft: this.imgLeft,
+        imgTop: this.imgTop,
+        picWidth: this.picWidth,
+        picHeight: this.picHeight,
+        orientedFileSize: 0
+        // orientedFileSize: orientedFileSize, // 先不用管
+      }
+
+      Object.assign(this.curActionData, { params: newActionData })
+      this.initialStatus = false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.orient_container {
+  //height: 2.62rem;
+  width: 100%;
+  .header {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: px-to-rem(8);
+  }
+
+  .footer {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .icon {
+      cursor: pointer;
+      margin-right: px-to-rem(16);
+    }
+  }
+
+  .label {
+    opacity: 0.7;
+  }
+
+  .interactive_container {
+    position: relative;
+    touch-action: none;
+    user-select: none;
+    height: 2.14rem;
+    margin-bottom: px-to-rem(8);
+    overflow: hidden;
+    cursor: auto;
+    .img_container {
+      width: 3.5rem;
+      height: 2.14rem;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      overflow: hidden;
+      img {
+        position: absolute;
+        user-select: none;
+        -webkit-user-drag: none;
+      }
+    }
+    .target_rect {
+      width: 1.72rem;
+      height: 1.29rem;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      flex-shrink: 0;
+      border: 1px dashed #2b85e4;
+      background: rgba(28, 193, 255, 0.1);
+      cursor: move;
+      .box-left-top,
+      .box-right-bottom,
+      .box-left-bottom,
+      .box-right-top {
+        position: absolute;
+        &::before,
+        &::after {
+          content: '';
+          position: absolute;
+          background: #2b85e4;
+        }
+        &::before {
+          width: 0.12rem;
+          height: 2px;
+        }
+        &::after {
+          height: 0.12rem;
+          width: 2px;
+        }
+      }
+      .box-left-top {
+        left: -1px;
+        top: -1px;
+        cursor: nwse-resize;
+        &::before,
+        &::after {
+          left: 0;
+          top: 0;
+        }
+      }
+      .box-right-top {
+        right: -1px;
+        top: -1px;
+        cursor: nesw-resize;
+        &::before,
+        &::after {
+          right: 0;
+          top: 0;
+        }
+      }
+      .box-right-bottom {
+        right: -1px;
+        bottom: -1px;
+        cursor: nwse-resize;
+        &::before,
+        &::after {
+          right: 0;
+          bottom: 0;
+        }
+      }
+      .box-left-bottom {
+        left: -1px;
+        bottom: -1px;
+        cursor: nesw-resize;
+        &::before,
+        &::after {
+          left: 0;
+          bottom: 0;
+        }
+      }
+      .center {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        width: 0.1rem;
+        height: 0.1rem;
+        position: relative;
+        margin: 0;
+        padding: 0;
+        // border: 1px solid #ccc;
+        &::before,
+        &::after {
+          content: '';
+          display: block;
+          position: absolute;
+          background: #2b85e4;
+        }
+        &::after {
+          top: 0px;
+          left: 40%;
+          width: 20%;
+          height: 100%;
+        }
+
+        &::before {
+          width: 100%;
+          height: 20%;
+          top: 40%;
+          left: 0;
+        }
+      }
+    }
+    .gray_rect {
+      border: 1px dashed white !important;
+      .box-left-top,
+      .box-right-bottom,
+      .box-left-bottom,
+      .box-right-top,
+      .center {
+        &::before,
+        &::after {
+          content: '';
+          background: white;
+        }
+      }
+    }
+  }
+  .generate_btn {
+    height: 2.145rem;
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    .el-button--mini {
+      border-radius: 0.04rem;
+    }
+  }
+  .btn_info_line {
+    display: flex;
+    width: 100%;
+    height: 0.48rem;
+    padding: 0.08rem 0.12rem;
+    justify-content: space-between;
+    align-items: center;
+    box-sizing: border-box;
+    .left {
+      .name {
+        color: #ffffff99;
+      }
+    }
+    .right {
+      display: flex;
+      align-items: center;
+    }
+    .el-icon-refresh-right {
+      width: 0.16rem;
+      height: 0.16rem;
+      color: #e8f2fe;
+      margin-left: 0.04rem;
+      cursor: pointer;
+      &::before {
+        font-size: 0.18rem;
+      }
+    }
+  }
+}
+</style>

+ 124 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/Rename.vue

@@ -0,0 +1,124 @@
+<!-- eslint-disable vue/multi-word-component-names-->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div class="action_inner">
+    <div class="label">命名:</div>
+    <div class="input">
+      <span>{{
+        value || action.type === '1' ? 'YYYYMMDDHHMMSS' : 'YYYYMMDDHHMMSS_XXXX'
+      }}</span>
+    </div>
+    <!--     
+    <div class="icon">
+      <ct-icon name="edit" @click="open" color="#e8f3fe"></ct-icon>
+    </div>
+     -->
+  </div>
+</template>
+<script>
+import RenameFileName from './RenameFileName.vue'
+import Vue from 'vue'
+import { validateRepeatName } from '../../../dict/util.js'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+
+export default {
+  model: {
+    prop: 'value',
+    event: 'change'
+  },
+  props: {
+    value: {
+      type: String,
+      default: ''
+    },
+    action: Object
+  },
+  inject: ['flyPoints'],
+  data() {
+    return {
+      edit: false
+    }
+  },
+  methods: {
+    open() {
+      const VueComp = Vue.extend(RenameFileName)
+      this.component = new VueComp({
+        propsData: {
+          value: this.value,
+          type: +this.action.type
+        }
+      }).$mount()
+      this.component.$on('success', this.success)
+      this.component.$on('close', this.close)
+      document.body.appendChild(this.component.$el)
+    },
+    close() {
+      this.edit = false
+      document.body.removeChild(this.component.$el)
+    },
+    success(name) {
+      console.log(name, this.flyPoints)
+      const { valid, msg } = validateRepeatName(
+        this.flyPoints,
+        this.action,
+        name,
+        this.action.type === '1' ? '2' : '1'
+      )
+      if (!valid) {
+        CommonMessage.error(msg)
+        return
+      }
+      this.$emit('change', name)
+      this.close()
+    }
+  },
+  beforeDestroy() {
+    if (this.component) {
+      this.component.$destroy()
+      document.body.removeChild(this.component.$el)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.action_inner {
+  display: flex;
+  align-items: center;
+  gap: px-to-rem(8);
+  .label {
+    opacity: 0.7;
+  }
+  .input {
+    flex: 1;
+    ::v-deep input {
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+      background: transparent;
+      border-radius: px-to-rem(4);
+      height: px-to-rem(32);
+      border: px-to-rem(1) solid transparent;
+      &:hover {
+        border: px-to-rem(1) solid rgba(79, 159, 255, 0.6);
+      }
+      &:focus {
+        border: px-to-rem(1) solid rgba(79, 159, 255, 0.8);
+      }
+    }
+  }
+  .icon {
+    .ct-icon__inline-block {
+      cursor: pointer;
+      ::v-deep .ct-icon {
+        width: auto !important;
+        height: px-to-rem(20) !important;
+        line-height: px-to-rem(20);
+        .icon-ctw {
+          font-size: px-to-rem(20) !important;
+          color: #e8f3fe !important;
+        }
+      }
+    }
+  }
+}
+</style>

+ 204 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/RenameFileName.vue

@@ -0,0 +1,204 @@
+<template>
+  <div class="rename-line-box">
+    <absolute-container
+      :title="`重命名${type === 0 ? '图片' : '视频'}`"
+      :width="480"
+      @close="close"
+      canClose
+      industryClass="renameAirLineName  common-iw-s"
+    >
+      <div class="content-info">
+        <div class="top-info">
+          <div class="addItem">
+            <span class="addItem_text">YYYYMMDDHHMMSS_XXXX</span>
+          </div>
+          <div class="content">
+            <div class="input-bg">
+              <el-input
+                :placeholder="`输入${type === 0 ? '图片' : '视频'}名称`"
+                ref="inputRef"
+                size="small"
+                v-model.trim="fileName"
+                class="search-input"
+                maxlength="20"
+                :clearable="false"
+              >
+              </el-input>
+            </div>
+          </div>
+        </div>
+        <div class="botBtn">
+          <base-button type="primary" class="submitBtn" @click="submit">
+            确定
+          </base-button>
+          <base-button type="tiny" class="cancelBtn" @click="close">
+            取消
+          </base-button>
+        </div>
+      </div>
+    </absolute-container>
+  </div>
+</template>
+
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+
+export default {
+  props: {
+    value: String,
+    // 类型 0 图片 1 视频
+    type: {
+      type: Number,
+      default: 0
+    }
+  },
+  components: {
+    AbsoluteContainer,
+    BaseButton
+  },
+  data() {
+    return {
+      fileName: ''
+    }
+  },
+  methods: {
+    /**
+     * 提交
+     */
+    submit() {
+      this.$emit('success', this.fileName)
+    },
+
+    close() {
+      this.$emit('close')
+    }
+  },
+  mounted() {
+    this.fileName = this.value
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.rename-line-box {
+  width: 100vw;
+  height: 100vh;
+  z-index: 100;
+  position: fixed;
+  top: 0;
+  left: 0;
+  align-items: center;
+  justify-content: center;
+  display: flex;
+  background: rgba(0, 0, 0, 0.7);
+  .content-info {
+    width: px-to-rem(480);
+    flex-direction: column;
+    .top-info {
+      display: flex;
+      flex-direction: column;
+    }
+    .addItem {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      margin-left: px-to-rem(12);
+      margin-top: px-to-rem(10);
+      &_star {
+        color: #ed5158;
+        font-size: px-to-rem(16);
+      }
+      &_text {
+        font-family: PingFangSC, PingFang SC;
+        font-weight: 400;
+        font-size: px-to-rem(16);
+        color: #e8f3fe;
+        line-height: px-to-rem(20);
+        text-shadow: 0px 0px px-to-rem(2) rgba(74, 141, 254, 0.7);
+      }
+    }
+    .submitBtn {
+      margin-right: px-to-rem(10);
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: px-to-rem(16);
+    }
+
+    .cancelBtn {
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      background: rgba(79, 159, 255, 0.2);
+      font-size: px-to-rem(16) !important;
+    }
+  }
+  .botBtn {
+    display: flex;
+    margin-bottom: px-to-rem(12);
+    justify-content: center;
+
+    .el-button {
+      padding: 0;
+      width: px-to-rem(108);
+      height: px-to-rem(32);
+      font-size: var(--font-size);
+
+      + .el-button {
+        margin-left: px-to-rem(12);
+      }
+    }
+  }
+}
+.add-air-line-box .renameAirLineName {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+.renameAirLineName {
+  ::v-deep .el-dialog {
+    .el-dialog__header {
+      padding-left: px-to-rem(12);
+    }
+  }
+  ::v-deep .innercomp-abcontainer-header {
+    width: px-to-rem(480);
+    span {
+      font-size: px-to-rem(18);
+    }
+  }
+}
+.content {
+  padding: px-to-rem(12) px-to-rem(10);
+  .input-bg {
+    display: flex;
+    height: px-to-rem(32);
+    background: rgba(79, 159, 255, 0.2);
+    border-radius: px-to-rem(4);
+    border: px-to-rem(1) solid rgba(79, 159, 255, 0);
+  }
+  // 搜索
+  .input-bg .search-input ::v-deep .el-input__inner {
+    color: #e8f3fe;
+    font-size: px-to-rem(16);
+    text-align: left;
+    border-width: px-to-rem(1);
+    border-style: solid;
+    border-color: transparent;
+    line-height: px-to-rem(30);
+    background: transparent;
+    overflow: hidden;
+    padding: 0 px-to-rem(30) 0 px-to-rem(12);
+    &::placeholder {
+      color: rgba(232, 243, 254, 0.7);
+    }
+    &:focus {
+      border-color: rgb(79, 159, 255);
+    }
+  }
+}
+</style>

+ 48 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/Space.vue

@@ -0,0 +1,48 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="action_inner">
+    <div class="action_inner_name">{{ label }}</div>
+    <step-input
+      :min="min"
+      :max="max"
+      :step="step"
+      :value="value"
+      :suffix-text="suffixText"
+      @change="$emit('change', $event)"
+    ></step-input>
+  </div>
+</template>
+<script>
+import StepInput from '../../../baseComponents/stepInput/StepInput.vue'
+export default {
+  components: {
+    StepInput
+  },
+  props: {
+    label: {
+      type: String,
+      default: ''
+    },
+    value: {
+      type: Number,
+      default: 0
+    },
+    min: {
+      type: Number,
+      default: 0
+    },
+    max: {
+      type: Number,
+      default: 100
+    },
+    step: {
+      type: Number,
+      default: 1
+    },
+    suffixText: {
+      type: String,
+      default: ''
+    }
+  }
+}
+</script>

+ 257 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/actions/VirtualSnapshotPreview.vue

@@ -0,0 +1,257 @@
+<template>
+  <div class="action_inner">
+    <div class="header">
+      <span>虚拟快照预览</span>
+      <div>
+        <el-tooltip
+          content="航线飞行执行过飞行任务后才可以使用此功能。"
+          placement="top"
+        >
+          <el-button size="mini" type="primary">精准复拍</el-button>
+        </el-tooltip>
+      </div>
+    </div>
+    <div class="container">
+      <div class="image-content" @mousewheel="handleMouseWheel">
+        <div
+          class="frame"
+          ref="frame"
+          @mousemove="handleMouseMove"
+          @mouseup="handleMouseUp"
+        >
+          <div class="border-dash" :style="style" @mousedown="handleMouseDown">
+            <div class="border-dash-inner">
+              <div class="border top left"></div>
+              <div class="border top right"></div>
+              <div class="border bottom left"></div>
+              <div class="border bottom right"></div>
+              <div class="center"></div>
+            </div>
+          </div>
+        </div>
+        <img :src="imageUrl" @load="handleImgLoad" :style="imgStyle" />
+      </div>
+    </div>
+    <div>{{ parseTime(new Date()) }}</div>
+  </div>
+</template>
+<script>
+import { parseTime } from '@component-gallery/utils/funCommon/common'
+export default {
+  name: 'VirtualSnapshotPreview',
+  data() {
+    return {
+      // 容器宽高
+      container: {
+        _w: 0,
+        _h: 0
+      },
+      // 控制器宽高
+      control: {
+        width: 100,
+        height: 100,
+        top: 100,
+        left: 100,
+        scale: 1
+      },
+      parseTime
+    }
+  },
+  props: {
+    point: Object,
+    action: Object
+  },
+  computed: {
+    imageUrl() {
+      const { imageUrl, virtualPhotoUrl } = this.action.params
+      return imageUrl || virtualPhotoUrl
+    },
+    style() {
+      const { width, height, top, left } = this.control
+      return {
+        width: width + 'px',
+        height: height + 'px',
+        top: top + 'px',
+        left: left + 'px'
+      }
+    },
+    imgStyle() {
+      const { width, height, top, left, scale } = this.control
+      const { _w, _h } = this.container
+      let x = (_w - width) / 2 - left
+      let y = (_h - height) / 2 - top
+      return {
+        transform: `translate(${x * scale}px,${y * scale}px) `
+      }
+    }
+  },
+  methods: {
+    handleMouseWheel(e) {
+      e.preventDefault()
+      const { deltaY } = e
+      const { width, height, top, left } = this.control
+      const { _w, _h } = this.container
+      let _left, _top, _width, _height
+      if (deltaY < 0) {
+        _width = width * 1.01
+        _height = height * 1.01
+      } else {
+        _width = width * 0.99
+        _height = height * 0.99
+      }
+      if (_width < 100) {
+        return
+      } else if (_width > _w) {
+        _width = _w
+        _height = _h
+      }
+      this.control.width = _width
+      this.control.height = _height
+      this.control.scale = _width / _w
+      _left = left - (_width - width) / 2
+      _top = top - (_height - height) / 2
+      this.setLeftTop(_left, _top, this.control)
+    },
+
+    setLeftTop(_left, _top, control) {
+      const { width, height } = control
+      const { _w, _h } = this.container
+      let left, top
+      if (_left < 0) {
+        left = 0
+      } else if (_left > _w - width) {
+        left = _w - width
+      } else {
+        left = _left
+      }
+      if (_top < 0) {
+        top = 0
+      } else if (_top > _h - height) {
+        top = _h - height
+      } else {
+        top = _top
+      }
+      this.control.left = left
+      this.control.top = top
+    },
+
+    handleMouseDown(e) {
+      e.preventDefault()
+      this.start = e
+      this._control = { ...this.control }
+    },
+    handleMouseUp() {
+      this.start = null
+      this._control = null
+    },
+    handleMouseMove(e) {
+      if (this.start) {
+        const { clientX, clientY } = e
+        const { clientX: x, clientY: y } = this.start
+        const { left, top } = this._control
+        const diffX = clientX - x + left
+        const diffY = clientY - y + top
+        this.setLeftTop(diffX, diffY, this._control)
+      }
+    },
+    handleImgLoad() {
+      this.$nextTick(() => {
+        const { width, height } = this.$refs.frame.getBoundingClientRect()
+        this.control.width = width / 2
+        this.control.height = height / 2
+        this.control.top = height / 4
+        this.control.left = width / 4
+        this.container._w = width
+        this.container._h = height
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.action_inner {
+  width: 100%;
+  height: 100%;
+  .header {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    span {
+      opacity: 0.7;
+    }
+  }
+  .image-content {
+    margin: 12px 0px 6px 0px;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: relative;
+    overflow: hidden;
+    img {
+      width: 100%;
+      height: auto;
+    }
+    .frame {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      .border-dash {
+        position: absolute;
+        max-width: 100%;
+        max-height: 100%;
+        border: px-to-rem(2) dashed #00ee8b;
+        background-color: rgba(255, 255, 255, 0.1);
+        z-index: 1;
+        cursor: pointer;
+        .border-dash-inner {
+          position: relative;
+          width: 100%;
+          height: 100%;
+          .border {
+            position: absolute;
+            width: 5%;
+            height: 5%;
+            max-width: px-to-rem(30);
+            max-width: px-to-rem(30);
+          }
+          .top {
+            top: px-to-rem(-1);
+            border-top: px-to-rem(2) solid #00ee8b;
+          }
+          .bottom {
+            bottom: px-to-rem(-1);
+            border-bottom: px-to-rem(2) solid #00ee8b;
+          }
+          .left {
+            left: px-to-rem(-1);
+            border-left: px-to-rem(2) solid #00ee8b;
+          }
+          .right {
+            right: px-to-rem(-1);
+            border-right: px-to-rem(2) solid #00ee8b;
+          }
+          .center {
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            width: 0.4rem;
+            height: 0.4rem;
+            background-image: url('../../../img//airLine/point_center.png');
+            background-size: auto 100%;
+            max-width: 100%;
+            max-height: 100%;
+            background-repeat: no-repeat;
+            background-position: center;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 150 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/frustum-flypoints.js

@@ -0,0 +1,150 @@
+import { frustumInstance } from '../airLineEditPage/AirPointAirLine.vue'
+import waypointCameraHandle from '../../dict/waypoint-camera-handle'
+import { frustumView } from './AuxiliaryViewWindow.vue'
+export default class FrustumFlyPoints {
+  constructor({ cameraInfo, flyPoints, currentCamera }) {
+    this.cameraInfo = cameraInfo
+    this.currentCamera = currentCamera
+    this.flyPoints = flyPoints
+  }
+  // 根据缩放比例,获取适应当前焦距的相机信息
+  getCameraInfo(scale, colorType = 'action') {
+    const { result } = waypointCameraHandle.getMatchCameraInfo(
+      scale,
+      this.currentCamera.list
+    )
+    const cameraInfo = {
+      ...result,
+      focalLength: Math.round(
+        scale * result.realFocalLength * result.focalScaleZoom
+      ),
+      colorType
+    }
+    return cameraInfo
+  }
+  getInfo(actions, point) {
+    const params = {
+      heading: point.heading,
+      pitch: 0,
+      lng: point.lng,
+      lat: point.lat,
+      height: point.height,
+      zoomVal: this.currentCamera.cameraZoomMin || 1,
+      cameraInfo: this.cameraInfo
+    }
+    actions.forEach((item) => {
+      if (item.type === '7' && item.pointIndex === point.index) {
+        params.heading = item.rotate
+      } else if (item.type === '8' && item.rotate !== 0) {
+        params.heading = item.rotate
+      } else if (item.type === '9') {
+        params.pitch = item.rotate
+      } else if (item.type === '11') {
+        params.zoomVal = item.rotate
+        params.cameraInfo = this.getCameraInfo(item.rotate)
+      }
+    })
+    return params
+  }
+
+  // 通过历史动作,获取当前动作的显示信息
+  getPointInfo(pointIndex, point) {
+    const actions = this.flyPoints
+      .slice(0, pointIndex + 1)
+      .reduce((pre, cur) => [...pre, ...cur.actions], [])
+    return this.getInfo(actions, point)
+  }
+
+  getActionInfo(id) {
+    const point = this.flyPoints.find((item) =>
+      item.actions.some((i) => i.id === id)
+    )
+    const actions = this.flyPoints.reduce(
+      (pre, cur) => [...pre, ...cur.actions],
+      []
+    )
+    const index = actions.findIndex((item) => item.id === id)
+    return this.getInfo(actions.slice(0, index + 1), point)
+  }
+
+  // 当前point激活的视锥体显示
+  setActiveFrustum(point) {
+    const flyPoint = this.flyPoints.find((item) =>
+      item.actions.some((i) => i.id === this.activeId)
+    )
+    const { crearedIns } = frustumInstance
+    point.cameraInfo.colorType = 'action'
+    if (crearedIns.includes('active')) {
+      frustumInstance.update('active', point)
+    } else {
+      frustumInstance.addFrustum(point, point.cameraInfo, { id: 'active' })
+    }
+    Object.assign(flyPoint, point)
+    frustumView.setView(point)
+    this.updateZoomVal(point.zoomVal)
+  }
+
+  // 更新第一视角缩放比例
+  updateZoomVal(zoomVal) {
+    const cameraInfo = this.getCameraInfo(zoomVal)
+    const { firstView } = frustumView
+    const zoomFov = firstView._computedFov(cameraInfo)
+    firstView.fovRatio = zoomFov / firstView.wideFov
+    firstView.updateZoomFrame(zoomVal)
+    return cameraInfo
+  }
+  // 设置当前激活的action
+  setActiveId(activeId) {
+    if (activeId) {
+      const info = this.getActionInfo(activeId)
+      this.activeId = activeId
+      this.actionInfo = info
+      this.setActiveFrustum(info)
+    } else {
+      frustumInstance.deleteId('active')
+      this.activeId = null
+      this.actionInfo = {}
+    }
+  }
+  // 更新当前数据
+  update(action) {
+    if (this.activeId === action.id) {
+      const { heading, pitch } = this.actionInfo
+      if (['7', '8'].includes(action.type)) {
+        this.actionInfo.heading = action.rotate
+        frustumInstance.updateRotate(
+          { z: action.rotate, x: pitch, y: 0 },
+          'active'
+        )
+      } else if (action.type === '9') {
+        this.actionInfo.pitch = action.rotate
+        frustumInstance.updateRotate(
+          { z: heading, x: action.rotate, y: 0 },
+          'active'
+        )
+      } else if (action.type === '11') {
+        const cameraInfo = this.updateZoomVal(action.rotate)
+        cameraInfo.colorType = 'action'
+        this.actionInfo.cameraInfo = cameraInfo
+        frustumInstance.updateCamera('active', cameraInfo)
+      } else if (action.type === '0') {
+        if (action.position) {
+          const { heading, pitch } = action.position
+          this.actionInfo.heading = heading
+          this.actionInfo.pitch = pitch
+          frustumInstance.updateRotate('active', { z: heading, x: pitch, y: 0 })
+        } else {
+          const {
+            gimbalPitchRotateAngle: pitch,
+            gimbalYawRotateAngle: heading
+          } = action.params
+          frustumInstance.updateRotate('active', { z: heading, x: pitch, y: 0 })
+        }
+      }
+      frustumView.setView(this.actionInfo)
+    } else {
+      this.setActiveId(action.id)
+    }
+    return this.actionInfo
+  }
+}

+ 170 - 0
src/components/common-comp-uav-fly-manage/src/components/airPointAirLineComp/frustum-view.js

@@ -0,0 +1,170 @@
+import CTMapOl from '@ct/ct_map_ol'
+import dayjs from 'dayjs'
+export default class FrustumView {
+  // 初始化地图
+  constructor({ domId, point, updateFlyPoint, currentCamera, cameraInfo }) {
+    const mapRef = {
+      mapObj: null,
+      mapType: '3D',
+      domId,
+      name: 'test',
+      viewerStatus: {
+        zoom: 12,
+        center: [point.lng, point.lat],
+        tileType: 'satellite'
+      }
+    }
+    mapRef.mapInstance = CTMapOl.MapControl.common.intiMapInstance(
+      { mapRef },
+      { maxZoom: 18, minZoom: 5 }
+    )
+    this.mapRef = mapRef
+    this.point = point
+    this.updateFlyPoint = updateFlyPoint
+    this.currentCamera = currentCamera
+    this.cameraInfo = cameraInfo
+  }
+
+  // 创建视锥体第一视图
+  create({ zoomEl, irEl, switchType }) {
+    const { mapRef, currentCamera, cameraInfo } = this
+    const mapElement = mapRef.mapInstance.canvas
+    let firstView = null
+    const { lng, lat, height, heading, pitch, cameraMode } = this.point
+    const irCamera = currentCamera.list.find((item) => item.value === 'ir')
+    if (!firstView) {
+      firstView = new CTMapOl.InteractionControl.lib.FirstView(mapRef, {
+        lng,
+        lat,
+        alt: height || 500,
+        heading: heading || 0,
+        pitch: pitch || -15,
+        cameraList: currentCamera.list,
+        cameraEquivalentFocalLength:
+          cameraInfo.cameraEquivalentFocalLength || 24,
+        irCameraEquivalentFocalLength:
+          cameraInfo.cameraEquivalentFocalLength || 24,
+        irCamera: !!irCamera,
+        zoomEl,
+        irEl,
+        mapEl: mapElement
+      })
+      firstView.noZoom = false
+      firstView.bindWheelEvent(this.updateWheelEvent)
+      firstView.onZoomRatioChange(this.updateZoomVal)
+      firstView.onMapMoveChange(this.updateDirection)
+      this.firstView = firstView
+      this.setCameraMode(cameraMode || switchType || 'wide')
+    }
+  }
+
+  // 向上更新飞行点
+  update(object) {
+    this.updateFlyPoint(object)
+  }
+  // 向下更新视图
+  setView = (point) => {
+    if (point) {
+      if (!this.firstView) return console.error('firstView is null')
+      const { lng, lat, height, heading, pitch, cameraInfo } = point
+      const originObject = {
+        ...this.firstView.originObject,
+        lng,
+        lat,
+        alt: height,
+        heading: heading || 0,
+        pitch: pitch || -15,
+        cameraInfo
+      }
+      this.firstView.updateFistView(originObject)
+      this.point = point
+    }
+  }
+
+  // 地图触发更新相机
+  updateWheelEvent = (zoomInfo) => {
+    this.update({ cameraInfo: zoomInfo.cameraParams })
+  }
+
+  // 地图触发的更新方向角
+  updateDirection = (e) => {
+    if (e === 'MouseMove') this.isMove = true
+    if (e === 'moveEnd') {
+      if (this.isMove) {
+        this.isMove = false
+      } else {
+        return
+      }
+    }
+    const { firstView } = this
+    const heading = (firstView.viewer.camera.heading * 180) / Math.PI
+    const pitch = (firstView.viewer.camera.pitch * 180) / Math.PI
+    let newHeading
+    if (heading > 180) {
+      newHeading = -(180 - (heading - 180))
+    } else {
+      newHeading = heading
+    }
+    firstView.saveCurrentDirection()
+    this.update({ heading: newHeading, pitch, distance: firstView.distance })
+  }
+
+  // 地图触发更新缩放
+  updateZoomVal = (zoomVal) => {
+    this.update({ zoomVal })
+  }
+
+  // 向下更新相机模式
+  setCameraMode = (cameraMode) => {
+    const { firstView } = this
+    firstView.switchVisualAngle(cameraMode)
+    // this.cameraMode = cameraMode
+    // if (cameraMode === 'ir') {
+    //   firstView.noZoom = true
+    //   firstView.destroyWheelEvent()
+    //   firstView.offZoomRatioChange(this.updateZoomVal)
+    // } else {
+    //   firstView.noZoom = false
+    //   firstView.bindWheelEvent(this.updateWheelEvent)
+    //   firstView.onZoomRatioChange(this.updateZoomVal)
+    // }
+  }
+
+  // 拍照
+  takePicture = () => {
+    return new Promise((resolve) => {
+      const { zoomRatio, viewer } = this.firstView
+      let heading = (viewer.camera.heading * 180) / Math.PI
+      let pitch = (viewer.camera.pitch * 180) / Math.PI
+
+      let newHeading = heading
+      if (heading > 180) {
+        newHeading = -(180 - (heading - 180))
+      } else {
+        newHeading = heading
+      }
+      this.firstView.takePicture(async (e) => {
+        resolve({
+          virtualPhotoBlob: e,
+          virtualPhotoUrl: URL.createObjectURL(e),
+          zoomRatio,
+          focalLength: zoomRatio * this.cameraInfo.cameraEquivalentFocalLength,
+          aircraftHeading: newHeading,
+          gimbalPitchRotateAngle: pitch,
+          gimbalYawRotateAngle: newHeading,
+          orientedCameraType: this.payloadEnumValue,
+          fixedHeadingPitch: {
+            heading: newHeading,
+            pitch,
+            zoomRatio
+          },
+          AFPos: 0,
+          gimbalPort: 0,
+          orientedFileSize: 0,
+          cameraInfo: this.point.cameraInfo,
+          dateTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
+        })
+      })
+    })
+  }
+}

+ 104 - 0
src/components/common-comp-uav-fly-manage/src/components/checkboxs/CheckboxInterval.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-checkbox-group v-model="list" @change="handleMonthsChange">
+    <el-checkbox
+      class="item-width"
+      v-for="item in options"
+      :label="item.value"
+      :key="item.value"
+      >{{ item.label }}</el-checkbox
+    >
+  </el-checkbox-group>
+</template>
+
+<script>
+export default {
+  name: 'checkboxInterval',
+  data() {
+    return {
+      list: []
+    }
+  },
+  watch: {
+    value: {
+      handler(val) {
+        this.list = val
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    },
+    options: {
+      type: Array,
+      default: () => []
+    }
+  },
+  methods: {
+    handleMonthsChange(val) {
+      this.$emit('update:value', val)
+      this.$emit('change', val)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.item-width {
+  width: 20%;
+  height: px-to-rem(32);
+  line-height: px-to-rem(32);
+}
+::v-deep .el-checkbox {
+  color: #e8f2fe;
+  font-size: px-to-rem(16);
+
+  .el-checkbox__input.is-checked + .el-checkbox__label {
+    color: #e8f2fe;
+  }
+
+  .el-checkbox__inner {
+    border-color: #1373e6;
+    background: none;
+    border-radius: px-to-rem(3) !important;
+    width: px-to-rem(16);
+    height: px-to-rem(16);
+
+    &::after {
+      border: px-to-rem(2) solid #fff;
+      height: px-to-rem(7);
+      left: px-to-rem(5);
+      top: px-to-rem(2);
+      width: px-to-rem(3);
+      box-sizing: content-box;
+      content: '';
+      border-left: 0;
+      border-top: 0;
+      position: absolute;
+      transition: transform 0.15s ease-in 0.05s;
+      transform-origin: center;
+    }
+  }
+
+  .is-checked .el-checkbox__inner {
+    border-color: #1373e6;
+    background: #1373e6;
+  }
+
+  .el-checkbox__input.is-indeterminate .el-checkbox__inner {
+    background: #1373e6;
+    border-color: #1373e6;
+
+    &::before {
+      top: px-to-rem(5);
+      height: px-to-rem(4);
+    }
+  }
+}
+</style>

+ 532 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/DialogFlyAlgorithm.vue

@@ -0,0 +1,532 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 算法配置
+ -->
+<template>
+  <div>
+    <styled-dialog
+      industryClass="common-iw-s fly-edit-algorithm-dialog"
+      :visible="dialogShow"
+      title="算法配置"
+      @close="close"
+      :modal="true"
+      :canClose="true"
+      :appendToBody="true"
+    >
+      <div class="fly-edit-algorithm-dialog-body">
+        <el-form ref="formRef" :model="formDataComputed">
+          <div class="fly-edit-algorithm">
+            <el-form-item>
+              <template v-slot:label>
+                <span><span class="required">*</span>选择算法</span>
+              </template>
+              <div class="el-select el-select&#45;&#45;mini">
+                <div class="el-select__tags" ref="tags">
+                  <span v-if="selected.length">
+                    <el-tag
+                      closable
+                      size="mini"
+                      type="info"
+                      @close="deleteTag(selected[0])"
+                      disable-transitions
+                      class="first-tag"
+                    >
+                      <span class="el-select__tags-text"
+                        >{{ selected[0].typeName }}
+                      </span>
+                    </el-tag>
+                    <el-tag
+                      v-if="selected.length > 1"
+                      :closable="false"
+                      size="mini"
+                      type="info"
+                      disable-transitions
+                    >
+                      <span class="el-select__tags-text"
+                        >+ {{ selected.length - 1 }}</span
+                      >
+                    </el-tag>
+                  </span>
+                </div>
+                <el-input
+                  v-model="searchValue"
+                  ref="elInput"
+                  @clear="clearInput"
+                  :placeholder="placeholder"
+                >
+                  <template v-slot:suffix>
+                    <i class="el-input__icon el-icon-search"></i>
+                  </template>
+                </el-input>
+              </div>
+              <div class="required__error" v-if="algorithmListError">
+                <i class="el-icon-warning"></i>
+                请选择算法
+              </div>
+              <el-scrollbar
+                v-loading="loading"
+                class="fly-edit-algorithm-options"
+              >
+                <div
+                  class="algorithm-checkbox"
+                  v-if="this.searchValue === '' && dictListComputed.length > 0"
+                >
+                  <el-checkbox
+                    label="全选"
+                    :value="selectAllCheckedComputed"
+                    :indeterminate="
+                      formDataComputed.algorithmList.length > 0 &&
+                      formDataComputed.algorithmList.length <
+                        dictListComputed.length
+                    "
+                    @change="selectAll"
+                  >
+                    <div class="algorithm-name">全选</div>
+                  </el-checkbox>
+                </div>
+                <el-checkbox-group v-model="formDataComputed.algorithmList">
+                  <div
+                    v-for="item in dictListComputed"
+                    :key="item.typeValue"
+                    class="algorithm-checkbox"
+                  >
+                    <el-checkbox :label="item.typeValue">
+                      <div class="algorithm-name">{{ item.typeName }}</div>
+                    </el-checkbox>
+                  </div>
+                </el-checkbox-group>
+
+                <div v-if="dictListComputed.length === 0" class="loading-datas">
+                  <span class="empty-text">暂无数据</span>
+                </div>
+              </el-scrollbar>
+            </el-form-item>
+          </div>
+        </el-form>
+        <el-row type="flex" class="btns-container" justify="center">
+          <base-button
+            type="primary"
+            :heightStyle="32"
+            :width="108"
+            @click="handleSubmit()"
+          >
+            确定
+          </base-button>
+          <base-button :heightStyle="32" :width="108" @click="close">
+            取消
+          </base-button>
+        </el-row>
+      </div>
+    </styled-dialog>
+  </div>
+</template>
+<script>
+import StyledDialog from '@component-gallery/base-components/styled-dialog/StyledDialog.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import { queryAlgorithmList, uavSynAnalysisTask } from '../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { requestSDK } from '@ct/iframe-connect-sdk'
+
+export default {
+  components: {
+    StyledDialog,
+    BaseButton
+  },
+  props: {
+    dialogShow: {
+      type: Boolean,
+      default: true
+    },
+    dialogType: {
+      type: String,
+      default: 'img'
+    },
+    formData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      searchValue: '',
+      dictList: [],
+      algorithmListError: false,
+      loading: false,
+      timer: null,
+      user: null
+    }
+  },
+  computed: {
+    formDataComputed() {
+      return this.formData
+    },
+    /**
+     * 选择数据
+     * @returns {*[]}
+     */
+    selected() {
+      const _selected = this.dictList.filter((item) =>
+        this.formDataComputed.algorithmList.includes(item.typeValue)
+      )
+
+      return _selected || []
+    },
+    /**
+     * 全选按钮是否选中
+     * @returns {boolean}
+     */
+    selectAllCheckedComputed() {
+      if (this.dictListComputed.length === 0) {
+        return false
+      }
+      return (
+        this.formDataComputed.algorithmList.length ===
+        this.dictListComputed.length
+      )
+    },
+    /**
+     * 过滤查询数据
+     * @returns {*}
+     */
+    dictListComputed() {
+      return this.fuzzySearch(this.dictList, 'typeName', this.searchValue) || []
+    },
+    placeholder() {
+      if (
+        this.formDataComputed.algorithmList?.length ||
+        this.searchValue !== ''
+      ) {
+        return ''
+      }
+      return '请选择'
+    }
+  },
+  watch: {
+    selected: {
+      handler: function () {
+        //通过计算设置输入框的paddingLeft
+        this.timer = setTimeout(() => {
+          const textLength = this.selected[0]?.typeName?.length ?? 0
+          console.log('textLength', textLength)
+          let _width = 12
+          if (textLength === 2) {
+            _width = 12
+          }
+          if (textLength > 4) {
+            _width = 22
+          }
+          if (this.selected?.length > 1) {
+            _width = 24
+          }
+          const width = this.$refs.tags?.getBoundingClientRect()?.width
+          this.$refs.elInput.$el.children[0].style.paddingLeft = this.pxToRem(
+            width <= 12 ? 12 : width + _width
+          )
+          clearTimeout(this.timer)
+          this.timer = null
+          this.searchValue = ''
+        }, 0)
+      }
+    }
+  },
+  async mounted() {
+    await this.getInfo()
+    this.queryAlgorithmListApi()
+  },
+  methods: {
+    async getInfo() {
+      const { user } = await requestSDK('getInfo')
+      this.user = user
+    },
+    pxToRem(px) {
+      return `${px / 100}rem`
+    },
+    /**
+     * 删除标签
+     * @param tag
+     */
+    deleteTag(tag) {
+      this.formDataComputed.algorithmList =
+        this.formDataComputed.algorithmList.filter(
+          (item) => item !== tag.typeValue
+        )
+    },
+    /**
+     * 查询可选数据
+     */
+    async queryAlgorithmListApi() {
+      const params = {
+        alarmSource: '12'
+      }
+      this.loading = true
+      queryAlgorithmList(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.dictList = res.data
+          }
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    /**
+     * 提交数据
+     */
+    handleSubmit() {
+      this.algorithmListError = this.formDataComputed.algorithmList.length === 0
+      if (this.algorithmListError) {
+        return
+      }
+      this.dialogAlgorithmHandleSubmit()
+    },
+    /**
+     * 算法接口数据组装
+     */
+    dialogAlgorithmHandleSubmit() {
+      let params = []
+      const { paramsLists, algorithmList, type, deviceCode, flightRouteId } =
+        this.formDataComputed
+      const key = type === 'image' ? 'imgUrls' : 'videoUrls'
+      const idKey = type === 'image' ? 'picId' : 'videoId'
+      for (let i = 0; i < paramsLists.length; i++) {
+        for (let j = 0; j < algorithmList.length; j++) {
+          let data = {
+            fileInfo: {
+              [key]:
+                type === 'image'
+                  ? paramsLists[i]?.photoAccessUrl
+                  : paramsLists[i]?.videoAccessUrl,
+              type: type
+            },
+            algorithmCode: algorithmList[j],
+            extraFieldInfo: {
+              deviceCode: deviceCode,
+              airTaskId: paramsLists[i]?.taskRecordId,
+              airLineId: flightRouteId,
+              trailTime: paramsLists[i]?.createTime,
+              [idKey]: paramsLists[i].id
+            }
+          }
+          if (type === 'image') {
+            delete data.extraFieldInfo.trailTime
+          }
+          params.push(data)
+        }
+      }
+      this.uavSynAnalysisTaskApi(params)
+    },
+    /**
+     * 调用算法配置接口
+     * @param params
+     */
+    uavSynAnalysisTaskApi(params) {
+      this.loading = true
+      uavSynAnalysisTask(params)
+        .then((res) => {
+          this.$emit('success')
+          this.close()
+          CommonMessage.success(res.msg)
+        })
+        .catch((err) => {
+          CommonMessage.error(err?.msg || '保存失败')
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    /**
+     * 关闭弹窗
+     */
+    close() {
+      this.$emit('update:dialogShow', false)
+    },
+    /**
+     * 模糊搜索
+     * @param arr
+     * @param key
+     * @param value
+     * @returns {*}
+     */
+    fuzzySearch(arr, key, value) {
+      const regex = new RegExp(value, 'i')
+      return arr.filter((obj) => regex.test(obj[key]))
+    },
+    clearInput() {
+      this.formDataComputed.algorithmList = []
+    },
+    selectAll(val) {
+      if (val) {
+        this.formDataComputed.algorithmList = this.dictListComputed.map(
+          (item) => {
+            return item.typeValue
+          }
+        )
+      } else {
+        this.formDataComputed.algorithmList = []
+      }
+    }
+  },
+  beforeDestroy() {
+    clearTimeout(this.timer)
+    this.timer = null
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$form_item_h: px-to-rem(32);
+$form_item_label_w: px-to-rem(90);
+
+.fly-edit-algorithm-dialog-body {
+  padding: px-to-rem(12) px-to-rem(12) 0 px-to-rem(12);
+  .fly-edit-algorithm {
+    ::v-deep .el-form-item {
+      font-size: px-to-rem(14);
+      margin-bottom: px-to-rem(12);
+      &:not(:first-child) {
+        margin-left: px-to-rem(12);
+      }
+      .el-form-item__label {
+        width: $form_item_label_w;
+        height: $form_item_h;
+        font-weight: 400;
+        font-size: px-to-rem(16);
+        color: #e8f3fe;
+      }
+      .el-form-item__content {
+        margin-left: $form_item_label_w;
+        padding: 0;
+        .el-select {
+          position: relative;
+          width: 100%;
+          .el-select__tags {
+            position: absolute;
+          }
+        }
+        .el-input {
+          height: $form_item_h;
+          line-height: $form_item_h;
+          .el-input__inner {
+            height: $form_item_h;
+            line-height: $form_item_h;
+            font-size: px-to-rem(16);
+          }
+        }
+      }
+      .required {
+        color: #ed5158;
+        padding-right: px-to-rem(2);
+      }
+      .required__error {
+        padding-left: px-to-rem(12);
+        height: px-to-rem(32);
+        background: rgb(237 81 88 / 20%);
+        border-radius: px-to-rem(4);
+        color: #ed5158;
+        font-size: px-to-rem(14);
+        line-height: px-to-rem(32);
+        font-weight: 400;
+
+        .el-icon-warning {
+          margin-right: px-to-rem(6);
+        }
+      }
+    }
+    ::v-deep .el-select__tags {
+      pointer-events: none;
+      .first-tag.el-tag {
+        margin-left: px-to-rem(12);
+        min-width: px-to-rem(44);
+        max-width: px-to-rem(100);
+      }
+      .el-tag {
+        pointer-events: auto;
+        .el-select__tags-text {
+          line-height: 1;
+          font-size: px-to-rem(14);
+          padding: 0 px-to-rem(7);
+        }
+      }
+    }
+  }
+  .fly-edit-algorithm-options {
+    height: px-to-rem(384);
+    margin-top: px-to-rem(12);
+    background: #0f1926;
+    padding-left: px-to-rem(12);
+    padding-right: px-to-rem(12);
+    border-radius: px-to-rem(4);
+    ::v-deep .el-scrollbar__wrap {
+      margin-top: px-to-rem(4);
+    }
+    ::v-deep .el-checkbox {
+      display: block;
+      .el-checkbox__input {
+        padding-bottom: px-to-rem(1);
+      }
+      .el-checkbox__input.is-checked + .el-checkbox__label {
+        color: #4f9fff;
+      }
+      .el-checkbox__label {
+        display: inline-table;
+      }
+      .algorithm-name {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        width: px-to-rem(330);
+        font-size: px-to-rem(16);
+      }
+    }
+    .algorithm-checkbox {
+      height: px-to-rem(32);
+      display: flex;
+      align-items: center;
+    }
+    .loading-datas {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 100%;
+      flex-direction: column;
+      font-size: px-to-rem(14);
+      margin-top: px-to-rem(120);
+      color: #ffffff;
+      .empty-text {
+        pointer-events: none;
+        color: #e8f3fe;
+        font-size: px-to-rem(16);
+        text-align: center;
+
+        &:before {
+          content: url('~@component-gallery/assets/image/ar-label/noData.png');
+          display: block;
+          margin-bottom: px-to-rem(-18);
+        }
+      }
+    }
+  }
+}
+
+.btns-container {
+  gap: px-to-rem(12);
+  padding: 0 0 px-to-rem(12) 0;
+  .base-button {
+    font-size: px-to-rem(16);
+  }
+}
+.fly-edit-algorithm-dialog {
+  background: rgba(0, 0, 0, 0.2);
+  ::v-deep .el-dialog {
+    width: px-to-rem(500) !important;
+    margin-top: px-to-rem(278) !important;
+    .innercomp-styleddialog-header {
+      font-size: px-to-rem(18);
+    }
+  }
+}
+</style>

+ 194 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/DialogFlyEditName.vue

@@ -0,0 +1,194 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 修改用户名称
+ -->
+<template>
+  <div>
+    <styled-dialog
+      industryClass="common-iw-s fly-edit-name-dialog"
+      :visible="dialogShow"
+      :title="configData.dialogTitle"
+      @close="close"
+      :modal="true"
+      :canClose="true"
+      :appendToBody="true"
+    >
+      <div class="fly-edit-name-dialog-body">
+        <el-form ref="formRef" :model="formDataObj">
+          <div class="fly-edit-name-row">
+            <el-form-item
+              :label="configData.name"
+              prop="name"
+              :rules="[
+                { required: true, message: configData.nameError },
+                {
+                  min: 0,
+                  max: 30,
+                  message: '最多支持输入30个字',
+                  trigger: 'blur'
+                }
+              ]"
+            >
+              <el-input v-model="formDataObj.name"></el-input>
+            </el-form-item>
+          </div>
+        </el-form>
+        <el-row type="flex" class="btns-container" justify="center">
+          <base-button
+            type="primary"
+            :heightStyle="32"
+            :width="108"
+            @click="handleSubmit()"
+          >
+            确定
+          </base-button>
+          <base-button :heightStyle="32" :width="108" @click="close">
+            取消
+          </base-button>
+        </el-row>
+      </div>
+    </styled-dialog>
+  </div>
+</template>
+<script>
+import StyledDialog from '@component-gallery/base-components/styled-dialog/StyledDialog.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+
+export default {
+  components: {
+    StyledDialog,
+    BaseButton
+  },
+  props: {
+    dialogShow: {
+      type: Boolean,
+      default: true
+    },
+    dialogType: {
+      type: String,
+      default: 'img'
+    },
+    formData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      formDataObj: {
+        name: ''
+      }
+    }
+  },
+  computed: {
+    configData() {
+      const config = {
+        img: {
+          dialogTitle: '编辑图片名称',
+          name: '图片名称',
+          nameError: '请输入图片名称'
+        },
+        video: {
+          dialogTitle: '编辑视频名称',
+          name: '视频名称',
+          nameError: '请输入视频名称'
+        }
+      }
+      return config[this.dialogType]
+    }
+  },
+  mounted() {
+    this.initData()
+  },
+  methods: {
+    initData() {
+      const dotIndex = this.formData.name.lastIndexOf('.')
+      const suffix =
+        dotIndex !== -1 ? this.formData.name.substring(dotIndex + 1) : ''
+      const name =
+        dotIndex !== -1
+          ? this.formData.name.substring(0, dotIndex)
+          : this.formData.name
+      this.formDataObj = { ...this.formData, suffix, name }
+    },
+    handleSubmit() {
+      this.$refs.formRef.validate((valid) => {
+        if (valid) {
+          const name = this.formDataObj.name + '.' + this.formDataObj.suffix
+          this.$emit('dialogHandleSubmit', { ...this.formDataObj, name })
+        }
+      })
+    },
+    close() {
+      this.$emit('update:dialogShow', false)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$form_item_h: px-to-rem(32);
+$form_item_label_w: px-to-rem(90);
+$form_item_label_m_r: px-to-rem(12);
+
+.fly-edit-name-dialog-body {
+  padding: px-to-rem(12) px-to-rem(12) 0 px-to-rem(12);
+  .fly-edit-name-row {
+    ::v-deep .el-form-item {
+      margin-bottom: 0;
+      &:not(:first-child) {
+        margin-left: px-to-rem(12);
+      }
+      .el-form-item__label {
+        width: $form_item_label_w;
+        height: $form_item_h;
+        /*  margin-right: $form_item_label_m_r;*/
+        font-weight: 400;
+        color: #e8f3fe;
+        font-size: px-to-rem(16);
+      }
+      .el-form-item__content {
+        margin-left: $form_item_label_w;
+        padding: 0;
+        .el-input {
+          height: $form_item_h;
+          line-height: $form_item_h;
+          .el-input__inner {
+            height: $form_item_h;
+            line-height: $form_item_h;
+            font-size: px-to-rem(16);
+          }
+        }
+        .el-form-item__error {
+          position: initial;
+          padding: 0;
+          font-size: px-to-rem(14);
+          &::before {
+            position: initial;
+          }
+        }
+      }
+    }
+  }
+}
+.btns-container {
+  gap: px-to-rem(12);
+  padding: px-to-rem(18) 0 px-to-rem(12) 0;
+  .base-button {
+    font-size: px-to-rem(16);
+  }
+}
+.fly-edit-name-dialog {
+  ::v-deep .el-dialog {
+    width: px-to-rem(500) !important;
+    margin-top: px-to-rem(400) !important;
+    .innercomp-styleddialog-header {
+      font-size: px-to-rem(18);
+    }
+  }
+}
+</style>

+ 587 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/DialogFlyMap.vue

@@ -0,0 +1,587 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/26
+ * @Description: 地图中查看弹窗
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <absolute-container
+    industryClass="common-iw-s fly-map-dialog"
+    :right="24"
+    :top="616"
+    :width="570"
+    :title="nowDataComputed.name"
+    @close="close"
+    v-drag
+  >
+    <div class="fly-map-dialog-body">
+      <span
+        v-if="showImageViewer"
+        class="close_btn"
+        @click="handleImageViewerClose"
+      ></span>
+      <medium-viewer
+        class="medium-viewer"
+        :urlList="dataListChildren.map((item) => item.url)"
+        :initial-index.sync="fileIndex"
+        :mediumViewerFull="showImageViewer"
+      >
+        <el-image
+          v-if="tabTypeComputed === 'img'"
+          class="medium"
+          :class="[showImageViewer && 'medium-full']"
+          :src="showDataUrl"
+          fit=""
+        ></el-image>
+        <video-player
+          v-if="tabTypeComputed === 'video'"
+          class="video_res_player"
+          :class="[showImageViewer && 'video_res_player_full']"
+          :playsinline="true"
+          :options="playerOptionsComputed"
+          @loadeddata="onLoadedData($event)"
+          @playing="onPlayerPlayIng($event)"
+          @pause="onPlayerPause($event)"
+          @timeupdate="onPlayerTimeupdate($event)"
+        />
+      </medium-viewer>
+      <fly-attribute
+        :isOpen="activeImgAttStatus"
+        :bottom="tabTypeComputed === 'img' ? 24 : 48"
+        :left="showImageViewer ? 24 : 18"
+        :attribute="nowDataComputed"
+        @isHidden="isHiddenImgAttStatus"
+        :attributeList="attributeListComputed"
+      />
+      <div
+        v-if="!showImageViewer"
+        class="bottom"
+        :class="[tabTypeComputed === 'video' && 'bottom-video']"
+      >
+        <div
+          class="bottom-item"
+          v-for="item in flyMapOperateComputed"
+          :key="item.code"
+          @click="bottomItemClick(item)"
+          :class="[item.code === 'download' && downloadIng && 'is-disabled']"
+        >
+          <ct-icon
+            :c-tip="item.name"
+            :name="item.icon"
+            class="bottom-item-icon"
+          ></ct-icon>
+        </div>
+      </div>
+    </div>
+  </absolute-container>
+</template>
+<script>
+import { videoPlayer } from 'vue-video-player'
+import uavFly from '../../img/icon_uav_point@2x.png'
+import CTMapOl from '@ct/ct_map_ol'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import screenfull from 'screenfull'
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import MediumViewer from './MediumViewer.vue'
+import FlyAttribute from './FlyAttribute.vue'
+import { flyMapOperate } from '../../entry/data'
+import 'video.js/dist/video-js.css'
+import 'vue-video-player/src/custom-theme.css'
+
+import { queryHisRecordByVideoTime } from '../../service'
+import { deleteData, downloadByUrlsApi } from '../../dict/fly-result-methods'
+import { dragBind } from '@component-gallery/utils/funCommon/common'
+
+let uavFlyInstance = null //飞行地图实例
+let mapFlyDataTrack = null //飞行轨迹数据
+let flyDetailData = null
+export default {
+  inject: ['mapRef'],
+  components: {
+    MediumViewer,
+    FlyAttribute,
+    AbsoluteContainer,
+    videoPlayer
+  },
+  props: {
+    dialogShow: {
+      type: Boolean,
+      default: true
+    },
+    dataListChildren: {
+      type: Array
+    },
+
+    activeImgAttStatus: {
+      type: Boolean
+    },
+    mapId: {
+      type: String
+    },
+    selectData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  computed: {
+    attributeListComputed() {
+      return [
+        {
+          label: this.tabTypeComputed === 'img' ? '图片名称' : '视频名称',
+          key: 'name'
+        },
+        {
+          label: '飞行任务',
+          key: 'planTaskName'
+        },
+        {
+          label: '无人机',
+          key: 'flyName'
+        },
+        {
+          label: '拍摄时间',
+          key: 'createTime'
+        }
+      ]
+    },
+    nowDataComputed() {
+      return {
+        flyName: this.treeNodeComputed.name,
+        planTaskName: this.taskListDataComputed.planTaskName,
+        ...this.dataListChildren[this.fileIndex]
+      }
+    },
+    flyMapOperateComputed() {
+      return flyMapOperate
+    },
+    showDataUrl() {
+      return this.dataListChildren[this.fileIndex].url
+    },
+    /**
+     * 选中任务列表数据
+     * @returns {*}
+     */
+    taskListDataComputed() {
+      return { ...this.selectData.listData }
+    },
+    /**
+     * 选中树节点数据
+     * @returns {*}
+     */
+    treeNodeComputed() {
+      return {
+        ...this.selectData.treeNode
+      }
+    },
+    tabTypeComputed() {
+      return this.selectData.type
+    },
+    playerOptionsComputed() {
+      return {
+        playbackRates: [], //播放速度
+        autoplay: true, //如果true,浏览器准备好时开始回放。
+        loop: false, // 导致视频一结束就重新开始。
+        muted: true, // 默认情况下将会消除任何音频。
+        preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
+        language: 'zh-CN',
+        fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
+        hls: false,
+        sources: [
+          {
+            type: 'video/mp4',
+            src: this.showDataUrl || '' // url地址
+          }
+        ],
+        aspectRatio: '16:9',
+        poster: '', // 封面地址
+        choosed: false, //被选中的
+        notSupportedMessage: '未上传视频!', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
+        controlBar: {
+          timeDivider: false,
+          durationDisplay: false,
+          remainingTimeDisplay: false,
+          fullscreenToggle: false //全屏按钮
+        }
+      }
+    }
+  },
+  directives: {
+    drag: {
+      bind: (el, binding) => {
+        dragBind(el)
+      }
+    }
+  },
+  data() {
+    return {
+      fileIndex: 0,
+      duration: 0,
+      timerCount: 0,
+      showImageViewer: false, //是否全屏展示
+      count: 8,
+      downloadIng: false
+    }
+  },
+  // mounted() {},
+  methods: {
+    /**
+     * 视频播放器开始播放
+     */
+    onPlayerPlayIng() {
+      this.timerCount = 0
+      uavFlyInstance?.play()
+    },
+    /**
+     * 视频播放器暂停
+     * @param player
+     */
+    onPlayerPause(player) {
+      uavFlyInstance?.pause()
+    },
+    /**
+     * 播放器时间更新回调 视频Timeupdate250ms 更新一次,无人机拍摄视频时经纬度 2s上报一个 因此Timeupdate执行 8 次无人机飞行一个点
+     * @param player
+     */
+    onPlayerTimeupdate(player) {
+      this.count--
+      if (this.count <= 0) {
+        this.count = 8
+        if (flyDetailData && player?.cache_?.currentTime) {
+          this.timerCount = parseInt(player.cache_.currentTime)
+          if (flyDetailData.length - 1 >= this.timerCount) {
+            let nowAllData = flyDetailData[this.timerCount]
+            let playTime = new Date(nowAllData?.saveDateTime)?.getTime()
+            playTime && uavFlyInstance?.setTime(playTime)
+          }
+        }
+      }
+    },
+    /**
+     * 视频数据加载
+     * @param player
+     */
+    onLoadedData(player) {
+      this.duration = player.cache_.duration
+      this.getFlyDetailDataApi()
+    },
+    /**
+     * 获取视频轨迹数据
+     */
+    getFlyDetailDataApi() {
+      this.clearFly()
+      let param = {
+        taskRecordId: this.nowDataComputed.taskRecordId,
+        videoStartTime: this.nowDataComputed.createTime,
+        duration: this.duration,
+        accessNode: this.treeNodeComputed.accessNode
+      }
+      queryHisRecordByVideoTime(param)
+        .then((res) => {
+          if (res.code === 200) {
+            console.log('飞行轨迹数据', res)
+            if (res.data?.length <= 1) {
+              return
+            }
+            flyDetailData = res.data
+            mapFlyDataTrack = res.data.map((item) => {
+              return {
+                longitude: item.aircraftLocation.longitude,
+                latitude: item.aircraftLocation.latitude,
+                height: item.aircraftLocation.altitude,
+                timestamp: new Date(item?.saveDateTime)?.getTime()
+              }
+            })
+            this.initFlyInstance()
+          }
+        })
+        .catch((e) => {
+          console.log('========>', e)
+          CommonMessage.error(e?.msg)
+        })
+    },
+    /**
+     * 初始化地图实例
+     */
+    initFlyInstance() {
+      if (mapFlyDataTrack?.length <= 1) {
+        return
+      }
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      uavFlyInstance?.destroy()
+      uavFlyInstance = null
+      let style = {
+        strokeColor: '#4F9FFF',
+        strokeWidth: 2,
+        strokeOpacity: 1,
+        dashLineOptions: undefined
+      }
+      uavFlyInstance = new CTMapOl.DataSourceControl.lib.TrackPlayBack({
+        mapRef: mapRef,
+        data: mapFlyDataTrack,
+        duration: this.duration * 1000,
+        trackedEntity: false,
+        circleRun: false,
+        autoRotation: true,
+        styles: {
+          normal: {
+            src: uavFly,
+            width: 33,
+            height: 31
+          },
+          lineBefore: {
+            ...style,
+            strokeColor: 'rgba(255, 149, 0, 0.7)'
+          },
+          lineAfter: style
+        }
+      })
+    },
+    /**
+     * 关闭图片预览
+     */
+    isHiddenImgAttStatus() {
+      this.$emit('update:activeImgAttStatus', false)
+    },
+    /**
+     * 点击下面操作按钮
+     * @param val
+     */
+    bottomItemClick(val) {
+      const fnObject = {
+        download: this.downloadHandler,
+        del: this.delHandler,
+        full: this.fullHandler
+      }
+      fnObject[val.code] && fnObject[val.code](val)
+    },
+    /**
+     * 下载
+     */
+    async downloadHandler() {
+      if (this.downloadIng) {
+        return
+      }
+      this.downloadIng = true
+      let params = {}
+      if (this.tabTypeComputed === 'img') {
+        params = {
+          photoUrls: [this.nowDataComputed.photoAccessUrl],
+          fileName: this.nowDataComputed.name
+        }
+      } else {
+        params = {
+          videoUrls: [this.nowDataComputed.videoAccessUrl],
+          fileName: this.nowDataComputed.name
+        }
+      }
+
+      await downloadByUrlsApi(params)
+      this.downloadIng = false
+    },
+    /**
+     * 删除
+     */
+    async delHandler() {
+      let params = {
+        id: [this.nowDataComputed.id],
+        dataType: this.tabTypeComputed === 'img' ? 'photo' : 'video',
+        taskRecordId: this.nowDataComputed.taskRecordId,
+        accessNode: this.treeNodeComputed.accessNode
+      }
+      const res = await deleteData(
+        params,
+        `是否要删除该${this.tabTypeComputed === 'img' ? '图片' : '视频'}?`
+      )
+      if (res.code !== 200) {
+        return
+      }
+      this.$emit('del-dialog-data', this.nowDataComputed)
+      //删除最后一张索引-1操作
+      if (this.dataListChildren.length - 1 === this.fileIndex) {
+        this.fileIndex = this.fileIndex - 1
+      }
+      CommonMessage.success('删除成功')
+    },
+    /**
+     * 全屏
+     */
+    fullHandler() {
+      if (!screenfull.enabled) {
+        CommonMessage.warning('浏览器不支持该功能')
+        return false
+      }
+      this.$nextTick(() => {
+        let el = document.querySelector('.fly-map-dialog-body')
+        el.requestFullscreen()
+        screenfull.on('change', this.setShowImageViewer)
+      })
+    },
+    setShowImageViewer() {
+      this.showImageViewer = screenfull.isFullscreen
+    },
+    /**
+     * 清除地图实例及数据定时器
+     */
+    clearFly() {
+      uavFlyInstance?.destroy()
+      uavFlyInstance = null
+      mapFlyDataTrack = null
+      flyDetailData = null
+    },
+    /**
+     * 关闭图片预览
+     */
+    handleImageViewerClose() {
+      document.exitFullscreen()
+    },
+    /**
+     * 关闭弹窗
+     */
+    close() {
+      this.$emit('close-dialog-fly-map')
+    }
+  },
+  beforeDestroy() {
+    this.clearFly()
+    screenfull.off('change', this.setShowImageViewer)
+  }
+}
+</script>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-map-dialog {
+  .innercomp-abcontainer-header.highlight-title span {
+    font-size: px-to-rem(18);
+  }
+}
+</style>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.fly-map-dialog {
+  z-index: 31;
+  .fly-map-dialog-body {
+    padding: px-to-rem(12);
+    height: 100%;
+    position: relative;
+    .bottom {
+      height: px-to-rem(30);
+      position: absolute;
+      right: px-to-rem(12);
+      bottom: px-to-rem(24);
+      &.bottom-video {
+        bottom: px-to-rem(48);
+      }
+      &-item {
+        display: inline-block;
+        width: px-to-rem(30);
+        height: px-to-rem(30);
+        margin-right: px-to-rem(6);
+        line-height: px-to-rem(30);
+        border-radius: px-to-rem(4);
+        background: rgba(23, 37, 55, 0.9);
+        text-align: center;
+        cursor: pointer;
+        &-icon {
+          ::v-deep .ct-icon {
+            width: auto !important;
+            .icon-ctw {
+              color: rgba(232, 243, 254, 1) !important;
+              font-size: px-to-rem(18) !important;
+            }
+          }
+        }
+        &.is-disabled .bottom-item-icon {
+          ::v-deep .ct-icon {
+            .icon-ctw {
+              color: rgba(232, 243, 254, 0.4) !important;
+            }
+          }
+        }
+      }
+    }
+  }
+  .medium {
+    width: px-to-rem(546);
+    height: px-to-rem(308);
+    border-radius: px-to-rem(8);
+    &.medium-full {
+      width: 100vw;
+      height: 100vh;
+      ::v-deep .el-image__inner {
+        object-fit: contain;
+      }
+    }
+  }
+  .close_btn {
+    width: px-to-rem(38);
+    height: px-to-rem(38);
+    position: fixed;
+    top: px-to-rem(24);
+    right: px-to-rem(24);
+    z-index: 2;
+    border-radius: 50%;
+    color: rgb(0 0 0 / 50%);
+    cursor: pointer;
+    background-image: url('../../img/icon_full_close.png');
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+  }
+  ::v-deep .video_res_player {
+    width: px-to-rem(546);
+    height: px-to-rem(308);
+    .video-js,
+    video {
+      height: 100%;
+      width: 100%;
+      object-fit: contain;
+      border-radius: px-to-rem(8);
+    }
+    &.video_res_player_full {
+      width: 100vw;
+      height: 100vh;
+      .vjs-control-bar {
+        bottom: px-to-rem(13);
+      }
+    }
+    .vjs-big-play-button {
+      font-size: 0 !important;
+      display: block;
+      background: transparent;
+      pointer-events: auto;
+      cursor: pointer;
+      width: px-to-rem(40);
+      height: px-to-rem(40);
+      position: absolute;
+      z-index: 999;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      .vjs-icon-placeholder {
+        &::before {
+          display: block;
+          background: url('../../img/icon_play_ty@2x.png') no-repeat;
+          background-size: cover;
+          content: '';
+        }
+      }
+    }
+    .video-js.vjs-playing {
+      .vjs-big-play-button {
+        display: none;
+      }
+    }
+    .vjs-error .vjs-error-display .vjs-modal-dialog-content {
+      pointer-events: none;
+      padding: px-to-rem(6) 0;
+    }
+    .video-js.vjs-error {
+      .vjs-big-play-button {
+        display: none;
+      }
+    }
+  }
+}
+</style>

+ 140 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyAttribute.vue

@@ -0,0 +1,140 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行结果属性弹窗
+ -->
+<template>
+  <absolute-container
+    title="属性"
+    :left="left"
+    :bottom="bottom"
+    :width="280"
+    @close="handleClose"
+    industryClass="fly-attribute"
+    v-if="isOpen"
+    :highlightTitle="false"
+  >
+    <div class="fly-attribute-content">
+      <div
+        class="fly-attribute-content-item"
+        v-for="item in attributeList"
+        :key="item.key"
+      >
+        <div class="fly-attribute-label">{{ item.label }}:</div>
+        <div class="fly-attribute-num" :c-tip="attribute[item.key] ?? ''"
+          >{{ attribute[item.key] ?? '' }}
+        </div>
+      </div>
+    </div>
+  </absolute-container>
+</template>
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+
+export default {
+  components: {
+    AbsoluteContainer
+  },
+  props: {
+    bottom: {
+      type: Number
+    },
+    left: {
+      type: Number
+    },
+    isOpen: {
+      type: Boolean,
+      default: false
+    },
+    attribute: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    },
+    attributeList: {
+      type: Array,
+      default: () => {
+        return [
+          {
+            label: '图片名称',
+            key: 'name'
+          },
+          {
+            label: '飞行任务',
+            key: 'planTaskName'
+          },
+          {
+            label: '无人机',
+            key: 'flyName'
+          },
+          {
+            label: '拍摄时间',
+            key: 'createTime'
+          }
+        ]
+      }
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    handleClose() {
+      this.$emit('isHidden', false)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-attribute {
+  z-index: 100;
+  ::v-deep .innercomp-abcontainer-header {
+    background: transparent;
+    span {
+      font-weight: 600;
+      font-size: px-to-rem(18);
+    }
+  }
+  ::v-deep .previous-vacancies {
+    top: px-to-rem(47);
+    height: px-to-rem(1);
+    background: linear-gradient(
+      270deg,
+      rgba(255, 255, 255, 0) 0%,
+      #ffffff 50%,
+      rgba(255, 255, 255, 0) 100%
+    );
+  }
+  &-content {
+    padding: 0 px-to-rem(12) px-to-rem(12) px-to-rem(12);
+    font-size: px-to-rem(16);
+    &-item {
+      &:first-child {
+        margin-top: px-to-rem(12);
+      }
+      &:not(:first-child) {
+        margin-top: px-to-rem(6);
+      }
+      height: px-to-rem(20);
+      display: flex;
+      align-items: center;
+      .fly-attribute-label {
+        width: px-to-rem(80);
+        text-align: right;
+        color: rgba(232, 243, 254, 0.7);
+      }
+      .fly-attribute-num {
+        width: px-to-rem(173);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        color: #e8f3fe;
+      }
+    }
+  }
+}
+</style>

+ 171 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImg.vue

@@ -0,0 +1,171 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行图片
+ -->
+<template>
+  <div class="fly-img">
+    <div class="fly-img-left">
+      <div class="tree-content">
+        <fly-tree @selectTreeData="selectTreeData" />
+      </div>
+      <div class="list-content">
+        <fly-list
+          :selectedNodeData="selectedNodeData"
+          @cardClick="cardClick"
+          @deleteData="deleteFlyListData"
+          ref="flyList"
+        />
+      </div>
+    </div>
+    <div class="fly-img-right">
+      <fly-img-content ref="flyImgContent" :selectData="selectedComputed" />
+    </div>
+  </div>
+</template>
+<script>
+import FlyTree from './FlyTree.vue'
+import FlyList from './FlyList.vue'
+import FlyImgContent from './FlyImgContent.vue'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { deleteData } from '../../dict/fly-result-methods'
+
+export default {
+  components: { FlyTree, FlyList, FlyImgContent },
+  data() {
+    return {
+      selectedNodeData: null, //选中树节点
+      selectedData: null //飞行任务
+    }
+  },
+  props: {
+    type: String
+  },
+  computed: {
+    selectedComputed() {
+      return {
+        type: this.type,
+        treeNode: this.selectedNodeData,
+        listData: this.selectedData
+      }
+    }
+  },
+
+  methods: {
+    /**
+     * 点击树
+     * @param val
+     */
+    selectTreeData(val) {
+      this.selectedNodeData = val
+    },
+    /**
+     * 点击卡片
+     * @param val
+     */
+    cardClick(val) {
+      this.selectedData = val
+    },
+    async deleteFlyListData(val) {
+      let params = {
+        dataType: 'photo',
+        taskRecordId: val.taskRecordId,
+        accessNode: this.selectedNodeData.accessNode
+      }
+      const res = await deleteData(params, '是否确认删除该任务记录下的图片?')
+      if (res.code !== 200) {
+        return
+      }
+      this.$nextTick(() => {
+        this.$refs.flyImgContent?.refreshThumbnail()
+      })
+      CommonMessage.success('删除成功')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+$left_width: px-to-rem(740);
+@mixin fly-img-height {
+  height: px-to-rem(900);
+}
+.fly-img {
+  @include themeify(false) {
+    display: flex;
+    padding-right: px-to-rem(12);
+    .fly-img-left {
+      width: px-to-rem(740);
+      @include fly-img-height;
+      display: flex;
+      background-image: url('../../img/border-radius-left.svg');
+      background-size: 100% 100%;
+      @if $theme-name == 'theme-aquamarine' {
+        background-image: url('../../img/border-radius-left.svg');
+      }
+      @if $theme-name == 'theme-terracotta' {
+        background-image: url('../../img/border-radius-left.svg');
+      }
+      .tree-content {
+        width: px-to-rem(370);
+        @include fly-img-height;
+      }
+      .list-content {
+        width: px-to-rem(370);
+        @include fly-img-height;
+        border-left: px-to-rem(1) solid;
+        padding-top: px-to-rem(12);
+        border-image: linear-gradient(
+            180deg,
+            rgba(134, 184, 249, 0.7),
+            rgba(134, 184, 249, 0)
+          )
+          1 1;
+        @if $theme-name == 'theme-aquamarine' {
+          border-image: linear-gradient(
+              180deg,
+              rgba(134, 184, 249, 0.7),
+              rgba(134, 184, 249, 0)
+            )
+            1
+            1;
+        }
+        @if $theme-name == 'theme-terracotta' {
+          border-image: linear-gradient(
+              180deg,
+              rgba(134, 184, 249, 0.7),
+              rgba(134, 184, 249, 0)
+            )
+            1
+            1;
+        }
+      }
+    }
+    .fly-img-right {
+      flex: 1;
+      width: calc(100% - px-to-rem(4));
+      @include fly-img-height;
+      margin-left: px-to-rem(12);
+      background-image: url('../../img/border-radius.svg');
+      background-size: 100% 100%;
+      @if $theme-name == 'theme-aquamarine' {
+        background-image: url('../../img/border-radius.svg');
+      }
+      @if $theme-name == 'theme-terracotta' {
+        background-image: url('../../img/border-radius.svg');
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+.fly-img .commmomMapClass .map-tools {
+  display: none;
+  .tile-control {
+    display: none;
+  }
+}
+</style>

+ 68 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgContent.vue

@@ -0,0 +1,68 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 飞行结果图片左侧内容
+ * @Description: FlyImgContent.vue
+ -->
+<template>
+  <div class="fly-img-content">
+    <fly-right-top-tab @active="activeBtn" />
+    <fly-img-thumbnail
+      v-if="componentActive === 'img'"
+      :selectData="selectData"
+      ref="fly-img-thumbnail"
+    />
+    <fly-map
+      v-if="componentActive === 'map'"
+      :selectData="selectData"
+      tabType="photo"
+    ></fly-map>
+  </div>
+</template>
+<script>
+import FlyRightTopTab from './FlyRightTopTab.vue'
+import FlyImgThumbnail from './FlyImgThumbnail.vue'
+import FlyMap from './FlyMap.vue'
+
+export default {
+  components: {
+    FlyRightTopTab,
+    FlyImgThumbnail,
+    FlyMap
+  },
+  props: {
+    selectData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      componentActive: 'img'
+    }
+  },
+  methods: {
+    activeBtn(val) {
+      this.componentActive = val.code
+      if (val.code === 'img') {
+        this.$nextTick(() => this.refreshThumbnail())
+      }
+    },
+    refreshThumbnail() {
+      this.$refs['fly-img-thumbnail']?.getFlyImgThumbnailCardList()
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+.fly-img-content {
+  height: 100%;
+  border-image: linear-gradient(
+      180deg,
+      rgba(134, 184, 249, 0.7),
+      rgba(134, 184, 249, 0)
+    )
+    1 1;
+}
+</style>

+ 691 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnail.vue

@@ -0,0 +1,691 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 缩略图
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div class="fly-img-thumbnail">
+    <!--  切换 tab -->
+    <fly-img-thumbnail-tab
+      v-if="hasDataAll"
+      :activeRight="activeRight"
+      :thumbnailOne="showImgType === '1'"
+      :activeImgAttStatus.sync="activeImgAttStatus"
+      :cardDetail.sync="cardDetail"
+      :oneFormData="oneFormData"
+      :activeLeftCode="activeLeftCode"
+      :downloadLoading="downloadLoading"
+      @activeRight="activeRightBtn"
+      @activeLeft="activeLeftBtn"
+      @back="setActiveRight"
+    />
+    <!--    卡片展示-->
+    <div class="thumbnail-h-min" v-if="showImgType === '2'">
+      <el-scrollbar ref="scrollbar" class="thumbnail-scrollbar">
+        <div class="thumbnail-card">
+          <fly-img-thumbnail-card
+            v-for="item in thumbnailCardList"
+            :key="item.id"
+            :cardData="item"
+            @selectClick="selectClick"
+            @editName="editName"
+            @cardClick="cardClick"
+          />
+        </div>
+      </el-scrollbar>
+      <fly-pagination
+        layout="slot, total, sizes, prev, pager, next, jumper"
+        :currentPage.sync="pageData.page"
+        :total="pageData.total"
+        :page-size.sync="pageData.limit"
+        @changePagination="changePagination"
+        style="text-align: right"
+      >
+        <span class="info-page">
+          当前第{{ pageData.page }}/{{
+            Math.ceil(pageData.total / pageData.limit)
+          }}页
+        </span>
+      </fly-pagination>
+    </div>
+
+    <!--    单张展示-->
+    <div class="thumbnail-h-min" v-if="showImgType === '1' && !showImageViewer">
+      <fly-img-thumbnail-one
+        :cardData="oneFormDataAttributeComputed"
+        :activeImgAttStatus.sync="activeImgAttStatus"
+        @fullScreen="fullScreen"
+      />
+    </div>
+    <div
+      class="thumbnail-h-min"
+      :class="showImgType === '0' && 'thumbnail-h-min-no-data'"
+      v-if="showImgType === '0'"
+    >
+      <div v-if="loading" class="loading-datas">
+        <i class="el-icon-loading"></i>
+        <span>加载中</span>
+      </div>
+      <div v-else class="loading-datas">
+        <span class="empty-text">暂无数据</span>
+      </div>
+    </div>
+    <!--    修改名称-->
+    <dialog-fly-edit-name
+      v-if="dialogEditNameShow"
+      :dialogShow.sync="dialogEditNameShow"
+      :form-data="oneFormData"
+      @dialogHandleSubmit="dialogHandleSubmit"
+    />
+    <!--    全屏展示图片-->
+    <div
+      class="view-img fly_img_result_view_screen_full"
+      v-if="showImageViewer"
+    >
+      <span class="close_btn" @click="handleImageViewerClose"></span>
+      <medium-viewer
+        class="medium-viewer"
+        :urlList="thumbnailCardListUrlComputed"
+        :initial-index.sync="fileIndex"
+        :mediumViewerFull="showImageViewer"
+      >
+        <el-image
+          class="medium"
+          :src="thumbnailCardListUrlComputed[fileIndex]"
+        ></el-image>
+      </medium-viewer>
+      <fly-attribute
+        style="position: fixed; z-index: 999999999"
+        :isOpen.sync="activeImgAttStatus"
+        :bottom="24"
+        :left="24"
+        :attribute="attributeComputed"
+        @isHidden="() => (activeImgAttStatus = false)"
+      />
+    </div>
+    <dialog-fly-algorithm
+      v-if="dialogAlgorithm"
+      :dialogShow.sync="dialogAlgorithm"
+      :form-data="algorithmFormData"
+    />
+  </div>
+</template>
+<script>
+import FlyImgThumbnailTab from './FlyImgThumbnailTab.vue'
+import FlyImgThumbnailCard from './FlyImgThumbnailCard.vue'
+import FlyImgThumbnailOne from './FlyImgThumbnailOne.vue'
+import DialogFlyEditName from './DialogFlyEditName.vue'
+import FlyPagination from './FlyPagination.vue'
+import FlyAttribute from './FlyAttribute.vue'
+import DialogFlyAlgorithm from './DialogFlyAlgorithm.vue'
+import MediumViewer from './MediumViewer.vue'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import screenfull from 'screenfull'
+import dayjs from 'dayjs'
+import { queryTaskPhotos, renameRecords } from '../../service'
+import {
+  syncHandlerApi,
+  downloadByUrlsApi,
+  deleteData
+} from '../../dict/fly-result-methods'
+
+export default {
+  components: {
+    MediumViewer,
+    FlyPagination,
+    FlyImgThumbnailTab,
+    FlyImgThumbnailCard,
+    FlyImgThumbnailOne,
+    DialogFlyEditName,
+    DialogFlyAlgorithm,
+    FlyAttribute
+  },
+  props: {
+    selectData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+
+  data() {
+    return {
+      thumbnailCardList: [], //卡片数据
+      loading: false,
+      downloadLoading: false,
+      //左侧激活
+      activeLeftCode: 'all',
+      activeRight: ['sync'], //右侧激活
+      dialogEditNameShow: false, //编辑名称弹窗
+      oneFormData: {
+        photoAccessUrl: undefined,
+        id: undefined,
+        name: undefined,
+        photoUrl: undefined,
+        onlyOneData: undefined
+      }, //打开图片数据
+      activeImgAttStatus: true, //图片属性
+      cardDetail: false, //查看图片卡片
+      showImageViewer: false, //全屏预览
+      fileIndex: 0, //全屏预览图片索引
+      dialogAlgorithm: false, //算法配置弹窗
+      algorithmFormData: {}, //算法配置数据
+      pageData: {
+        page: 1,
+        limit: 12,
+        total: 0
+      },
+      hasDataAll: false
+    }
+  },
+  computed: {
+    showImgType() {
+      //TODO
+      if (this.thumbnailCardList.length === 0) {
+        return '0'
+      }
+
+      if (this.cardDetail) {
+        return '1'
+      }
+      return '2'
+    },
+    /**
+     * 选中任务列表数据
+     * @returns {*}
+     */
+    taskListDataComputed() {
+      return { ...this.selectData.listData }
+    },
+    /**
+     * 选中树节点数据
+     * @returns {*}
+     */
+    treeNodeComputed() {
+      return {
+        ...this.selectData.treeNode
+      }
+    },
+    attributeComputed() {
+      const data = this.thumbnailCardList[this.fileIndex]
+      return {
+        ...data,
+        flyName: this.treeNodeComputed.name,
+        planTaskName: this.taskListDataComputed.planTaskName
+      }
+    },
+    oneFormDataAttributeComputed() {
+      return {
+        ...this.oneFormData,
+        flyName: this.treeNodeComputed.name,
+        planTaskName: this.taskListDataComputed.planTaskName
+      }
+    },
+    thumbnailCardListUrlComputed() {
+      return this.thumbnailCardList.map((item) => item.photoAccessUrl)
+    }
+  },
+  watch: {
+    /**
+     * 监听列表变化
+     */
+    thumbnailCardList: {
+      handler(val) {
+        this.setActiveRight()
+      },
+      deep: true
+    },
+    /**
+     * 监听任务列表数据变化
+     */
+    taskListDataComputed: {
+      handler(newVal, oldVal) {
+        if (newVal.taskRecordId) {
+          console.log('监听selectData====>', newVal, oldVal, this.selectData)
+          this.cardDetail = false
+          this.activeLeftCode = 'all'
+          this.getFlyImgThumbnailCardList()
+        } else {
+          this.thumbnailCardList = []
+        }
+      },
+      deep: true
+    }
+  },
+  methods: {
+    /**
+     * 算法配置
+     */
+    algorithmHandler() {
+      this.algorithmFormData = {
+        algorithmList: [],
+        paramsLists: this.cardDetail
+          ? [this.oneFormData]
+          : this.thumbnailCardList.filter((item) => item.select),
+        deviceCode: this.treeNodeComputed.deviceCode,
+        flightRouteId: this.taskListDataComputed.flightRouteId,
+        type: 'image'
+      }
+
+      this.dialogAlgorithm = true
+    },
+    /**
+     *  同步数据源
+     */
+    syncHandler() {
+      let params = {
+        taskRecordId: this.taskListDataComputed.taskRecordId,
+        deviceCode: this.treeNodeComputed.deviceCode,
+        accessNode: this.treeNodeComputed?.accessNode,
+        mediaSyncStatus: this.taskListDataComputed.mediaSyncStatus
+      }
+      syncHandlerApi(params)
+    },
+    /**
+     * 下载
+     * @param
+     */
+
+    async downloadHandler() {
+      let params = {}
+      const thumbnailCardListSelect = this.thumbnailCardList.filter(
+        (item) => item.select
+      )
+      // thumbnailCardListSelect.length === 1 ||
+      if (this.cardDetail) {
+        params = {
+          photoUrls: this.cardDetail
+            ? [this.oneFormData.photoAccessUrl]
+            : [thumbnailCardListSelect[0].photoAccessUrl],
+          fileName: this.cardDetail
+            ? this.oneFormData.name
+            : thumbnailCardListSelect[0].name
+        }
+      } else {
+        params = {
+          fileName: `${this.taskListDataComputed.planTaskName}-${
+            this.treeNodeComputed.flyName
+          }_${dayjs(this.taskListDataComputed.startTime).format(
+            'YYYYMMDDHHmm'
+          )}-图片.zip`,
+          photoUrls: thumbnailCardListSelect.map((item) => item.photoAccessUrl),
+          isZip: 1
+        }
+      }
+      if (params.photoUrls.length > 24) {
+        return CommonMessage.info('图片单次可下载24张')
+      }
+      this.downloadLoading = true
+      await downloadByUrlsApi(params)
+      this.downloadLoading = false
+    },
+    /**
+     * 删除
+     */
+    async delHandler() {
+      const paramsListIds = this.thumbnailCardList
+        .filter((item) => item.select)
+        .map((item) => item.id)
+      let params = {
+        dataType: 'photo',
+        id: this.cardDetail ? [this.oneFormData.id] : paramsListIds,
+        taskRecordId: this.taskListDataComputed.taskRecordId,
+        accessNode: this.treeNodeComputed?.accessNode
+      }
+      const res = await deleteData(params)
+      if (res.code !== 200) {
+        return
+      }
+      this.getFlyImgThumbnailCardList()
+      CommonMessage.success('删除成功')
+    },
+    /**
+     * 全选
+     * @param val
+     */
+    allHandler(val) {
+      let index = this.activeRight.indexOf('all')
+      if (index > -1) {
+        this.activeRight = this.activeRight.filter((item) => item !== 'all')
+      } else {
+        this.activeRight.push(val.code)
+      }
+      this.thumbnailCardList.forEach((item) => {
+        item.select = index === -1
+      })
+    },
+    /**
+     * 右侧激活
+     * @param val
+     */
+    activeRightBtn(val) {
+      const fnObject = {
+        algorithm: this.algorithmHandler,
+        sync: this.syncHandler,
+        download: this.downloadHandler,
+        del: this.delHandler,
+        all: this.allHandler
+      }
+      fnObject[val.code] && fnObject[val.code](val)
+    },
+    /**
+     * 左侧激活
+     * @param val
+     */
+    activeLeftBtn(val) {
+      this.activeLeftCode = val.code
+      this.getFlyImgThumbnailCardList()
+    },
+    /**
+     * 卡片选中
+     * @param val
+     */
+    selectClick(val) {
+      this.thumbnailCardList.forEach((item) => {
+        if (item.id === val.id) {
+          item.select = !item.select
+        }
+      })
+    },
+    /**
+     * 点击卡片
+     * @param val
+     */
+    cardClick(val) {
+      this.oneFormData = {
+        ...val,
+        onlyOneData: false
+      }
+      this.cardDetail = true
+      this.activeImgAttStatus = true
+      this.activeRight = ['sync']
+      this.activeRight = this.activeRight.concat([
+        'algorithm',
+        'download',
+        'del'
+      ])
+    },
+    /**
+     * 编辑名称,传入 id 保存数据时直接使用不在记录
+     */
+    editName(val) {
+      this.oneFormData = {
+        name: val.name,
+        id: val.id
+      }
+      this.dialogEditNameShow = true
+    },
+    /**
+     * 修改名称保存
+     * @param val
+     */
+    dialogHandleSubmit(val) {
+      this.renameApi(val)
+      this.dialogEditNameShow = false
+    },
+    /**
+     * 调用修改名称接口
+     * @param val
+     */
+    renameApi(val) {
+      let params = {
+        dataType: 'photo',
+        accessNode: this.treeNodeComputed.accessNode,
+        ...val
+      }
+      renameRecords(params).then((res) => {
+        if (res.code === 200) {
+          /*     const data = this.thumbnailCardList.find((item) => item.id === val.id)
+          data.name = val.name*/
+          this.getFlyImgThumbnailCardList(false)
+        }
+      })
+    },
+
+    /**
+     * 全屏展示图片
+     * @param val
+     */
+    fullScreen(val) {
+      if (!screenfull.enabled) {
+        CommonMessage.warning('浏览器不支持该功能')
+        return false
+      }
+      this.fileIndex = this.thumbnailCardList.findIndex(
+        (item) => item.id === val.id
+      )
+      this.showImageViewer = true
+      this.$nextTick(() => {
+        let el = document.querySelector('.fly_img_result_view_screen_full')
+        el.requestFullscreen()
+        screenfull.on('change', this.setShowImageViewer)
+      })
+    },
+    /**
+     * 设置是否全屏预览
+     */
+    setShowImageViewer() {
+      this.showImageViewer = screenfull.isFullscreen
+    },
+    /**
+     * 关闭图片预览
+     */
+    handleImageViewerClose() {
+      document.exitFullscreen()
+    },
+    /**
+     * 参数处理
+     */
+    paramsHandle(clearData) {
+      if (clearData) {
+        this.pageData.page = 1
+      }
+      let params = {
+        taskRecordId: this.taskListDataComputed.taskRecordId,
+        accessNode: this.treeNodeComputed?.accessNode,
+        picType: this.activeLeftCode,
+        ...this.pageData
+      }
+      delete params.total
+      // 全部不传picType字段
+      if (this.activeLeftCode === 'all') {
+        delete params.picType
+        this.hasDataAll = false
+      }
+      return params
+    },
+    /**
+     * 获取数据
+     */
+    getFlyImgThumbnailCardList(clearData = true) {
+      this.thumbnailCardList = []
+      if (!this.taskListDataComputed.taskRecordId) {
+        return
+      }
+      this.loading = true
+      const params = this.paramsHandle(clearData)
+      queryTaskPhotos(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.thumbnailCardList =
+              res.rows?.map((item) => {
+                return {
+                  ...item,
+                  select: !clearData && this.activeRight.includes('all')
+                }
+              }) || []
+            this.pageData.total = res.total
+            this.cardDetail = this.taskListDataComputed?.photoCount === 1
+            console.log('this.thumbnailCardList===>', this.thumbnailCardList)
+            this.handlerOneData()
+            if (
+              this.activeLeftCode === 'all' &&
+              this.thumbnailCardList.length > 0
+            ) {
+              this.hasDataAll = true
+            }
+          }
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    /**
+     * 处理只有一条数据情况
+     */
+    handlerOneData() {
+      if (
+        this.thumbnailCardList?.length === 1 &&
+        this.activeLeftCode === 'all'
+      ) {
+        this.cardDetail = true
+        this.oneFormData = {
+          ...this.thumbnailCardList[0],
+          onlyOneData: true
+        }
+        //单条数据设置为选中直接打开
+        this.thumbnailCardList[0].select = true
+      }
+    },
+    changePagination() {
+      this.getFlyImgThumbnailCardList(false)
+    },
+    /**
+     * 设置右侧 tab状态
+     */
+    setActiveRight() {
+      this.activeRight = ['sync']
+      if (this.thumbnailCardList.some((item) => item.select === true)) {
+        this.activeRight = this.activeRight.concat([
+          'algorithm',
+          'download',
+          'del'
+        ])
+      }
+      //确保数组中有数据
+      if (
+        this.thumbnailCardList.length > 0 &&
+        this.thumbnailCardList.every((item) => item.select === true)
+      ) {
+        this.activeRight = this.activeRight.concat(['all'])
+      }
+    }
+  },
+  beforeDestroy() {
+    screenfull.off('change', this.setShowImageViewer)
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+.fly-img-thumbnail {
+  .no-data-m-t {
+    margin-top: px-to-rem(70);
+  }
+
+  .mt0 {
+    margin-top: 0;
+  }
+  .info-page {
+    color: #e8f2fe;
+    font-size: px-to-rem(16);
+  }
+  .thumbnail-h-min {
+    height: px-to-rem(806);
+    &.thumbnail-h-min-no-data {
+      height: calc(px-to-rem(806) + px-to-rem(56));
+    }
+  }
+
+  .thumbnail-scrollbar {
+    height: calc(px-to-rem(806) - px-to-rem(24));
+  }
+
+  .thumbnail-card {
+    display: grid;
+    margin: 0 px-to-rem(12) 0 px-to-rem(12);
+    grid-template-columns: repeat(3, 1fr);
+    grid-column-gap: px-to-rem(12);
+    grid-row-gap: px-to-rem(12);
+  }
+
+  ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+    background: rgba(79, 159, 255, 0.4);
+  }
+
+  .empty-text {
+    pointer-events: none;
+    color: #e8f3fe;
+    font-size: px-to-rem(16);
+    margin-top: px-to-rem(80);
+    text-align: center;
+
+    &:before {
+      content: url('~@component-gallery/assets/image/ar-label/noData.png');
+      display: block;
+    }
+  }
+
+  .loading-datas {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    flex-direction: column;
+    font-size: px-to-rem(14);
+    color: #ffffff;
+
+    span {
+      margin-top: px-to-rem(12);
+    }
+  }
+
+  .has-thumbnail-data.loading-datas {
+    margin-top: px-to-rem(10);
+    height: px-to-rem(60);
+  }
+  .view-img {
+    .close_btn {
+      width: px-to-rem(38);
+      height: px-to-rem(38);
+      position: fixed;
+      top: px-to-rem(24);
+      right: px-to-rem(24);
+      z-index: 2;
+      border-radius: 50%;
+      color: rgb(0 0 0 / 50%);
+      cursor: pointer;
+      background-image: url('../../img/icon_full_close.png');
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+    }
+    .medium-viewer {
+      .medium {
+        width: 100vw;
+        height: 100vh;
+      }
+      ::v-deep .el-image__inner {
+        object-fit: contain;
+      }
+    }
+  }
+  @include themeify(false) {
+    @if $theme-name == 'theme-aquamarine' {
+      ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+        background-color: rgb(2 137 109 / 40%);
+      }
+    }
+
+    @if $theme-name == 'theme-terracotta' {
+      ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+        background-color: rgba(255, 238, 177, 0.4);
+      }
+    }
+  }
+}
+</style>

+ 207 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnailCard.vue

@@ -0,0 +1,207 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 缩略图卡片
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div
+    :class="[
+      'fly-img-thumbnail-card',
+      isShowName && cardData?.select && 'fly-img-thumbnail-card-selectde'
+    ]"
+  >
+    <el-image
+      @click.stop="cardClick(cardData)"
+      class="img"
+      :src="cardData?.photoAccessUrl"
+      lazy
+    >
+      <template v-slot:placeholder>
+        <div class="image-slot">
+          <i class="el-icon-loading"></i>
+          <span>图片加载中</span>
+        </div>
+      </template>
+    </el-image>
+    <div class="card-content-top-left" @click.stop="selectClick(cardData)">
+      <template v-if="isShowName">
+        <img
+          v-if="cardData?.select"
+          src="../../img/fly-img-card-active.svg"
+          alt=""
+        />
+        <img v-else src="../../img/fly-img-card.svg" alt="" />
+      </template>
+      <template v-if="!isShowName">
+        <img
+          v-if="selectList.includes(cardData.id)"
+          src="../../img/fly-img-card-active.svg"
+          alt=""
+        />
+        <img v-else src="../../img/fly-img-card.svg" alt="" />
+      </template>
+    </div>
+    <div
+      v-if="isShowName"
+      class="card-content-top-right"
+      @click.stop="editName(cardData)"
+      :c-tip="'重命名'"
+    >
+      <i class="iconfont icon-tongyong_icon_bianji"></i>
+    </div>
+    <div
+      class="card-content-bottom"
+      :class="isShowName && 'card-content-bottom-bg'"
+    >
+      <span class="text" :c-tip="cardData.name">
+        <template v-if="isShowName">
+          {{ cardData?.name }}
+        </template>
+      </span>
+      <span
+        class="status-text"
+        :style="{ background: status[cardData?.eventStatus]?.background ?? '' }"
+        >{{ status[cardData?.eventStatus]?.name ?? status['2'].name }}</span
+      >
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  components: {},
+  props: {
+    cardData: {
+      type: Object,
+      default: () => ({})
+    },
+    isShowName: {
+      type: Boolean,
+      default: true
+    },
+    selectList: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      status: {
+        2: {
+          name: '有告警',
+          background: 'linear-gradient( 135deg, #FFA43C 0%, #FF8D0B 100%)'
+        },
+        1: {
+          name: '无告警',
+          background: 'linear-gradient( 90deg, #15BD94 0%, #00A179 100%)'
+        },
+        0: {
+          name: '未识别',
+          background: 'linear-gradient( 90deg, #52A1E5 0%, #1F7CCC 100%)'
+        }
+      }
+    }
+  },
+  methods: {
+    cardClick(val) {
+      this.$emit('cardClick', val)
+    },
+    editName(val) {
+      this.$emit('editName', val)
+    },
+    selectClick(val) {
+      this.$emit('selectClick', val)
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$border-radius: px-to-rem(5);
+.fly-img-thumbnail-card {
+  height: px-to-rem(184);
+  border: px-to-rem(2) solid transparent;
+  border-radius: $border-radius;
+  position: relative;
+  .img {
+    width: 100%;
+    height: 100%;
+    border-radius: $border-radius;
+    object-fit: cover;
+    .image-slot {
+      height: calc(100% - px-to-rem(32));
+      flex-direction: column;
+      font-size: px-to-rem(14);
+      color: #ffffff;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+  .card-content-top-left {
+    width: px-to-rem(24);
+    height: px-to-rem(24);
+    position: absolute;
+    top: px-to-rem(6);
+    left: px-to-rem(6);
+    cursor: pointer;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .card-content-top-right {
+    width: px-to-rem(26);
+    height: px-to-rem(26);
+    line-height: px-to-rem(26);
+    position: absolute;
+    top: px-to-rem(6);
+    right: px-to-rem(6);
+    text-align: center;
+    background: rgba(23, 37, 55, 0.8);
+    border-radius: px-to-rem(4);
+    .iconfont {
+      font-size: px-to-rem(16);
+    }
+  }
+  .card-content-bottom {
+    width: 100%;
+    height: px-to-rem(32);
+    position: absolute;
+    border-bottom-left-radius: $border-radius;
+    border-bottom-right-radius: $border-radius;
+    bottom: 0;
+    left: 0;
+    padding: 0 px-to-rem(6) 0 px-to-rem(12);
+
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    color: #ffffff;
+    &.card-content-bottom-bg {
+      background: rgba(0, 0, 0, 0.7);
+    }
+    .text {
+      width: calc(100% - px-to-rem(52));
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      font-size: px-to-rem(16);
+    }
+    .status-text {
+      width: px-to-rem(52);
+      height: px-to-rem(24);
+      margin-left: px-to-rem(6);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: px-to-rem(14);
+      border-radius: px-to-rem(4);
+      background: linear-gradient(90deg, #52a1e5 0%, #1f7ccc 100%);
+    }
+  }
+  &-selectde {
+    border-color: #1373e6;
+  }
+}
+</style>

+ 110 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnailOne.vue

@@ -0,0 +1,110 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 缩略图详情
+ -->
+<!-- eslint-disable vue/no-deprecated-slot-scope-attribute -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<!-- eslint-disable vue/no-mutating-props -->
+
+<template>
+  <div class="fly-img-thumbnail-one">
+    <el-image class="img" :src="cardData?.photoAccessUrl" alt="" lazy>
+      <template v-slot:placeholder>
+        <div class="image-slot">
+          <i class="el-icon-loading"></i>
+          <span>图片加载中</span>
+        </div>
+      </template>
+    </el-image>
+    <fly-attribute
+      :isOpen.sync="activeImgAttStatus"
+      :bottom="12"
+      :left="12"
+      :attribute="cardData"
+      @isHidden="isHiddenBtn"
+    />
+    <div class="full-screen" @click="fullScreen(cardData)">
+      <i class="iconfont icon-guotu_icon_quanpingfangda"></i>
+    </div>
+  </div>
+</template>
+<script>
+import FlyAttribute from './FlyAttribute.vue'
+
+export default {
+  components: {
+    FlyAttribute
+  },
+  props: {
+    cardData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    },
+    activeImgAttStatus: {
+      type: Boolean
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    /**
+     * 隐藏属性
+     * @param val
+     */
+    isHiddenBtn(val) {
+      this.$emit('update:activeImgAttStatus', val)
+    },
+    /**
+     * 全屏预览
+     * @param val
+     */
+    fullScreen(val) {
+      this.$emit('fullScreen', val)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-img-thumbnail-one {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  .img {
+    display: inline-block;
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    .image-slot {
+      height: 100%;
+      flex-direction: column;
+      font-size: px-to-rem(14);
+      color: #ffffff;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+  .full-screen {
+    width: px-to-rem(30);
+    height: px-to-rem(30);
+    line-height: px-to-rem(30);
+    position: absolute;
+    right: px-to-rem(12);
+    top: px-to-rem(12);
+    background: rgba(23, 37, 55, 0.9);
+    border-radius: px-to-rem(4);
+    color: #ffffff;
+    text-align: center;
+    .iconfont {
+      font-size: px-to-rem(16);
+    }
+  }
+}
+</style>

+ 287 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyImgThumbnailTab.vue

@@ -0,0 +1,287 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 筛选框及右侧页签
+ -->
+<template>
+  <div class="fly-img-thumbnail-tab">
+    <div class="fly-img-thumbnail-tab-left">
+      <template>
+        <div class="img-name" v-if="!isMap && thumbnailOne">
+          <img
+            @click="back"
+            :class="[
+              'img-name-back',
+              oneFormData?.onlyOneData && 'img-name-disabled'
+            ]"
+            src="../../img/fly-back.svg"
+            alt=""
+          />
+          {{ oneFormData.name }}
+        </div>
+        <template v-if="tabType === 'img' && (!thumbnailOne || isMap)">
+          <span
+            v-for="item in filterTab"
+            :key="item.code"
+            :class="['item-tab', item.code === activeLeftCode && 'active']"
+            @click="activeBtn(item)"
+          >
+            <img
+              class="img"
+              v-if="item.code === activeLeftCode"
+              src="../../img/tab-thumbnail-active.svg"
+              alt=""
+            />
+            <span>
+              {{ item.name }}
+            </span>
+          </span>
+        </template>
+      </template>
+    </div>
+    <div class="fly-img-thumbnail-tab-right">
+      <el-button
+        v-if="thumbnailOne"
+        type="text"
+        :class="['item-tab', activeImgAttStatus ? 'active' : '']"
+        @click="activeImgAttBtn"
+      >
+        <ct-icon
+          :name="activeImgAttStatus ? 'attribute-active' : 'attribute'"
+          class="item-tab-icon"
+        />
+        {{ tabType === 'video' ? '视频属性' : '图片属性' }}
+      </el-button>
+
+      <el-button
+        type="text"
+        v-for="item in operateTabComputed"
+        :key="item.code"
+        :disabled="item.code !== 'all' && !activeRight.includes(item.code)"
+        :class="[
+          'item-tab',
+          downloadLoading && item.code === 'download' && 'is-disabled'
+        ]"
+        @click="activeRightBtn(item)"
+      >
+        <ct-icon
+          v-if="item.code !== 'all'"
+          :name="item.icon"
+          class="item-tab-icon"
+        ></ct-icon>
+        <span v-if="item.code === 'all'" class="all-select-icon">
+          <span
+            class="item-tab-all"
+            :class="[activeRight.includes(item.code) && 'item-tab-all-active']"
+          ></span>
+        </span>
+
+        {{ item.name }}
+      </el-button>
+    </div>
+  </div>
+</template>
+<script>
+import { filterTab, operateTab } from '../../entry/data'
+
+export default {
+  props: {
+    downloadLoading: { type: Boolean },
+    isMap: {
+      type: Boolean,
+      default: false
+    },
+    activeRight: {
+      type: Array,
+      default: () => []
+    },
+    thumbnailOne: {
+      type: Boolean,
+      default: true
+    },
+    activeImgAttStatus: {
+      type: Boolean
+    },
+    cardDetail: {
+      type: Boolean
+    },
+    oneFormData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    },
+    activeLeftCode: {
+      type: [String, Number],
+      default: 'all'
+    },
+    tabType: {
+      type: String,
+      default: 'img'
+    }
+  },
+  data() {
+    return {
+      filterTab
+    }
+  },
+  computed: {
+    //TODO
+    operateTabComputed() {
+      let codeValue = 'all'
+      let includesLust = []
+      if (!this.thumbnailOne) {
+        codeValue = 'att'
+      }
+      if (!this.isMap) {
+        return operateTab.filter((item) => item.code !== codeValue)
+      }
+      if (this.isMap) {
+        includesLust = ['algorithm', 'download', 'del', 'att', 'all']
+      }
+      return operateTab.filter((item) => !includesLust.includes(item.code))
+    }
+  },
+  methods: {
+    activeBtn(val) {
+      this.$emit('activeLeft', val)
+    },
+    activeRightBtn(val) {
+      if (val === 'download' && this.downloadLoading) {
+        return
+      }
+      this.$emit('activeRight', val)
+    },
+    activeImgAttBtn() {
+      this.$emit('update:activeImgAttStatus', !this.activeImgAttStatus)
+    },
+    back() {
+      if (this.oneFormData?.onlyOneData) {
+        return
+      }
+      this.$emit('back')
+      this.$emit('update:cardDetail', false)
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.fly-img-thumbnail-tab {
+  @include flex;
+  height: px-to-rem(56);
+  justify-content: space-between;
+  font-weight: 400;
+  font-size: px-to-rem(16);
+  text-align: center;
+  font-style: normal;
+  cursor: pointer;
+  margin: 0 px-to-rem(12);
+  &-left {
+    display: flex;
+    align-items: center;
+
+    .img-name {
+      font-size: px-to-rem(18);
+      height: px-to-rem(30);
+      display: flex;
+      align-items: center;
+      &-back {
+        width: px-to-rem(30);
+        height: px-to-rem(30);
+        margin-right: px-to-rem(12);
+      }
+      &-disabled {
+        opacity: 0.7;
+      }
+    }
+    .item-tab {
+      width: px-to-rem(66);
+      height: px-to-rem(32);
+      line-height: px-to-rem(32);
+      color: rgba(232, 243, 254, 0.7);
+      font-size: px-to-rem(16);
+    }
+    .active {
+      color: #e8f3fe;
+      text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+      position: relative;
+      .img {
+        width: 100%;
+        height: 100%;
+        position: absolute;
+        left: 0;
+        top: 0;
+      }
+    }
+  }
+  &-right {
+    display: flex;
+    align-items: center;
+    height: px-to-rem(32);
+    font-size: px-to-rem(16);
+    .item-tab {
+      height: px-to-rem(32);
+      margin-left: px-to-rem(12);
+      @include flex;
+      color: #e8f3fe;
+      line-height: normal;
+      padding: 0;
+      font-size: px-to-rem(16);
+      &.active {
+        color: #4f9fff;
+      }
+      .all-select-icon {
+        width: px-to-rem(20);
+        height: px-to-rem(20);
+        .item-tab-all {
+          display: block;
+          width: px-to-rem(14);
+          height: px-to-rem(14);
+          border: 1px solid #e8f3fe;
+          border-radius: 50%;
+        }
+        .item-tab-all-active {
+          background-image: url('../../img/fly-img-card-active.svg');
+          background-size: 150%;
+          background-position: px-to-rem(-3) px-to-rem(-2);
+          background-repeat: no-repeat;
+          border: none;
+        }
+      }
+      .item-tab-icon {
+        width: px-to-rem(14) !important;
+        height: 100%;
+        line-height: 100%;
+        margin-right: px-to-rem(6);
+        margin-top: px-to-rem(2);
+        ::v-deep .ct-icon {
+          width: auto !important;
+          .icon-ctw {
+            color: inherit !important;
+            font-size: px-to-rem(16) !important;
+          }
+        }
+      }
+    }
+    ::v-deep .el-button {
+      span {
+        display: flex;
+        align-items: center;
+      }
+      i {
+        font-size: px-to-rem(20);
+        margin-right: px-to-rem(6);
+      }
+    }
+    ::v-deep .is-disabled {
+      color: rgba(232, 243, 254, 0.4);
+    }
+  }
+}
+</style>

+ 406 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyList.vue

@@ -0,0 +1,406 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行结果列表
+ -->
+<!-- eslint-disable vue/no-deprecated-slot-scope-attribute -->
+<!-- eslint-disable vue/no-deprecated-dollar-scopedslots-api -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div class="fly_list">
+    <div class="search-input">
+      <el-input
+        v-model="searchValue"
+        placeholder="请输入关键字"
+        @input="searchInput"
+        clearable
+      >
+        <template #append>
+          <i class="iconfont icon-pc_sousuo" />
+        </template>
+      </el-input>
+      <div class="sort" :c-tip="'按任务执行时间排序'">
+        <i
+          class="iconfont icon-shangfan"
+          :class="[!this.sortFlag && 'sort-active']"
+          @click="sortData(false)"
+        ></i>
+        <i
+          class="iconfont icon-xiafan"
+          :class="[this.sortFlag && 'sort-active']"
+          @click="sortData(true)"
+        ></i>
+      </div>
+    </div>
+    <div class="fly_list_content" v-if="flyListData.length > 0 && !loading">
+      <el-scrollbar class="fly_list_content-scrollbar">
+        <fly-list-card
+          v-for="item in flyListData"
+          :key="item.taskRecordId + '_l'"
+          :itemData="item"
+          @delete="deleteData"
+          @cardClick="cardClick(item)"
+          :class="[
+            item.taskRecordId === flyActiveData.taskRecordId && 'active'
+          ]"
+        >
+          <template v-for="(slot, slotName) in $scopedSlots" #[slotName]="item">
+            <slot :name="slotName" v-bind="item" />
+          </template>
+          <div class="picture">
+            <img
+              v-if="syncStatus[item?.mediaSyncStatus]?.icon"
+              :src="syncStatus[item?.mediaSyncStatus]?.icon"
+              class="img"
+              alt=""
+            />
+            <span>{{ syncStatus[item?.mediaSyncStatus]?.name ?? '-' }}</span>
+          </div>
+        </fly-list-card>
+      </el-scrollbar>
+      <div style="text-align: right">
+        <fly-pagination
+          :currentPage.sync="pagination.currentPage"
+          :total="pagination.total"
+          :pageSize="pagination.pageSize"
+          @changePagination="changePagination"
+        />
+      </div>
+    </div>
+    <div class="fly_list_content" v-else>
+      <div class="loading-datas" v-if="!loading">
+        <span class="empty-text">暂无数据</span>
+      </div>
+      <div v-if="loading" class="loading-datas">
+        <i class="el-icon-loading"></i>
+        <span>加载中</span>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import FlyListCard from './FlyListCard.vue'
+import FlyPagination from './FlyPagination.vue'
+import { cloneDeep } from 'lodash-es'
+import { queryTaskList } from '../../service'
+import { syncStatus } from '../../entry/data'
+
+export default {
+  name: 'FlyList',
+  components: {
+    FlyListCard,
+    FlyPagination
+  },
+  props: {
+    selectedNodeData: {
+      type: [Object, null],
+      default: null
+    }
+  },
+  data() {
+    return {
+      searchValue: undefined,
+      flyListOriginalData: [],
+      flyListData: [],
+      flyActiveData: { taskRecordId: undefined }, //当前激活
+      loading: false,
+      syncStatus: syncStatus,
+      sortFlag: true,
+      pagination: {
+        currentPage: 1,
+        pageSize: 8,
+        total: 0
+      }
+    }
+  },
+  watch: {
+    selectedNodeData: {
+      handler: function () {
+        this.searchValue = undefined
+        this.sortFlag = true
+        this.pagination = {
+          currentPage: 1,
+          pageSize: 8,
+          total: 0
+        }
+        this.getFlyListData()
+      }
+    }
+  },
+  methods: {
+    /**
+     * 获取数据
+     */
+    getFlyListData(flag = true) {
+      this.loading = true
+      this.flyListData = []
+      if (flag) {
+        this.$emit('cardClick', null)
+      }
+      const params = {
+        deviceCode: this.selectedNodeData?.deviceCode,
+        sort: this.sortFlag ? 1 : 0,
+        pageNo: this.pagination.currentPage,
+        pageSize: this.pagination.pageSize,
+        flightRouteName: this.searchValue,
+        accessNode: this.selectedNodeData?.accessNode
+      }
+      queryTaskList(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.flyListData = res.rows || []
+            this.pagination.total = res.total
+            this.flyListOriginalData = cloneDeep(res.rows)
+            if (this.flyListData && flag) {
+              this.flyActiveData = { ...this.flyListData[0] }
+              this.$emit('cardClick', this.flyActiveData)
+            }
+          }
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    /**
+     * 分页查询
+     */
+    changePagination() {
+      this.getFlyListData(false)
+    },
+    /**
+     * 删除
+     * @param val
+     */
+    deleteData(val) {
+      this.$emit('deleteData', val)
+    },
+    /**
+     * 点击卡片
+     * @param val
+     */
+    cardClick(val) {
+      this.flyActiveData = { ...val }
+      console.log('点击任务列表卡片', this.flyActiveData)
+      this.$emit('cardClick', this.flyActiveData)
+    },
+    /**
+     * 搜索
+     */
+    searchInput() {
+      this.pagination.currentPage = 1
+      this.getFlyListData()
+    },
+    /**
+     * 排序
+     */
+    sortData(flag) {
+      this.pagination.currentPage = 1
+      this.sortFlag = flag
+      this.getFlyListData(false)
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+
+$_input_h: px-to-rem(32);
+.fly_list {
+  height: 100%;
+
+  /*  .iconfont {
+    font-size: px-to-rem(16);
+  }*/
+
+  .search-input {
+    height: $_input_h;
+    display: flex;
+    margin: 0 px-to-rem(12) px-to-rem(12) px-to-rem(12);
+
+    ::v-deep .el-input {
+      display: flex;
+      align-items: center;
+
+      .el-input__inner {
+        height: $_input_h;
+        background: rgba(79, 159, 255, 0.2);
+        border-top-left-radius: px-to-rem(4);
+        border-bottom-left-radius: px-to-rem(4);
+        border-width: px-to-rem(1);
+        border-style: solid;
+        border-color: rgba(79, 159, 255, 0);
+        padding-left: px-to-rem(12);
+        color: #e8f3fe;
+        font-size: px-to-rem(16);
+        &::placeholder {
+          color: rgba(232, 243, 254, 0.7);
+        }
+      }
+
+      .el-input__icon {
+        color: #e8f3fe;
+      }
+
+      .el-input-group__append {
+        display: flex;
+        align-items: center;
+        height: $_input_h;
+        padding-left: 0;
+        color: #e8f3fe;
+        border-style: solid;
+        border-top-right-radius: px-to-rem(4);
+        border-bottom-right-radius: px-to-rem(4);
+        border-color: rgba(79, 159, 255, 0);
+        background: rgba(79, 159, 255, 0.2);
+      }
+    }
+
+    .sort {
+      margin: px-to-rem(2) 0 0 px-to-rem(12);
+      padding: px-to-rem(6) 0;
+      color: #e8f3fe;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      flex-direction: column;
+      i {
+        height: px-to-rem(12);
+        font-size: px-to-rem(8);
+        display: block;
+      }
+
+      .sort-active {
+        color: #4f9fff;
+      }
+    }
+  }
+
+  .fly_list_content {
+    height: calc(100% - px-to-rem(30));
+
+    &-scrollbar {
+      height: calc(100% - px-to-rem(30));
+    }
+
+    ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+      background: rgba(79, 159, 255, 0.4);
+    }
+
+    .picture {
+      display: flex;
+      text-align: right;
+
+      .img {
+        display: block;
+        width: px-to-rem(20);
+        height: px-to-rem(20);
+        margin-right: px-to-rem(6);
+      }
+    }
+
+    .empty-text {
+      color: #e8f3fe;
+      font-size: px-to-rem(16);
+      text-align: center;
+
+      &:before {
+        content: url('~@component-gallery/assets/image/ar-label/noData.png');
+        display: block;
+      }
+    }
+
+    .active {
+      background-image: url('../../img/task_list_active.svg');
+      background-size: 100% 100%;
+    }
+
+    .loading-datas {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 100%;
+      flex-direction: column;
+      font-size: px-to-rem(14);
+      color: #ffffff;
+
+      span {
+        margin-top: px-to-rem(12);
+      }
+    }
+
+    .loading-datas i {
+      font-size: px-to-rem(28);
+    }
+  }
+
+  @include themeify(false) {
+    @if $theme-name == 'theme-aquamarine' {
+      .search-input {
+        ::v-deep .el-input {
+          .el-input__inner {
+            background: rgba(2, 137, 109, 0.2);
+            border-color: transparent;
+            color: #ffffff;
+
+            &:hover,
+            &:focus {
+              border-color: rgba(2, 137, 109, 1);
+            }
+
+            &::placeholder {
+              color: rgba(255, 255, 255, 0.7);
+            }
+          }
+
+          .el-input-group__append {
+            background: rgba(2, 137, 109, 0.2);
+            border-color: transparent;
+            color: #ffffff;
+          }
+        }
+      }
+
+      .fly_list_content {
+        ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+          background-color: rgb(2 137 109 / 40%);
+        }
+      }
+    }
+
+    @if $theme-name == 'theme-terracotta' {
+      .search-input {
+        ::v-deep .el-input {
+          .el-input__inner {
+            background: rgba(100, 86, 46, 0.4);
+            color: #e4e7c1;
+            border-color: #ffeeb1;
+
+            &:hover,
+            &:focus {
+              border-color: #ffeeb1;
+            }
+
+            &::placeholder {
+              color: rgba(228, 231, 193, 0.7);
+            }
+          }
+
+          .el-input-group__append {
+            background: rgba(100, 86, 46, 0.4);
+            color: #e4e7c1;
+            border-color: #ffeeb1;
+          }
+        }
+      }
+      .fly_list_content {
+        ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+          background-color: rgba(255, 238, 177, 0.4);
+        }
+      }
+    }
+  }
+}
+</style>

+ 163 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyListCard.vue

@@ -0,0 +1,163 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行结果列表卡片
+ -->
+<template>
+  <div class="fly-list-card" @click="cardClick(itemData)">
+    <img class="list_bg" src="../../img/air_line_list_bg.svg" alt="" />
+    <div class="title">
+      <div class="title-left">
+        <img class="img" src="../../img/fly-list-card-title.svg" />
+        <span :c-tip="`${itemData?.planTaskName}`">
+          {{ itemData?.planTaskName }}
+        </span>
+      </div>
+      <span
+        :c-tip="'删除'"
+        class="iconfont icon-tongyong_icon_shipinguanliliebiaoshanchu title-right"
+        @click.stop="deleteData(itemData)"
+      ></span>
+    </div>
+    <div class="bottom">
+      <div class="time">
+        {{ formatTimeComputed(itemData.startTime) }}
+      </div>
+      <div class="picture">
+        <slot :temData="itemData"></slot>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import dayjs from 'dayjs'
+
+export default {
+  name: 'FightListCard',
+  props: {
+    itemData: {
+      type: Object,
+      default: function () {
+        return {
+          name: '',
+          time: ''
+        }
+      }
+    }
+  },
+  computed: {
+    /**
+     * 格式化时间
+     * @returns {function(*): *}
+     */
+    formatTimeComputed() {
+      return (time) => {
+        return dayjs(time).format('YYYY-MM-DD HH:mm')
+      }
+    }
+  },
+  methods: {
+    /**
+     * 删除
+     * @param val
+     */
+    deleteData(val) {
+      this.$emit('delete', val)
+    },
+    /**
+     * 点击卡片
+     * @param val
+     */
+    cardClick(val) {
+      this.$emit('cardClick', val)
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-list-card {
+  font-size: px-to-rem(16);
+  height: px-to-rem(92);
+  margin-left: px-to-rem(12);
+  margin-right: px-to-rem(12);
+  &:not(:first-child) {
+    margin-top: px-to-rem(12);
+  }
+  cursor: pointer;
+  position: relative;
+  .list_bg {
+    position: absolute;
+    display: block;
+    width: 100%;
+    height: 100%;
+    pointer-events: none;
+  }
+  .img {
+    display: block;
+    width: px-to-rem(20);
+    height: px-to-rem(20);
+    margin-right: px-to-rem(6);
+  }
+  .title {
+    display: flex;
+    justify-content: space-between;
+    padding-top: px-to-rem(16);
+    margin-left: px-to-rem(12);
+    .title-left {
+      display: flex;
+      align-items: center;
+      width: calc(100% - px-to-rem(20));
+      position: relative;
+
+      span {
+        display: block;
+        width: calc(100% - px-to-rem(20));
+        font-family: PingFangSC, PingFang SC;
+        font-weight: 500;
+
+        color: #e8f3fe;
+        text-shadow: 0 0 px-to-rem(2) rgba(74, 141, 254, 0.7);
+        text-align: left;
+        font-style: normal;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+      &::after {
+        content: '';
+        width: 100%;
+        height: px-to-rem(10);
+        margin: 0 px-to-rem(-12);
+        position: absolute;
+        z-index: -1;
+        background: #2985f4;
+        opacity: 0.63;
+        filter: blur(6px);
+      }
+    }
+    .title-right {
+      font-size: px-to-rem(20);
+      color: #e8f3fe;
+    }
+  }
+  .bottom {
+    display: flex;
+    justify-content: space-between;
+    height: px-to-rem(16);
+    font-size: px-to-rem(16);
+    color: rgba(232, 243, 254, 0.7);
+    text-align: left;
+    margin-top: px-to-rem(20);
+    margin-left: px-to-rem(12);
+    .time {
+      width: px-to-rem(162);
+    }
+    .picture {
+      display: flex;
+      justify-content: end;
+    }
+  }
+}
+</style>

+ 644 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyMap.vue

@@ -0,0 +1,644 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/26
+ * @Description: 地图展示
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div class="fly-map">
+    <!--  切换 tab -->
+    <fly-img-thumbnail-tab
+      :activeRight="activeRight"
+      isMap
+      :activeLeftCode="activeLeftCode"
+      :tabType="`${tabTypeComputed}`"
+      :downloadLoading="downloadLoading"
+      :activeImgAttStatus.sync="activeImgAttStatus"
+      @activeRight="activeRightBtn"
+      @activeLeft="activeLeftBtn"
+      v-if="hasDataAll"
+      :thumbnailOne="dialogShow"
+    />
+    <div class="fly-map-box" :class="[!hasDataAll && 'fly-map-box-max']">
+      <!--      :listenGlobalEvent="false"-->
+      <div class="fly-map-content fly-map-content-wrap" id="domId">
+        <common-map
+          ref="commonMap"
+          :mapId="mapId"
+          chooseMapMemoryKey="FlyMap"
+          mapToolElement=".fly-map-content-wrap"
+          :resolveEventName="resolveEventName"
+          :tileModes="[2]"
+          @init-map-resolve="initMapResolve"
+          compassDomId="fly-map-compass"
+        ></common-map>
+      </div>
+    </div>
+    <dialog-fly-map
+      v-if="dialogShow"
+      :activeImgAttStatus.sync="activeImgAttStatus"
+      :dataListChildren="dataListChildren"
+      :dialogShow.sync="dialogShow"
+      :selectData="selectData"
+      @close-dialog-fly-map="closeDialogFlyMap"
+      :mapId="mapId"
+      @del-dialog-data="delDialogData"
+    ></dialog-fly-map>
+    <restricted-flight-zone
+      :mapId="mapId"
+      :right="24"
+      :bottom="152"
+      :resolveEventName="resolveEventName"
+    ></restricted-flight-zone>
+  </div>
+</template>
+<script>
+import CommonMap from '@component-gallery/map'
+import FlyImgThumbnailTab from './FlyImgThumbnailTab.vue'
+import DialogFlyMap from './DialogFlyMap.vue'
+import RestrictedFlightZone from '../../baseComponents/restrictedFlightZone/RestrictedFlightZone.vue'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import dayjs from 'dayjs'
+import {
+  getAirLineInfo,
+  queryTaskPhotos,
+  queryTaskVideos
+} from '../../service/index'
+import { mountFlyResultInfoWindow } from '../../dict/fly-result-map'
+import {
+  addOverlay,
+  getHeights,
+  getLinkLines,
+  handleAirLineAndPointData,
+  handleOrthophotoData,
+  planMapFocusArea,
+  planMapFocusEntity,
+  setAirLineAndPoint,
+  setOrthoImage
+} from '../../dict/plan-map'
+import {
+  syncHandlerApi,
+  downloadByUrlsApi,
+  deleteData,
+  groupByLocation
+} from '../../dict/fly-result-methods'
+import { debounce } from 'lodash-es'
+
+export default {
+  components: {
+    FlyImgThumbnailTab,
+    CommonMap,
+    DialogFlyMap,
+    RestrictedFlightZone
+  },
+  inject: ['mapRef'],
+  data() {
+    return {
+      activeLeftCode: 'all',
+      hasDataAll: false,
+      resolveEventName: 'flyMap-content-init-map-resolve', // 禁飞区
+      mapId: 'fly-map', //小地图
+      activeRight: ['sync', 'download', 'del'], //右上角激活
+      mapInstance: {}, //地图绘制内容用于清空地图内容
+      dataList: [], //数据
+      dialogShow: false, //弹窗
+      activeImgAttStatus: true, //图片属性
+      downloadLoading: false, //下载状态
+      dataListChildren: [] //选中子集数据
+    }
+  },
+  props: {
+    selectData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  computed: {
+    /**
+     * 选中任务列表数据
+     * @returns {*}
+     */
+    taskListDataComputed() {
+      return { ...this.selectData.listData }
+    },
+    /**
+     * 选中树节点数据
+     * @returns {*}
+     */
+    treeNodeComputed() {
+      return {
+        ...this.selectData.treeNode
+      }
+    },
+    tabTypeComputed() {
+      return this.selectData.type
+    }
+  },
+  watch: {
+    /**
+     * 监听任务列表数据变化
+     */
+    taskListDataComputed: {
+      handler(newVal, oldVal) {
+        this.clearDraw()
+        if (newVal.taskRecordId) {
+          this.init()
+        } else {
+          this.dataList = []
+        }
+      }
+    }
+  },
+  methods: {
+    initMapResolve: debounce(function (val) {
+      this.init()
+    }),
+    init() {
+      this.dataList = []
+      this.activeLeftCode = 'all'
+      this.getDataFormApi()
+    },
+    activeLeftBtn(val) {
+      this.clearDraw('mountInfoWindowInstance')
+      this.clearDraw('overlayInstance')
+      this.activeLeftCode = val.code
+      this.getDataFormApi(false)
+    },
+    /**
+     * 获取数据
+     */
+    async getDataFormApi(flag = true) {
+      this.dataList = []
+      if (!this.taskListDataComputed.taskRecordId) {
+        return
+      }
+      let params = {
+        taskRecordId: this.taskListDataComputed.taskRecordId,
+        accessNode: this.treeNodeComputed?.accessNode,
+        limit: 999999,
+        page: 1,
+        picType: this.activeLeftCode
+      }
+      if (this.activeLeftCode === 'all') {
+        delete params.picType
+      }
+      if (this.tabTypeComputed === 'img') {
+        await this.getMapPhotoListApi(params)
+      } else {
+        await this.queryTaskVideosApi(params)
+      }
+
+      flag && (await this.getAirLineInfo())
+      await this.setInfoWindow()
+    },
+    /**
+     * 获取图片数据
+     */
+    async getMapPhotoListApi(params) {
+      try {
+        this.dataList = []
+        const res = await queryTaskPhotos(params)
+        if (res.code === 200) {
+          if (this.activeLeftCode === 'all') {
+            this.hasDataAll = res.rows.length > 0
+          }
+          this.dataList = groupByLocation(res.rows, this.tabTypeComputed)
+        }
+      } catch (e) {
+        CommonMessage.error(e?.msg)
+      }
+    },
+    /**
+     * 获取视频数据
+     */
+    async queryTaskVideosApi(params) {
+      try {
+        this.dataList = []
+        const res = await queryTaskVideos(params)
+        if (res.code === 200) {
+          if (this.activeLeftCode === 'all') {
+            this.hasDataAll = res.rows.length > 0
+          }
+          this.dataList = groupByLocation(res.rows, this.tabTypeComputed)
+        }
+      } catch (e) {
+        CommonMessage.error(e?.msg)
+      }
+    },
+    async getAirLineInfo() {
+      const params = {
+        snapshotId: this.taskListDataComputed.snapshotId,
+        flightRouteId: this.taskListDataComputed.flightRouteId,
+        uavNestCode: this.treeNodeComputed.deviceCode,
+        uavCode: this.treeNodeComputed.deviceCode
+      }
+      // 调用获取航线详情的接口
+      const res = await getAirLineInfo(params)
+      if (res.code !== 200) {
+        return
+      }
+      this.clearDraw()
+      const uavNestPoint = [
+        +res.data.uavNestLongitude,
+        +res.data.uavNestLatitude
+      ]
+      const currentLine = res.data
+
+      const uavPoint = [+res.data.uavLongitude, +res.data.uavLatitude]
+      // 根据currentLine.uavNestLongitude是否存在来选择uavNestPoint或uavPoint
+      let orthoUavPoint = currentLine.uavNestLongitude ? uavNestPoint : uavPoint
+      orthoUavPoint.push(+this.treeNodeComputed.altitude)
+      const type = currentLine.type
+      this.airLineType = type
+      // type 为 1 航点航线  type 为 2 正射影像
+      if (!currentLine?.kmzJson) {
+        this.isShowAirLineCardStatus = -1
+        this.isShowAirLineCard = false
+        return
+      }
+      // 获取地图实例
+      let mapRef = this.mapRef.getMapRef(this.mapId)
+      // 绘制的线会穿透地形,需要关闭地形深度检测,就会把多余的航线隐藏,需要在页面
+      //beforeDestroy()中重置
+      mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = true
+      if (type === '1') {
+        // 处理航线和点线的数据
+        this.handleAirLineAndPointLineData(
+          currentLine,
+          this.treeNodeComputed,
+          orthoUavPoint,
+          mapRef
+        )
+      }
+      if (type === '2') {
+        // 处理正射影像线的数据
+        this.handleOrthophotoLineData(
+          currentLine,
+          this.treeNodeComputed,
+          orthoUavPoint,
+          mapRef
+        )
+      }
+    },
+    /**
+     * 处理航点航线的数据
+     * @param currentLine 当前航线数据
+     * @param currentNode 当前设备数据
+     * @param orthoUavPoint 机巢/无人机坐标
+     * @param mapRef 地图实例
+     */
+    async handleAirLineAndPointLineData(
+      currentLine,
+      currentNode,
+      orthoUavPoint,
+      mapRef
+    ) {
+      // 处理航点航线的数据
+      const { linePoints, lineInfo } = handleAirLineAndPointData(currentLine)
+      // 将线点坐标转换为经纬度格式
+      const positions = linePoints.map((item) => {
+        return { lng: Number(item[0]), lat: Number(item[1]) }
+      })
+      // 获取第一个线点的高度值
+      const heightVal = linePoints[0][2]
+      // 将kmzJson格式的字符串转换为json对象
+      const airLineJson = JSON.parse(currentLine.kmzJson)
+      // 获取高程信息
+      const { heightRes } = await getHeights(
+        positions,
+        airLineJson,
+        mapRef,
+        heightVal,
+        currentNode.altitude
+      )
+      // 根据高度信息对线点进行处理
+      let newLinePoints = linePoints.map((item, index) => {
+        let linePoint_ = heightRes[index]
+        return [Number(item[0]), Number(item[1]), Number(linePoint_['ASLT'])]
+      })
+      // 创建线点数组
+      let points = [orthoUavPoint, ...newLinePoints, orthoUavPoint]
+      // 起飞点到首航点坐标
+      const firstPoints = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[0],
+        heightRes
+      )
+      // 返航点坐标到起飞点坐标
+      const lastPoints = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[positions.length - 1],
+        heightRes
+      )
+      const linkLines = firstPoints.concat(lastPoints)
+      // 绘制航线航点
+      const airLineAndPointMap = setAirLineAndPoint(
+        mapRef,
+        points,
+        lineInfo,
+        linkLines,
+        false
+      )
+      // 创建聚焦区域点数组
+      let focusAreaPonits = linePoints
+      focusAreaPonits.push([
+        orthoUavPoint[0],
+        orthoUavPoint[1],
+        orthoUavPoint[2]
+      ])
+      this.mapInstance = {
+        ...this.mapInstance,
+        ...airLineAndPointMap
+      }
+      // 设置聚焦区域
+      planMapFocusArea(focusAreaPonits, mapRef)
+    },
+    /**
+     * 处理正射影响的数据
+     * @param currentLine 当前航线数据
+     * @param currentNode 当前设备数据
+     * @param orthoUavPoint 机巢/无人机坐标
+     * @param mapRef 地图实例
+     */
+    async handleOrthophotoLineData(
+      currentLine,
+      currentNode,
+      orthoUavPoint,
+      mapRef
+    ) {
+      // 处理航迹数据
+      const { linePoints, polygonPoints, lineInfo } =
+        handleOrthophotoData(currentLine)
+      // 生成经纬度数组
+      const positions = linePoints.map((item) => {
+        return { lng: Number(item[0]), lat: Number(item[1]) }
+      })
+      // 获取高度值
+      const heightVal = linePoints[0][2]
+      // 解析当前航迹为JSON对象
+      const airLineJson = JSON.parse(currentLine.kmzJson)
+      // 获取高度信息
+      const { heightRes } = await getHeights(
+        positions,
+        airLineJson,
+        mapRef,
+        heightVal,
+        currentNode.altitude
+      )
+      // 生成新的航迹点数组
+      let newLinePoints = linePoints.map((item, index) => {
+        // 获取对应位置的高度信息
+        let linePoint_ = heightRes[index]
+        return [Number(item[0]), Number(item[1]), Number(linePoint_['ASLT'])]
+      })
+      // 获取连接线数组
+      const linkLines = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[0],
+        heightRes
+      )
+      // 设置正射影像地图
+      const orthoImageMap = setOrthoImage(
+        mapRef,
+        newLinePoints,
+        [polygonPoints],
+        orthoUavPoint,
+        lineInfo,
+        linkLines
+      )
+      this.mapInstance = {
+        ...this.mapInstance,
+        ...orthoImageMap
+      }
+      let entity =
+        orthoImageMap.orthoAirLineInstance0._dataSource._entityCollection
+      // 规划地图聚焦区域
+      planMapFocusEntity(entity, mapRef)
+    },
+    /**
+     * 点击地图弹窗
+     * @param val
+     */
+    clickImg(val) {
+      this.activeRight = ['sync', 'download', 'del']
+      this.dataListChildren = []
+      this.dataList.forEach((item, index) => {
+        if (item.id === val.id) {
+          item.select = true
+          this.dataListChildren = item.children
+          this.activeRight = ['sync', 'algorithm', 'download', 'del']
+        } else {
+          item.select = false
+        }
+      })
+      this.dialogShow = true
+      this.activeImgAttStatus = true
+    },
+    /**
+     * 展示弹窗
+     */
+    async setInfoWindow() {
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = true
+      for (let i = 0; i < this.dataList.length; i++) {
+        let domId = `mount_id_${i}`
+        this.mapInstance[`mountInfoWindowInstance${i}`] =
+          mountFlyResultInfoWindow(domId, this.dataList[i], this.clickImg)
+        this.mapInstance[`overlayInstance${i}`] = await addOverlay(
+          mapRef,
+          [
+            +this.dataList[i].longitude,
+            +this.dataList[i].latitude,
+            +this.dataList[i].altitude
+          ],
+          domId,
+          [0, -16],
+          'bottom-center'
+        )
+      }
+    },
+
+    /**
+     * 清除绘制图形
+     */
+    clearDraw(keyStartsWith) {
+      this.dialogShow = false
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      const keysList = Object.keys(this.mapInstance)
+      if (keyStartsWith) {
+        keysList.forEach((key) => {
+          if (key.startsWith(keyStartsWith)) {
+            try {
+              this.mapInstance[key]?.customRemove(mapRef, this.mapInstance[key])
+            } catch (e) {
+              console.log('e======>', e)
+            }
+            this.mapInstance[key] = null
+          }
+        })
+      } else {
+        keysList.forEach((key) => {
+          if (this.mapInstance[key]) {
+            try {
+              this.mapInstance[key]?.customRemove(mapRef, this.mapInstance[key])
+            } catch (e) {
+              console.log('e======>', e)
+            }
+            this.mapInstance[key] = null
+          }
+        })
+        this.mapInstance = {}
+      }
+    },
+    /**
+     *  同步数据源
+     */
+    syncHandler() {
+      let params = {
+        taskRecordId: this.taskListDataComputed.taskRecordId,
+        deviceCode: this.treeNodeComputed.deviceCode,
+        accessNode: this.treeNodeComputed?.accessNode,
+        mediaSyncStatus: this.taskListDataComputed.mediaSyncStatus
+      }
+      syncHandlerApi(params)
+    },
+    /**
+     * 下载
+     */
+    async downloadHandler() {
+      let params = {
+        fileName: `${this.taskListDataComputed.planTaskName}-${
+          this.treeNodeComputed.flyName
+        }_${dayjs(this.taskListDataComputed.startTime).format(
+          'YYYYMMDDHHmm'
+        )}-${this.tabTypeComputed === 'img' ? '图片' : '视频'}.zip`,
+        [this.tabTypeComputed === 'img' ? 'photoUrls' : 'videoUrls']:
+          this.dataList
+            .map((item) => item.children.map((item) => item.url))
+            .flat(),
+        isZip: 1
+      }
+      this.downloadLoading = true
+      await downloadByUrlsApi(params)
+      this.downloadLoading = false
+    },
+    /**
+     * 删除航线下所有图片
+     * @returns {Promise<void>}
+     */
+    async delHandler() {
+      const params = {
+        dataType: this.tabTypeComputed === 'img' ? 'photo' : 'video',
+        taskRecordId: this.taskListDataComputed.taskRecordId,
+        accessNode: this.treeNodeComputed?.accessNode,
+        id: this.dataListChildren.map((item) => item.id)
+      }
+      const res = await deleteData(
+        params,
+        this.tabTypeComputed === 'img'
+          ? '是否要删除地图航线轨迹的全部图片?'
+          : '是否要删除地图航线轨迹的全部视频?'
+      )
+      if (res.code !== 200) {
+        return
+      }
+      CommonMessage.success('删除成功')
+      this.init()
+    },
+    /**
+     * 点击顶部操作区
+     * @param val
+     */
+    activeRightBtn(val) {
+      const fnObject = {
+        sync: this.syncHandler,
+        download: this.downloadHandler,
+        del: this.delHandler
+      }
+      fnObject[val.code] && fnObject[val.code](val)
+    },
+    /**
+     * 关闭弹窗
+     */
+    closeDialogFlyMap() {
+      this.dialogShow = false
+      this.dataList.forEach((item) => {
+        item.select = false
+      })
+      this.activeRight = ['sync', 'download', 'del']
+    },
+    /**
+     * 删除图片刷新数据
+     */
+    delDialogData() {
+      this.init()
+    }
+  },
+  beforeDestroy() {
+    let mapRef = this.mapRef.getMapRef(this.mapId)
+    mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = false
+    this.clearDraw()
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.fly-map {
+  @include themeify(false) {
+    .fly-map-box {
+      height: px-to-rem(806);
+      &.fly-map-box-max {
+        height: px-to-rem(862);
+      }
+      position: relative;
+      overflow: hidden;
+      z-index: 1;
+      .fly-map-content {
+        height: 100%;
+        ::v-deep .map-tools {
+          display: block;
+          position: absolute;
+          bottom: 0;
+          .zoom-tool {
+            left: unset;
+            right: px-to-rem(12);
+            bottom: px-to-rem(56);
+          }
+
+          .compass-tool {
+            position: absolute;
+            right: px-to-rem(30);
+            bottom: px-to-rem(34);
+          }
+          .scale-line {
+            left: unset;
+            right: px-to-rem(12);
+            bottom: px-to-rem(12);
+            .ctmap-union-scale-line-inner {
+              width: px-to-rem(86) !important;
+            }
+          }
+          .tile-control {
+            position: absolute;
+            right: px-to-rem(56);
+            bottom: px-to-rem(56);
+            display: none;
+            .ctmap-union-layer-switcher {
+              width: px-to-rem(88) !important;
+              padding: 0 px-to-rem(4) 0 px-to-rem(3);
+              &:hover {
+                width: px-to-rem(339) !important;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 172 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyPagination.vue

@@ -0,0 +1,172 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/25
+ * @Description: 分页组件
+ -->
+<template>
+  <div class="fly-pagination">
+    <el-pagination
+      :layout="layout"
+      :total="total"
+      small
+      :current-page="currentPage"
+      :page-size="pageSize"
+      :pageSizes="pageSizes"
+      @current-change="currentChange"
+      @size-change="sizeChange"
+      popper-class="fly-pagination-popper-class"
+    >
+      <slot>
+        <span class="other-info">第{{ currentPage }}页/共{{ total }}条</span>
+      </slot>
+    </el-pagination>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    total: {
+      type: Number,
+      default: 0
+    },
+    pageSize: {
+      type: Number,
+      default: 10
+    },
+    currentPage: {
+      type: Number,
+      default: 1
+    },
+    layout: {
+      type: String,
+      default: 'slot,prev, pager, next'
+    },
+    pageSizes: {
+      type: Array,
+      default: () => [12, 24, 36, 48]
+    }
+  },
+  methods: {
+    currentChange(val) {
+      this.$emit('update:currentPage', val)
+      this.$emit('changePagination', {
+        pageSize: this.pageSize,
+        currentPage: this.currentPage
+      })
+    },
+    sizeChange(val) {
+      this.$emit('update:pageSize', val)
+      this.$emit('update:currentPage', 1)
+      this.$emit('changePagination', {
+        pageSize: this.pageSize,
+        currentPage: this.currentPage
+      })
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-pagination {
+  .other-info {
+    color: #e8f2fe;
+    font-size: px-to-rem(16);
+  }
+
+  ::v-deep {
+    .el-pagination {
+      .el-pager {
+        li {
+          background: transparent;
+          color: #e8f2fe;
+          font-size: px-to-rem(16);
+          &.active {
+            background: #4f9fff;
+            border-radius: px-to-rem(4);
+          }
+        }
+      }
+
+      .btn-prev,
+      .btn-next {
+        padding: 0;
+        .el-icon {
+          font-size: px-to-rem(16);
+        }
+      }
+      .el-pagination__total,
+      .el-pagination__jump {
+        font-size: px-to-rem(16);
+        color: #e8f2fe;
+      }
+      .el-pagination__total {
+        padding-left: px-to-rem(6);
+      }
+      .el-pagination__jump {
+        margin-left: px-to-rem(6);
+      }
+      .el-pagination__sizes {
+        margin-right: 0;
+      }
+      button {
+        background: transparent;
+        color: #e8f2fe;
+      }
+      button:disabled {
+        color: rgba(232, 242, 254, 0.4);
+      }
+      .el-input__inner {
+        color: #e8f2fe;
+        font-size: px-to-rem(16);
+        background: rgba(79, 159, 255, 0.2);
+        border-color: transparent;
+        margin-top: px-to-rem(-4);
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.fly-pagination-popper-class {
+  border: none;
+  border-radius: px-to-rem(8);
+  box-shadow: none;
+  background: #0f1926;
+  min-width: px-to-rem(100) !important;
+  .popper__arrow {
+    display: none;
+  }
+  .el-scrollbar {
+    border-radius: px-to-rem(8);
+  }
+  .el-select-dropdown__wrap {
+    color: #e8f3fe;
+    overflow: hidden;
+    //width: px-to-rem(120);
+    border: none;
+    border-radius: px-to-rem(8);
+  }
+  .el-select-dropdown__item {
+    color: #e8f3fe;
+    background-color: transparent;
+    position: relative;
+    overflow: hidden;
+    //padding: 0 px-to-rem(20) 0 px-to-rem(10);
+    height: px-to-rem(34);
+    font-size: px-to-rem(16) !important;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    line-height: px-to-rem(34);
+  }
+  .el-select-dropdown__item:hover {
+    background: rgba(79, 159, 255, 0.4);
+    color: #e8f3fe;
+  }
+  .el-select-dropdown__item.selected {
+    background: rgba(79, 159, 255, 0.4);
+    color: #4f9fff;
+  }
+}
+</style>

+ 47 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyResult.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="fly_result">
+    <fly-tab @active="activeBtn" />
+    <fly-img v-if="componentActive === 'img'" :type="componentActive" />
+    <fly-video v-if="componentActive === 'video'" :type="componentActive" />
+  </div>
+</template>
+
+<script>
+import FlyTab from './FlyTab.vue'
+import FlyImg from './FlyImg.vue'
+import FlyVideo from './videoResoult/FlyVideo.vue'
+import { topTabList } from '../../entry/data'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+
+export default {
+  components: {
+    FlyTab,
+    FlyImg,
+    FlyVideo
+  },
+  data() {
+    return {
+      componentActive: topTabList[0].code
+    }
+  },
+  mounted() {
+    setupCTips()
+  },
+  methods: {
+    activeBtn(val) {
+      this.componentActive = val.code
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '../../style/fly-result';
+.fly_result {
+  margin-left: px-to-rem(24);
+}
+</style>
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/c-tip';
+</style>

+ 124 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyResultInfoWindow.vue

@@ -0,0 +1,124 @@
+<template>
+  <div
+    class="fly-map-info"
+    :class="[info.select && 'fly-map-info-active']"
+    @click="clickInfo"
+  >
+    <div class="fly-map-info-content">
+      <div class="num" :class="[info.select && 'num-active']">
+        {{ info.num }}
+      </div>
+      <img v-if="info.tabType === 'img'" class="img" :src="info.url" alt="" />
+      <div v-else class="video-box">
+        <video class="video" :src="info.url"></video>
+        <span class="playIcon"></span>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    info: {
+      type: Object
+    }
+  },
+  data() {
+    return {}
+  },
+  methods: {
+    /**
+     * 点击图片
+     */
+    clickInfo() {
+      this.$emit('clickImg', this.info)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.fly-map-info {
+  width: px-to-rem(110);
+  height: px-to-rem(62);
+  position: relative;
+  background-image: url('../../img/flyResult/map-info-bg.svg');
+  background-size: 100%;
+  border-radius: px-to-rem(4);
+  padding: px-to-rem(4);
+  &.fly-map-info-active {
+    background-image: url('../../img/flyResult/map-info-bg-active.svg');
+    &:after {
+      background-image: url('../../img/flyResult/map-win-active.svg');
+    }
+  }
+  &-content {
+    width: 100%;
+    height: 100%;
+    .num {
+      width: px-to-rem(33);
+      height: px-to-rem(22);
+      line-height: px-to-rem(22);
+      position: absolute;
+      right: 0;
+      top: 0;
+      background-image: url('../../img/flyResult/map-win-num.svg');
+      background-size: 100% 100%;
+      background-repeat: no-repeat;
+      font-weight: 600;
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      text-shadow: 0 px-to-rem(2) px-to-rem(4) #af5c00;
+      text-align: center;
+      z-index: 1;
+      &.num-active {
+        background-image: url('../../img/flyResult/map-win-num-active.svg');
+      }
+    }
+    .img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+    .video {
+      display: block;
+      width: 100%;
+      height: 100%;
+      border-radius: px-to-rem(5);
+      object-fit: cover;
+    }
+    .video-box {
+      width: 100%;
+      height: 100%;
+      position: relative;
+    }
+    .playIcon {
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+      position: absolute;
+      z-index: 999;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      display: block;
+      background: url('../../img/icon_play_ty@2x.png') no-repeat;
+      background-size: cover;
+    }
+  }
+  &:after {
+    position: absolute;
+    display: block;
+    width: px-to-rem(52);
+    height: px-to-rem(70);
+    z-index: -1;
+    content: '';
+    background-image: url('../../img/flyResult/map-win.svg');
+    background-size: 100% 100%;
+    bottom: px-to-rem(-40);
+    left: calc((px-to-rem(110) - px-to-rem(52)) / 2);
+  }
+}
+</style>

+ 108 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyRightTopTab.vue

@@ -0,0 +1,108 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行结果地图缩略图 tab
+ -->
+<template>
+  <div class="fly-right-top-tab">
+    <span
+      v-for="(item, index) in rightTopTab"
+      :key="item.code"
+      :class="['item-tab', currentIndex == index && 'active']"
+      @click="activeBtn(item, index)"
+    >
+      <img
+        class="img"
+        v-if="currentIndex == index"
+        src="../../img/tab_active.png"
+        alt=""
+      />
+      {{ item.name }}
+    </span>
+    <div class="fly-right-top-tab-line"></div>
+  </div>
+</template>
+<script>
+export default {
+  data() {
+    return {
+      currentIndex: 0
+    }
+  },
+  props: {
+    rightTopTab: {
+      type: Array,
+      default: () => {
+        return [
+          {
+            name: '缩图展示',
+            code: 'img'
+          },
+          {
+            name: '地图展示',
+            code: 'map'
+          }
+        ]
+      }
+    }
+  },
+  methods: {
+    activeBtn(val, index) {
+      if (val.disabled) {
+        return
+      }
+      this.currentIndex = index
+      this.$emit('active', val)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-right-top-tab {
+  width: 100%;
+  display: flex;
+  position: relative;
+  .item-tab {
+    width: px-to-rem(124);
+    height: px-to-rem(48);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-family: PingFangSC, PingFang SC;
+    font-weight: 400;
+    font-size: px-to-rem(18);
+    color: rgba(232, 243, 254, 0.7);
+    text-align: center;
+    font-style: normal;
+    cursor: pointer;
+  }
+  .active {
+    color: #e8f3fe;
+    text-shadow: 0 0 px-to-rem(10) rgba(74, 141, 254, 0.7);
+    position: relative;
+
+    .img {
+      width: 100%;
+      position: absolute;
+      left: 0;
+      bottom: 0;
+    }
+  }
+  &-line {
+    width: 100%;
+    height: px-to-rem(1);
+    position: absolute;
+    bottom: px-to-rem(1);
+    background: linear-gradient(
+      270deg,
+      rgba(176, 212, 255, 0) 0%,
+      #b0d4ff 52%,
+      rgba(176, 212, 255, 0) 100%
+    );
+    opacity: 0.35;
+  }
+}
+</style>

+ 82 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyTab.vue

@@ -0,0 +1,82 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行结果图片视频切换 tab
+ -->
+<template>
+  <div class="fly-results-tab">
+    <div
+      class="list-tab"
+      @click="activeBtn(item)"
+      v-for="item in topTabList"
+      :key="item.code"
+    >
+      <img
+        class="icon_img"
+        :src="currentCode === item.code ? item.activeIcon : item.icon"
+        alt=""
+      />
+      <div
+        class="name"
+        :style="{
+          textShadow: `0 ${pxToRem(1)} ${pxToRem(4)} ${item.textShadowColor}`
+        }"
+      >
+        {{ item.name }}
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { topTabList } from '../../entry/data'
+
+export default {
+  name: 'fly-results-tab',
+  data() {
+    return {
+      topTabList,
+      currentCode: topTabList[0].code
+    }
+  },
+  methods: {
+    pxToRem(px) {
+      return `${px / 100}rem`
+    },
+    activeBtn(val) {
+      this.currentCode = val.code
+      this.$emit('active', val)
+    }
+  }
+}
+</script>
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.fly-results-tab {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr); /* 两列网格 */
+  grid-gap: px-to-rem(12);
+  width: px-to-rem(424);
+  height: px-to-rem(64);
+  margin-bottom: px-to-rem(12);
+  margin-top: px-to-rem(23);
+  .list-tab {
+    color: #e8f3fe;
+    height: px-to-rem(64);
+    position: relative;
+    cursor: pointer;
+    .icon_img {
+      width: 100%;
+      height: 100%;
+      position: absolute;
+    }
+    .name {
+      font-weight: 600;
+      font-size: px-to-rem(18);
+      position: absolute;
+      right: px-to-rem(28);
+      bottom: px-to-rem(17);
+    }
+  }
+}
+</style>

+ 317 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/FlyTree.vue

@@ -0,0 +1,317 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/4
+ * @Description: 飞行结果左侧树
+ -->
+<template>
+  <div class="leftCanera-box fly-tree-zj">
+    <div class="leftMenu-searchInfo leftth">
+      <div class="searchInfo">
+        <div class="label-tree">
+          <base-virtual-tree
+            ref="flyTree"
+            class="tree-fixed-first-level"
+            :data="flyTreeData"
+            v-if="cameraTreeOpenSatus !== null && flyTreeData"
+            node-key="code"
+            :default-expanded-keys="defaultShowNodes"
+            :expand-on-click-node="false"
+            :default-expand-all="cameraTreeOpenSatus"
+            :current-node-key="currentNodeKey"
+            :filter-node-method="filterNode"
+            :props="defaultProps"
+            @node-click="chooseCameraItem"
+            :indent="6"
+          >
+            <template v-slot="{ node, data }">
+              <!--span中的样式只是为了解决node没有被使用,eslint过不去的问题,没起其他实质性作用-->
+              <span
+                class="custom-tree-node"
+                :class="node ? 'mei-sha-yong' : ''"
+              >
+                <span v-if="data.firstRow" class="fast-btn1">
+                  <i
+                    class="iconfont_tools icon-jilianicon"
+                    @click.stop="onAndOff"
+                  ></i>
+                  <i
+                    class="iconfont_tools icon-shuaxinicon menu-wh"
+                    @click.stop="getTree()"
+                  ></i>
+                </span>
+                <span
+                  style="display: flex"
+                  class="custom-tree-node"
+                  :c-tip="data.name"
+                >
+                  <ct-icon
+                    class="icon-class"
+                    v-if="data.deviceCode"
+                    :name="data.uavNestCode ? 'drone-airport' : 'uav'"
+                  />
+                  <font
+                    class="node-name"
+                    style="pointer-events: none; width: calc(100% - 2em)"
+                    v-html="$options.filters.filterDiscolour(data)"
+                  />
+                </span>
+              </span>
+            </template>
+          </base-virtual-tree>
+          <div v-if="flyTreeData === null" class="loading-datas">
+            <i class="el-icon-loading"></i>
+            <span>加载中</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getDeviceAreaTree } from '../../service/index'
+import { Input, Tree } from 'element-ui'
+import BaseVirtualTree from '@component-gallery/base-components/base-virtual-tree/BaseVirtualTree.vue'
+import { queryDefultUav } from '../../service/index'
+
+export default {
+  name: 'device-tree',
+  components: {
+    [Input.name]: Input,
+    [Tree.name]: Tree,
+    [BaseVirtualTree.name]: BaseVirtualTree
+  },
+  data() {
+    return {
+      // 设备树
+      flyTreeData: null,
+      defaultProps: {
+        children: 'list',
+        label: 'name'
+      },
+      currentNodeKey: '',
+      cameraTreeOpenSatus: true, //是否展开视频树
+      defaultShowNodes: [],
+      requestId: false,
+      choosedTiming: null
+    }
+  },
+  mounted() {
+    // setupCTips()
+    this.getTree(true)
+  },
+  filters: {
+    filterDiscolour(data) {
+      let value = data.name
+      if (!data.flyCode) {
+        value = `${data.name ?? ''}(${data.onlineNum}/${data.num})`
+      }
+      return `<span
+                style=" width: 100%;
+                display: block;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                color: ${
+                  ['1', '2'].includes(data.nodeStatus) &&
+                  'rgba(232,243,254,0.5)'
+                };
+                white-space: nowrap;">
+                    ${value ?? ''}
+            </span`
+    }
+  },
+  methods: {
+    /**
+     * 获取设备树
+     */
+    async getTree(flag = false) {
+      const requestId = (this.requestId = Date.now())
+      this.cameraTreeOpenSatus = true
+      this.defaultShowNodes = []
+      this.flyTreeData = null
+      let resp
+      try {
+        //物联设备树
+        resp = await getDeviceAreaTree({
+          needDevice: true,
+          orderStatus: '1',
+          queryType: 1
+        })
+        if (requestId !== this.requestId) {
+          return false
+        }
+        if (resp.code === 200) {
+          resp.data[0]['firstRow'] = true
+          this.defaultShowNodes.push(resp.data[0].code)
+          this.flyTreeData = this.convertDeviceByCode(resp.data)
+          if (flag) {
+            this.queryDefultUavApi()
+          }
+        } else {
+          this.flyTreeData = []
+        }
+      } catch (e) {
+        if (requestId !== this.requestId) {
+          return false
+        }
+      }
+    },
+    /**
+     * 获取默认选中数据
+     */
+    queryDefultUavApi() {
+      this.$emit('selectTreeData', null)
+      const nodeData = this.findDeviceByCode(this.flyTreeData)
+      if (nodeData) {
+        this.chooseCameraItem(nodeData)
+        this.currentNodeKey = nodeData.code
+        this.$refs.flyTree.setCurrentKey(this.currentNodeKey)
+      }
+    },
+    /**
+     * 处理树数据
+     * @param data
+     * @returns {*}
+     */
+    convertDeviceByCode(data) {
+      function traverse(nodes) {
+        for (let node of nodes) {
+          node['flyCode'] = node?.deviceCode
+          node['flyName'] = node?.name
+          node['isFlyUva'] = true
+          node['nodeStatus'] = node?.state
+          if (Object.prototype.hasOwnProperty.call(node, 'uavNestCode')) {
+            node.deviceCode = node.uavNestCode
+            node.code = node.uavNestCode
+            node.name = node.uavNestName
+            node['isFlyUva'] = false
+            node['nodeStatus'] = node?.uavNestStatus
+          }
+          if (
+            Object.prototype.hasOwnProperty.call(node, 'list') &&
+            Array.isArray(node.list)
+          ) {
+            traverse(node.list)
+          }
+        }
+      }
+
+      traverse(data)
+      return data
+    },
+    /**
+     * 查找默认选中数据
+     * @param data
+     * @param deviceCode
+     * @returns {*|null}
+     */
+    findDeviceByCode(data) {
+      function search(arr) {
+        for (let i = 0; i < arr.length; i++) {
+          let item = arr[i]
+          if (item.deviceCode) {
+            return item
+          }
+          //确保不是空数组
+          if (item.list?.length) {
+            // TODO 验证
+            const res = search(item.list)
+            if (res) {
+              return res
+            }
+          }
+        }
+        return null
+      }
+
+      return search(data)
+    },
+    //展开收起 TODO setTimeout status
+    onAndOff() {
+      let nodesMap = this.$refs.flyTree.store.nodesMap
+      // 判断当前项是否展开,如果展开,则将所有节点折叠到二级节点
+      for (let key in nodesMap) {
+        // 判断节点的深度是否大于等于2,将深度大于2的节点折叠
+        if (nodesMap[key].level > 1) {
+          nodesMap[key].expanded = this.cameraTreeOpenSatus ? false : true
+        }
+      }
+      this.cameraTreeOpenSatus = !this.cameraTreeOpenSatus
+    },
+
+    /**
+     * 树结点过滤
+     */
+    filterNode(value, data, node) {
+      if (!value) {
+        return true
+      }
+      let _array = [] //这里使用数组存储 只是为了存储值。
+      this.getReturnNode(node, _array, value)
+      let result = false
+      _array.forEach((item) => {
+        result = result || item
+      })
+      return result
+    },
+    getReturnNode(node, _array, value) {
+      let isPass =
+        node.data && node.data.name && node.data.name.indexOf(value) !== -1
+      if (isPass) {
+        _array.push(isPass)
+      }
+      if (!isPass && Number(node.level) != 1 && node.parent) {
+        this.getReturnNode(node.parent, _array, value)
+      }
+    },
+    /**
+     * 点击树
+     * @param data
+     * @returns {boolean}
+     */
+    chooseCameraItem(data) {
+      if (!data.deviceCode) {
+        let nodesMap = this.$refs.flyTree.store.nodesMap
+        for (let key in nodesMap) {
+          if (
+            nodesMap[key].level !== 1 &&
+            nodesMap[key].data.code === data.code
+          ) {
+            console.log('nodesMap[key]', nodesMap[key])
+            if (nodesMap[key].expanded) {
+              nodesMap[key].collapse()
+            } else {
+              nodesMap[key].expand()
+            }
+          }
+        }
+        this.$nextTick(() => {
+          this.$refs.flyTree.setCurrentKey(this.currentNodeKey)
+        })
+        return false
+      }
+      clearTimeout(this.choosedTiming)
+      this.choosedTiming = setTimeout(() => {
+        this.currentNodeKey = data.code
+        this.$emit('selectTreeData', data)
+      }, 50)
+    }
+  },
+  beforeDestroy() {
+    clearTimeout(this.choosedTiming)
+    this.choosedTiming = null
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/assets/font/iconfont.css';
+@import '../../style/fly-tree.scss';
+.searchInfo
+  .label-tree
+  ::v-deep
+  .base-virtual-tree.el-tree
+  .el-tree-node
+  .el-tree-node__content {
+  padding-right: 0;
+}
+</style>

+ 138 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/MediumViewer.vue

@@ -0,0 +1,138 @@
+<template>
+  <div
+    class="fly-result-medium-viewer"
+    :class="[mediumViewerFull && 'fly-result-medium-viewer-full']"
+  >
+    <template v-if="length >= 1">
+      <span class="prevLeft" :class="[isFirst && 'disabled']" @click="prev">
+        <i class="el-icon-arrow-left" />
+      </span>
+      <span class="nextRight" :class="[isLast && 'disabled']" @click="next">
+        <i class="el-icon-arrow-right" />
+      </span>
+    </template>
+    <div class="fly-result-medium-viewer-content">
+      <slot></slot>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  components: {},
+  props: {
+    urlList: {
+      type: Array,
+      default: () => []
+    },
+    initialIndex: {
+      type: Number,
+      default: 0
+    },
+    mediumViewerFull: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      index: this.initialIndex
+    }
+  },
+
+  computed: {
+    length() {
+      return this.urlList.length
+    },
+    isFirst() {
+      return this.index === 0
+    },
+    isLast() {
+      console.log('urlList===>', this.urlList.length - 1)
+      return this.index === this.urlList.length - 1
+    }
+  },
+  methods: {
+    /**
+     * 上一张
+     */
+    prev() {
+      if (this.isFirst) {
+        return
+      }
+      this.index = this.index - 1
+      this.$emit('update:initialIndex', this.index)
+    },
+    /**
+     * 下一张
+     */
+    next() {
+      if (this.isLast) {
+        return
+      }
+      this.index = this.index + 1
+      this.$emit('update:initialIndex', this.index)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$_size: px-to-rem(30);
+$_size_full: px-to-rem(60);
+.fly-result-medium-viewer {
+  position: relative;
+  height: 100%;
+  &.fly-result-medium-viewer-full {
+    .prevLeft,
+    .nextRight {
+      top: calc(50% - $_size_full / 2);
+      width: $_size_full;
+      height: $_size_full;
+    }
+    .prevLeft {
+      left: px-to-rem(24);
+    }
+    .nextRight {
+      right: px-to-rem(24);
+    }
+  }
+  .prevLeft {
+    left: px-to-rem(6);
+  }
+  .nextRight {
+    right: px-to-rem(6);
+  }
+  .prevLeft,
+  .nextRight {
+    position: absolute;
+    top: calc(50% - $_size / 2);
+    width: $_size;
+    height: $_size;
+    border-radius: 50%;
+    color: var(--mv--text--color);
+    z-index: 99;
+    background: var(--mv--bg--color);
+    font-size: px-to-rem(16);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+    &:hover {
+      background: var(--mv--bg--color-h);
+      color: var(--mv--text--color-h);
+    }
+    i {
+      font-weight: 600;
+    }
+  }
+  .disabled {
+    cursor: not-allowed;
+    background: var(--mv--bg--color-d);
+    color: var(--mv--text--color-d);
+    &:hover {
+      background: var(--mv--bg--color-d);
+      color: var(--mv--text--color-d);
+    }
+  }
+}
+</style>

+ 679 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/DetailLeftPop.vue

@@ -0,0 +1,679 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 无人机、机巢详情
+ -->
+<!-- eslint-disable vue/no-deprecated-filter -->
+<template>
+  <div class="left_pop" :class="[openState && 'is_open']">
+    <div class="top">
+      <div
+        class="title_item"
+        v-for="(item, index) in titleArr"
+        :key="item.code"
+        :class="[index == currentIndex && 'active']"
+        @click="currentIndex = index"
+      >
+        <span class="name">{{ item.name }}</span>
+      </div>
+      <div
+        class="change_icon"
+        :class="[openState && 'is_open']"
+        @click="handleOpen"
+      ></div>
+    </div>
+    <div class="content">
+      <div class="device_name">
+        <ct-icon
+          v-show="currentIndex == 0"
+          name="drone-airport"
+          size="20"
+        ></ct-icon>
+        <ct-icon v-show="currentIndex == 1" name="uav" size="20"></ct-icon>
+        <span class="name_val">{{
+          isDJDevice ? currentDataInfo?.uavName : currentDataInfo?.name
+        }}</span>
+      </div>
+      <!-- 机场 -->
+      <div class="device_status airport" v-show="currentIndex == 0">
+        <div
+          class="status_item"
+          v-for="(item, index) in isDJDevice
+            ? airportDeviceStatusArrDJ
+            : airportDeviceStatusArr"
+          :key="index + 'air'"
+          :c-tip="handleTips(item)"
+          c-tip-placement="top"
+          c-tip-class="c-tip-normal"
+        >
+          <template v-if="item.iconType == 'ty'">
+            <i class="iconfont_tools" :class="item.icon"></i>
+          </template>
+          <template v-else>
+            <ct-icon :name="item.icon" size="20"></ct-icon>
+          </template>
+          <div class="rate" style="" v-if="item.rate">
+            <div
+              v-if="item.rateNumber == 'getRateNumberN'"
+              class="rate_inner"
+              :style="`height: ${getRateNumberN}`"
+            ></div>
+            <div
+              v-if="item.rateNumber == 'getRateNumberA'"
+              class="rate_inner"
+              :style="`height: ${getRateNumberA}`"
+            ></div>
+            <div
+              v-else
+              class="rate_inner"
+              :style="`height: ${currentDataInfo?.[item.code] * 100}%`"
+            ></div>
+          </div>
+        </div>
+      </div>
+      <!--无人机 -->
+      <div class="device_status_drone" v-show="currentIndex == 1">
+        <div
+          class="status_item"
+          v-for="(item, index) in isDJDevice
+            ? droneDeviceStatusArrDJ
+            : droneDeviceStatusArr"
+          :key="index + 'drone'"
+          :c-tip="item.des"
+          c-tip-placement="top"
+          c-tip-class="c-tip-normal"
+        >
+          <template v-if="item.iconType == 'ty'">
+            <i class="iconfont_tools" :class="item.icon"></i>
+          </template>
+          <template v-else>
+            <ct-icon :name="item.icon" size="20"></ct-icon>
+          </template>
+          <span class="type_des" :class="item.code">{{
+            item.dics
+              ? item.dics[currentDataInfo?.[item.code]]
+              : currentDataInfo?.[item.code]
+          }}</span>
+        </div>
+      </div>
+
+      <!-- 机场详细信息 -->
+      <div class="info_cont airport" v-show="currentIndex == 0">
+        <div
+          class="info_item"
+          v-for="(item, index) in isDJDevice ? airportInfoDJ : airportInfo"
+          :key="item.code + index"
+        >
+          <div class="info_title">{{ item.name }}</div>
+          <div class="info_title_des"
+            >{{
+              item.dics
+                ? item.dics[currentDataInfo?.[item.code]] ??
+                  currentDataInfo?.[item.code]
+                : currentDataInfo?.[item.code] | resetString
+            }}
+            <span class="unit" v-if="item.unit"> {{ item.unit }} </span>
+          </div>
+        </div>
+      </div>
+      <!-- 无人机详细信息 -->
+      <div class="info_cont drone" v-show="currentIndex == 1">
+        <div
+          class="info_item"
+          v-for="(item, index) in isDJDevice ? drroneInfoDJ : drroneInfo"
+          :key="item.code + index"
+        >
+          <div class="info_title">{{ item.name }}</div>
+          <div class="info_title_des">
+            {{
+              item.dics
+                ? item.dics[currentDataInfo?.[item.code]] ??
+                  currentDataInfo?.[item.code]
+                : currentDataInfo?.[item.code] | resetString
+            }}
+            <span class="unit" v-if="item.unit"> {{ item.unit }} </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  nestConnectedMap,
+  cabinMap,
+  liftMap,
+  lift310Map,
+  aircraftPowerStateMap,
+  chargeStrMap,
+  airStateStrMap,
+  squareXMap,
+  squareYMap,
+  flightModeMap,
+  compassErrorMap,
+  liveStreamingMap,
+  cameraModeMap,
+  airLinkModeMap,
+  signalStateMap,
+  aircraftConnectedMap,
+  rtkMap,
+  rtkReadyMap,
+  rtkEnableMap,
+  isFixedMap,
+  qualityMap,
+  droneChargeMap,
+  typeMap,
+  supplementLightStateMap,
+  coverStateMap,
+  emergencyStopStateMap,
+  alarmStateMap,
+  horizonMap,
+  linkWorkmodeMap,
+  mediaStateMap,
+  usbDeviceConnectedMap,
+  gimbalPitchdMap,
+  maintenanceStateMap
+} from '../../../dict/dict'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+export default {
+  name: 'LeftPop',
+  props: {
+    currentDataInfo: {
+      type: Object,
+      default: () => ({})
+    },
+    flyDetailData: {
+      type: [Object, Array],
+      default: () => []
+    }
+  },
+  computed: {
+    isDJDevice() {
+      let info = this.flyDetailData[0]
+      return this.DJTypeArr.includes(info?.nestType)
+    },
+    isNestType310() {
+      let info = this.flyDetailData[0]
+      return info?.nestType == 310
+    }
+  },
+  filters: {
+    resetString: function (value) {
+      // console.log('filters', value)
+      if (!value && value != 0) return ''
+      value = value.toString()
+      let _i = value.indexOf('.')
+      if (_i == -1) {
+        return value
+      }
+      if (_i > -1) {
+        return (value = value.slice(0, _i + 2))
+      }
+    }
+  },
+  mounted() {
+    setupCTips()
+  },
+  data() {
+    let this_ = this
+    return {
+      DJTypeArr: [200, 210, 201], // 基站类型大疆设备
+      titleArr: [
+        { name: '机场信息', code: 'airport' },
+        { name: '无人机信息', code: 'drone' }
+      ],
+      // 机场详细信息表头
+      airportDeviceStatusArr: [
+        {
+          iconType: 'ct',
+          icon: 'basestation-status',
+          isOnLine: true,
+          code: 'nestConnected',
+          dics: nestConnectedMap,
+          des: '基站'
+        },
+        {
+          iconType: 'ct',
+          icon: 'usb-status',
+          isOnLine: true,
+          code: 'usbDeviceConnectedMap',
+          dics: usbDeviceConnectedMap,
+          des: '遥控器USB'
+        },
+        {
+          iconType: 'ct',
+          icon: 'remotecontrol-status',
+          isOnLine: true,
+          code: 'rcConnected',
+          dics: usbDeviceConnectedMap,
+          des: '遥控器'
+        },
+        {
+          iconType: 'ct',
+          icon: 'uav',
+          isOnLine: true,
+          code: 'aircraftConnected',
+          dics: aircraftConnectedMap,
+          des: ''
+        },
+        {
+          iconType: 'ct',
+          rate: true,
+          icon: 'drone-sdcard',
+          des: '无人机SD卡已用空间',
+          code: 'airSdMemoryUseRate'
+        },
+        {
+          iconType: 'ct',
+          rate: true,
+          icon: 'cps-used',
+          des: 'cps已用空间',
+          code: 'cpsMemoryUseRate'
+        }
+      ],
+      // 无人机详细信息表头
+      droneDeviceStatusArr: [
+        {
+          icon: 'rtk',
+          des: 'RTK定位解类型',
+          code: 'rtk',
+          dics: rtkMap
+        },
+        {
+          iconType: 'ty',
+          icon: 'icon-tongyong_icon_xiaoxitishi_chenggong',
+          des: 'RTK可用状态',
+          code: 'rtkReady',
+          dics: rtkReadyMap
+        },
+        {
+          icon: 'open',
+          des: 'RTK开关状态',
+          code: 'rtkEnable',
+          dics: rtkEnableMap
+        },
+        {
+          iconType: 'ty',
+          icon: 'icon-tongyong_gongnengtubiao_icon_weixingyaogan',
+          des: 'RTK卫星数量',
+          code: 'rtkSatelliteCount'
+        },
+        {
+          icon: 'signal',
+          des: 'RTK网络状态',
+          code: 'rtkNetworkChannelMsg'
+        }
+      ],
+      // 机机场详细信息列表内容
+      airportInfo: [
+        { name: '舱门状态', code: 'cabin', dics: cabinMap },
+        {
+          name: '平台状态',
+          code: 'lift',
+          dics: this_.isNestType310 ? lift310Map : liftMap
+        },
+        { name: '电池电量', code: 'chargePercentage' },
+        {
+          name: '电源状态',
+          code: 'aircraftPowerState',
+          dics: aircraftPowerStateMap
+        },
+        { name: '充电状态', code: 'chargeStr', dics: chargeStrMap },
+        { name: '电池温度', code: 'batteryTemperature', unit: '°C' },
+        { name: '电流', code: 'current', unit: 'A' },
+        { name: '电压', code: 'voltage', unit: 'V' },
+        { name: '舱内温度', code: 'temperature', unit: '°C' },
+        { name: '空调状态', code: 'airStateStr', dics: airStateStrMap },
+        { name: '归中X装置', code: 'squareX', dics: squareXMap },
+        { name: '归中Y装置', code: 'squareY', dics: squareYMap },
+        { name: '数据同步', code: 'mediaState', dics: mediaStateMap },
+        { name: '网速', code: 'sendTraffic', unit: 'kb/s' }
+      ],
+      // 无人机详细信息列表内容
+      drroneInfo: [
+        { name: '机头朝向', code: 'aircraftHeadDirection' },
+        { name: '云台角度', code: 'gimbalPitch' },
+        { name: '水平速度', code: 'aircraftHSpeed' },
+        { name: '垂直速度', code: 'aircraftVSpeed' },
+        { name: '高度', code: 'height', unit: 'm' },
+        { name: '距离', code: 'homeDistance', unit: 'm' },
+        { name: '上传信号', code: 'uploadSignal' },
+        { name: '下载信号', code: 'downloadSignal' },
+        { name: '卫星数量', code: 'satelliteCount' },
+        { name: '飞行模式', code: 'flightMode', dics: flightModeMap },
+        { name: '磁罗盘状态', code: 'compassError', dics: compassErrorMap },
+        { name: '推流状态', code: 'liveStreaming', dics: liveStreamingMap },
+        { name: '信道干扰强度', code: 'avgFrequencyInterference', unit: 'dBm' },
+        { name: '图传信号', code: 'signalState', dics: signalStateMap },
+        { name: '相机模式', code: 'cameraMode', dics: cameraModeMap },
+        { name: '通讯链路模式', code: 'airLinkMode', dics: airLinkModeMap }
+      ],
+      // 大疆无人机机场详细信息表头
+      airportDeviceStatusArrDJ: [
+        {
+          icon: 'basestation-status',
+          isOnLine: true,
+          code: 'nestConnected',
+          des: '基站',
+          dics: nestConnectedMap
+        },
+        {
+          icon: 'uav',
+          isOnLine: true,
+          code: 'aircraftConnected',
+          des: '',
+          dics: aircraftConnectedMap
+        },
+        {
+          rate: 'true',
+          code: 'droneStorage',
+          icon: 'drone-sdcard',
+          des: '无人机存储已用空间'
+        },
+        {
+          rate: 'true',
+          code: 'nestStorage',
+          icon: 'cps-used',
+          des: '基站已用空间'
+        }
+      ],
+      // 大疆无人机详细信息表头
+      droneDeviceStatusArrDJ: [
+        {
+          icon: 'convergence-success',
+          des: '是否收敛',
+          code: 'isFixed',
+          dics: isFixedMap
+        },
+        {
+          icon: 'gear',
+          des: '搜星档位',
+          code: 'quality',
+          dics: qualityMap
+        },
+        {
+          icon: 'drone-RTK',
+          des: 'RTK搜星数量',
+          code: 'rtkNumber'
+        },
+        {
+          icon: 'drone-GPS',
+          des: 'gps搜星数量',
+          code: 'gpsNumber'
+        }
+      ],
+      // 大疆机场详细信息列表内容
+      airportInfoDJ: [
+        { name: '网络类型', code: 'type', dics: typeMap },
+        { name: '网络速率', code: 'rate', unit: 'kb/s' },
+        { name: '待同步文件', code: 'remainUpload' },
+        { name: '充电状态', code: 'droneCharge', dics: droneChargeMap },
+        { name: '电池', code: 'maintenanceState', dics: maintenanceStateMap },
+        { name: '舱内温度', code: 'temperature', unit: '°C' },
+        {
+          name: '补光灯状态',
+          code: 'supplementLightState',
+          dics: supplementLightStateMap
+        },
+        { name: '舱盖状态', code: 'coverState', dics: coverStateMap },
+        {
+          name: '急停状态',
+          code: 'emergencyStopState',
+          dics: emergencyStopStateMap
+        },
+        { name: '声光报警', code: 'alarmState', dics: alarmStateMap }
+      ],
+      // 大疆无人机详细信息列表内容
+      drroneInfoDJ: [
+        { name: '机头朝向', code: 'attitudeHead' },
+        {
+          name: '云台角度',
+          code: 'gimbalPitch',
+          dics: gimbalPitchdMap,
+          unit: '°'
+        },
+        { name: '水平速度', code: 'horizontalSpeed' },
+        { name: '垂直速度', code: 'verticalSpeed' },
+        { name: '高度', code: 'height', unit: 'm' },
+        { name: '距离', code: 'homeDistance', unit: 'm' },
+        { name: '限高距离', code: 'heightLimit', unit: 'm' },
+        { name: '限远距离', code: 'distanceLimit', unit: 'm' },
+        { name: '夜航灯', code: 'nightLightsState', dics: horizonMap },
+        { name: '水平避障', code: 'horizon', dics: horizonMap },
+        { name: '上视避障', code: 'upside', dics: horizonMap },
+        { name: '下视避障', code: 'downside', dics: horizonMap },
+        { name: '图传链路模式', code: 'linkWorkmode', dics: linkWorkmodeMap }
+      ],
+      currentIndex: 0,
+      openState: true,
+      currentUavStatusData: null
+    }
+  },
+  methods: {
+    handleOpen() {
+      this.openState = !this.openState
+    },
+    handleTips(item) {
+      // console.log('tips', item)
+      if (item.rate) {
+        return `${item.des}${this.$options.filters.resetString(
+          this.currentDataInfo?.[item.code] * 100
+        )}%`
+      }
+      if (item.isOnLine) {
+        return `${item.des}${
+          item.dics?.[this.currentDataInfo?.[item.code]] ||
+          this.currentDataInfo?.[item.code]
+        }`
+      }
+      return `${item.des}`
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+@mixin ctIcon {
+  display: inline-block;
+  ::v-deep .ct-icon {
+    line-height: 1;
+    width: px-to-rem(20) !important;
+    font-size: px-to-rem(20) !important;
+    color: rgba(79, 159, 255, 1) !important;
+    -webkit-text-stroke: px-to-rem(1) rgba(0, 0, 0, 0.4);
+    .icon-ctw {
+      color: inherit !important;
+      font-size: inherit !important;
+    }
+  }
+}
+.left_pop {
+  width: px-to-rem(451);
+  height: px-to-rem(48);
+  background: linear-gradient(
+    91deg,
+    rgba(6, 43, 89, 0.5) 0%,
+    rgba(14, 40, 73, 0.21) 27%,
+    rgba(23, 37, 55, 0) 100%
+  );
+  position: absolute;
+  z-index: 20;
+  left: px-to-rem(1);
+  overflow: hidden;
+  transition: all 0.3s;
+  &.is_open {
+    height: calc(100% - px-to-rem(40)) !important;
+  }
+  .top {
+    width: px-to-rem(396);
+    height: px-to-rem(48);
+    background: linear-gradient(
+      91deg,
+      rgba(6, 43, 89, 0.5) 0%,
+      rgba(14, 40, 73, 0.21) 27%,
+      rgba(23, 37, 55, 0) 100%
+    );
+    display: flex;
+    align-items: center;
+    .title_item {
+      text-align: center;
+      width: px-to-rem(124);
+      height: 100%;
+      line-height: px-to-rem(48);
+      cursor: pointer;
+      .name {
+        color: rgba(232, 243, 254, 0.7);
+        font-size: px-to-rem(18);
+        font-family: PingFangSC, PingFang SC;
+        font-weight: 400;
+      }
+      &.active {
+        position: relative;
+        .name {
+          font-weight: 500;
+          color: rgba(232, 243, 254, 1);
+        }
+        &::before {
+          position: absolute;
+          content: '';
+          bottom: 0;
+          left: 0;
+          width: 100%;
+          height: px-to-rem(11);
+          background: url('../../../img/tab_active.png') no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+    }
+    .change_icon {
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+      margin-left: px-to-rem(6);
+      cursor: pointer;
+      transform: rotate(180deg);
+      background: url('../../../img/open_status@2x.png') no-repeat;
+      background-size: cover;
+      transition: transform 0.3s;
+      &.is_open {
+        transform: rotate(0deg);
+      }
+    }
+  }
+  .content {
+    padding: 0 px-to-rem(12);
+    .device_name {
+      height: px-to-rem(48);
+      line-height: px-to-rem(48);
+      .ct-icon__inline-block {
+        @include ctIcon;
+        ::v-deep .ct-icon {
+          color: #e8f3fe !important;
+        }
+      }
+    }
+    .device_status {
+      @include flex;
+      .status_item {
+        min-width: px-to-rem(20);
+        height: px-to-rem(20);
+        margin-right: px-to-rem(12);
+        @include flex;
+        .rate {
+          width: px-to-rem(2);
+          height: px-to-rem(14);
+          background: #e8f3fe;
+          border-radius: px-to-rem(1);
+          position: relative;
+          margin-left: px-to-rem(4);
+          .rate_inner {
+            position: absolute;
+            left: 0;
+            width: 100%;
+            bottom: 0;
+            background: #15bd94;
+          }
+        }
+      }
+    }
+    .device_status_drone {
+      @include flex;
+      .status_item {
+        @include flex;
+        .iconfont_tools {
+          font-size: px-to-rem(20);
+          color: rgba(79, 159, 255, 1);
+        }
+        .type_des {
+          font-family: PingFangSC, PingFang SC;
+          font-weight: 400;
+          font-size: px-to-rem(16);
+          color: #e8f3fe;
+          text-align: left;
+          margin-left: px-to-rem(6);
+          -webkit-text-stroke: px-to-rem(1) rgba(0, 0, 0, 0.4);
+          cursor: default;
+          &.rtkNetworkChannelMsg {
+            display: none;
+          }
+        }
+        margin-right: px-to-rem(12);
+      }
+    }
+    .ct-icon__inline-block {
+      @include ctIcon;
+    }
+    .name_val {
+      margin-left: px-to-rem(6);
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 400;
+      font-size: px-to-rem(18);
+      color: #e8f3fe;
+      -webkit-text-stroke: px-to-rem(1) rgba(0, 0, 0, 0.4);
+    }
+    .info_cont {
+      user-select: none;
+      @include flex;
+      flex-wrap: wrap;
+      width: px-to-rem(248);
+      margin-top: px-to-rem(12);
+      .info_item {
+        width: px-to-rem(118);
+        margin-right: px-to-rem(6);
+        margin-left: px-to-rem(6);
+        margin-bottom: px-to-rem(12);
+        &:nth-child(2n) {
+          margin-right: 0;
+        }
+        &:nth-child(2n + 1) {
+          margin-left: 0;
+        }
+        .info_title {
+          font-family: PingFangSC, PingFang SC;
+          font-weight: 400;
+          font-size: px-to-rem(16);
+          color: #e8f3fe;
+          text-align: left;
+          -webkit-text-stroke: px-to-rem(1) rgba(0, 0, 0, 0.4);
+        }
+        .info_title_des {
+          line-height: 1;
+          font-family: PingFangSC, PingFang SC;
+          font-weight: 600;
+          font-size: px-to-rem(18);
+          height: px-to-rem(18);
+          color: #e8f3fe;
+          text-align: left;
+          -webkit-text-stroke: px-to-rem(1) rgba(0, 0, 0, 0.4);
+          margin-top: px-to-rem(6);
+        }
+      }
+    }
+  }
+}
+</style>

+ 333 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/DialogFrameExtraction.vue

@@ -0,0 +1,333 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/28
+ * @Description: 视频抽帧弹窗
+ -->
+<template>
+  <div>
+    <styled-dialog
+      industryClass="common-iw-s frame-extraction-dialog"
+      :visible="dialogShow"
+      title="视频抽帧"
+      @close="close"
+      :modal="true"
+      :canClose="true"
+      :appendToBody="true"
+    >
+      <div class="frame-extraction-body">
+        <el-form ref="formRef" :model="formData">
+          <div class="fly-edit-name-row">
+            <el-form-item label="视频信息:">
+              {{ formData.videoName }}
+            </el-form-item>
+            <el-form-item label="时长:"> {{ durationFormat }}</el-form-item>
+            <el-form-item label="抽帧间隔(秒/帧)">
+              <div class="frequency">
+                <el-input
+                  v-model.number="formData.frequency"
+                  controls-position="right"
+                  type="number"
+                  :min="boundaryValue.min"
+                  :max="boundaryValue.max"
+                  @input="handleInput"
+                  :class="[isFocused && 'is-focus']"
+                >
+                </el-input>
+                <div
+                  class="spin-button"
+                  @mouseenter="isFocused = true"
+                  @mouseleave="isFocused = false"
+                >
+                  <i class="el-icon-caret-top" @click="add"></i>
+                  <i class="el-icon-caret-bottom" @click="decrease"></i>
+                </div>
+              </div>
+              <div class="number-error" v-if="formData.extractCount < 0">
+                请调整参数
+              </div>
+            </el-form-item>
+            <el-form-item>
+              预计得到
+              <span class="num-picture">{{ formData.extractCount }}</span>
+              张图片
+            </el-form-item>
+          </div>
+        </el-form>
+        <el-row type="flex" class="btns-container" justify="center">
+          <base-button
+            type="primary"
+            :heightStyle="32"
+            :width="108"
+            @click="handleSubmit()"
+          >
+            确定
+          </base-button>
+          <base-button :heightStyle="32" :width="108" @click="close">
+            取消
+          </base-button>
+        </el-row>
+      </div>
+    </styled-dialog>
+  </div>
+</template>
+
+<script>
+import StyledDialog from '@component-gallery/base-components/styled-dialog/StyledDialog.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import {
+  formatSeconds,
+  getVideoDuration
+} from '../../../dict/fly-result-methods'
+import { doVideoExtraction } from '../../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+
+export default {
+  components: { BaseButton, StyledDialog },
+  props: {
+    dialogShow: { type: Boolean, default: false },
+    videoData: {
+      type: Object,
+      default: () => ({})
+    },
+    selectData: {
+      type: Object,
+      default: () => ({})
+    },
+    selectedNodeData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  data() {
+    return {
+      duration: 0, //视频时长
+      durationFormat: '',
+      isFocused: false,
+      formData: {
+        id: this.videoData.id,
+        videoName: this.videoData.name,
+        frequency: 1,
+        taskName: this.selectData.planTaskName,
+        extractCount: 0 //预计得到图片数量
+      }
+    }
+  },
+  watch: {
+    'formData.frequency': {
+      handler(val) {
+        this.formData.extractCount = Math.ceil(this.duration / val)
+      },
+      deep: true
+    }
+  },
+  computed: {
+    /**
+     * 数字范围
+     * @returns {{max: number, min: number}}
+     */
+    boundaryValue() {
+      return {
+        max: 9999,
+        min: 1
+      }
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  methods: {
+    /**
+     * 初始化视频长度
+     * @returns {Promise<void>}
+     */
+    async init() {
+      this.duration = await getVideoDuration(this.videoData.videoAccessUrl)
+      this.formData.extractCount = Math.ceil(
+        this.duration / this.formData.frequency
+      )
+      this.durationFormat = formatSeconds(this.duration)
+    },
+    /**
+     * 限制只能输入 1-9999 如果输入值不是数字或者不在范围内,则重置为最接近的有效值
+     * @param value
+     */
+    handleInput(value) {
+      if (
+        isNaN(value) ||
+        value < this.boundaryValue.min ||
+        value > this.boundaryValue.max
+      ) {
+        this.formData.frequency =
+          Math.max(
+            this.boundaryValue.min,
+            Math.min(value, this.boundaryValue.max)
+          ) || this.boundaryValue.min
+      }
+    },
+    /**
+     * 加 1
+     */
+    add() {
+      if (this.formData.frequency < this.boundaryValue.max) {
+        this.formData.frequency = this.formData.frequency + 1
+      }
+    },
+    /**
+     * 减 1
+     */
+    decrease() {
+      if (this.formData.frequency > this.boundaryValue.min) {
+        this.formData.frequency = this.formData.frequency - 1
+      }
+    },
+    /**
+     * 确定
+     */
+    handleSubmit() {
+      if (this.formData.extractCount <= 0) {
+        return
+      }
+      this.videoExtractionApi()
+    },
+    /**
+     * 调用视频抽帧接口
+     */
+    videoExtractionApi() {
+      doVideoExtraction({
+        ...this.formData,
+        frequency: `${this.formData.frequency}`,
+        accessNode: this.selectedNodeData.accessNode
+      })
+        .then((res) => {
+          if (res.code === 200) {
+            this.$globalEventBus.$emit('frame-extraction-progress-visibility', {
+              ...this.formData,
+              isShowDialog: true
+            })
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err.msg)
+        })
+        .finally(() => {
+          this.close()
+        })
+    },
+    /**
+     * 关闭
+     */
+    close() {
+      this.$emit('update:dialogShow', false)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+$form_item_h: px-to-rem(32);
+$form_item_label_w: px-to-rem(134);
+$form_item_label_m_r: px-to-rem(12);
+@mixin borderWidthArrow {
+  content: '';
+  display: inline-block;
+  height: 0;
+  width: 0;
+  border-style: solid;
+  border-width: px-to-rem(6) px-to-rem(4) px-to-rem(6) px-to-rem(4);
+  transform: translateY(px-to-rem(-3));
+}
+@mixin inputNumberStyle {
+  background: transparent;
+  height: calc($form_item_h / 2);
+  line-height: calc($form_item_h / 2);
+  border: none;
+}
+
+.frame-extraction-dialog {
+  ::v-deep .el-dialog {
+    width: px-to-rem(500) !important;
+    margin-top: px-to-rem(350) !important;
+    .el-dialog__header {
+      font-size: px-to-rem(18);
+    }
+  }
+  .frame-extraction-body {
+    padding: px-to-rem(12);
+    font-size: px-to-rem(16);
+    ::v-deep .el-form {
+      .el-form-item {
+        font-size: px-to-rem(16);
+        margin-bottom: 0;
+        .el-form-item__label {
+          width: $form_item_label_w;
+          min-height: $form_item_h;
+          line-height: $form_item_h;
+          font-weight: 400;
+          font-size: px-to-rem(16);
+          color: #e8f3fe;
+        }
+        .el-form-item__content {
+          min-height: $form_item_h;
+          line-height: $form_item_h;
+          margin-left: $form_item_label_w;
+          font-size: px-to-rem(16);
+          padding: 0;
+          .el-input {
+            height: $form_item_h;
+            line-height: $form_item_h;
+            .el-input__inner {
+              height: $form_item_h;
+              line-height: $form_item_h;
+              font-size: px-to-rem(16);
+              &::-webkit-inner-spin-button {
+                display: none;
+              }
+            }
+          }
+        }
+      }
+      .frequency {
+        position: relative;
+
+        .spin-button {
+          width: px-to-rem(20);
+          height: px-to-rem(20);
+          position: absolute;
+          right: px-to-rem(6);
+          top: px-to-rem(6);
+          cursor: pointer;
+          z-index: 1;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          flex-direction: column;
+          i {
+            height: 50%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+          }
+        }
+        .is-focus .el-input__inner {
+          border-color: rgb(79, 159, 255);
+        }
+      }
+    }
+
+    .number-error {
+      color: #ed5158;
+    }
+    .num-picture {
+      color: rgba(79, 159, 255, 1);
+    }
+    .btns-container {
+      padding-top: px-to-rem(6);
+      gap: px-to-rem(12);
+      .base-button {
+        font-size: px-to-rem(16);
+      }
+    }
+  }
+}
+</style>

+ 397 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/DialogFrameExtractionProgress.vue

@@ -0,0 +1,397 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/29
+ * @Description: 视频抽帧小弹窗
+ -->
+<template>
+  <absolute-container
+    :title="titleComputed"
+    :left="left"
+    :bottom="bottom"
+    :width="420"
+    @close="handleClose"
+    industryClass="frame-extraction-progress"
+    v-if="isOpen"
+    :highlightTitle="false"
+    v-drag="dragContainer"
+  >
+    <div class="frame-extraction-progress-content">
+      <el-scrollbar class="content-scrollbar">
+        <div
+          v-for="item in frameExtractionList"
+          :key="item.id"
+          class="content-list"
+        >
+          <div class="content-list-main">
+            <div class="name" :class="[item.state === 2 && 'name-cancel']">
+              <ct-icon name="video-frame"></ct-icon>
+              <span class="videoName" :c-tip="item.videoName">
+                {{ item.videoName }}
+              </span>
+            </div>
+            <el-progress
+              :showText="false"
+              class="progress"
+              :color="progressComputed(item).color"
+              :percentage="progressComputed(item).percentage"
+            ></el-progress>
+            <div class="result success" v-if="item.state === 1">
+              <span class="state">
+                抽帧完成({{ item.count }}/{{ item.totalCount }})
+              </span>
+              <ct-icon name="check-box"></ct-icon>
+            </div>
+            <div class="result state-ing" v-if="item.state === 0">
+              <span class="state">
+                {{ progressComputed(item).percentageRate }}
+                &nbsp;
+                {{ item.count }}/
+                {{ item.totalCount }}
+              </span>
+              <ct-icon
+                style="cursor: pointer"
+                name="tag-delete"
+                @click="cancelExtract(item)"
+              ></ct-icon>
+            </div>
+            <div class="result cancel" v-if="item.state === 2">
+              <span class="state"> 已取消 </span>
+              <ct-icon name="close-active"></ct-icon>
+            </div>
+          </div>
+        </div>
+      </el-scrollbar>
+    </div>
+  </absolute-container>
+</template>
+<script>
+import AbsoluteContainer from '@component-gallery/base-components/absolute-container/AbsoluteContainer.vue'
+import { postMsgUtil } from '@ct/iframe-connect-sdk'
+import { cancelExtractTask, queryExtractionProgress } from '../../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+
+export default {
+  components: {
+    AbsoluteContainer
+  },
+  props: {
+    bottom: {
+      type: Number
+    },
+    left: {
+      type: Number
+    },
+    dragContainer: {
+      type: String,
+      default: 'fly_video_result_drag'
+    }
+  },
+  computed: {
+    progressComputed() {
+      return (val) => {
+        let stateColor = ''
+        switch (val.state) {
+          case 2:
+            stateColor = 'rgba(232, 243, 254, 0.4)'
+            break
+          case 1:
+            stateColor = 'rgba(21, 189, 148, 1)'
+            break
+          case 0:
+            stateColor = 'rgba(79, 159, 255, 1)'
+            break
+        }
+        const percentage = (val.count / val.totalCount) * 100 || 0
+        return {
+          percentage: percentage,
+          color: stateColor,
+          percentageRate: percentage.toFixed(0) + '%'
+        }
+      }
+    },
+    titleComputed() {
+      const completedDataLength = this.frameExtractionList.filter(
+        (item) => item.state === 1
+      ).length
+      return `视频抽帧(${completedDataLength}/${this.frameExtractionList.length})`
+    }
+  },
+  directives: {
+    drag: {
+      bind(el, binding) {
+        const className = binding.value
+        const container = document.getElementsByClassName(className)[0]
+        if (!container) {
+          console.error(`Element with class name ${className} not found`)
+          return
+        }
+
+        let disX,
+          disY,
+          isDragging = false
+        const rect = el.getBoundingClientRect()
+        const initialBottom =
+          container.getBoundingClientRect().height - rect.height
+        const onMouseDown = (e) => {
+          isDragging = true
+          disX = e.clientX - el.offsetLeft
+          disY = initialBottom
+          e.preventDefault()
+          e.stopPropagation()
+          document.addEventListener('mousemove', onMouseMove)
+          document.addEventListener('mouseup', onMouseUp)
+        }
+
+        const onMouseMove = (e) => {
+          if (!isDragging) return
+          const containerRect = container.getBoundingClientRect()
+          const l = e.clientX - disX
+          const b = disY - e.clientY
+          const maxL = containerRect.width - el.offsetWidth
+          const minB = 0
+          const maxB = containerRect.height - el.offsetHeight // Element should not go below the bottom of the container
+          el.style.left = Math.min(Math.max(l, 0), maxL) + 'px'
+          el.style.bottom = Math.min(Math.max(b, minB), maxB) + 'px'
+        }
+        const onMouseUp = () => {
+          isDragging = false
+          document.removeEventListener('mousemove', onMouseMove)
+          document.removeEventListener('mouseup', onMouseUp)
+        }
+        el.addEventListener('mousedown', onMouseDown)
+        el._cleanupDrag = () => {
+          el.removeEventListener('mousedown', onMouseDown)
+          document.removeEventListener('mousemove', onMouseMove)
+          document.removeEventListener('mouseup', onMouseUp)
+        }
+      },
+      unbind(el) {
+        if (el._cleanupDrag) {
+          el._cleanupDrag()
+        }
+      }
+    }
+  },
+  data() {
+    return {
+      frameExtractionList: [],
+      isOpen: false,
+      videoData: {}
+    }
+  },
+  mounted() {
+    this.init()
+  },
+  methods: {
+    init() {
+      this.initEvent()
+      this.initSocket()
+    },
+    /**
+     * 监听 eventBus
+     */
+    initEvent() {
+      this.$globalEventBus.$on(
+        'frame-extraction-progress-visibility',
+        this.progressVisibility
+      )
+    },
+    /**
+     * eventBus监听事件
+     * @param val
+     */
+    progressVisibility(val) {
+      this.isOpen = val.isShowDialog
+      if (this.isOpen) {
+        this.videoData = val
+        this.queryExtractionProgressApi()
+      }
+    },
+    /**
+     * 调用进度展示接口
+     */
+    queryExtractionProgressApi() {
+      this.frameExtractionList = []
+      queryExtractionProgress({
+        id: this.videoData.id
+      }).then((res) => {
+        if (res.code === 200) {
+          console.log('res.data', res.data)
+          this.frameExtractionList = res.data.map((item) => {
+            return {
+              ...item,
+              state: +item.extractStatus,
+              count: item.extractCount,
+              totalCount: item.extractTotalCount
+            }
+          })
+        }
+      })
+    },
+    /**
+     * 初始化 socket
+     * @returns {Promise<void>}
+     */
+    initSocket() {
+      try {
+        postMsgUtil.trigger(null, 'registerCommonNoticePush', {
+          type: ['UAV_VIDEO_TASK']
+        })
+        postMsgUtil.listen('commonNoticePushListen', (data) => {
+          console.log('socket数据=============>', data)
+          this.updateFrameExtractionList(data)
+        })
+      } catch (e) {
+        console.log('e')
+      }
+    },
+    /**
+     * 更新数据
+     * @param updateData
+     */
+    updateFrameExtractionList(updateData) {
+      const itemIndex = this.frameExtractionList.findIndex(
+        (item) => item.taskId === updateData.id
+      )
+      if (itemIndex !== -1 && this.videoData.id === updateData.videoId) {
+        this.$set(this.frameExtractionList, itemIndex, {
+          ...this.frameExtractionList[itemIndex],
+          ...updateData
+        })
+      }
+    },
+
+    /**
+     * 关闭弹窗
+     */
+    handleClose() {
+      this.isOpen = false
+    },
+    /**
+     * 取消抽帧
+     * @param item
+     */
+    cancelExtract(item) {
+      cancelExtractTask({
+        videoId: item.videoId, //抽帧的视频ID
+        taskId: item.taskId //抽帧任务ID
+      })
+        .then((res) => {
+          if (res.code === 200) {
+            this.queryExtractionProgressApi()
+            CommonMessage.success('取消成功')
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err?.msg || '取消失败')
+        })
+    }
+  },
+  beforeDestroy() {
+    this.$globalEventBus.$off(
+      'frame-extraction-progress-visibility',
+      this.progressVisibility
+    )
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+$_f_t_cancel: rgba(232, 243, 254, 0.4);
+.frame-extraction-progress {
+  z-index: 100;
+  ::v-deep .innercomp-abcontainer-header {
+    background: transparent;
+    .innercomp-abcontainer-header__title span {
+      font-weight: 600;
+      font-size: px-to-rem(18);
+    }
+  }
+  ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+    background: rgba(79, 159, 255, 0.4);
+  }
+  ::v-deep .ct-icon {
+    width: auto !important;
+    margin-top: px-to-rem(1);
+    .icon-ctw {
+      color: inherit !important;
+      font-size: px-to-rem(16) !important;
+      vertical-align: initial !important;
+    }
+  }
+  ::v-deep .previous-vacancies {
+    top: px-to-rem(46);
+    height: px-to-rem(1);
+    background: linear-gradient(
+      270deg,
+      rgba(255, 255, 255, 0) 0%,
+      #ffffff 50%,
+      rgba(255, 255, 255, 0) 100%
+    );
+  }
+  &-content {
+    padding: 0 0 px-to-rem(12) 0;
+    font-size: px-to-rem(16);
+    .content-scrollbar {
+      height: px-to-rem(172);
+    }
+    .content-list {
+      padding: 0 px-to-rem(12);
+
+      &-main {
+        height: px-to-rem(32);
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        border-bottom: px-to-rem(1) solid rgba(232, 243, 254, 0.2);
+        font-size: px-to-rem(16);
+        color: rgba(232, 242, 254, 1);
+
+        .name {
+          width: px-to-rem(136);
+          display: flex;
+          justify-content: left;
+
+          .videoName {
+            margin-left: px-to-rem(6);
+            width: px-to-rem(110);
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+          &.name-cancel {
+            color: $_f_t_cancel;
+          }
+        }
+        .progress {
+          flex: 1;
+          padding-right: px-to-rem(12);
+          ::v-deep .el-progress-bar__outer {
+            height: px-to-rem(2) !important;
+          }
+        }
+
+        .result {
+          width: px-to-rem(158);
+          display: flex;
+          justify-content: right;
+
+          .state {
+            margin-right: px-to-rem(6);
+          }
+        }
+        .success {
+          ::v-deep .ct-icon {
+            color: rgba(19, 190, 136, 1);
+          }
+        }
+        .cancel {
+          color: $_f_t_cancel;
+        }
+      }
+    }
+  }
+}
+</style>

+ 155 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/FlyVideo.vue

@@ -0,0 +1,155 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 飞行结果视频
+ -->
+<!-- eslint-disable vue/no-deprecated-slot-scope-attribute -->
+<template>
+  <div class="fly-video">
+    <div class="fly-video-left">
+      <div class="tree-content">
+        <fly-tree @selectTreeData="selectTreeData" />
+      </div>
+      <div class="list-content">
+        <fly-list
+          :selectedNodeData="selectedNodeData"
+          @cardClick="cardClick"
+          @deleteData="deleteData"
+          ref="flyList"
+        >
+        </fly-list>
+      </div>
+    </div>
+    <div class="fly-video-right">
+      <fly-video-content
+        ref="flyVideoContent"
+        :selectData="selectData"
+        :selectedNodeData="selectedNodeData"
+      />
+    </div>
+  </div>
+</template>
+<script>
+import FlyTree from '../FlyTree.vue'
+import FlyList from '../FlyList.vue'
+import FlyVideoContent from './FlyVideoContent.vue'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { deleteRecords } from '../../../service'
+
+export default {
+  components: { FlyTree, FlyList, FlyVideoContent },
+  data() {
+    return {
+      selectedNodeData: null, //选中树节点
+      selectData: null //飞行任务
+    }
+  },
+  props: {
+    type: String
+  },
+
+  methods: {
+    /**
+     * 点击树
+     * @param val
+     */
+    selectTreeData(val) {
+      this.selectedNodeData = val
+    },
+    /**
+     * 点击卡片
+     * @param val
+     */
+    cardClick(val) {
+      console.log('列表选中', val)
+      this.selectData = val
+    },
+    // 点击删除
+    deleteData(val) {
+      console.log('删除点击', val)
+      const this_ = this
+      this_
+        .$confirm('是否确认删除该任务记录下的视频?', '提示', {
+          confirmButtonText: '确认',
+          cancelButtonText: '取消',
+          customClass: 'c-confirm c-confirm-fly-result'
+        })
+        .then(async () => this_.delApi(val))
+    },
+    delApi(val) {
+      let params = {
+        dataType: 'video',
+        taskRecordId: val.taskRecordId,
+        accessNode: this.selectedNodeData.accessNode
+      }
+      deleteRecords(params)
+        .then((res) => {
+          if (res.code === 200) {
+            CommonMessage.success('删除成功')
+            this.$refs.flyVideoContent.$refs.videoShow.getVideoList()
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err?.msg || '删除失败')
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$left_width: px-to-rem(740);
+@mixin fly-img-height {
+  height: px-to-rem(900);
+}
+.fly-video {
+  display: flex;
+  padding-right: px-to-rem(12);
+  .fly-video-left {
+    width: px-to-rem(740);
+    @include fly-img-height;
+    display: flex;
+    background-image: url('../../../img/border-radius-left.svg');
+    background-size: 100% 100%;
+    .tree-content {
+      width: px-to-rem(370);
+      @include fly-img-height;
+    }
+    .list-content {
+      width: px-to-rem(370);
+      @include fly-img-height;
+      border-left: px-to-rem(1) solid;
+      padding-top: px-to-rem(12);
+      border-image: linear-gradient(
+          180deg,
+          rgba(134, 184, 249, 0.7),
+          rgba(134, 184, 249, 0)
+        )
+        1 1;
+      .diy_cont {
+        display: flex;
+        align-items: center;
+        ::v-deep .ct-icon__inline-block {
+          display: inline-block;
+          .ct-icon {
+            width: auto !important;
+            margin-right: px-to-rem(6);
+            .icon-ctw {
+              font-size: px-to-rem(20) !important;
+              color: #e8f3fe !important;
+            }
+          }
+        }
+      }
+    }
+  }
+  .fly-video-right {
+    flex: 1;
+    @include fly-img-height;
+    margin-left: px-to-rem(12);
+    background-image: url('../../../img/border-radius.svg');
+    background-size: 100% 100%;
+  }
+}
+</style>

+ 88 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/FlyVideoContent.vue

@@ -0,0 +1,88 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 飞行结果视频页面右侧内容
+ -->
+<template>
+  <div class="right_video_cont">
+    <fly-right-top-tab :rightTopTab="rightTopTab" @active="activeBtn" />
+    <!-- 视频展示 -->
+    <video-show
+      v-show="currentShow === 'video'"
+      :selectData="selectData"
+      :selectedNodeData="selectedNodeData"
+      ref="videoShow"
+    ></video-show>
+    <!-- 地图展示 -->
+    <fly-map
+      v-if="currentShow === 'map'"
+      tabType="video"
+      :selectData="selectDataComputed"
+    ></fly-map>
+  </div>
+</template>
+
+<script>
+import FlyRightTopTab from '../FlyRightTopTab.vue'
+import VideoShow from './VideoShow.vue'
+import FlyMap from '../FlyMap.vue'
+
+export default {
+  name: 'RightVideoCont',
+  components: {
+    FlyMap,
+    FlyRightTopTab,
+    VideoShow
+  },
+  props: {
+    selectData: {
+      type: Object,
+      default: () => ({})
+    },
+    selectedNodeData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  computed: {
+    selectDataComputed() {
+      return {
+        type: 'video',
+        treeNode: this.selectedNodeData,
+        listData: this.selectData
+      }
+    }
+  },
+  data() {
+    return {
+      currentShow: 'video',
+
+      rightTopTab: [
+        {
+          name: '视频展示',
+          code: 'video'
+        },
+        {
+          name: '地图展示',
+          code: 'map'
+        }
+      ]
+    }
+  },
+  methods: {
+    activeBtn(actItem) {
+      this.currentShow = actItem.code
+      if (actItem.code === 'video') {
+        this.$nextTick(() => this.$refs.videoShow.getVideoList())
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.right_video_cont {
+  width: 100%;
+}
+</style>

+ 579 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/FrameExtraction.vue

@@ -0,0 +1,579 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/29
+ * @Description: 抽帧图片
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div class="frame-extraction">
+    <el-scrollbar
+      class="frame-extraction-scrollbar"
+      v-if="collapseList.length > 0"
+    >
+      <el-collapse v-model="openList" @change="collapseChange">
+        <el-collapse-item
+          v-for="item in collapseList"
+          :key="item.taskId"
+          :name="item.taskId"
+        >
+          <template v-slot:title>
+            <div class="header-left" @click.stop>
+              <el-checkbox
+                :indeterminate="indeterminateState(item)"
+                @change="checkboxChange(item)"
+                :value="item.select"
+              ></el-checkbox>
+              <ct-icon name="pic-list"></ct-icon>
+              <div>
+                {{ item.extractContent }}
+                {{ item.extractPhotoList.length || 0 }}张
+              </div>
+            </div>
+            <div>
+              <i
+                class="iconfont collapse-item-icon icon-tongyong_icon_luxiangshouqi"
+              />
+              <i
+                class="iconfont collapse-item-icon icon-tongyong_icon_luxiangzhankai"
+              />
+            </div>
+          </template>
+          <el-scrollbar
+            class="frame-card-scrollbar"
+            :class="[
+              item.extractPhotoList.length <= 3 && 'frame-card-scrollbar-min'
+            ]"
+          >
+            <div class="card-list" v-if="item.extractPhotoList.length > 0">
+              <fly-img-thumbnail-card
+                v-for="_item in item.extractPhotoList"
+                :key="_item.id"
+                :card-data="{ ..._item, paternalId: item.id }"
+                :isShowName="false"
+                class="card"
+                @selectClick="selectCard"
+                :selectList="selectList"
+                @cardClick="cardClick"
+              />
+            </div>
+            <div
+              v-if="item.extractPhotoList.length === 0"
+              class="frame-card-scrollbar-no-data"
+            >
+              <span class="empty-text">暂无数据</span>
+            </div>
+          </el-scrollbar>
+        </el-collapse-item>
+      </el-collapse>
+    </el-scrollbar>
+    <div v-if="collapseList.length === 0" class="no-data">
+      <span v-if="!loading" class="empty-text">暂无数据</span>
+      <div v-if="loading" class="loading">
+        <i class="el-icon-loading"></i> <span>加载中</span>
+      </div>
+    </div>
+    <!-- 算法配置 -->
+    <dialog-fly-algorithm
+      v-if="dialogAlgorithm"
+      :dialogShow.sync="dialogAlgorithm"
+      :form-data="algorithmFormData"
+    />
+    <!--    全屏展示图片-->
+    <div
+      class="view-img fly_img_result_view_screen_full"
+      v-if="showImageViewer"
+    >
+      <span class="close_btn" @click="handleImageViewerClose"></span>
+      <el-image
+        class="medium"
+        :src="showImageViewerUrl"
+        fit="contain"
+      ></el-image>
+    </div>
+  </div>
+</template>
+<script>
+import FlyImgThumbnailCard from '../FlyImgThumbnailCard.vue'
+import DialogFlyAlgorithm from '../DialogFlyAlgorithm.vue'
+import {
+  downloadByUrlsApi,
+  syncHandlerApi
+} from '../../../dict/fly-result-methods'
+import {
+  delExtractTaskPhotos,
+  queryVideoExtractionList
+} from '../../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import dayjs from 'dayjs'
+import screenfull from 'screenfull'
+
+export default {
+  components: {
+    DialogFlyAlgorithm,
+    FlyImgThumbnailCard
+  },
+  props: {
+    showComponents: {
+      type: String,
+      default: '3'
+    },
+    videoDownloadLoading: {
+      type: Boolean,
+      default: false
+    },
+    selectData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    },
+    selectedNodeData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    },
+    videoDetailData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      showImageViewer: false,
+      showImageViewerUrl: '', //全屏展示图片
+      collapseList: [], //所有数据
+      selectList: [], //选中 id
+      dialogAlgorithm: false, //算法配置
+      algorithmFormData: {},
+      loading: false, //数据加载状态
+      openList: [] //展开数据
+    }
+  },
+  mounted() {
+    this.initEvent()
+    this.getData()
+  },
+  methods: {
+    /**
+     * 监听事件
+     */
+    initEvent() {
+      this.$globalEventBus.$on('flyVideoResultOptClick', this.handleTopOptClick)
+    },
+    /**
+     * 半选中状态
+     * @returns {function(*): boolean|*}
+     */
+    indeterminateState(item) {
+      return item.extractPhotoList.every((_item) =>
+        this.selectList.includes(_item.id)
+      )
+        ? false
+        : item.extractPhotoList.some((_item) =>
+            this.selectList.includes(_item.id)
+          )
+    },
+
+    /**
+     * 获取自定字段并扁平化数据
+     * @param field
+     * @returns {FlatArray<*[], 1>[]}
+     */
+    getParamsList(field) {
+      return this.collapseList
+        .map((item) =>
+          item.extractPhotoList
+            .filter((item) => this.selectList.includes(item.id))
+            .map((item) => (field ? item[field] : item))
+        )
+        .flat()
+    },
+    /**
+     * 抽帧图片下载
+     */
+    async downloadHandler() {
+      let params = {}
+      let fileName = `视频抽帧照片-${dayjs().format('YYYYMMDDHHmmss')}.zip`
+      params = {
+        photoUrls: this.getParamsList('photoAccessUrl'),
+        fileName,
+        isZip: 1
+      }
+      this.$emit('update:videoDownloadLoading', true)
+      await downloadByUrlsApi(params)
+      this.$emit('update:videoDownloadLoading', false)
+    },
+    /**
+     * 图片删除
+     */
+    async delHandler() {
+      this.$confirm('是否要删除选择的图片?', '提示', {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        customClass: 'c-confirm c-confirm-fly-result'
+      }).then(() => {
+        this.delExtractTaskPhotosApi()
+      })
+    },
+    /**
+     * 调用删除接口
+     * @returns {Promise<void>}
+     */
+    async delExtractTaskPhotosApi() {
+      try {
+        await delExtractTaskPhotos({
+          extractionPhotoIds: this.getParamsList('id'),
+          accessNode: this.selectedNodeData.accessNode,
+          extractionTaskIds: this.collapseList
+            .filter((item) => item.select)
+            .map((item) => item.taskId)
+        })
+        this.getData()
+        CommonMessage.success('删除成功')
+      } catch (e) {
+        CommonMessage.error(e.msg || '删除失败')
+      }
+    },
+    /**
+     * 图片全选
+     */
+    allHandler(val) {
+      this.collapseList.forEach((item) => {
+        item.select = !val.isSelectAll
+        this.checkboxChange({ ...item, select: val.isSelectAll })
+      })
+      this.$emit('frame-extraction-data', this.collapseList, this.selectList)
+    },
+    /**
+     * 视频抽帧
+     */
+    frameExtractionHandler() {
+      this.$emit('dialog-frame-extraction', true)
+    },
+    /**
+     * 算法配置
+     */
+    algorithmHandler() {
+      this.algorithmFormData = {
+        algorithmList: [],
+        paramsLists: this.getParamsList().map((item) => {
+          return {
+            ...item,
+            taskRecordId: this.selectData.taskRecordId
+          }
+        }),
+        deviceCode: this.selectedNodeData.deviceCode,
+        flightRouteId: this.selectData.flightRouteId,
+        type: 'image'
+      }
+      this.dialogAlgorithm = true
+    },
+    /**
+     *  同步数据源
+     */
+    syncHandler() {
+      let params = {
+        taskRecordId: this.selectData.taskRecordId,
+        deviceCode: this.selectedNodeData.deviceCode,
+        accessNode: this.selectedNodeData?.accessNode,
+        mediaSyncStatus: this.selectData.mediaSyncStatus
+      }
+      syncHandlerApi(params)
+    },
+    /**
+     * 操作区
+     * @param val
+     */
+
+    handleTopOptClick(val) {
+      console.log('val', val)
+      if (this.showComponents !== '3') {
+        return
+      }
+      const fnObject = {
+        download: this.downloadHandler,
+        del: this.delHandler,
+        all: this.allHandler,
+        frameExtraction: this.frameExtractionHandler,
+        algorithm: this.algorithmHandler,
+        sync: this.syncHandler
+      }
+      fnObject[val.code] && fnObject[val.code](val)
+    },
+    /**
+     * 获取数据
+     * @returns {Promise<void>}
+     */
+    async getData() {
+      this.selectList = []
+      this.collapseList = []
+      let params = {
+        id: this.videoDetailData.id,
+        accessNode: this.selectedNodeData.accessNode
+      }
+      this.loading = true
+      try {
+        const res = await queryVideoExtractionList(params)
+        if (res.code === 200) {
+          this.collapseList = res.data || []
+          this.collapseList.forEach((item) => {
+            item['select'] = false
+            if (item.extractStatus === '1') {
+              this.openList.push(item.taskId)
+            }
+          })
+        }
+      } catch (e) {
+        CommonMessage.error(e.msg)
+      } finally {
+        this.loading = false
+      }
+      this.$emit('frame-extraction-data', this.collapseList, this.selectList)
+    },
+    /**
+     * 选中单个
+     * @param val
+     */
+    selectCard(val) {
+      let indexToRemove = this.selectList.findIndex((item) => item === val.id)
+      if (indexToRemove !== -1) {
+        this.selectList.splice(indexToRemove, 1)
+      } else {
+        this.selectList.push(val.id)
+      }
+      this.collapseList.forEach((item) => {
+        if (item.id === val.paternalId) {
+          item.select = item.extractPhotoList.every((_item) =>
+            this.selectList.includes(_item.id)
+          )
+        }
+      })
+
+      this.$emit('frame-extraction-data', this.collapseList, this.selectList)
+    },
+    /**
+     * 点击卡片
+     * @param val
+     * @returns {boolean}
+     */
+    cardClick(val) {
+      console.log('val===>', val)
+      if (!screenfull.enabled) {
+        CommonMessage.warning('浏览器不支持该功能')
+        return false
+      }
+      this.showImageViewerUrl = val.photoAccessUrl
+      this.showImageViewer = true
+      this.$nextTick(() => {
+        let el = document.querySelector('.fly_img_result_view_screen_full')
+        el.requestFullscreen()
+        screenfull.on('change', this.setShowImageViewer)
+      })
+    },
+    /**
+     * 设置预览
+     */
+    setShowImageViewer() {
+      this.showImageViewer = screenfull.isFullscreen
+    },
+    /**
+     * 关闭图片预览
+     */
+    handleImageViewerClose() {
+      document.exitFullscreen()
+    },
+    /**
+     * 展开收起
+     * @param val
+     */
+    collapseChange(val) {
+      console.log('val===>', val)
+      this.openList = val
+    },
+    /**
+     * 全选功能
+     * @param val
+     */
+    checkboxChange(val) {
+      val.select = !val.select
+      val.extractPhotoList.forEach((item) => {
+        if (val.select) {
+          let indexToRemove = this.selectList.findIndex(
+            (_item) => _item === val.id
+          )
+          if (indexToRemove === -1) {
+            this.selectList.push(item.id)
+          }
+        } else {
+          this.selectList = this.selectList.filter((_item) => _item !== item.id)
+        }
+      })
+      this.$emit('frame-extraction-data', this.collapseList, this.selectList)
+    }
+  },
+  destroyed() {
+    this.$globalEventBus.$off('flyVideoResultOptClick', this.handleTopOptClick)
+    screenfull.off('change', this.setShowImageViewer)
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin no_data {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  font-size: px-to-rem(16);
+  color: #ffffff;
+  .empty-text {
+    pointer-events: none;
+    color: #e8f3fe;
+    font-size: px-to-rem(16);
+    text-align: center;
+    margin-top: px-to-rem(12);
+    &:before {
+      content: url('~@component-gallery/assets/image/ar-label/noData.png');
+      display: block;
+    }
+  }
+}
+$_h_content: px-to-rem(768);
+$_h_card: px-to-rem(396);
+.frame-extraction {
+  margin: 0 px-to-rem(12);
+  ::v-deep .ct-icon__inline-block {
+    margin: 0 px-to-rem(6);
+    .ct-icon {
+      .icon-ctw {
+        color: #e8f3fe !important;
+        font-size: px-to-rem(18) !important;
+      }
+    }
+  }
+  ::v-deep .el-checkbox {
+    pointer-events: auto;
+  }
+
+  &-scrollbar {
+    height: $_h_content;
+    ::v-deep .el-scrollbar__bar .el-scrollbar__thumb {
+      background: rgba(79, 159, 255, 0.4);
+    }
+  }
+  .no-data {
+    height: $_h_content;
+    @include no_data;
+  }
+  .loading {
+    display: contents;
+    span {
+      font-size: px-to-rem(16);
+      margin-top: px-to-rem(6);
+    }
+  }
+  ::v-deep .el-collapse {
+    border: none;
+    .el-collapse-item {
+      margin-bottom: px-to-rem(12);
+      .el-collapse-item__header {
+        justify-content: space-between;
+        background: rgba(79, 159, 255, 0.2);
+        height: px-to-rem(36);
+        border: none;
+        font-weight: 400;
+        font-size: px-to-rem(16);
+        color: #e8f3fe;
+        padding: px-to-rem(12);
+        .el-collapse-item__arrow {
+          display: none;
+        }
+        .collapse-item-icon {
+          &.icon-tongyong_icon_luxiangshouqi {
+            display: block;
+          }
+          &.icon-tongyong_icon_luxiangzhankai {
+            display: none;
+          }
+        }
+        &.is-active {
+          .collapse-item-icon {
+            &.icon-tongyong_icon_luxiangshouqi {
+              display: none;
+            }
+            &.icon-tongyong_icon_luxiangzhankai {
+              display: block;
+            }
+          }
+        }
+        .header-left {
+          display: flex;
+          align-items: center;
+          pointer-events: none;
+        }
+      }
+      .el-collapse-item__wrap {
+        background: rgba(79, 159, 255, 0.1);
+        border: none;
+        .el-collapse-item__content {
+          padding-bottom: 0;
+        }
+      }
+    }
+  }
+  .frame-card-scrollbar {
+    height: $_h_card;
+    margin: 0 px-to-rem(12) 0 px-to-rem(12);
+    &.frame-card-scrollbar-min {
+      height: calc($_h_card / 2 + px-to-rem(8));
+    }
+    .card-list {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      grid-column-gap: px-to-rem(12);
+      .card {
+        height: px-to-rem(180);
+        margin-top: px-to-rem(12);
+      }
+    }
+    &-no-data {
+      height: calc($_h_card / 2);
+      @include no_data;
+    }
+  }
+  .view-img {
+    width: 100vw;
+    height: 100vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    .close_btn {
+      width: px-to-rem(38);
+      height: px-to-rem(38);
+      position: fixed;
+      top: px-to-rem(48);
+      right: px-to-rem(8);
+      z-index: 2;
+      border-radius: 50%;
+      color: rgb(0 0 0 / 50%);
+      font-size: px-to-rem(18);
+      text-align: center;
+      line-height: px-to-rem(38);
+      transform: translate(-50%, -50%);
+      cursor: pointer;
+      background: url('../../../img/icon_full_close.png') no-repeat;
+      background-size: 100% 100%;
+    }
+    .medium {
+      width: 100vw;
+      height: 100vh;
+    }
+    ::v-deep .el-image__inner {
+      object-fit: contain;
+    }
+  }
+}
+</style>

+ 633 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoResultList.vue

@@ -0,0 +1,633 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 视频列表
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div
+    class="VideoResultList"
+    :class="[videoList.length === 0 && 'video-result-list-no-data']"
+  >
+    <div v-show="videoList.length > 1">
+      <el-scrollbar class="VideoResultListScroll">
+        <div class="list_cont">
+          <div
+            class="video_res_item_wrap"
+            v-for="(item, index) in videoList"
+            :key="index + 'url'"
+          >
+            <div
+              class="video_res_item"
+              :class="[item.checked && 'item_checked']"
+              @click="handleItemClick(item, index)"
+            >
+              <div class="top">
+                <div
+                  class="radio_top"
+                  :class="[item.checked && 'radio_checked']"
+                  @click.stop="handleRadioCheck(item, index)"
+                ></div>
+                <div
+                  class="edit"
+                  @click.stop="handleEditName(item)"
+                  :c-tip="'重命名'"
+                >
+                  <i class="iconfont icon-tongyong_icon_bianji"></i>
+                </div>
+              </div>
+              <div class="video_pic_wrap">
+                <video
+                  class="video_pic_cont"
+                  :src="item.videoAccessUrl"
+                ></video>
+                <span class="playIcon"></span>
+              </div>
+              <div class="bottom">
+                <div
+                  class="name"
+                  :c-tip="item.name"
+                  c-tip-placement="top"
+                  c-tip-class="c-tip-normal"
+                  >{{ item.name }}
+                </div>
+                <div class="tag" :class="'type' + item.eventStatus"
+                  >{{ alarmWarnType[item.eventStatus] }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </el-scrollbar>
+      <fly-pagination
+        :currentPage.sync="pageData.page"
+        :total="pageData.total"
+        :page-size.sync="pageData.limit"
+        @changePagination="changePagination"
+        layout="slot, total, sizes, prev, pager, next, jumper"
+        style="text-align: right"
+      >
+        <span class="info-page">
+          当前第{{ pageData.page }}/{{
+            Math.ceil(pageData.total / pageData.limit)
+          }}页
+        </span>
+      </fly-pagination>
+    </div>
+    <div v-show="!loading && videoList.length == 0" class="loading-datas">
+      <span class="empty-text">暂无数据</span>
+    </div>
+    <div v-if="loading" class="loading-datas">
+      <i class="el-icon-loading"></i>
+      <span>加载中</span>
+    </div>
+    <!-- 修改视频名称 -->
+    <dialog-fly-edit-name
+      v-if="dialogEditNameShow"
+      :dialogShow.sync="dialogEditNameShow"
+      :form-data="oneFormData"
+      dialogType="video"
+      @dialogHandleSubmit="dialogHandleSubmit"
+    />
+    <!-- 算法配置 -->
+    <dialog-fly-algorithm
+      v-if="dialogAlgorithm"
+      :dialogShow.sync="dialogAlgorithm"
+      :form-data="algorithmFormData"
+    />
+  </div>
+</template>
+
+<script>
+import DialogFlyEditName from '../DialogFlyEditName.vue'
+import DialogFlyAlgorithm from '../DialogFlyAlgorithm.vue'
+import FlyPagination from '../FlyPagination.vue'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import {
+  queryTaskVideos,
+  deleteRecords,
+  renameRecords
+} from '../../../service/index'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+import {
+  downloadByUrlsApi,
+  syncHandlerApi
+} from '../../../dict/fly-result-methods'
+import dayjs from 'dayjs'
+
+export default {
+  name: 'VideoResultTop',
+  components: {
+    FlyPagination,
+    DialogFlyEditName,
+    DialogFlyAlgorithm
+  },
+  props: {
+    // 任务列表选择数据
+    selectData: {
+      type: Object,
+      default: () => ({})
+    },
+    // 无人机设备树选中数据
+    selectedNodeData: {
+      type: Object,
+      default: () => ({})
+    },
+    isShowDetail: {
+      type: Boolean,
+      default: false
+    },
+    videoDownloadLoading: {
+      type: Boolean,
+      default: false
+    },
+    showComponents: {
+      type: String,
+      default: '2'
+    }
+  },
+  watch: {
+    selectData: {
+      deep: true,
+      handler() {
+        // 任务列表选中数据变化
+        this.pageData = {
+          page: 1,
+          limit: 12,
+          total: 0
+        }
+        this.videoList = []
+        this.$emit('update:isShowDetail', false)
+        this.getVideoList()
+      }
+    },
+    selectedNodeData: {
+      deep: true,
+      handler(val) {
+        console.log('watch ---> selectedNodeData', val)
+        // 设备列表选中数据变化
+        this.$emit('update:isShowDetail', false)
+        this.videoList = []
+      }
+    }
+  },
+  data() {
+    return {
+      videoList: [],
+      dialogEditNameShow: false,
+      oneFormData: {},
+      dialogAlgorithm: false,
+      algorithmFormData: {},
+      loading: false,
+      cardDetail: false, //查看图片卡片
+      alarmWarnType: {
+        2: '有告警',
+        1: '无告警',
+        0: '未识别'
+      },
+      pageData: {
+        page: 1,
+        limit: 12,
+        total: 0
+      }
+    }
+  },
+  mounted() {
+    setupCTips()
+    this.initEvent()
+  },
+  methods: {
+    initEvent() {
+      this.$globalEventBus.$on('flyVideoResultOptClick', this.handleTopOptClick)
+    },
+    /**
+     *  同步数据源
+     */
+    syncHandler() {
+      let params = {
+        taskRecordId: this.selectData.taskRecordId,
+        deviceCode: this.selectedNodeData.deviceCode,
+        accessNode: this.selectedNodeData?.accessNode,
+        mediaSyncStatus: this.selectData.mediaSyncStatus
+      }
+      syncHandlerApi(params)
+    },
+    // 顶部操作栏点击事件
+    handleTopOptClick(val) {
+      console.log('this.showComponents ', this.showComponents)
+      if (!['1', '2'].includes(this.showComponents)) {
+        return
+      }
+      const fnObject = {
+        algorithm: this.algorithmHandler,
+        download: this.downloadHandler,
+        del: this.delHandler,
+        all: this.allHandler,
+        frameExtraction: this.frameExtractionHandler,
+        sync: this.syncHandler
+      }
+      fnObject[val.code] && fnObject[val.code](val)
+    },
+    frameExtractionHandler() {
+      this.$emit('dialog-frame-extraction', true)
+    },
+    // 算法配置
+    algorithmHandler() {
+      this.algorithmFormData = {
+        algorithmList: [],
+        paramsLists: this.isShowDetail
+          ? [this.oneFormData]
+          : this.videoList.filter((item) => item.checked),
+        deviceCode: this.selectedNodeData.deviceCode,
+        flightRouteId: this.selectData.flightRouteId,
+        type: 'video'
+      }
+      this.dialogAlgorithm = true
+    },
+    // 删除
+    delHandler() {
+      const this_ = this
+      this_
+        .$confirm('是否确认删除所选视频?', '提示', {
+          confirmButtonText: '确认',
+          cancelButtonText: '取消',
+          customClass: 'c-confirm c-confirm-fly-result'
+        })
+        .then(() => this_.delApi())
+    },
+    //调用下载接口
+    async downloadHandler() {
+      let params = {}
+      const videoListChecked = this.videoList.filter((item) => item.checked)
+      //videoListChecked.length === 1 ||
+      if (this.isShowDetail) {
+        params = {
+          videoUrls: this.isShowDetail
+            ? [this.oneFormData.videoAccessUrl]
+            : [videoListChecked[0].videoAccessUrl],
+          fileName: this.isShowDetail
+            ? this.oneFormData.name
+            : videoListChecked[0].name
+        }
+      } else {
+        params = {
+          videoUrls: videoListChecked.map((item) => item.videoAccessUrl),
+          fileName: `${this.selectData.planTaskName}-${
+            this.selectedNodeData.flyName
+          }_${dayjs(this.selectData.startTime).format(
+            'YYYYMMDDHHmm'
+          )}-视频.zip`,
+          isZip: 1
+        }
+      }
+      if (params.videoUrls.length > 12) {
+        return CommonMessage.info('视频单次可下载12个')
+      }
+      this.$emit('update:videoDownloadLoading', true)
+      await downloadByUrlsApi(params)
+      this.$emit('update:videoDownloadLoading', false)
+    },
+    delApi() {
+      const paramsListIds = this.videoList
+        .filter((item) => item.checked)
+        .map((item) => item.id)
+      let params = {
+        dataType: 'video',
+        id: this.isShowDetail ? [this.oneFormData.id] : paramsListIds,
+        taskRecordId: this.selectData.taskRecordId,
+        accessNode: this.selectedNodeData?.accessNode
+      }
+      deleteRecords(params)
+        .then((res) => {
+          if (res.code === 200) {
+            CommonMessage.success('删除成功')
+            this.$emit('update:isShowDetail', false)
+            this.getVideoList()
+          }
+        })
+        .catch((err) => {
+          CommonMessage.error(err?.msg || '删除失败')
+        })
+    },
+    // 全选
+    // todo
+    allHandler(val) {
+      if (val.isSelectAll) {
+        // 取消全选
+        this.videoList.forEach((item) => {
+          item.checked = false
+        })
+      } else {
+        // 全部选中
+        this.videoList.forEach((item) => {
+          item.checked = true
+        })
+      }
+    },
+    /**
+     * 翻页
+     */
+    changePagination() {
+      this.getVideoList()
+    },
+    // 获取视频列表
+    getVideoList() {
+      this.videoList = []
+      if (!this.selectData?.taskRecordId) {
+        return
+      }
+      this.loading = true
+      let params = {
+        taskRecordId: this.selectData.taskRecordId,
+        accessNode: this.selectedNodeData?.accessNode,
+        ...this.pageData
+      }
+
+      queryTaskVideos(params)
+        .then((res) => {
+          if (res.code === 200 && res.rows.length > 0) {
+            this.pageData.total = res.total
+            this.videoList = res.rows?.map((item) => {
+              return { ...item, checked: false }
+            })
+            if (this.videoList.length === 1) {
+              this.oneFormData = { ...this.videoList[0] }
+              this.$emit('listClickItem', { ...this.videoList[0], index: 0 })
+            }
+            this.$emit('videoListResoult', this.videoList)
+          } else {
+            this.$emit('videoListResoult', this.videoList)
+          }
+          this.loading = false
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    // 页面单选
+    handleRadioCheck(item, index) {
+      console.log(item, index)
+      let status = !item.checked
+      this.$set(item, 'checked', status)
+    },
+    // 编辑名称
+    handleEditName(itemData) {
+      this.oneFormData = { ...itemData }
+      this.dialogEditNameShow = true
+    },
+    // 点击某一项
+    handleItemClick(item, index) {
+      this.oneFormData = { ...item }
+      this.$emit('listClickItem', { ...item, index, isMT: true })
+    },
+    // 编辑名称提交
+    dialogHandleSubmit(formData) {
+      this.renameApi(formData)
+      this.dialogEditNameShow = false
+    },
+    renameApi(val) {
+      let params = {
+        dataType: 'video',
+        accessNode: this.selectedNodeData.accessNode,
+        name: val.name,
+        id: val.id
+      }
+      renameRecords(params).then((res) => {
+        if (res.code === 200) {
+          this.getVideoList()
+        }
+      })
+    }
+  },
+  destroyed() {
+    this.$globalEventBus.$off('flyVideoResultOptClick', this.handleTopOptClick)
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+.VideoResultList {
+  height: px-to-rem(806);
+  &.video-result-list-no-data {
+    height: calc(px-to-rem(806) + px-to-rem(56));
+  }
+  .info-page {
+    color: #e8f2fe;
+    font-size: px-to-rem(16);
+  }
+  .empty-text {
+    pointer-events: none;
+    color: #e8f3fe;
+    font-size: px-to-rem(14);
+    margin-top: px-to-rem(80);
+    text-align: center;
+
+    &:before {
+      content: url('~@component-gallery/assets/image/ar-label/noData.png');
+      display: block;
+    }
+  }
+  .loading-datas {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    flex-direction: column;
+    font-size: px-to-rem(14);
+    color: #ffffff;
+
+    span {
+      margin-top: px-to-rem(12);
+    }
+  }
+  .VideoResultListScroll {
+    height: px-to-rem(782);
+  }
+  ::v-deep .el-scrollbar {
+    .el-scrollbar__bar .el-scrollbar__thumb {
+      background: rgba(79, 159, 255, 0.4);
+    }
+    .el-scrollbar__bar.is-horizontal {
+      display: none;
+    }
+  }
+  .list_cont {
+    @include flex;
+    flex-wrap: wrap;
+    padding: 0 px-to-rem(12);
+  }
+  .video_res_item_wrap {
+    padding: 0 px-to-rem(6);
+    margin-bottom: px-to-rem(12);
+    width: 33.3%;
+    &:nth-child(3n + 1) {
+      padding-left: 0;
+    }
+    &:nth-child(3n) {
+      padding-right: 0;
+    }
+  }
+  .video_res_item {
+    // width: px-to-rem(332);
+    height: px-to-rem(185);
+    position: relative;
+    &.item_checked {
+      &::before {
+        pointer-events: none;
+        content: '';
+        display: block;
+        position: absolute;
+        z-index: 1;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        border: px-to-rem(2) solid #1373e6;
+        border-radius: px-to-rem(4);
+      }
+    }
+    .top {
+      position: absolute;
+      z-index: 100;
+      top: px-to-rem(6);
+      left: px-to-rem(6);
+      right: px-to-rem(6);
+      @include flex;
+      align-items: flex-start;
+      justify-content: space-between;
+      .radio_top {
+        cursor: pointer;
+        width: px-to-rem(24);
+        height: px-to-rem(24);
+        background: url('../../../img/icon_radio@2x.png') no-repeat;
+        background-size: cover;
+      }
+      .radio_checked {
+        background: url('../../../img/icon_radio_check@2x.png') no-repeat;
+        background-size: cover;
+      }
+      .edit {
+        width: px-to-rem(26);
+        height: px-to-rem(26);
+        line-height: px-to-rem(26);
+        text-align: center;
+        background: rgba(23, 37, 55, 0.8);
+        border-radius: px-to-rem(4);
+        cursor: pointer;
+      }
+    }
+    .bottom {
+      position: absolute;
+      bottom: 0;
+      width: 100%;
+      height: px-to-rem(32);
+      background: rgba(0, 0, 0, 0.7);
+      border-bottom-left-radius: px-to-rem(5);
+      border-bottom-right-radius: px-to-rem(5);
+      padding: 0 px-to-rem(6) px-to-rem(0) px-to-rem(6);
+      @include flex;
+      justify-content: space-between;
+      .name {
+        color: #ffffff;
+        font-size: px-to-rem(14);
+        flex: 1;
+        margin-right: px-to-rem(12);
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+      .tag {
+        font-size: px-to-rem(12);
+        min-width: px-to-rem(54);
+        text-align: center;
+        height: px-to-rem(20);
+        line-height: px-to-rem(20);
+        border-radius: px-to-rem(4);
+        &.type0 {
+          background: linear-gradient(90deg, #52a1e5 0%, #1f7ccc 100%);
+        }
+        &.type1 {
+          background: linear-gradient(90deg, #15bd94 0%, #00a179 100%);
+        }
+        &.type2 {
+          background: linear-gradient(135deg, #ffa43c 0%, #ff8d0b 100%);
+        }
+      }
+    }
+    .video_pic_wrap {
+      width: 100%;
+      height: 100%;
+      position: relative;
+      .video_pic_cont {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        border-radius: px-to-rem(5);
+        object-fit: cover;
+      }
+      .playIcon {
+        width: px-to-rem(40);
+        height: px-to-rem(40);
+        position: absolute;
+        z-index: 999;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        display: block;
+        background: url('../../../img/icon_play_ty@2x.png') no-repeat;
+        background-size: cover;
+        content: '';
+      }
+    }
+    ::v-deep .video_res_player {
+      width: 100%;
+      height: 100%;
+      .video-js,
+      video {
+        width: 100%;
+        height: 100%;
+        border-radius: px-to-rem(5);
+      }
+      .vjs-big-play-button {
+        font-size: 0 !important;
+        display: block;
+        background: transparent;
+        pointer-events: auto;
+        cursor: pointer;
+        width: px-to-rem(40);
+        height: px-to-rem(40);
+        position: absolute;
+        z-index: 999;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        .vjs-icon-placeholder {
+          &::before {
+            display: block;
+            background: url('../../../img/icon_play_ty@2x.png') no-repeat;
+            background-size: cover;
+            content: '';
+          }
+        }
+      }
+      .video-js.vjs-playing {
+        .vjs-big-play-button {
+          display: none;
+        }
+      }
+      .video-js .vjs-modal-dialog {
+        border-radius: px-to-rem(5);
+      }
+      .vjs-error .vjs-error-display .vjs-modal-dialog-content {
+        pointer-events: none;
+        padding: px-to-rem(6) 0;
+      }
+    }
+  }
+}
+</style>

+ 855 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoResultOneDetail.vue

@@ -0,0 +1,855 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 视频详情
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<!-- eslint-disable vue/no-mutating-props -->
+<template>
+  <div
+    class="detail_page fly_video_result_detail"
+    :class="[
+      videoIsBig ? 'video_is_big' : 'video_is_small',
+      fullScreenFlag && 'detail_page_full_screen'
+    ]"
+  >
+    <div
+      v-show="!fullScreenFlag"
+      class="full_screen"
+      @click.stop="handleOpenFullScreen"
+    >
+      <i class="iconfont icon-guotu_icon_quanpingfangda"></i>
+    </div>
+    <div
+      v-show="fullScreenFlag"
+      class="full_screen_close"
+      @click.stop="handleOpenFullScreen"
+    ></div>
+    <template v-if="isMorethenOne && fullScreenFlag">
+      <div
+        class="full_arraw arraw_left"
+        :class="[isFirst && 'disable']"
+        @click.stop="handlePre"
+      >
+        <i class="el-icon-arrow-left" />
+      </div>
+      <div
+        class="full_arraw arraw_right"
+        :class="[isLast && 'disable']"
+        @click.stop="handleNext"
+      >
+        <i class="el-icon-arrow-right" />
+      </div>
+    </template>
+    <!-- 机场信息 无人机信息 -->
+    <detail-left-pop
+      :class="[fullScreenFlag && 'full_screen_left']"
+      v-show="videoIsBig"
+      :currentDataInfo="currentDataInfo"
+      :flyDetailData="flyDetailData"
+    ></detail-left-pop>
+    <div class="contnet">
+      <div class="video_play_content">
+        <video-player
+          class="video_res_player"
+          :playsinline="true"
+          ref="videoPlayer"
+          :options="getPlayerOptions"
+          @loadeddata="onLoadedData($event)"
+          @playing="onPlayerPlayIng($event)"
+          @pause="onPlayerPause($event)"
+          @timeupdate="onPlayerTimeupdate($event)"
+        />
+        <div
+          v-if="!videoIsBig"
+          @click.stop="handleVideoWrapClick"
+          class="window-switch-icon"
+        >
+          <ct-icon name="window-switch" />
+        </div>
+      </div>
+      <div class="map_content map_content_wrap">
+        <common-map
+          :mapId="mapId"
+          chooseMapMemoryKey=""
+          :tileModes="[2]"
+          mapToolElement=".map_content_wrap"
+          :listenGlobalEvent="false"
+          @init-map-resolve="handleInitMapResolve"
+          resolveEventName="flyVideoResoult"
+        ></common-map>
+        <div
+          v-if="videoIsBig"
+          @click.stop="handleMapWrapClick"
+          class="window-switch-icon"
+        >
+          <ct-icon name="window-switch" />
+        </div>
+      </div>
+    </div>
+    <restricted-flight-zone
+      v-if="!videoIsBig"
+      class="flight-zone"
+      :left="12"
+      :bottom="126"
+      :mapId="mapId"
+    />
+    <!-- 属性信息弹窗 -->
+    <fly-attribute
+      v-show="videoIsBig"
+      :isOpen.sync="showAttribute"
+      :bottom="fullScreenFlag ? 48 : 52"
+      :left="fullScreenFlag ? 24 : 12"
+      :attributeList="attributeList"
+      :attribute="videoDetailData"
+      @isHidden="isHiddenBtn"
+    />
+  </div>
+</template>
+
+<script>
+import screenfull from 'screenfull'
+import 'video.js/dist/video-js.css'
+import 'vue-video-player/src/custom-theme.css'
+import { videoPlayer } from 'vue-video-player'
+import FlyAttribute from '../FlyAttribute.vue'
+import DetailLeftPop from './DetailLeftPop.vue'
+import CommonMap from '@component-gallery/map'
+import RestrictedFlightZone from '../../../baseComponents/restrictedFlightZone/RestrictedFlightZone.vue'
+import CTMapOl from '@ct/ct_map_ol'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { queryHisRecordByVideoTime } from '../../../service/index'
+import { addMutyMarker } from '../../../dict/plan-map'
+import uavFly from '../../../img/icon_uav_point@2x.png'
+import { debounce } from 'lodash-es'
+
+let uavFlyInstance = null
+let pointInstance = null
+let mapFlyDataTrack = null
+
+// 解构展开对象末级末级节点
+function getLeafKeyValue(obj, leafKeyValue = {}) {
+  Object.keys(obj).forEach((key) => {
+    const value = obj[key]
+    let pkey = key
+    if (typeof value === 'object' && value !== null) {
+      // 如果值是对象,继续递归
+      getLeafKeyValue(value, leafKeyValue)
+    } else {
+      // 如果值不是对象,则认为是最末级的键值对
+      if (!leafKeyValue[key]) {
+        leafKeyValue[key] = value
+      } else {
+        leafKeyValue[key + pkey] = value
+      }
+    }
+  })
+  return leafKeyValue
+}
+
+export default {
+  name: 'VideoResDetail',
+  components: {
+    FlyAttribute,
+    DetailLeftPop,
+    CommonMap,
+    videoPlayer,
+    RestrictedFlightZone
+  },
+  inject: ['mapRef'],
+  props: {
+    showAttribute: {
+      type: Boolean,
+      default: false
+    },
+    videoDetailData: {
+      type: Object,
+      default: () => ({})
+    },
+    isVideoIsBig: {
+      type: Boolean,
+      default: true
+    },
+    videoList: {
+      type: Array,
+      default: () => []
+    },
+    selectedNodeData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  computed: {
+    // 视频配置项
+    getPlayerOptions() {
+      console.log('getPlayerOptions', this.videoDetailData)
+      let playerOptions_ = JSON.parse(JSON.stringify(this.playerOptions))
+      playerOptions_.sources[0].src = this.videoDetailData.videoAccessUrl || ''
+      playerOptions_.autoplay = this.videoDetailData.isMT ? true : false
+      return playerOptions_
+    },
+    // 数据是否单条
+    isMorethenOne() {
+      return this.videoList.length > 1
+    },
+    // 全屏翻页第一页
+    isFirst() {
+      return this.full_index === 0
+    },
+    // 全屏翻页最后一页
+    isLast() {
+      return this.full_index === this.videoList.length - 1
+    }
+  },
+  data() {
+    let this_ = this
+    return {
+      attributeList: [
+        {
+          label: '视频名称',
+          key: 'name'
+        },
+        {
+          label: '飞行任务',
+          key: 'planTaskName'
+        },
+        {
+          label: '无人机',
+          key: 'flyName'
+        },
+        {
+          label: '拍摄时间',
+          key: 'createTime'
+        }
+      ],
+      flyDetailData: [],
+      attributeData: {},
+      mapId: 'flyVideoResoultMap',
+      playerOptions: {
+        playbackRates: [], //播放速度
+        autoplay: false, //如果true,浏览器准备好时开始回放。
+        loop: false, // 导致视频一结束就重新开始。
+        muted: true, // 默认情况下将会消除任何音频。
+        preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
+        language: 'zh-CN',
+        fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
+        hls: false,
+        sources: [
+          {
+            // type: 'application/x-mpegURL',
+            type: 'video/mp4',
+            src: this_.videoDetailData.videoAccessUrl || '' // url地址
+          }
+        ],
+        aspectRatio: '16:9',
+        poster: '', // 封面地址
+        choosed: false, //被选中的
+        notSupportedMessage: '未上传视频!', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
+        controlBar: {
+          timeDivider: false,
+          durationDisplay: false,
+          remainingTimeDisplay: false,
+          fullscreenToggle: false //全屏按钮
+        }
+      },
+      videoIsBig: true,
+      timer: null,
+      currentDataInfo: null,
+      timerCount: 0,
+      isPlayIng: false,
+      fullScreenFlag: false,
+      full_index: 0,
+      videoDuration: null
+    }
+  },
+  mounted() {
+    this.fullscreenInit()
+    // console.log('this.videoDetailData', this.videoDetailData)
+    this.full_index = this.videoDetailData.index
+  },
+  methods: {
+    isHiddenBtn() {
+      this.$emit('update:showAttribute', false)
+      this.$emit('attrClose')
+    },
+    handleInitMapResolve: debounce(function () {
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      console.log('地图加载成功', mapRef)
+      if (this.flyDetailData.length > 0) {
+        uavFlyInstance = null
+        pointInstance = null
+        this.initFlyInstance(this.videoDuration)
+        this.initStartPoint(mapFlyDataTrack[0])
+        let nowAllData = this.flyDetailData[this.timerCount]
+        console.log('nowAllData', nowAllData)
+        let playTime = new Date(nowAllData?.saveDateTime).getTime()
+        console.log('playTime====>', playTime)
+        playTime && uavFlyInstance?.setTime(playTime)
+        if (this.isPlayIng) {
+          uavFlyInstance?.play()
+        } else {
+          uavFlyInstance?.pause()
+        }
+      }
+    }, 100),
+    // 切换地图主要地方
+    handleMapWrapClick() {
+      if (this.videoIsBig) {
+        this.videoIsBig = false
+        this.$emit('update:isVideoIsBig', this.videoIsBig)
+      }
+    },
+    // 切换视频主要地方
+    handleVideoWrapClick() {
+      if (!this.videoIsBig) {
+        this.videoIsBig = true
+        this.$emit('update:isVideoIsBig', this.videoIsBig)
+      }
+    },
+    // 初始化地图飞行器实例
+    initFlyInstance(duration) {
+      console.log('初始化地图飞行器实例', duration)
+      if (mapFlyDataTrack?.length <= 1) {
+        return
+      }
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      uavFlyInstance?.destroy()
+      uavFlyInstance = null
+      let style = {
+        strokeColor: '#4F9FFF', //'#ffffff'
+        strokeWidth: mapRef.mapType == '3D' ? 8 : 4,
+        strokeOpacity: 1,
+        dashLineOptions: undefined
+      }
+      uavFlyInstance = new CTMapOl.DataSourceControl.lib.TrackPlayBack({
+        mapRef: mapRef,
+        data: mapFlyDataTrack,
+        duration: duration * 1000,
+        trackedEntity: false,
+        circleRun: false,
+        autoRotation: true,
+        styles: {
+          normal: {
+            src: uavFly,
+            width: 33,
+            height: 31
+          },
+          lineBefore: { ...style, strokeColor: 'rgba(255, 149, 0, 0.7)' },
+          lineAfter: style
+        }
+      })
+      let is3D = mapRef.mapType == '3D'
+      setTimeout(() => {
+        let lineafter_ = uavFlyInstance.layer.lineafter
+        let linebefore_ = uavFlyInstance.layer.linebefore
+        if (is3D) {
+          mapRef.mapInstance.zoomTo(
+            [lineafter_, linebefore_],
+            new Cesium.HeadingPitchRange(0, Math.PI / -2, 0)
+          )
+        } else {
+          CTMapOl.ViewControl.common.fitView(
+            { mapRef },
+            {
+              target: { feature: [lineafter_, linebefore_] },
+              duration: 1000,
+              padding: [100, 100, 100, 100]
+            }
+          )
+        }
+      }, 200)
+    },
+    // 初始化飞行轨迹的起点
+    initStartPoint(sPoint) {
+      if (!sPoint) {
+        return
+      }
+      pointInstance?.destroy()
+      pointInstance = null
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      let coordinates = [sPoint.longitude, sPoint.latitude]
+      let styleKey = 'navPoint'
+      let singlePoint = true
+      pointInstance = addMutyMarker(mapRef, coordinates, styleKey, singlePoint)
+    },
+    // 获取飞行轨迹的数据
+    getFlyDetailData(duration) {
+      console.log('请求飞行轨迹的数据', this.videoDetailData)
+      let param = {
+        taskRecordId: this.videoDetailData.taskRecordId,
+        // taskRecordId: '1853698944999235584',
+        videoStartTime: this.videoDetailData.createTime,
+        duration: duration,
+        accessNode: this.selectedNodeData.accessNode
+      }
+      queryHisRecordByVideoTime(param).then((res) => {
+        if (res.code === 200) {
+          console.log('飞行轨迹数据', res)
+          if (res.data?.length <= 1) {
+            return
+          }
+          this.flyDetailData = res.data
+          mapFlyDataTrack = res.data.map((item) => {
+            return {
+              longitude: item.aircraftLocation.longitude,
+              latitude: item.aircraftLocation.latitude,
+              height: item.aircraftLocation.altitude,
+              timestamp: new Date(item?.saveDateTime).getTime()
+            }
+          })
+          console.log('mapFlyDataTrack', mapFlyDataTrack)
+          this.updateCurrentDataInfo()
+          this.initFlyInstance(duration)
+          this.initStartPoint(mapFlyDataTrack[0])
+        }
+      })
+    },
+    // 暂停播放
+    pauseTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+        this.timer = null
+      }
+      uavFlyInstance?.pause()
+    },
+    // 开始播放取数据
+    startTimer() {
+      this.timer = setInterval(() => {
+        this.updateCurrentDataInfo()
+        this.timerCount++
+      }, 1000)
+    },
+    // 更新当前播放的时间的数据
+    updateCurrentDataInfo() {
+      let nowAllData = this.flyDetailData[this.timerCount]
+      let lastKeysObj = nowAllData && getLeafKeyValue(nowAllData)
+      let nestStorage = lastKeysObj?.usedused / lastKeysObj?.totaltotal
+      let droneStorage = lastKeysObj?.used / lastKeysObj?.total
+      this.currentDataInfo = {
+        ...lastKeysObj,
+        droneStorage,
+        nestStorage
+      }
+    },
+    // 视频播放器加载数据成功回调、
+    onLoadedData(player) {
+      console.log('onLoadedData', player)
+      let duration = player.cache_.duration
+      this.videoDuration = duration
+      this.getFlyDetailData(duration)
+    },
+    // 视频播放器开始播放
+    onPlayerPlayIng(player) {
+      console.log('onPlayerPlayIng', player)
+      this.isPlayIng = true
+      uavFlyInstance?.play()
+      this.startTimer()
+    },
+    // 视频播放器暂停
+    onPlayerPause(player) {
+      console.log('onPlayerPause', player)
+      this.isPlayIng = false
+      this.pauseTimer()
+    },
+    // 播放器时间更新回调
+    onPlayerTimeupdate(player) {
+      if (this.isPlayIng && player?.cache_?.currentTime) {
+        // 获取当前播放的时间
+        this.timerCount = parseInt(player.cache_.currentTime)
+        // 获取当前播放的时间的数据更新数据
+        this.updateCurrentDataInfo()
+        let nowAllData = this.flyDetailData[this.timerCount]
+        let playTime = new Date(nowAllData?.saveDateTime).getTime()
+        playTime && uavFlyInstance?.setTime(playTime)
+      }
+    },
+    // 全屏开关处理逻辑__start
+    handleOpenFullScreen() {
+      if (!screenfull.enabled) {
+        CommonMessage.warning('浏览器不支持该功能')
+        return false
+      }
+      let el = document.querySelector('.fly_video_result_detail')
+      if (!this.fullScreenFlag) {
+        el.requestFullscreen()
+      } else {
+        document.exitFullscreen()
+      }
+    },
+    fullscreenChange() {
+      this.fullScreenFlag = screenfull.isFullscreen
+    },
+    fullscreenInit() {
+      if (screenfull.enabled) {
+        screenfull.on('change', this.fullscreenChange)
+      }
+    },
+    fullscreenDestroy() {
+      if (screenfull.enabled) {
+        screenfull.off('change', this.fullscreenChange)
+      }
+    },
+    // 全屏开关处理逻辑__end
+    // 全屏模式下前一页
+    // todo
+    handlePre() {
+      if (this.isFirst) {
+        return
+      }
+      const len = this.videoList.length
+      this.full_index = (this.full_index - 1 + len) % len
+      this.$emit('full_index', this.full_index)
+    },
+    // 全屏模式下下一页
+    handleNext() {
+      if (this.isLast) {
+        return
+      }
+      const len = this.videoList.length
+      this.full_index = (this.full_index + 1) % len
+      this.$emit('full_index', this.full_index)
+    }
+  },
+  beforeDestroy() {
+    clearInterval(this.timer)
+    this.timer = null
+    uavFlyInstance = null
+    pointInstance = null
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+.detail_page {
+  .flight-zone {
+    z-index: 30;
+  }
+  height: calc(px-to-rem(806) - px-to-rem(8));
+  position: relative;
+  .full_screen_left {
+    padding: px-to-rem(24) px-to-rem(24) px-to-rem(24) 0;
+    height: px-to-rem(72);
+    ::v-deep {
+      .top {
+        padding-left: 0;
+      }
+      .content {
+        padding-left: px-to-rem(24);
+      }
+    }
+  }
+  .contnet {
+    width: calc(100% - px-to-rem(2));
+    height: 100%;
+    .video_play_content {
+      position: absolute;
+      z-index: 10;
+      ::v-deep .video_res_player {
+        width: 100%;
+        height: 100%;
+        .video-js,
+        video {
+          width: 100%;
+          height: 100%;
+          border-radius: px-to-rem(8);
+        }
+        .video-js {
+          .vjs-poster {
+            border-radius: px-to-rem(8);
+          }
+        }
+        .vjs-big-play-button {
+          font-size: 0 !important;
+          display: block;
+          background: transparent;
+          pointer-events: auto;
+          cursor: pointer;
+          width: px-to-rem(40);
+          height: px-to-rem(40);
+          position: absolute;
+          z-index: 999;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          .vjs-icon-placeholder {
+            &::before {
+              display: block;
+              background: url('../../../img/icon_play_ty@2x.png') no-repeat;
+              background-size: cover;
+              content: '';
+            }
+          }
+        }
+        .video-js.vjs-playing {
+          .vjs-big-play-button {
+            display: none;
+          }
+        }
+        .vjs-error .vjs-error-display .vjs-modal-dialog-content {
+          pointer-events: none;
+          padding: px-to-rem(6) 0;
+        }
+        .video-js.vjs-error {
+          .vjs-big-play-button {
+            display: none;
+          }
+        }
+      }
+    }
+    .map_content {
+      position: absolute;
+      ::v-deep .map-tools {
+        display: none;
+      }
+    }
+  }
+  .video_play_content {
+    position: absolute;
+    ::v-deep .video_res_player {
+      width: 100%;
+      height: 100%;
+      .video-js,
+      video {
+        width: 100%;
+        height: 100%;
+        border-radius: px-to-rem(8);
+      }
+      .video-js {
+        .vjs-poster {
+          border-radius: px-to-rem(8);
+        }
+      }
+      .vjs-big-play-button {
+        font-size: 0 !important;
+        display: block;
+        background: transparent;
+        pointer-events: auto;
+        cursor: pointer;
+        width: px-to-rem(40);
+        height: px-to-rem(40);
+        position: absolute;
+        z-index: 999;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        .vjs-icon-placeholder {
+          &::before {
+            display: block;
+            background: url('../../../img/icon_play_ty@2x.png') no-repeat;
+            background-size: cover;
+            content: '';
+          }
+        }
+      }
+      .video-js.vjs-playing {
+        .vjs-big-play-button {
+          display: none;
+        }
+      }
+      .vjs-error .vjs-error-display .vjs-modal-dialog-content {
+        pointer-events: none;
+        padding: px-to-rem(6) 0;
+      }
+      .video-js.vjs-error {
+        .vjs-big-play-button {
+          display: none;
+        }
+      }
+    }
+    ::v-deep .vjs-control-bar {
+      height: px-to-rem(40);
+      background: rgba(0, 0, 0, 0.7);
+      .vjs-button {
+        .vjs-icon-placeholder::before {
+          padding-top: px-to-rem(5);
+        }
+      }
+      .vjs-volume-horizontal {
+        margin-top: px-to-rem(5);
+      }
+    }
+  }
+  .full_screen {
+    position: absolute;
+    z-index: 20;
+    right: px-to-rem(12);
+    top: px-to-rem(12);
+    width: px-to-rem(30);
+    height: px-to-rem(30);
+    background: rgba(23, 37, 55, 0.9);
+    border-radius: px-to-rem(4);
+    color: #ffffff;
+    text-align: center;
+    line-height: px-to-rem(30);
+    cursor: pointer;
+    .iconfont {
+      font-size: px-to-rem(16);
+      color: inherit;
+    }
+  }
+  .full_screen_close {
+    cursor: pointer;
+    position: absolute;
+    z-index: 20;
+    right: px-to-rem(24);
+    top: px-to-rem(24);
+    width: px-to-rem(38);
+    height: px-to-rem(38);
+    background: url('~@component-gallery/assets/image/common/wiseblue/icon_close@2x.png');
+    background-size: 100% 100%;
+  }
+  .full_arraw {
+    position: fixed;
+    top: 50%;
+    z-index: 99;
+    width: 0.61rem;
+    height: 0.61rem;
+    border-radius: 50%;
+    font-size: 0.22rem;
+    text-align: center;
+    transform: translate(0, -50%);
+    line-height: 0.64rem;
+    cursor: pointer;
+    background: rgba(23, 37, 55, 0.8);
+    color: #e8f3fe;
+    &:hover {
+      background: rgba(23, 37, 55, 0.6);
+      color: rgba(232, 243, 254, 0.6);
+    }
+    &.arraw_left {
+      left: px-to-rem(24);
+    }
+    &.arraw_right {
+      right: px-to-rem(24);
+    }
+    &.disable {
+      cursor: not-allowed;
+      background: rgba(23, 37, 55, 0.4);
+      color: rgba(232, 243, 254, 0.4);
+      &:hover {
+        background: rgba(23, 37, 55, 0.4);
+        color: rgba(232, 243, 254, 0.4);
+      }
+    }
+    i {
+      font-weight: 600;
+    }
+  }
+  &.video_is_small {
+    .video_play_content {
+      width: px-to-rem(380);
+      height: px-to-rem(214);
+      right: px-to-rem(12);
+      bottom: px-to-rem(12);
+      &.full_screen_video {
+        right: px-to-rem(24);
+        bottom: px-to-rem(24);
+        ::v-deep .vjs-control-bar {
+          bottom: px-to-rem(1);
+        }
+      }
+      .window-switch-icon {
+        left: px-to-rem(12);
+        bottom: px-to-rem(52);
+      }
+    }
+    .map_content {
+      width: 100%;
+      height: 100%;
+      ::v-deep .map-tools {
+        display: block;
+        position: absolute;
+        bottom: 0;
+        left: px-to-rem(100);
+        right: initial !important;
+        .zoom-tool {
+          left: px-to-rem(-88);
+          right: unset;
+          bottom: px-to-rem(52);
+        }
+        .compass-tool {
+          position: absolute;
+          right: px-to-rem(-20);
+          bottom: px-to-rem(32);
+        }
+        .scale-line {
+          left: px-to-rem(-88);
+          right: unset;
+          bottom: px-to-rem(12);
+          .ctmap-union-scale-line-inner {
+            width: px-to-rem(86) !important;
+          }
+        }
+        .tile-control {
+          display: none;
+        }
+      }
+    }
+  }
+  &.video_is_big {
+    .video_play_content {
+      width: 100%;
+      height: 100%;
+    }
+    .map_content {
+      width: px-to-rem(384);
+      height: px-to-rem(216);
+      right: px-to-rem(12);
+      bottom: px-to-rem(52);
+      z-index: 10;
+    }
+    .window-switch-icon {
+      left: px-to-rem(12);
+      bottom: px-to-rem(12);
+    }
+  }
+  .window-switch-icon {
+    width: px-to-rem(30);
+    height: px-to-rem(30);
+    background: rgba(23, 37, 55, 0.9);
+    border-radius: px-to-rem(4);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    ::v-deep .ct-icon {
+      width: px-to-rem(18) !important;
+      .icon-ctw-window-switch {
+        font-size: px-to-rem(18) !important;
+        color: #e8f3fe !important;
+      }
+    }
+  }
+  &.detail_page_full_screen {
+    &.video_is_big {
+      .video_play_content {
+        ::v-deep .video_res_player {
+          .vjs-control-bar {
+            bottom: px-to-rem(1);
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+.fly_video_result_detail {
+  &.video_is_small {
+    .flight-zone {
+      &.innercomp-abcontainer {
+        &.restricted-flight-zone {
+          &.highlight-title {
+            position: absolute;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 318 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoResultTopLine.vue

@@ -0,0 +1,318 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/16
+ * @Description: 视频列表筛选框及右侧页签
+ -->
+<template>
+  <div class="VideoResultTop" v-if="showDetail || videoListLength">
+    <div class="left img_name" v-if="showDetail || showFrameExtraction">
+      <img
+        @click="handleBack"
+        :class="[
+          'img_name_back',
+          !showFrameExtraction && isOneVideo && 'img_name_disabled'
+        ]"
+        src="../../../img/fly-back.svg"
+        alt=""
+      />
+      <span
+        class="video_info_name"
+        :c-tip="videoDetailData.name"
+        c-tip-placement="top"
+        c-tip-class="c-tip-normal"
+        >{{ videoDetailData.name }}
+      </span>
+      <div
+        class="video_info_name frame_extraction"
+        @click="lookFrameExtraction"
+      >
+        <ct-icon name="pic-list"></ct-icon>
+        <span>查看抽帧图片</span>
+        <ct-icon name="arrow-right"></ct-icon>
+      </div>
+    </div>
+    <div v-else class="left"></div>
+    <div class="right" v-show="videoListLength">
+      <div
+        class="opt_item"
+        :class="[attrFlag && 'active', showDetail && 'opt_item_normal']"
+        @click="handleChangeAttr"
+        v-show="showDetail && isVideoIsBig && showComponents !== '3'"
+      >
+        <ct-icon
+          :name="attrFlag ? 'attribute-active' : 'attribute'"
+          class="item-tab-icon"
+        ></ct-icon>
+        <span class="font_class">视频属性</span>
+      </div>
+      <div
+        class="opt_item"
+        v-for="(item, index) in videoOperateTab"
+        :key="item.code + index + 'code'"
+        @click="handleOptClick(item)"
+        :class="[
+          showDetail && item.code,
+          (isHaveSelect || showDetail) && 'opt_item_normal',
+          'opt_item' + item.code,
+          item.code == 'download' &&
+            videoDownloadLoading &&
+            'videoDownloadLoading',
+          showFrameExtraction && 'showFrameExtraction',
+          showFrameExtraction &&
+            item.code == 'frameExtraction' &&
+            'opt_item_normal',
+          item.code == 'sync' && 'opt_item_normal'
+        ]"
+      >
+        <template
+          v-if="!(item.code === 'frameExtraction' && showComponents === '2')"
+        >
+          <ct-icon
+            v-if="item.code !== 'all'"
+            :name="item.icon"
+            class="item-tab-icon"
+          ></ct-icon>
+          <span v-if="item.code === 'all'" class="all-select-icon">
+            <span
+              class="item-tab-all"
+              :class="[isSelectAll && 'item-tab-all-active']"
+            ></span>
+          </span>
+          <span class="font_class">{{ item.name }}</span>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { videoOperateTab } from '../../../entry/data'
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+
+export default {
+  name: 'VideoResultTopLine',
+  data() {
+    return {
+      videoOperateTab,
+      attrFlag: true
+    }
+  },
+  props: {
+    showAttribute: {
+      type: Boolean,
+      default: false
+    },
+    showDetail: {
+      type: Boolean,
+      default: false
+    },
+    videoDetailData: {
+      type: Object,
+      default: () => ({})
+    },
+    isVideoIsBig: {
+      type: Boolean,
+      default: true
+    },
+    videoDownloadLoading: {
+      type: Boolean,
+      default: false
+    },
+    showFrameExtraction: {
+      type: Boolean,
+      default: false
+    },
+    videoListLength: {
+      type: Number,
+      default: 0
+    },
+    isHaveSelect: {
+      type: Boolean,
+      default: false
+    },
+    isSelectAll: {
+      type: Boolean,
+      default: false
+    },
+    showComponents: {
+      type: String
+    }
+  },
+  watch: {
+    showAttribute(val) {
+      this.attrFlag = val
+    }
+  },
+  computed: {
+    isOneVideo() {
+      return this.videoListLength === 1
+    }
+  },
+  mounted() {
+    setupCTips()
+  },
+  methods: {
+    //  点击返回
+    handleBack() {
+      if (this.showFrameExtraction) {
+        this.$emit('update:showDetail', true)
+        this.$emit('update:showFrameExtraction', false)
+        return
+      }
+      this.$emit('update:showDetail', false)
+    },
+    lookFrameExtraction() {
+      this.$emit('update:showDetail', false)
+      this.$emit('update:showFrameExtraction', true)
+    },
+    //  切换属性
+    handleChangeAttr() {
+      this.attrFlag = !this.attrFlag
+      this.$emit('update:showAttribute', this.attrFlag)
+    },
+    //  操作按钮点击
+    handleOptClick(item) {
+      if (item.code === 'all') {
+        item.isSelectAll = this.isSelectAll
+      }
+      this.$globalEventBus.$emit('flyVideoResultOptClick', item)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@mixin flex {
+  display: flex;
+  align-items: center;
+}
+.VideoResultTop {
+  height: px-to-rem(20);
+  margin: px-to-rem(18) 0;
+  padding: px-to-rem(0) px-to-rem(12);
+  @include flex;
+  justify-content: space-between;
+  .right {
+    @include flex;
+  }
+  .img_name {
+    @include flex;
+    .img_name_back {
+      width: px-to-rem(30);
+      height: px-to-rem(30);
+      cursor: pointer;
+      margin-right: px-to-rem(12);
+      &.img_name_disabled {
+        opacity: 0.4;
+        pointer-events: none;
+      }
+    }
+    .video_info_name {
+      font-family: PingFangSC, PingFang SC;
+      font-weight: 500;
+      font-size: px-to-rem(18);
+      color: #e8f3fe;
+      max-width: px-to-rem(450);
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      cursor: default;
+    }
+    .frame_extraction {
+      display: flex;
+      align-items: center;
+      margin-left: px-to-rem(12);
+      & > span {
+        margin: 0 px-to-rem(6);
+      }
+      ::v-deep .ct-icon {
+        .icon-ctw {
+          color: #e8f3fe !important;
+          font-size: px-to-rem(16) !important;
+        }
+      }
+    }
+  }
+  .opt_item {
+    margin-right: px-to-rem(12);
+    margin-top: px-to-rem(1);
+    cursor: pointer;
+    color: rgba(232, 243, 254, 0.4);
+    pointer-events: none;
+    @include flex;
+    &.all {
+      display: none;
+    }
+    &.showFrameExtraction.all {
+      display: flex;
+    }
+    .item-tab-icon {
+      font-size: px-to-rem(20);
+      margin-right: px-to-rem(6);
+      margin-bottom: px-to-rem(3);
+      color: rgba(232, 243, 254, 0.4);
+      height: px-to-rem(20);
+      line-height: px-to-rem(20);
+      ::v-deep .ct-icon {
+        width: auto !important;
+        .icon-ctw {
+          color: inherit !important;
+          font-size: px-to-rem(16) !important;
+        }
+      }
+    }
+    .font_class {
+      font-size: px-to-rem(16);
+    }
+    .all-select-icon {
+      width: px-to-rem(20);
+      height: px-to-rem(20);
+      margin-right: px-to-rem(6);
+      .item-tab-all {
+        display: block;
+        width: px-to-rem(14);
+        height: px-to-rem(14);
+        border: px-to-rem(1) solid #e8f3fe;
+        border-radius: 50%;
+        margin-top: px-to-rem(3);
+      }
+      .item-tab-all-active {
+        background-image: url('../../../img/fly-img-card-active.svg');
+        background-size: 150%;
+        background-position: px-to-rem(-3) px-to-rem(-2);
+        background-repeat: no-repeat;
+        border: none;
+      }
+    }
+    span {
+      line-height: 1;
+      color: inherit;
+    }
+    &.opt_itemall {
+      color: rgba(232, 243, 254, 1);
+      pointer-events: auto;
+    }
+    &.opt_item_normal {
+      color: rgba(232, 243, 254, 1);
+      pointer-events: auto;
+      .item-tab-icon {
+        color: rgba(232, 243, 254, 1);
+        pointer-events: auto;
+      }
+    }
+    &.active {
+      pointer-events: auto;
+      color: #4f9fff;
+      .item-tab-icon {
+        color: #4f9fff;
+        pointer-events: auto;
+      }
+    }
+    &.videoDownloadLoading {
+      opacity: 0.4;
+      pointer-events: none;
+    }
+  }
+}
+</style>

+ 228 - 0
src/components/common-comp-uav-fly-manage/src/components/flyResult/videoResoult/VideoShow.vue

@@ -0,0 +1,228 @@
+<!--
+ * @Author: shiyzhang
+ * @Date: 2024/11/15
+ * @Description: 视频
+ -->
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<template>
+  <div class="VideoResult">
+    <!-- 顶部操作按钮 -->
+    <video-result-top
+      :isVideoIsBig.sync="isVideoIsBig"
+      :showDetail.sync="showDetail"
+      :showAttribute.sync="showAttr"
+      :videoDetailData="videoDetailData"
+      :videoDownloadLoading="videoDownloadLoading"
+      :isHaveSelect="isHaveSelect"
+      :isSelectAll="isSelectAll"
+      :videoListLength="videoListLength"
+      :showFrameExtraction.sync="showFrameExtraction"
+      :showComponents="showComponents"
+    ></video-result-top>
+    <div class="fly_video_result_drag">
+      <!-- 列表展示 -->
+      <video-result-list
+        ref="videoResultList"
+        @listClickItem="listClickItem"
+        v-show="showComponents === '2'"
+        :isShowDetail.sync="showDetail"
+        :selectData="selectData"
+        :videoDownloadLoading.sync="videoDownloadLoading"
+        :selectedNodeData="selectedNodeData"
+        @videoListResoult="videoListResoult"
+        :showComponents="showComponents"
+        @dialog-frame-extraction="frameExtractionHandler"
+      ></video-result-list>
+      <!-- 详情展示 -->
+      <video-result-one-detail
+        v-if="showComponents === '1'"
+        :isVideoIsBig.sync="isVideoIsBig"
+        :showAttribute.sync="showAttr"
+        :videoDetailData="videoDetailData"
+        :videoList="videoList"
+        :selectedNodeData="selectedNodeData"
+        @full_index="handleFullindex"
+      ></video-result-one-detail>
+      <!--抽帧列表-->
+      <frame-extraction
+        v-if="showComponents === '3'"
+        :videoDownloadLoading.sync="videoDownloadLoading"
+        :selectedNodeData="selectedNodeData"
+        :selectData="selectData"
+        :videoDetailData="videoDetailData"
+        @dialog-frame-extraction="frameExtractionHandler"
+        @frame-extraction-data="frameExtractionData"
+      />
+    </div>
+    <dialog-frame-extraction
+      v-if="dialogFrameExtraction"
+      :dialogShow.sync="dialogFrameExtraction"
+      :videoData="videoDetailData"
+      :selectData="selectData"
+      :selectedNodeData="selectedNodeData"
+      class="fly_video_result_detail"
+    />
+    <dialog-frame-extraction-progress
+      :videoDetailData="videoDetailData"
+      :left="12"
+      :bottom="12"
+      dragContainer="fly_video_result_drag"
+    />
+  </div>
+</template>
+
+<script>
+import VideoResultTop from './VideoResultTopLine.vue'
+import VideoResultList from './VideoResultList.vue'
+import VideoResultOneDetail from './VideoResultOneDetail.vue'
+import FrameExtraction from './FrameExtraction.vue'
+import DialogFrameExtractionProgress from './DialogFrameExtractionProgress.vue'
+import DialogFrameExtraction from './DialogFrameExtraction.vue'
+
+export default {
+  name: 'VideoShow',
+  components: {
+    DialogFrameExtraction,
+    DialogFrameExtractionProgress,
+    VideoResultTop,
+    VideoResultList,
+    VideoResultOneDetail,
+    FrameExtraction
+  },
+  props: {
+    selectData: {
+      type: Object,
+      default: () => ({})
+    },
+    selectedNodeData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  watch: {
+    selectedNodeData: {
+      handler() {
+        this.showFrameExtraction = false
+        this.showDetail = false
+      }
+    }
+  },
+  computed: {
+    videoListLength() {
+      return this.videoList.length || 0
+    },
+    /**
+     * 是否有选中,算法配置,下载,删除状态
+     * @returns {boolean}
+     */
+    isHaveSelect() {
+      if (this.showComponents === '3') {
+        return this.frameExtractionSelectList.length > 0 //this.frameExtractionList.some((item) => item.select === true)
+      }
+      return this.videoList.some((item) => item.checked === true)
+    },
+    /**
+     * 全选按钮状态
+     * @returns {this is *[]|boolean}
+     */
+    isSelectAll() {
+      if (this.showComponents === '3') {
+        return this.frameExtractionList.length > 0
+          ? this.frameExtractionList.every((item) => item.select === true)
+          : false
+      }
+      return this.videoList.length > 0
+        ? this.videoList.every((item) => item.checked === true)
+        : false
+    },
+    /**
+     * 显示组件
+     * @returns {string}
+     */
+    showComponents() {
+      //抽帧列表
+      // eslint-disable-next-line no-unreachable
+      if (this.showFrameExtraction) {
+        return '3'
+      }
+      //详情展示
+      if (this.showDetail) {
+        return '1'
+      }
+      //展示列表
+      return '2'
+    }
+  },
+  data() {
+    return {
+      showDetail: false,
+      showFrameExtraction: false,
+      showAttr: true,
+      videoDetailData: {},
+      videoList: [],
+      frameExtractionList: [],
+      frameExtractionSelectList: [],
+      isVideoIsBig: true,
+      videoDownloadLoading: false,
+      dialogFrameExtraction: false //视频抽帧弹窗
+    }
+  },
+  methods: {
+    /**
+     * 视频抽帧确定
+     */
+    frameExtractionHandler() {
+      console.log('videoDetailData', this.videoDetailData)
+      this.dialogFrameExtraction = true
+    },
+    // 列表点击事件
+    listClickItem(data) {
+      this.showDetail = true
+      this.videoDetailData = {
+        ...data,
+        planTaskName: this.selectData.planTaskName,
+        flyName: this.selectedNodeData.name
+      }
+    },
+    // 当前任务下的视频数量
+    videoListResoult(data) {
+      this.showFrameExtraction = false
+      this.videoList = data
+    },
+    /**
+     * 视频抽帧数据
+     * @param data
+     * @param selectList
+     */
+    frameExtractionData(data, selectList) {
+      this.frameExtractionList = [...data]
+      if (this.showComponents === '3') {
+        this.frameExtractionSelectList = selectList
+      }
+    },
+    // 更新当前视频详情数据
+    handleFullindex(index) {
+      this.videoDetailData = {
+        ...this.videoList[index],
+        index,
+        planTaskName: this.selectData.planTaskName,
+        flyName: this.selectedNodeData.name
+      }
+    },
+    /**
+     * 调用查询
+     */
+    getVideoList() {
+      this.showFrameExtraction = false
+      this.showDetail = false
+      this.$nextTick(() => this.$refs.videoResultList.getVideoList())
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.VideoResult {
+  position: relative;
+}
+</style>

+ 45 - 0
src/components/common-comp-uav-fly-manage/src/components/operationPlan/AirLineWindow.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="airline-window">{{
+    info.name + formatTimeFromSeconds(info.time) + ' ' + info.distance
+  }}</div>
+</template>
+
+<script>
+import { formatTimeFromSeconds } from '../../dict/plan-map'
+export default {
+  name: 'AirLineWindow',
+  props: {
+    info: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  computed: {
+    formatTimeFromSeconds() {
+      return formatTimeFromSeconds
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+.airline-window {
+  color: #e8f3fe;
+  height: px-to-rem(36);
+  font-size: px-to-rem(16);
+  min-width: px-to-rem(160);
+  padding: 0 px-to-rem(12);
+  text-align: center;
+  &.start-pop {
+    background: url('../../img/start-pop.png') no-repeat;
+    background-size: 100% 100%;
+    line-height: px-to-rem(32);
+  }
+  &.end-pop {
+    background: url('../../img/end-pop.png') no-repeat;
+    background-size: 100% 100%;
+    line-height: px-to-rem(40);
+  }
+}
+</style>

+ 953 - 0
src/components/common-comp-uav-fly-manage/src/components/operationPlan/OperationPlan.vue

@@ -0,0 +1,953 @@
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<!--  eslint-disable vue/no-unused-components -->
+<template>
+  <div class="operation-plan" v-if="visible">
+    <img
+      src="../../img/back.png"
+      class="back-img"
+      v-if="pageType === 'view'"
+      @click="close"
+    />
+    <div class="operation-plan-header">
+      <div class="operation-plan-title">{{ pageTitle }}</div>
+    </div>
+    <el-scrollbar class="scrollbar">
+      <el-form
+        :disabled="pageType === 'view'"
+        hide-required-asterisk
+        :rules="rules"
+        :model="form"
+        ref="operationPlanForm"
+      >
+        <el-form-item label-position="top" prop="planName" label="计划名称">
+          <el-input
+            v-model="form.planName"
+            :maxlength="20"
+            clearable
+            placeholder="输入计划名称"
+          ></el-input>
+        </el-form-item>
+        <el-form-item
+          label-position="top"
+          class="select-tree"
+          prop="deviceCode"
+          label="执行设备"
+        >
+          <!-- todo 虚拟树 、日历部分抽取组件 -->
+          <select-tree
+            onlyLeafSelect
+            showPrefixIcon
+            filterable
+            expandOnClickNode
+            :disabled="pageType === 'view'"
+            :defaultProps="{ label: 'name', children: 'list' }"
+            nodeKey="code"
+            industryClass="common-iw-s operation-select-tree"
+            v-model="form.deviceCode"
+            :dataSource="uavTreeData"
+            @clear="clearDeviceCode"
+            @refresh="getDeviceAreaTreeData"
+          ></select-tree>
+        </el-form-item>
+        <el-form-item label-position="top" prop="airline" label="执行航线">
+          <el-select
+            placeholder="输入关键字"
+            :popper-append-to-body="false"
+            popper-class="operation-plan-select"
+            @change="setAirLineInMap"
+            filterable
+            v-model="form.airline"
+          >
+            <el-option
+              v-for="item in airlineList"
+              :value="item.flightRouteId"
+              :label="item.flightRouteName"
+              :key="item.flightRouteId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <div class="form-item">
+          <div class="form-item-label">任务策略</div>
+          <div class="form-item-content">
+            <tab-select
+              :options="taskStrategyList"
+              :value.sync="form.currentTask"
+              :disabled="pageType === 'view'"
+              @change="setCurrentValue('currentTask')"
+            ></tab-select>
+          </div>
+        </div>
+        <div v-if="form.currentTask === '0'" class="form-item-flex">
+          <el-form-item
+            label-position="top"
+            prop="executionTime"
+            label="执行时间"
+            class="no-bg"
+          >
+            <!--          <div class="form-item-label">执行时间</div>-->
+            <el-date-picker
+              popper-class="operation-plan-datetime"
+              :editable="false"
+              class="datetime"
+              :picker-options="pickerOptions"
+              v-model="form.executionTime"
+              format="yyyy-MM-dd HH:mm"
+              value-format="yyyy-MM-dd HH:mm:ss"
+              type="datetime"
+              clear-icon="el-icon-close custom-close-icon"
+              prefix-icon="icon iconfont icon-tongyong_icon_shijian"
+              placeholder="选择日期时间"
+            >
+            </el-date-picker>
+          </el-form-item>
+        </div>
+        <div v-if="form.currentTask === '1'" class="form-item-flex">
+          <el-form-item
+            label-position="top"
+            prop="executionDate"
+            label="执行日期"
+            class="no-bg"
+          >
+            <div class="form-item-flex">
+              <!--              <div class="form-item-label">执行日期</div>-->
+              <el-date-picker
+                v-model="form.executionDate"
+                class="c-date-editor search-date"
+                popper-class="c-date-editor-picker"
+                type="daterange"
+                range-separator="至"
+                :picker-options="pickerOptions"
+                value-format="yyyy-MM-dd"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                prefix-icon="iconfont_tools icon-tongyong-shaixuanriqi alarmfilte"
+              >
+              </el-date-picker>
+            </div>
+          </el-form-item>
+
+          <div class="form-item-flex">
+            <div class="form-item-label form-item-label-no-bg">重复频率</div>
+            <div class="time-switch">
+              <div class="row">
+                <el-radio
+                  @change="clearImmediate"
+                  v-model="form.repeatFrequency"
+                  label="0"
+                  >每天
+                </el-radio>
+              </div>
+            </div>
+            <div class="time-switch">
+              <div class="row">
+                <el-radio
+                  @change="clearImmediate"
+                  class="item-width"
+                  v-model="form.repeatFrequency"
+                  label="1"
+                  >每周
+                </el-radio>
+                <el-checkbox
+                  :indeterminate="weekAllIndeterminate"
+                  @change="
+                    handleMonthAllChange(
+                      $event,
+                      'weeks',
+                      'weekList',
+                      'weekAllIndeterminate'
+                    )
+                  "
+                  v-model="weekAll"
+                  class="item-width"
+                  >全部
+                </el-checkbox>
+              </div>
+              <div class="row-content">
+                <checkboxs
+                  :value.sync="form.weeks"
+                  :options="weekList"
+                  @change="
+                    handleListChange(
+                      $event,
+                      'weekAll',
+                      'weekList',
+                      'weekAllIndeterminate'
+                    )
+                  "
+                ></checkboxs>
+              </div>
+            </div>
+            <div class="time-switch">
+              <div class="row">
+                <el-radio
+                  @change="clearImmediate"
+                  class="item-width"
+                  v-model="form.repeatFrequency"
+                  label="2"
+                  >每月
+                </el-radio>
+                <el-checkbox
+                  :indeterminate="monthAllIndeterminate"
+                  @change="
+                    handleMonthAllChange(
+                      $event,
+                      'months',
+                      'monthList',
+                      'monthAllIndeterminate'
+                    )
+                  "
+                  v-model="monthAll"
+                  class="item-width"
+                  >全部
+                </el-checkbox>
+              </div>
+              <checkboxs
+                :value.sync="form.months"
+                :options="monthList"
+                @change="
+                  handleListChange(
+                    $event,
+                    'monthAll',
+                    'monthList',
+                    'monthAllIndeterminate'
+                  )
+                "
+              ></checkboxs>
+            </div>
+          </div>
+          <div class="form-item-flex">
+            <el-form-item
+              label-position="top"
+              prop="flyTime"
+              label="执行时间"
+              class="no-bg"
+            >
+              <!--              <div class="form-item-label">执行时间</div>-->
+              <div class="form-picker-item">
+                <div class="form-picker-item-content">
+                  <el-time-picker
+                    popper-class="operation-plan-time-panel"
+                    class="time-picker"
+                    format="HH:mm"
+                    value-format="HH:mm:ss"
+                    v-model="form.flyTime"
+                    :editable="false"
+                    placeholder="选择时间"
+                  >
+                  </el-time-picker>
+                </div>
+              </div>
+            </el-form-item>
+          </div>
+        </div>
+        <div class="form-item">
+          <div class="form-item-label">数据上传</div>
+          <div class="form-item-content">
+            <tab-select
+              :options="dataUploadModes"
+              :value.sync="form.uploadMode"
+              :disabled="pageType === 'view'"
+            ></tab-select>
+          </div>
+        </div>
+        <div class="form-item">
+          <div class="form-item-label">录屏</div>
+          <div class="form-item-content">
+            <tab-select
+              :options="screenRecordModes"
+              :value.sync="form.screenRecord"
+              disabled
+            ></tab-select>
+          </div>
+        </div>
+      </el-form>
+    </el-scrollbar>
+    <div class="operation-plan-footer" v-if="pageType !== 'view'">
+      <base-button
+        type="primary"
+        class="submit-btn footer-btn"
+        @click="submitForm"
+        >确定
+      </base-button>
+      <base-button class="footer-btn" @click="close">取消</base-button>
+    </div>
+    <air-line-card
+      :type="airLineType"
+      v-if="(airLineType === '1' || airLineType === '2') && form.airline"
+      :lineInfo="lineInfo"
+    ></air-line-card>
+  </div>
+</template>
+
+<script>
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import eventPath from '@component-gallery/build-event-bus-path'
+import AirLineCard from '../airLineCard/AirLineCard'
+import SelectTree from '../selectTree/SelectTree'
+import TabSelect from '../tabSelect/TabSelect'
+import Checkboxs from '../checkboxs/CheckboxInterval'
+import { requestSDK } from '@ct/iframe-connect-sdk'
+import {
+  operationPlanType,
+  taskStrategyList,
+  dataUploadModes,
+  screenRecordModes,
+  weekList,
+  monthList,
+  rules,
+  setUserInfo
+} from '../../dict/dict'
+import {
+  getDeviceAreaTree,
+  getAirLineList,
+  findAirPlanById,
+  editAirPlan,
+  addAirPlan,
+  getAirLineInfo,
+  getPlanName
+} from '../../service'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import {
+  findDeviceNodeByCode,
+  setAirLineAndPoint,
+  setOrthoImage,
+  handleAirLineAndPointData,
+  handleOrthophotoData,
+  getHeights,
+  getLinkLines,
+  planMapFocusArea,
+  planMapFocusEntity
+} from '../../dict/plan-map'
+
+export default {
+  name: 'OperationPlan',
+  props: {
+    pageType: {
+      type: String,
+      default: 'add'
+    },
+    mapId: {
+      type: String,
+      default: ''
+    },
+    planId: {
+      type: String,
+      default: ''
+    },
+    planName: {
+      type: String,
+      default: ''
+    },
+    snapshotId: {
+      type: [String, null],
+      default: ''
+    }
+  },
+  inject: ['mapRef'],
+  data() {
+    return {
+      visible: true,
+      weekAll: false,
+      monthAll: false,
+      weekAllIndeterminate: false,
+      monthAllIndeterminate: false,
+      form: {
+        planName: '', // 计划名称
+        deviceCode: '', // 设备编码
+        airline: '', // 航线
+        currentTask: '2', // 任务策略
+        // autoContinueFly: '1', // 自动断点续飞
+        uploadMode: '2', // 数据上传
+        screenRecord: '1', // 录屏
+        executionTime: '', // 执行时间
+        executionDate: '', // 执行日期
+        flyTime: '', // 执行时间
+        repeatFrequency: '0', // 重复频率
+        weeks: [], // 周
+        months: [] // 月
+      },
+      airlineList: [],
+      airDeviceList: [],
+      deviceTree: [],
+      uavTreeData: [],
+      rules,
+      airLineAndPointMap: {},
+      orthoImageMap: {},
+      airLineType: null, // 当前航点航线类型
+      lineInfo: {}, // 当前航线信息
+      pickerOptions: {
+        disabledDate(time) {
+          const today = new Date()
+          const startOfDay = new Date(
+            today.getFullYear(),
+            today.getMonth(),
+            today.getDate()
+          ).getTime()
+          return time.getTime() < startOfDay
+        }
+      }
+    }
+  },
+  components: {
+    BaseButton,
+    AirLineCard,
+    SelectTree,
+    TabSelect,
+    Checkboxs
+  },
+  computed: {
+    pageTitle() {
+      return operationPlanType[this.pageType].title
+    },
+    taskStrategyList() {
+      return taskStrategyList
+    },
+    dataUploadModes() {
+      return dataUploadModes
+    },
+    screenRecordModes() {
+      return screenRecordModes
+    },
+    weekList() {
+      return weekList
+    },
+    monthList() {
+      return monthList
+    }
+  },
+  async created() {
+    this.getDeviceAreaTreeData()
+    const typeMap = ['edit', 'view', 'copy']
+    // todo 数组优化逻辑
+    if (typeMap.includes(this.pageType)) {
+      await this.getAirPlanByInfo()
+    }
+    if (this.pageType === 'add') {
+      await this.getDefaultPlanName()
+    }
+  },
+  watch: {
+    'form.deviceCode': {
+      handler(val, oldVal) {
+        if (val !== oldVal) {
+          this.getAirLineListSource()
+        }
+      }
+    }
+  },
+  mounted() {
+    this.initEvent()
+  },
+  beforeDestroy() {
+    let mapRef = this.mapRef.getMapRef(this.mapId)
+    mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = false
+  },
+  methods: {
+    // 清除deviceCode
+    clearDeviceCode() {
+      this.form.airline = ''
+      this.lineInfo = {}
+      this.airLineType = null
+      this.clearAirLineMap('1')
+      this.clearAirLineMap('2')
+    },
+    initEvent() {
+      /**
+       * 监听打开事件
+       * visible: 是否显示
+       * type: 页面类型 add/edit/view
+       * */
+      this.$globalEventBus.$on(
+        `${eventPath.commonCompUavFlyManage}_open-peration-plan`,
+        (options) => {
+          this.visible = options.visible
+        }
+      )
+      this.$globalEventBus.$on(
+        `${eventPath.commonCompMap}__init-map-resolve`,
+        ({ status }) => {
+          if (status) {
+            this.setAirLineInMap(this.form.airline)
+          }
+        }
+      )
+    },
+    // 设置当前选中值
+    setCurrentValue(key) {
+      if (key === 'currentTask') {
+        this.form.executionTime = '' // 执行时间
+        this.form.executionDate = '' // 执行日期
+        this.form.flyTime = '' // 执行时间
+        this.form.repeatFrequency = '0' // 重复频率
+        this.form.weeks = [] // 周
+        this.form.months = [] // 月
+      }
+    },
+    // 处理月半选中状态
+    handleListChange(value, allKey, listKey, indetermineKey) {
+      let checkedCount = value.length
+      this[allKey] = checkedCount === this[listKey].length
+      this[indetermineKey] =
+        checkedCount > 0 && checkedCount < this[listKey].length
+    },
+    clearImmediate() {
+      this.form.weeks = []
+      this.form.months = []
+      this.weekAll = false
+      this.monthAll = false
+      this.weekAllIndeterminate = false
+      this.monthAllIndeterminate = false
+    },
+    // 处理月全选
+    handleMonthAllChange(val, valueKey, listKey, indetermineKey) {
+      this.form[valueKey] = val ? this[listKey].map((item) => item.value) : []
+      this[indetermineKey] = false
+    },
+    // 获取无人机机场数据
+    getDeviceAreaTreeData(fn) {
+      const params = {
+        needDevice: true,
+        orderStatus: '1',
+        queryType: 1
+      }
+      getDeviceAreaTree(params)
+        .then((res) => {
+          if (res.code === 200) {
+            this.uavTreeData = res.data || []
+            this.$nextTick(() => {
+              fn && fn()
+            })
+          }
+        })
+        .catch((e) => {
+          this.uavTreeData = []
+        })
+    },
+    // 获取航线列表
+    async getAirLineListSource() {
+      // this.form.deviceCode = '00000000002'
+      if (!this.form.deviceCode) {
+        this.airlineList = []
+        return
+      }
+      const params = {
+        deviceCodes: this.form.deviceCode
+      }
+      const res = await getAirLineList(params)
+      if (res.code === 200) {
+        this.airlineList = res.rows || []
+      }
+    },
+    // 获取计划详情
+    async getAirPlanByInfo() {
+      const res = await findAirPlanById({ planId: this.planId })
+      if (res.code === 200) {
+        const data = res.data
+        this.form.planName = data.planName
+        this.form.deviceCode = data.uavNestCode || data.deviceCode
+        this.form.airline = data.flightRouteId
+        this.form.uploadMode = data.gainDataMode + ''
+        this.form.screenRecord = data.gainVideo + ''
+        this.form.currentTask = data.planType + ''
+        this.form.executionTime = data.regularExecutionDate
+        this.form.flyTime = data.cycleExecutionTime
+        this.form.repeatFrequency = data.cycleExecutionUnit
+        if (this.form.currentTask === '1') {
+          this.form.executionDate = [data.planStartTime, data.planEndTime]
+        }
+        // todo 优化为map
+        const intervalValuesMap = {
+          1: {
+            valueKey: 'weeks',
+            listKey: 'weekList',
+            allKey: 'weekAll',
+            indeterminateKey: 'weekAllIndeterminate'
+          },
+          2: {
+            valueKey: 'months',
+            listKey: 'monthList',
+            allKey: 'monthAll',
+            indeterminateKey: 'monthAllIndeterminate'
+          }
+        }
+        const intervalKey = intervalValuesMap[this.form.repeatFrequency]
+        if (intervalKey?.valueKey) {
+          this.form[intervalKey.valueKey] = data.intervalValues?.split(',')
+          this.handleListChange(
+            this.form[intervalKey.valueKey],
+            intervalKey.allKey,
+            intervalKey.listKey,
+            intervalKey.indeterminateKey
+          )
+        }
+        if (this.pageType === 'copy') {
+          this.form.planName = this.planName
+          this.form.currentTask = '2' // 复制时需启用为立即执行策略
+          this.form.executionTime = '' // 复制时需清空执行时间
+          this.form.flyTime = ''
+          this.form.executionDate = []
+          this.form.weeks = [] // 复制时需清空日历值
+          this.form.months = []
+        }
+        if (
+          this.pageType === 'view' ||
+          this.pageType === 'edit' ||
+          this.pageType === 'copy'
+        ) {
+          await this.getAirLineListSource()
+          await this.setAirLineInMap(data.flightRouteId)
+        }
+        console.log(this.form, '获取计划详情')
+        // this.form.autoContinueFly = data.auto
+      }
+      await this.getInfo(res.data)
+    },
+    submitForm() {
+      this.$refs.operationPlanForm.validate((valid) => {
+        if (valid) {
+          let intervalValues = []
+          if (this.form.repeatFrequency === '1') {
+            intervalValues = this.form.weeks
+          }
+          if (this.form.repeatFrequency === '2') {
+            intervalValues = this.form.months
+          }
+          if (
+            (this.form.repeatFrequency === '1' ||
+              this.form.repeatFrequency === '2') &&
+            !intervalValues?.length
+          ) {
+            CommonMessage.w('重复频率需勾选具体时间。')
+            return
+          }
+          const currentNode = findDeviceNodeByCode(
+            this.uavTreeData,
+            this.form.deviceCode
+          )
+          const params = {
+            planId: this.pageType === 'edit' ? this.planId : null, // 计划ID
+            name: this.form.planName, // 计划名称
+            flightRouteId: this.form.airline, // 航线ID
+            gainDataMode: this.form.uploadMode, // 数据上传 0-暂不保存,2-保存到服务器,4-边飞边传
+            gainVideo: this.form.screenRecord, // 录屏
+            auto: '1', // 0-否, 1-是(默认1-是,传1)
+            planType: this.form.currentTask, // 任务策略
+            cycleExecutionUnit: this.form.repeatFrequency, // 重复频率 0 日, 1 周, 2 月
+            deviceCode: this.form.deviceCode, // 设备编码
+            uavNestCode: currentNode?.uavNestCode, // 机场编码
+            uavCode: currentNode?.deviceCode, // 无人机编码
+            accessNode: currentNode?.accessNode // 接入节点
+          }
+          // 单次定时
+          if (params.planType === '0') {
+            params.regularExecutionDate = this.form.executionTime // 执行时间 planType = 0必填,立即执行
+          }
+          // 重复定时
+          if (params.planType === '1') {
+            params.startTime = this.form.executionDate[0]
+            params.endTime = this.form.executionDate[1]
+            params.cycleExecutionTime = this.form.flyTime // 执行日期 planType = 1 必填
+            if (params.cycleExecutionUnit === '0') {
+              params.dayInterval = 1 // 每日时传入间隔时间为1
+            }
+            params.intervalValues = intervalValues // 月
+          }
+          this.requestAddOrEdit(params)
+        } else {
+          console.log(valid, '表单验证失败')
+        }
+      })
+    },
+    close() {
+      this.clearDeviceCode()
+      this.$emit('close')
+    },
+    // 请求新增或编辑
+    requestAddOrEdit(params) {
+      // todo 合并
+      const requestMap = {
+        add: {
+          request: addAirPlan,
+          message: '新增计划成功'
+        },
+        copy: {
+          request: addAirPlan,
+          message: '新增计划成功'
+        },
+        edit: {
+          request: editAirPlan,
+          message: '修改计划成功'
+        }
+      }
+      requestMap[this.pageType].request(params).then((res) => {
+        if (res.code === 200) {
+          if (res.data.code === 400) {
+            CommonMessage.w(
+              res.data.msg || '同一条航线,每天仅限执行一次飞行任务'
+            )
+            return
+          }
+          this.clearDeviceCode()
+          CommonMessage.s(requestMap[this.pageType].message)
+          this.$emit('close', {
+            needRefresh: true
+          })
+        }
+      })
+    },
+    // 清除航线和航点 clearType: 1-清除航点航线,2-清除正射影像
+    clearAirLineMap(clearType) {
+      const clearKey =
+        clearType === '1' ? 'airLineAndPointMap' : 'orthoImageMap'
+      // todo 清除逻辑整合
+      const mapRef = this.mapRef.getMapRef(this.mapId)
+      // todo 改为Object.keys
+      const keys = Object.keys(this[clearKey])
+      keys.forEach((key) => {
+        if (this[clearKey][key]) {
+          try {
+            this[clearKey][key].customRemove(mapRef, this[clearKey][key])
+          } catch (e) {
+            console.error(e)
+          }
+          this[clearKey][key] = null
+        }
+      })
+    },
+    /**
+     * 设置数据格式
+     * @param value
+     */
+    async setAirLineInMap(value) {
+      const currentAirLine = this.airlineList.find(
+        (item) => item.flightRouteId === value
+      )
+      this.airLineType = null
+      this.clearAirLineMap('2')
+      this.clearAirLineMap('1')
+      if (!value) {
+        return
+      }
+      const currentNode = findDeviceNodeByCode(
+        this.uavTreeData,
+        this.form.deviceCode
+      )
+      const params = {
+        flightRouteId: value,
+        uavNestCode: currentNode?.uavNestCode,
+        uavCode: currentNode?.deviceCode
+      }
+      /*   if (currentAirLine) {
+           params.snapshotId = currentAirLine.snapshotId
+         }
+         */
+
+      params.snapshotId = this.snapshotId
+
+      // todo 优化代码行数、不超过1000行
+      const res = await getAirLineInfo(params)
+      if (res.code !== 200) {
+        return
+      }
+      const uavNestPoint = [
+        +res.data.uavNestLongitude,
+        +res.data.uavNestLatitude
+      ]
+      const uavPoint = [+res.data.uavLongitude, +res.data.uavLatitude]
+      const orthoUavPoint = currentNode?.uavNestCode ? uavNestPoint : uavPoint
+      const currentLine = res.data
+      const type = currentLine.type
+      this.airLineType = type
+      // type 为 1 航点航线  type 为 2 正射影像
+      if (!currentLine?.kmzJson) {
+        return
+      }
+      let mapRef = this.mapRef.getMapRef(this.mapId)
+      mapRef.mapInstance.scene.globe.depthTestAgainstTerrain = true
+      if (type === '1') {
+        // 处理航线和点线的数据
+        this.handleAirLineAndPointLineData(
+          currentLine,
+          currentNode,
+          orthoUavPoint,
+          mapRef
+        )
+      }
+      if (type === '2') {
+        // 处理正射影像线的数据
+        this.handleOrthophotoLineData(
+          currentLine,
+          currentNode,
+          orthoUavPoint,
+          mapRef
+        )
+      }
+    },
+    /**
+     * 处理航点航线的数据
+     * @param currentLine 当前航线数据
+     * @param currentNode 当前设备数据
+     * @param orthoUavPoint 机巢/无人机坐标
+     * @param mapRef 地图实例
+     */
+    async handleAirLineAndPointLineData(
+      currentLine,
+      currentNode,
+      orthoUavPoint,
+      mapRef
+    ) {
+      // 处理航点航线的数据
+      const { linePoints, lineInfo } = handleAirLineAndPointData(currentLine)
+      // 将线点坐标转换为经纬度格式
+      const positions = linePoints.map((item) => {
+        return { lng: Number(item[0]), lat: Number(item[1]) }
+      })
+      // 获取第一个线点的高度值
+      const heightVal = linePoints[0][2]
+      // 将kmzJson格式的字符串转换为json对象
+      const airLineJson = JSON.parse(currentLine.kmzJson)
+      // 获取高程信息
+      const { heightRes } = await getHeights(
+        positions,
+        airLineJson,
+        mapRef,
+        heightVal,
+        currentNode.altitude
+      )
+      // 根据高度信息对线点进行处理
+      let newLinePoints = linePoints.map((item, index) => {
+        let linePoint_ = heightRes[index]
+        return [Number(item[0]), Number(item[1]), Number(linePoint_['ASLT'])]
+      })
+      // 更新线信息
+      this.lineInfo = lineInfo
+      // 清除飞行线图层
+      this.clearAirLineMap('1')
+      // 创建线点数组
+      let points = [orthoUavPoint, ...newLinePoints, orthoUavPoint]
+      // 起飞点到首航点坐标
+      const firstPoints = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[0],
+        heightRes
+      )
+      // 返航点坐标到起飞点坐标
+      const lastPoints = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[positions.length - 1],
+        heightRes
+      )
+      const linkLines = firstPoints.concat(lastPoints)
+      // 绘制航线航点
+      this.airLineAndPointMap = setAirLineAndPoint(
+        mapRef,
+        points,
+        this.lineInfo,
+        linkLines
+      )
+      // 创建聚焦区域点数组
+      let focusAreaPonits = linePoints
+      focusAreaPonits.push([
+        orthoUavPoint[0],
+        orthoUavPoint[1],
+        orthoUavPoint[2]
+      ])
+      // 设置聚焦区域
+      planMapFocusArea(focusAreaPonits, mapRef)
+    },
+    /**
+     * 处理正射影响的数据
+     * @param currentLine 当前航线数据
+     * @param currentNode 当前设备数据
+     * @param orthoUavPoint 机巢/无人机坐标
+     * @param mapRef 地图实例
+     */
+    async handleOrthophotoLineData(
+      currentLine,
+      currentNode,
+      orthoUavPoint,
+      mapRef
+    ) {
+      const { linePoints, polygonPoints, lineInfo } =
+        // 处理航迹数据
+        handleOrthophotoData(currentLine)
+      const positions = linePoints.map((item) => {
+        // 生成经纬度数组
+        return { lng: Number(item[0]), lat: Number(item[1]) }
+      })
+      // 获取高度值
+      const heightVal = linePoints[0][2]
+      // 解析当前航迹为JSON对象
+      const airLineJson = JSON.parse(currentLine.kmzJson)
+      // 获取高度信息
+      const { heightRes } = await getHeights(
+        positions,
+        airLineJson,
+        mapRef,
+        heightVal,
+        currentNode.altitude
+      )
+      // 生成新的航迹点数组
+      let newLinePoints = linePoints.map((item, index) => {
+        // 获取对应位置的高度信息
+        let linePoint_ = heightRes[index]
+        return [Number(item[0]), Number(item[1]), Number(linePoint_['ASLT'])]
+      })
+      // 获取连接线数组
+      const linkLines = getLinkLines(
+        airLineJson,
+        currentNode,
+        positions[0],
+        heightRes
+      )
+      // 更新航迹信息
+      this.lineInfo = lineInfo
+      // 清除地图上的航迹
+      this.clearAirLineMap('2')
+      // 设置正射影像地图
+      this.orthoImageMap = setOrthoImage(
+        mapRef,
+        newLinePoints,
+        [polygonPoints],
+        orthoUavPoint,
+        this.lineInfo,
+        linkLines
+      )
+      let entity =
+        this.orthoImageMap.orthoAirLineInstance0._dataSource._entityCollection
+      // 规划地图聚焦区域
+      planMapFocusEntity(entity, mapRef)
+    },
+    async getInfo(data) {
+      const { user } = await requestSDK('getInfo')
+      setUserInfo({ user, id: this.pageType === 'edit' ? data.id : '' })
+    },
+    // 获取默认计划名称
+    async getDefaultPlanName() {
+      const { user } = await requestSDK('getInfo')
+      const params = {
+        tenantId: user.tenantId,
+        industryCode: user.industryCode
+      }
+      getPlanName(params).then((res) => {
+        if (res.code === 200) {
+          this.form.planName = res.data
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '../../style/operation-plan';
+</style>
+<style lang="scss">
+@import '../../style/tooltip';
+@import '../../style/operation-plan-no-scoped';
+.operation-select-tree {
+  width: px-to-rem(320) !important;
+}
+</style>

+ 56 - 0
src/components/common-comp-uav-fly-manage/src/components/operationPlan/OrthoImageWindow.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="ortho-image-window">
+    <p>总面积:{{ info?.area }}</p>
+    <i
+      v-if="showDelete"
+      @click="clickDelete"
+      class="iconfont icon-tongyong_icon_shipinguanliliebiaoshanchu"
+    ></i>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'OrthoImageWindow',
+  props: {
+    info: {
+      type: Object,
+      default: () => ({})
+    },
+    showDelete: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    clickDelete() {
+      this.$emit('clickDelete')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+
+.ortho-image-window {
+  height: px-to-rem(24);
+  background: #172537;
+  border-radius: px-to-rem(4);
+  display: flex;
+  align-items: center;
+  padding: 0 px-to-rem(12);
+  p {
+    margin-right: px-to-rem(12);
+  }
+  p,
+  i {
+    font-size: px-to-rem(14);
+    color: #e8f3fe;
+    cursor: pointer;
+  }
+  i {
+    font-size: px-to-rem(16);
+  }
+}
+</style>

+ 162 - 0
src/components/common-comp-uav-fly-manage/src/components/planMana/PlanCopy.vue

@@ -0,0 +1,162 @@
+<template>
+  <div>
+    <styled-dialog
+      industryClass="common-iw-s fly-edit-name-dialog"
+      :visible="dialogShow"
+      :title="configData.dialogTitle"
+      @close="close"
+      @open="open"
+      :modal="false"
+      :canClose="true"
+    >
+      <div class="fly-edit-name-dialog-body">
+        <el-form
+          ref="formRef"
+          :model="formDataNew"
+          class="fly-edit-name-dialog-body"
+        >
+          <div class="fly-edit-name-row">
+            <el-form-item
+              :label="configData.planName"
+              prop="planName"
+              :rules="[{ required: true, message: configData.nameError }]"
+            >
+              <el-input v-model="formDataNew.planName"></el-input>
+            </el-form-item>
+          </div>
+        </el-form>
+        <el-row type="flex" class="btns-container" justify="center">
+          <base-button
+            type="primary"
+            :heightStyle="32"
+            :width="108"
+            @click="handleSubmit()"
+          >
+            确定
+          </base-button>
+          <base-button :heightStyle="32" :width="108" @click="close">
+            取消
+          </base-button>
+        </el-row>
+      </div>
+    </styled-dialog>
+  </div>
+</template>
+<script>
+import StyledDialog from '@component-gallery/base-components/styled-dialog/StyledDialog.vue'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import { addAirPlan } from '../../service/index'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+export default {
+  components: {
+    StyledDialog,
+    BaseButton
+  },
+  props: {
+    dialogShow: {
+      type: Boolean,
+      default: true
+    },
+    formData: {
+      type: Object,
+      default: function () {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      configData: {
+        dialogTitle: '复制计划',
+        name: '计划名称',
+        nameError: '请输入'
+      },
+      formDataNew: {}
+    }
+  },
+  methods: {
+    open() {
+      this.formDataNew = { ...this.formData }
+      this.formDataNew.planName = this.formDataNew.planName + '_复制N'
+      this.$nextTick(() => {
+        this.$refs.formRef.clearValidate()
+      })
+    },
+    async handleSubmit() {
+      this.$refs.formRef.validate((valid) => {
+        if (!valid) {
+          return
+        }
+        const {
+          id,
+          createUser,
+          createTime,
+          updateTime,
+          updateUser,
+          ...newData
+        } = this.formDataNew
+        addAirPlan(newData).then((res) => {
+          if (res.code === 200) {
+            CommonMessage.s('复制计划成功')
+            this.$emit('update:dialogShow', false)
+            this.$emit('success')
+          }
+        })
+      })
+    },
+    close() {
+      this.$emit('update:dialogShow', false)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+$form_item_h: px-to-rem(32);
+$form_item_label_w: px-to-rem(64);
+$form_item_label_m_r: px-to-rem(12);
+.fly-edit-name-dialog-body {
+  padding: px-to-rem(18) px-to-rem(12) px-to-rem(12) px-to-rem(12);
+  .fly-edit-name-row {
+    ::v-deep .el-form-item {
+      font-size: px-to-rem(14);
+      &:not(:first-child) {
+        margin-left: px-to-rem(12);
+      }
+      .el-form-item__label {
+        width: $form_item_label_w;
+        height: $form_item_h;
+        line-height: $form_item_h;
+        margin-right: $form_item_label_m_r;
+        font-weight: 400;
+
+        color: #e8f3fe;
+      }
+      .el-form-item__content {
+        margin-left: calc($form_item_label_w + $form_item_label_m_r);
+        padding: 0;
+        .el-input {
+          height: $form_item_h;
+          line-height: $form_item_h;
+        }
+        .el-form-item__error {
+          position: initial;
+          &::before {
+            position: initial;
+          }
+        }
+      }
+    }
+  }
+}
+.btns-container {
+  gap: px-to-rem(12);
+}
+.fly-edit-name-dialog {
+  ::v-deep .el-dialog {
+    width: px-to-rem(500) !important;
+    margin-top: px-to-rem(400) !important;
+  }
+}
+</style>

+ 725 - 0
src/components/common-comp-uav-fly-manage/src/components/planMana/PlanMana.vue

@@ -0,0 +1,725 @@
+<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
+<!-- eslint-disable vue/no-deprecated-slot-scope-attribute -->
+<template>
+  <div class="plan_mana">
+    <el-row :gutter="12">
+      <el-form :label-width="pxToRem(76)">
+        <el-col :span="6">
+          <el-form-item label="名称" prop="planName">
+            <el-input
+              placeholder="请输入计划 / 任务名称关键字"
+              size="small"
+              v-model="queryParams.planName"
+              class="c-input"
+              maxlength="20"
+              @keyup.enter="handleQuery"
+              clearable
+            >
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="执行状态" prop="status">
+            <el-select
+              v-model.trim="queryParams.status"
+              class="c-select"
+              popper-class="c-select-dropdown"
+              placeholder="请选择"
+              multiple
+              collapse-tags
+              clearable
+              :popper-append-to-body="false"
+            >
+              <el-checkbox-group v-model="queryParams.status">
+                <el-option
+                  v-for="item in executionStatusList"
+                  :key="item.key"
+                  :label="item.value"
+                  :value="item.key"
+                >
+                  <el-checkbox style="pointer-events: none" :label="item.key">
+                    {{ item.value }}
+                  </el-checkbox>
+                </el-option>
+              </el-checkbox-group>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="任务策略" prop="planType">
+            <el-select
+              v-model.trim="queryParams.planType"
+              class="c-select"
+              popper-class="c-select-dropdown"
+              placeholder="请选择"
+              multiple
+              collapse-tags
+              clearable
+              :popper-append-to-body="false"
+            >
+              <el-checkbox-group v-model="queryParams.planType">
+                <el-option
+                  v-for="(item, index) in taskStrategy"
+                  :key="index"
+                  :label="item.value"
+                  :value="item.key"
+                >
+                  <el-checkbox style="pointer-events: none" :label="item.key">
+                    {{ item.value }}
+                  </el-checkbox>
+                </el-option>
+              </el-checkbox-group>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="计划时间">
+            <el-date-picker
+              v-model="planTime"
+              class="c-date-editor search-date"
+              popper-class="c-date-editor-picker"
+              type="daterange"
+              value-format="yyyy-MM-dd HH:mm:ss"
+              :default-time="['00:00:00', '23:59:59']"
+              range-separator="至"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              align="right"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="航线名称" prop="flightRouteName">
+            <el-input
+              placeholder="请输入航线名称关键字"
+              size="small"
+              v-model="queryParams.flightRouteName"
+              class="c-input"
+              maxlength="20"
+              @keyup.enter="handleQuery"
+              clearable
+            >
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="无人机" prop="deviceName">
+            <el-input
+              placeholder="请输入无人机名称关键字"
+              size="small"
+              v-model="queryParams.deviceName"
+              class="c-input"
+              maxlength="20"
+              @keyup.enter="handleQuery"
+              clearable
+            >
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item label="机场名称" prop="uavNestName">
+            <el-input
+              placeholder="请输入机场名称关键字"
+              size="small"
+              v-model="queryParams.uavNestName"
+              class="c-input"
+              maxlength="20"
+              @keyup.enter="handleQuery"
+              clearable
+            >
+            </el-input>
+          </el-form-item>
+        </el-col>
+      </el-form>
+    </el-row>
+    <el-row class="mb_12">
+      <el-col :span="12">
+        <base-button class="addBtn" @click="addForm()"> 新增</base-button>
+      </el-col>
+      <el-col :span="12" class="right_new">
+        <base-button class="addBtn mr_12" type="primary" @click="handleQuery()">
+          查询
+        </base-button>
+        <base-button class="addBtn" @click="resetForm()"> 重置</base-button>
+      </el-col>
+    </el-row>
+    <el-table
+      ref="planManaTable"
+      :data="tableData"
+      style="width: 100%"
+      :row-key="rowKey"
+      max-height="7.5rem"
+      tooltip-effect="aaaaaa c-tooltip-2"
+      :expand-row-keys="expandRowKeys"
+      @expand-change="expandChange"
+      v-loading="loading"
+    >
+      <el-table-column width="60" align="center" column-key="id">
+        <template #header>
+          <i
+            class="iconfont table_header"
+            :class="
+              expandAll
+                ? 'icon-tongyong_icon_luxiangzhankai'
+                : 'icon-tongyong_icon_luxiangshouqi'
+            "
+            @click="expandAllRow()"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column
+        v-for="(item, index) in planManaCloums"
+        :key="index + 'cl'"
+        :label="item.label"
+        :prop="item.prop"
+        :min-width="realPx(item.minWidth)"
+        align="left"
+        class="table-class"
+        :show-overflow-tooltip="true"
+      >
+        <template v-slot="scope" v-if="item.prop === 'planName'">
+          <span class="blue_column" @click="onLook(scope.row)">{{
+            scope.row.planName
+          }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'flightRouteName'">
+          <span>{{ getDataName(scope.row.flightRouteName) }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'deviceName'">
+          <span>{{ getDataName(scope.row.deviceName) }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'uavNestName'">
+          <span>{{ getDataName(scope.row.uavNestName) }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'scheduleExecTime'">
+          <span>{{ getDisplayTime(scope.row, 'scheduleExecTime') }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'realityTime'">
+          <span>{{ getDisplayTime(scope.row, 'realityTime') }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'status'">
+          <div v-if="scope.row?.executionStatesCount">
+            <div :class="hasCount(scope.row.executionStatesCount).className">
+              <div class="count-top"></div>
+              {{ hasCount(scope.row.executionStatesCount).text }}
+            </div>
+          </div>
+          <div v-else :style="{ color: getColor(scope.row.status).color }">
+            <div
+              class="count-top"
+              :style="{ backgroundColor: getColor(scope.row.status).color }"
+            ></div>
+            {{ getColor(scope.row.status).text }}
+          </div>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'planType'">
+          <span v-if="scope.row?.executionStatesCount">{{
+            getPlanType(scope.row.planType)
+          }}</span>
+          <span v-else>-</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'mediumStatus'">
+          <div v-if="scope.row?.executionStatesCount">-</div>
+          <div v-else :class="getCe9(scope.row.mediumStatus).className"
+            >{{ getCe9(scope.row.mediumStatus).text }}
+          </div>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'updateUser'">
+          <span>{{ getDataName(scope.row.updateUser) }}</span>
+        </template>
+        <template v-slot="scope" v-else-if="item.prop === 'updateTime'">
+          <span>{{ getDataName(scope.row.updateTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="是否启用"
+        :width="realPx(100)"
+        align="left"
+        prop="recordPlanStatus"
+        fixed="right"
+        :show-overflow-tooltip="false"
+      >
+        <template slot-scope="scope">
+          <el-switch
+            class="setting-switch"
+            v-model="scope.row.isItEnabled"
+            :active-value="'0'"
+            :inactive-value="'1'"
+            :active-color="'#1373E6'"
+            :inactive-color="'#909399'"
+            :disabled="isDisableStatus(scope.row)"
+            @change="rowUpdateStatus(scope.row)"
+          >
+          </el-switch>
+        </template>
+      </el-table-column>
+      <el-table-column prop="" label="操作" width="96" fixed="right">
+        <template #default="scope">
+          <div class="table-btn" v-if="scope.row.executionStatesCount">
+            <el-button
+              c-tip="复制"
+              icon="iconfont icon-xunhangjihua-xinzeng"
+              class="button_icon"
+              c-tip-placement="top"
+              c-tip-class="c-tip-normal"
+              @click="copyRow(scope.row)"
+            ></el-button>
+            <el-button
+              data-v-0fc570f5=""
+              c-tip="编辑"
+              c-tip-placement="top"
+              c-tip-class="c-tip-normal"
+              icon="iconfont icon-kapianbianji"
+              class="button_icon"
+              :disabled="isHandCopyOrDel(scope.row)"
+              @click="handleEdit(scope.row)"
+            ></el-button>
+            <el-button
+              data-v-0fc570f5=""
+              c-tip="删除"
+              c-tip-placement="top"
+              c-tip-class="c-tip-normal"
+              icon="iconfont icon-xunhangjihua-shanchu"
+              class="button_icon"
+              :disabled="isHandCopyOrDel(scope.row)"
+              @click="handleDel(scope.row)"
+            ></el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="fd-pagination-box">
+      <el-pagination
+        class="page"
+        popper-class="event-pagination-popper-new"
+        layout="total,sizes, prev, pager, next, jumper"
+        :page-size="queryParams.pageSize"
+        :current-page="queryParams.pageNum"
+        :total="total"
+        @current-change="planCurrentChange"
+        @size-change="planSizeChange"
+        background
+      ></el-pagination>
+      <div class="fd-pagination-text">
+        当前第{{ queryParams.pageNum }}/{{
+          Math.ceil(total / queryParams.pageSize)
+        }}页
+      </div>
+    </div>
+    <plan-copy
+      :dialogShow.sync="isPlanCopy"
+      :formData="copy"
+      @success="searchForm"
+    ></plan-copy>
+  </div>
+</template>
+
+<script>
+import eventPath from '@component-gallery/build-event-bus-path'
+import BaseButton from '@component-gallery/base-components/base-button/BaseButton.vue'
+import {
+  airPlanFindByCondition,
+  copyPlanName,
+  deleteById,
+  deleteByPlanId,
+  updateIsItEnabled
+} from '../../service/index'
+import {
+  executionStatusList,
+  planManaCloums,
+  taskStrategy,
+  getRealPx,
+  colorMap,
+  mediaUploadMap,
+  planTypeMap
+} from '../../dict/dict'
+import PlanCopy from './PlanCopy.vue'
+import CommonMessage from '@component-gallery/utils/funCommon/message/common-message'
+import { requestSDK } from '@ct/iframe-connect-sdk'
+
+export default {
+  name: 'planMana',
+  mixins: [getRealPx],
+  components: { BaseButton, PlanCopy },
+  data() {
+    return {
+      expandAll: false,
+      loading: false,
+      expandRowKeys: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        status: [],
+        planType: []
+      },
+      planTime: [], // 计划时间
+      executionStatusList, // 执行状态
+      taskStrategy, // 任务策略枚举值
+      alarmDateSelect: false, // 告警时间是否选中
+      tableLoading: false, // 表格是否加载
+      planManaCloums, // 列表
+      tableData: [],
+      total: null,
+      copy: {},
+      isPlanCopy: false,
+      addVisible: false, // 添加计划
+      colorMap, // 执行状态
+      mediaUploadMap, // 媒体上传列表
+      planTypeMap, // 任务策略
+      user: null // 用户信息
+    }
+  },
+  watch: {
+    // 计划时间
+    planTime(val) {
+      this.queryParams.planStartTime = (val && val[0]) || ''
+      this.queryParams.planEndTime = (val && val[1]) || ''
+    }
+  },
+  created() {
+    this.searchForm()
+  },
+  async mounted() {
+    await this.getInfo()
+  },
+  methods: {
+    async getInfo() {
+      const { user } = await requestSDK('getInfo')
+      this.user = user
+    },
+    addForm() {
+      this.addVisible = true
+      console.log(this.addVisible, '新增')
+      // 触发事件,传递visible和type作为数据
+      this.$globalEventBus.$emit(
+        `${eventPath.commonCompUavFlyManage}__open-operation-plan`,
+        {
+          visible: this.addVisible,
+          type: 'add'
+        }
+      )
+    },
+    searchForm() {
+      this.loading = true
+      airPlanFindByCondition(this.queryParams).then((res) => {
+        if (res.code === 200) {
+          this.tableData = res.data.rows
+          this.total = res.data.total
+          this.loading = false
+        }
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.searchForm()
+    },
+    // 重置清空数据
+    resetForm() {
+      this.planTime = []
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        planName: '',
+        status: [],
+        planType: [],
+        flightRouteName: '',
+        deviceName: '',
+        uavNestName: ''
+      }
+      this.handleQuery()
+    },
+    // 修改
+    handleEdit(row) {
+      this.addVisible = true
+      this.$globalEventBus.$emit(
+        `${eventPath.commonCompUavFlyManage}__open-operation-plan`,
+        {
+          visible: this.addVisible,
+          type: 'edit',
+          id: row.planId
+          //snapshotId: row.snapshotId
+        }
+      )
+    },
+    // 查看
+    onLook(row) {
+      this.addVisible = true
+      this.$globalEventBus.$emit(
+        `${eventPath.commonCompUavFlyManage}__open-operation-plan`,
+        {
+          visible: this.addVisible,
+          type: 'view',
+          id: row.planId,
+          snapshotId: row.snapshotId
+        }
+      )
+    },
+    // 删除  todo
+    handleDel(row) {
+      if (row.executionStatesCount) {
+        // 删除航线计划
+        const message = '是否确认删除该航线计划?'
+        const params = { planId: row.planId }
+        this.confirmAndDelete(message, params, deleteByPlanId)
+      } else {
+        // 删除任务
+        const message = '是否确认删除该任务?'
+        const params = {
+          id: row.id,
+          planId: row.planId,
+          taskRecordId: row.taskRecordId
+        }
+        this.confirmAndDelete(message, params, deleteById)
+      }
+    },
+    confirmAndDelete(message, params, apiFunction) {
+      this.$confirm(message, '提示', {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        customClass: 'c-confirm c-confirm-fly-result'
+      })
+        .then(() => {
+          apiFunction(params).then((res) => {
+            if (res.code === 200) {
+              CommonMessage.s('删除成功')
+              this.searchForm()
+            } else {
+              CommonMessage.e('删除失败,请重试')
+            }
+          })
+        })
+        .catch((error) => {
+          console.error(error)
+        })
+    },
+    // 分页
+    planCurrentChange(val) {
+      this.queryParams.pageNum = val
+      this.searchForm()
+    },
+    planSizeChange(val) {
+      this.queryParams.pageSize = val
+      this.searchForm()
+    },
+    // 复制
+    copyRow(row) {
+      const { planId, industryCode } = row
+      const tenantId = this.user.tenantId
+      copyPlanName({ planId, tenantId, industryCode }).then((res) => {
+        if (res.code === 200) {
+          this.addVisible = true
+          this.$globalEventBus.$emit(
+            `${eventPath.commonCompUavFlyManage}__open-operation-plan`,
+            {
+              visible: this.addVisible,
+              type: 'copy',
+              id: planId,
+              title: res.data
+            }
+          )
+        }
+      })
+    },
+    // 执行状态
+    getColor(value) {
+      return colorMap[value] || { color: '#ED5158', text: '默认数据' }
+    },
+    // 任务策略
+    getPlanType(value) {
+      return planTypeMap[value] || '默认数据'
+    },
+    // 媒体上传
+    getCe9(value) {
+      return mediaUploadMap[value] || { className: 'nodata', text: '默认数据' }
+    },
+    // 开关按钮
+    rowUpdateStatus(row) {
+      console.log('行信息', row)
+      let params = {
+        planId: row.planId,
+        isItEnabled: row.isItEnabled,
+        enableTaskList: []
+      }
+      if (row.executionStatesCount) {
+        const currentTime = new Date()
+        const changeList = row.children.filter(
+          (item) =>
+            (item.status === '0' || item.status === '2') &&
+            new Date(item.scheduleExecTime) > currentTime
+        )
+        params.enabledType = '0'
+        params.enableTaskList = changeList.map((item) => ({
+          id: item.id,
+          taskRecordId: item.taskRecordId,
+          status: item.status
+        }))
+      } else {
+        params.enabledType = '1'
+        params.enableTaskList.push({
+          id: row.id,
+          taskRecordId: row.taskRecordId,
+          status: row.status
+        })
+      }
+      this.updateIsItEnabledRow(params)
+    },
+    // 调用计划或者任务的开启关闭
+    updateIsItEnabledRow(params) {
+      updateIsItEnabled(params).then((res) => {
+        if (res.code === 200) {
+          CommonMessage.s('操作成功')
+          this.searchForm()
+        } else {
+          CommonMessage.e('操作失败,请重试')
+        }
+      })
+    },
+    // 开启状态是否可以点击 todo
+    isDisableStatus(row) {
+      if (row.executionStatesCount) {
+        const result = this.hasCount(row.executionStatesCount)
+        return (
+          result.className === 'success' ||
+          result.className === 'inProgress' ||
+          this.isDisableStatusForChildren(row.children)
+        )
+      } else {
+        const isFuture = this.isFutureTime(row.scheduleExecTime)
+        return !(row.status === '0' || row.status === '2') || !isFuture
+      }
+    },
+    // 判断任务只要有一个是可以开启的则计划不置灰
+    isDisableStatusForChildren(children) {
+      for (let child of children) {
+        const isFuture = this.isFutureTime(child.scheduleExecTime)
+        if ((child.status === '0' || child.status === '2') && isFuture) {
+          return false // 如果有一个 child 不满足条件,则返回 false
+        }
+      }
+      return true // 所有 child 都满足条件,返回 true
+    },
+    // 判断是否是未来时间的方法
+    isFutureTime(scheduleTimeStr) {
+      const currentTime = new Date()
+      const scheduledTime = new Date(scheduleTimeStr) // 直接解析字符串
+      return scheduledTime > currentTime
+    },
+    // 通过数量判断一级执行状态  todo if   switch
+    hasCount(count) {
+      const keys = Object.keys(count)
+      const total = this.getTotal(count)
+      // 单个键的情况
+      if (keys.length === 1) {
+        const key = keys[0]
+        const value = count[key]
+        switch (key) {
+          case 'executionSucceeded':
+            return this.buildStatusObject(
+              `全部执行成功(${value}/${value})`,
+              'success'
+            )
+          case 'executionFailed':
+            return this.buildStatusObject(
+              `全部执行失败(${value}/${value})`,
+              'executionFailed'
+            )
+          case 'pendingExecution':
+            return this.buildStatusObject(
+              `全部待执行(${value}/${value})`,
+              'pendingExecution'
+            )
+          case 'inProgress':
+            return this.buildStatusObject(
+              `全部执行中(${value}/${value})`,
+              'inProgress'
+            )
+          default:
+            return this.buildStatusObject('默认数据', 'success')
+        }
+      }
+      // 多个键的情况
+      if (keys.length > 1) {
+        let text = ''
+        let className = ''
+        if (keys.includes('executionSucceeded')) {
+          text = `部分执行成功(${count.executionSucceeded}/${total})`
+          className = 'partialExecution'
+        } else if (keys.includes('inProgress')) {
+          text = `部分执行中(${count.inProgress}/${total})`
+          className = 'inProgressNew'
+        } else if (keys.includes('executionFailed')) {
+          text = `部分执行失败(${count.executionFailed}/${total})`
+          className = 'executionFailedNew'
+        } else {
+          // 如果没有匹配到任何特定状态,则使用默认数据
+          text = '默认数据'
+          className = 'success'
+        }
+        return this.buildStatusObject(text, className)
+      }
+      return this.buildStatusObject('默认数据', 'success')
+    },
+    // 计算总数
+    getTotal(count) {
+      return Object.values(count).reduce((sum, value) => sum + value, 0)
+    },
+    // 根据执行状态构建返回对象
+    buildStatusObject(text, className) {
+      return { text, className }
+    },
+    // 校验编辑删除按钮是否可以点击
+    isHandCopyOrDel(row) {
+      if (row.executionStatesCount) {
+        const isShow = this.hasCount(row.executionStatesCount)
+        return !(
+          isShow.className === 'pendingExecution' ||
+          isShow.className === 'executionFailed' ||
+          isShow.className === 'executionFailedNew'
+        )
+      } else {
+        return !(row.status === '0' || row.status === '2')
+      }
+    },
+    // 校验二级的id不和一级的重复
+    rowKey(val) {
+      return val?.children ? val.id : `n_${val.id}`
+    },
+    expandAllRow() {
+      this.expandAll = !this.expandAll
+      if (!this.expandAll) {
+        this.tableData.forEach((i) => {
+          this.$refs.planManaTable.toggleRowExpansion(i, this.expandAll)
+        })
+        this.expandRowKeys = []
+      } else {
+        this.expandRowKeys = this.tableData.map((item) => `${item.id}`)
+      }
+    },
+    expandChange(row, data) {
+      console.log(row, data)
+      this.expandRowKeys = this.expandRowKeys.filter(
+        (item) => `${item}` !== `${row.id}`
+      )
+      this.expandAll = this.expandRowKeys.length === this.tableData.length
+    },
+    // 计划执行时间和实际执行时间为空的时候进行判断
+    getDisplayTime(row, property) {
+      if (row.executionStatesCount) {
+        return '-'
+      } else {
+        return row[property] || '-'
+      }
+    },
+    getDataName(data) {
+      return data ? data : '-'
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '../../style/planmana';
+</style>
+<style lang="scss">
+@import '../../style/planmana-new';
+</style>

+ 36 - 0
src/components/common-comp-uav-fly-manage/src/components/selectTree/Demo.vue

@@ -0,0 +1,36 @@
+<markdown>
+# 下拉树组件
+</markdown>
+
+<template>
+  <div>
+    <SelectTree
+      :defaultProps="{ label: 'name', children: 'list' }"
+      nodeKey="code"
+      v-model="uavValue"
+      multiple
+      :dataSource="dataSource"
+      @refresh="getDeviceAreaTreeData"
+    >
+    </SelectTree>
+  </div>
+</template>
+
+<script>
+import SelectTree from './SelectTree'
+
+export default {
+  data() {
+    return {
+      uavValue: '',
+      dataSource: []
+    }
+  },
+  components: { SelectTree },
+  methods: {
+    getDeviceAreaTreeData() {
+      // 获取树数据
+    }
+  }
+}
+</script>

+ 595 - 0
src/components/common-comp-uav-fly-manage/src/components/selectTree/SelectTree.vue

@@ -0,0 +1,595 @@
+<!-- eslint-disable vue/no-deprecated-slot-scope-attribute -->
+<!-- eslint-disable vue/no-deprecated-dollar-scopedslots-api -->
+<template>
+  <div style="position: relative">
+    <el-select
+      class="main-select-tree c-input iwmiTreeSelect fly"
+      :popper-class="`iwmiTreeSelect ${industryClass} popperClass fly custom-popper-class`"
+      :class="[iconClass, !nowValue && 'no-value']"
+      ref="select"
+      :placeholder="placeholder"
+      size="mini"
+      v-model="nowValue"
+      :multiple="multiple"
+      style="width: 100%"
+      :clearable="clearable"
+      :collapse-tags="multiple"
+      :disabled="disabled"
+      :filterable="filterable"
+      :filter-method="filterMethod"
+      :popper-append-to-body="popperAppendToBody"
+      @clear="handleClear"
+      @remove-tag="handleSelectRmove"
+      @visible-change="handleVisibleChange"
+    >
+      <el-option
+        v-for="item in formatData(dataSource_)"
+        :key="item.value + item.label"
+        :label="item.label"
+        :value="item.value"
+        style="display: none"
+      />
+      <el-tree
+        :class="[
+          'tree-select__tree tree-select__tree_fly',
+          `tree-select__tree--${multiple ? 'checked' : 'radio'}`
+        ]"
+        ref="selecteltree"
+        :data="dataSource_"
+        :node-key="nodeKey"
+        :props="defaultProps"
+        :show-checkbox="multiple"
+        :highlight-current="!multiple"
+        :expand-on-click-node="expandOnClickNode"
+        :default-expand-all="true"
+        :filter-node-method="filterNode"
+        :check-strictly="checkStrictly"
+        @node-expand="onNodeExpand"
+        @node-click="handleNodeClick"
+        @check-change="handleCheckChange"
+        :indent="6"
+      >
+        <template
+          v-for="(slot, slotName) in $scopedSlots"
+          #[slotName]="slotProps"
+        >
+          <slot :name="slotName" v-bind="slotProps" />
+        </template>
+        <span class="custom-tree-node" slot-scope="{ data }">
+          <span class="custom-tree-left">
+            <ct-icon
+              class="icon-class"
+              v-if="data.deviceCode"
+              :name="data.uavNestCode ? 'drone-airport' : 'uav'"
+            />
+            <span
+              :style="{
+                color:
+                  ['1', '2'].includes(data.nodeStatus) &&
+                  'rgba(232,243,254,0.5)'
+              }"
+            >
+              <span
+                class="tree-name"
+                v-html="getHighlightedText(data, keyWord)"
+                :c-tip="data.name"
+              >
+              </span>
+            </span>
+          </span>
+          <span class="custom-tree-right">
+            <!-- 第一排显示的按钮 -->
+            <span class="fast-btn1" v-if="data.firstRow">
+              <i
+                class="iconfont icon-jilianicon"
+                title="展开/收起"
+                @click.stop="handleExpandeNode"
+              ></i>
+              <i
+                class="iconfont icon-shuaxinicon"
+                title="刷新"
+                @click.stop="handleRefresh"
+              ></i>
+            </span>
+          </span>
+        </span>
+      </el-tree>
+    </el-select>
+    <span
+      class="clear-btn"
+      @click="keyWord = ''"
+      v-if="keyWord.length > 0"
+    ></span>
+  </div>
+</template>
+
+<script>
+import { setupCTips } from '@component-gallery/utils/funCommon/c-tip'
+
+export default {
+  name: 'SelectTree',
+  data() {
+    return {
+      filterFlag: false,
+      nowValue: null,
+      expandStatus: true,
+      dataSource_: [],
+      nowNode: null, // 单选模式下的当前节点
+      keyWord: '' // 过滤关键字
+    }
+  },
+  props: {
+    defaultProps: {
+      type: Object,
+      default: () => {
+        return {
+          children: 'children',
+          label: 'label'
+        }
+      }
+    },
+    value: {
+      type: [String, Number, Array],
+      default: ''
+    },
+    clearable: {
+      type: Boolean,
+      default: true
+    },
+    dataSource: {
+      type: Array,
+      default: () => []
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    placeholder: {
+      type: String,
+      default: '请选择'
+    },
+    filterable: {
+      type: Boolean,
+      default: false
+    },
+    showCheckbox: {
+      type: Boolean
+    },
+    nodeKey: {
+      type: [String, Number],
+      default: 'id'
+    },
+    industryClass: {
+      type: String,
+      default: 'common-iw-s'
+    },
+    popperAppendToBody: {
+      type: Boolean,
+      default: true
+    },
+    // defaultExpandedKeys: {
+    //   type: Array,
+    //   default: () => []
+    // },
+    checkStrictly: {
+      type: Boolean,
+      default: false
+    },
+    onlyLeafSelect: {
+      type: Boolean,
+      default: false
+    },
+    showPrefixIcon: {
+      type: Boolean,
+      default: false
+    },
+    expandOnClickNode: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    value: {
+      immediate: true,
+      handler(v) {
+        const newV = Array.isArray(v) ? v.map((o) => o?.[this.nodeKey] || o) : v
+        this.nowValue = newV
+        if (newV) {
+          // 单选模式,setCheckedKeys无效
+          if (this.multiple) {
+            this.$refs.selecteltree?.setCheckedKeys(
+              Array.isArray(v) ? newV : [newV]
+            )
+          } else {
+            this.$refs.selecteltree?.setCurrentKey(newV, true)
+          }
+        }
+      }
+    },
+    dataSource(n) {
+      console.log(n)
+      this.isFirstLevel(n)
+      this.dataSource_ = this.convertDeviceByCode(n)
+    }
+  },
+  mounted() {
+    setupCTips()
+  },
+  computed: {
+    iconClass() {
+      if (!this.nowValue) {
+        return ''
+      }
+      if (this.showPrefixIcon) {
+        return this.nowNode?.uavNestCode ? 'uav-nest-icon' : 'uav-icon'
+      }
+      return ''
+    }
+  },
+  methods: {
+    // 四级菜单
+    formatData(data) {
+      return this.optionData(data)
+    },
+    convertDeviceByCode(data) {
+      function traverse(nodes) {
+        for (let node of nodes) {
+          node['nodeStatus'] = node?.state
+          if (Object.prototype.hasOwnProperty.call(node, 'uavNestCode')) {
+            node.code = node.uavNestCode
+            node.name = node.uavNestName
+            node['nodeStatus'] = node?.uavNestStatus
+          }
+          if (
+            Object.prototype.hasOwnProperty.call(node, 'list') &&
+            Array.isArray(node.list)
+          ) {
+            traverse(node.list)
+          }
+        }
+      }
+
+      traverse(data)
+      return data
+    },
+    handleSelectRmove() {
+      // 多选模式下移除单个tag
+      // 在collapse-tag下一定是移除第一个选项,直接切就行
+      if (Array.isArray(this.nowValue)) {
+        const newnodes = this.$refs.selecteltree
+          .getCheckedNodes(true, false)
+          .slice(1)
+        this.$refs.selecteltree.setCheckedNodes(newnodes)
+        this.nowValue = newnodes.map((o) => o[this.nodeKey])
+        this.$emit('input', this.nowValue)
+      }
+    },
+    // 是否第一行、第一级
+    isFirstLevel(tree) {
+      if (tree.length) {
+        tree[0].firstRow = true
+      }
+    },
+    optionData(array, result = []) {
+      array.forEach((item) => {
+        if (!item[this.defaultProps.label] || !item[this.nodeKey]) {
+          return
+        }
+        result.push({
+          label: item[this.defaultProps.label],
+          value: item[this.nodeKey]
+        })
+        if (
+          item[this.defaultProps.children] &&
+          item[this.defaultProps.children].length !== 0
+        ) {
+          this.optionData(item[this.defaultProps.children], result)
+        }
+      })
+      return JSON.parse(JSON.stringify(result))
+    },
+    // 单选模式,节点被点击
+    handleNodeClick(data) {
+      if (this.multiple) {
+        return
+      }
+      if (!data?.deviceCode && this.onlyLeafSelect) {
+        return
+      }
+      this.nowValue = data[this.nodeKey]
+      this.nowNode = data
+      this.$emit('input', data[this.nodeKey], data)
+      this.$refs.select.visible = false
+    },
+    // 以下为提供el-tree相同的透传方法
+    onNodeExpand(data, node) {
+      this.$emit('node-expand', data, node)
+    },
+    handleClear() {
+      const clearVal = this.multiple ? [] : ''
+      this.nowValue = clearVal
+      this.keyWord = ''
+      this.$emit('input', clearVal)
+      this.$emit('clear')
+    },
+    handleCheckChange() {
+      if (!this.multiple) {
+        return
+      }
+      const nodes = this.$refs.selecteltree.getCheckedNodes(
+        !this.checkStrictly,
+        false
+      )
+      const value = nodes.map((o) => o[this.nodeKey])
+      this.nowValue = value
+      this.$emit('input', value, nodes)
+    },
+    handleVisibleChange(f) {
+      if (!f) {
+        this.keyWord = ''
+      }
+      console.log('handleVisibleChange', this.keyWord)
+      if (f) {
+        this.$nextTick(() => {
+          if (this.nowValue) {
+            this.$refs.selecteltree.setCurrentKey(this.nowValue)
+          } else {
+            this.$refs.selecteltree.setCurrentKey(null)
+          }
+        })
+        const tree = this.$refs.selecteltree
+        this.filterFlag && tree.filter('')
+        this.filterFlag = false
+        let selectDom = null
+        if (this.multiple) {
+          selectDom = tree.$el.querySelector('.el-tree-node.is-checked')
+        } else {
+          selectDom = tree.$el.querySelector('.is-current')
+        }
+        if (this.nowValue) {
+          setTimeout(() => {
+            this.$refs.select.scrollToOption({ $el: selectDom })
+          }, 0)
+        }
+      }
+    },
+    filterMethod(val) {
+      this.filterFlag = true
+      this.keyWord = val
+      this.$refs.selecteltree.filter(val)
+    },
+    filterNode(value, data) {
+      if (!value) {
+        return true
+      }
+      const label = this.defaultProps.label || 'name'
+      const name = data[label]
+      if (name) {
+        return name.indexOf(value) !== -1
+      }
+      return -1
+    },
+    handleExpandeNode() {
+      let nodesMap = this.$refs.selecteltree.store.nodesMap
+      // 判断当前项是否展开,如果展开,则将所有节点折叠到二级节点
+      for (let key in nodesMap) {
+        // 判断节点的深度是否大于等于2,将深度大于2的节点折叠
+        if (nodesMap[key].level > 1) {
+          nodesMap[key].expanded = this.expandStatus ? false : true
+        }
+      }
+      this.expandStatus = !this.expandStatus
+    },
+    handleRefresh() {
+      const fn = () => {
+        if (this.nowValue) {
+          this.$refs.selecteltree.setCurrentKey(this.nowValue)
+        }
+        this.$refs.selecteltree.filter(this.keyWord)
+      }
+      this.$emit('refresh', fn)
+    },
+    getHighlightedText(data, keyword) {
+      if (!data.deviceCode) {
+        return `${data.name ?? ''}(${data.onlineNum}/${data.num})`
+      }
+      if (!data.name) {
+        return ''
+      }
+      if (!keyword) return data.name
+      const regex = new RegExp(`(${keyword})`, 'gi')
+      return data.name.replace(regex, `<span style="color: #4F9FFF;">$1</span>`)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/styled-tree-select';
+.main-select-tree.fly {
+  &.no-value {
+    ::v-deep .el-icon-circle-close {
+      display: none !important;
+    }
+  }
+  ::v-deep {
+    .el-input .el-input__inner {
+      font-size: px-to-rem(16) !important;
+    }
+    .el-input:hover {
+      .el-input__inner {
+        padding-right: px-to-rem(62) !important;
+      }
+    }
+  }
+  ::v-deep {
+    .el-select__tags {
+      flex-wrap: nowrap;
+      max-width: unset !important;
+      width: fit-content !important;
+      .el-tag {
+        color: #e8f3fe;
+        background-color: rgba(79, 159, 255, 0.2);
+        border: none;
+        padding: 0 0.12rem;
+        max-width: 1.29rem;
+        height: 0.24rem;
+        display: flex;
+        align-items: center;
+      }
+      .el-tag__close.el-icon-close {
+        color: #e8f3fe;
+        margin-left: 0.06rem;
+        margin-right: 0;
+        height: 0.2rem;
+        width: 0.2rem;
+        line-height: 0.2rem;
+        right: unset;
+        background-color: transparent;
+        transform: none;
+        &::before {
+          font-size: 0.2rem;
+          font-family: 'iconfont_tools';
+          content: '\ec12';
+        }
+      }
+    }
+  }
+}
+.el-tree.tree-select__tree_fly {
+  height: px-to-rem(256);
+  overflow: hidden;
+  .custom-tree-node {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  ::v-deep {
+    .el-tree-node {
+      &.is-current > .el-tree-node__content .custom-tree-node .tree-name {
+        color: #4f9fff;
+      }
+      .el-tree-node__content .el-tree-node__expand-icon {
+        margin-left: px-to-rem(6);
+      }
+    }
+  }
+  ::v-deep {
+    > .el-tree-node {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+    }
+    .el-tree-node > .el-tree-node__children {
+      overflow-y: auto;
+      flex: 1;
+    }
+  }
+  ::v-deep .el-tree__empty-block {
+    .el-tree__empty-text {
+      font-size: px-to-rem(16);
+      color: #e8f3fe;
+      &::before {
+        display: block;
+        content: '';
+        margin-left: px-to-rem(2);
+        width: px-to-rem(58);
+        height: px-to-rem(58);
+        background: url('~@component-gallery/assets/image/video/noData.png')
+          100% 100% / 100% 100% no-repeat;
+        margin-bottom: px-to-rem(12);
+      }
+    }
+  }
+}
+.clear-btn {
+  display: flex;
+  position: absolute;
+  width: px-to-rem(20);
+  right: px-to-rem(30);
+  top: px-to-rem(0);
+  align-self: center;
+  height: px-to-rem(32);
+  cursor: pointer;
+  &::before {
+    color: #e8f3fe;
+    align-self: center;
+    font-size: px-to-rem(20);
+    font-family: 'iconfont_tools';
+    content: '\ec12';
+  }
+}
+.fast-btn1 {
+  margin-right: px-to-rem(6);
+  .iconfont {
+    font-size: px-to-rem(20);
+  }
+  i + i {
+    margin-left: px-to-rem(6);
+  }
+}
+.custom-tree-left {
+  width: 90%;
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  .ct-icon__inline-block {
+    display: inline-block;
+    vertical-align: text-bottom;
+    ::v-deep .ct-icon {
+      width: px-to-rem(20) !important;
+      margin-right: px-to-rem(6) !important;
+      .icon-ctw {
+        font-size: px-to-rem(20) !important;
+        color: #e8f3fe !important;
+      }
+    }
+  }
+  .tree-name {
+    line-height: px-to-rem(32);
+  }
+}
+.uav-nest-icon {
+  ::v-deep .el-input {
+    &::before {
+      content: '\e828';
+      font-family: common-iconfont !important;
+      font-size: px-to-rem(20);
+      color: #e8f3fe;
+      margin-left: px-to-rem(12);
+    }
+  }
+}
+.uav-icon {
+  ::v-deep .el-input {
+    &::before {
+      content: '\e760';
+      font-family: common-iconfont !important;
+      font-size: px-to-rem(20);
+      color: #e8f3fe;
+      margin-left: px-to-rem(12);
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/styled-tree-select-noscope';
+.iwmiTreeSelect.fly {
+  font-size: px-to-rem(16);
+  .el-select-dropdown__wrap {
+    height: px-to-rem(256);
+    overflow: hidden;
+  }
+  .el-input .el-input__suffix .el-icon-circle-check {
+    display: none !important;
+  }
+}
+</style>

+ 100 - 0
src/components/common-comp-uav-fly-manage/src/components/tabSelect/TabSelect.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="unit-select" :class="disabled && 'disabled'">
+    <div
+      v-for="item in options"
+      :key="item.value"
+      class="unit-item"
+      :class="[
+        value === item.value && 'active',
+        options?.length === 2 && 'row-2'
+      ]"
+      @click="setCurrentValue(item.value)"
+      >{{ item.label }}</div
+    >
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'tabSelect',
+  props: {
+    options: {
+      type: Array,
+      default: () => []
+    },
+    value: {
+      type: String,
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    setCurrentValue(value) {
+      if (this.disabled) {
+        return
+      }
+      this.$emit('update:value', value)
+      this.$emit('change', value)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+@import '~@component-gallery/theme-chalk/src/mixins/px-to-rem';
+@import '~@component-gallery/theme-chalk/src/mixins/mixins';
+@import '~@component-gallery/theme-chalk/src/mixins/theme-mixins';
+.unit-select {
+  @include themeify(false) {
+    width: 100%;
+    display: flex;
+    height: px-to-rem(32);
+    background: url('../../img/swich-wrapper.png') no-repeat;
+    background-size: 100% 100%;
+    &.disabled {
+      opacity: 0.7;
+
+      .unit-item {
+        cursor: not-allowed;
+      }
+    }
+    .unit-item {
+      flex: 1;
+      height: 100%;
+      position: relative;
+      font-size: px-to-rem(16);
+      color: themed('global-text-color');
+      padding-left: px-to-rem(28);
+      cursor: pointer;
+      line-height: px-to-rem(32);
+
+      &.row-2 {
+        &.active {
+          background: url('../../img/row-2.png') no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+
+      &.active {
+        background: themed('azimuthal-measurement-active');
+        background-size: 100% 100%;
+
+        &::after {
+          height: px-to-rem(24);
+          width: px-to-rem(24);
+          content: '';
+          position: absolute;
+          top: 50%;
+          left: 0;
+          transform: translateY(-50%);
+          background: themed('azimuthal-measurement-tips');
+          background-size: 100% 100%;
+        }
+      }
+    }
+  }
+}
+</style>

+ 0 - 0
src/components/common-comp-uav-fly-manage/src/dict/air-line-mana-dict.js


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels