吉安感知网项目-前端
shuishen
2026-01-07 3a3cbd22dac6945039ba44e4ca14b1adaaaaa7ed
feat:反无大屏基建
23 files modified
43 files added
9 files deleted
16816 ■■■■ changed files
.vscode/settings.json 7 ●●●●● patch | view | raw | blame | history
applications/drone-command/env/.env.development 16 ●●●● patch | view | raw | blame | history
applications/drone-command/public/fonts/PANGMENZHENGDAOBIAOTITIMIANFEIBAN-2.TTF patch | view | raw | blame | history
applications/drone-command/public/fonts/font.css 11 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/Frame.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/fly.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/flying.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/full-charge.svg 13 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/low-battery.svg 6 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/medium-battery.svg 6 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/assets/images/aiNowFly/starting.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/topContainer/logo.png patch | view | raw | blame | history
applications/drone-command/src/assets/images/topContainer/top-bg.png patch | view | raw | blame | history
applications/drone-command/src/components/basic-container/main.vue 10 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/config/website.js 18 ●●●● patch | view | raw | blame | history
applications/drone-command/src/page/index/index.vue 81 ●●●● patch | view | raw | blame | history
applications/drone-command/src/page/index/sidebar/index.vue 17 ●●●● patch | view | raw | blame | history
applications/drone-command/src/page/index/top/index.vue 76 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/router/views/index.js 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/common.scss 39 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/sidebar.scss 3 ●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/theme/white.scss 6 ●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/top.scss 11 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/styles/variables.scss 4 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/auth.js 10 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/DrawPolygon.js 803 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/Material/index.js 18 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/common.js 8 ●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/compressImage.js 55 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/compressImage2.js 46 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/createRouteLine.js 923 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/eventBus.js 5 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/frustum/CesiumVideoFrustum.js 712 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/frustum/CoordinateTranslate.js 229 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/kmz.js 10 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/mapUtil.js 190 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/publicCesium.js 168 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/use-kmz-tsa.js 675 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/utils/cesium/useBoundary.js 210 ●●●● patch | view | raw | blame | history
applications/drone-command/src/views/README.md 56 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/areaStatistics.vue 18 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/defenseZone.vue 8 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/index.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/partition.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/precinctInfo.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/areaManage/sceneConfig.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceScrap.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/deviceStock.vue 19 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/index.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/basicManage/maintainRecord.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue 13 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/dataCockpit/index.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/detectionRange.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/index.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/detectionCountermeasure/taskSchedule.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/index.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/operationLog.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionDept.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionRole.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/permissionManage/permissionUser.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/alarmRecords.vue 9 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/historyTracks.vue 19 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/recordManage/index.vue 12 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/backlog.vue 434 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/calendarBox.vue 398 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/flightStatistics.vue 524 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/flyratio.vue 357 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/proportionStatic.vue 395 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/statistics.vue 498 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/components/taskOutcome.vue 315 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/dashboard.vue 196 ●●●●● patch | view | raw | blame | history
applications/drone-command/src/views/wel/index.vue 222 ●●●●● patch | view | raw | blame | history
pnpm-lock.yaml 8766 ●●●●● patch | view | raw | blame | history
.vscode/settings.json
New file
@@ -0,0 +1,7 @@
{
    "i18n-ally.localesPaths": [
        "applications/drone-command/src/lang",
        "applications/mobile-web-view/src/lang",
        "applications/task-work-order/src/lang"
    ]
}
applications/drone-command/env/.env.development
@@ -1,3 +1,13 @@
###
 # @Author       : yuan
 # @Date         : 2026-01-06 09:47:05
 # @LastEditors  : yuan
 # @LastEditTime : 2026-01-07 10:54:42
 # @FilePath     : \applications\drone-command\env\.env.development
 # @Description  :
 # Copyright 2026 OBKoro1, All Rights Reserved.
 # 2026-01-06 09:47:05
###
NODE_ENV = 'development'
#开发环境配置
@@ -10,10 +20,10 @@
VITE_APP_BASE=/drone-command
# 服务地址
VITE_APP_URL = https://wrj.shuixiongit.com/api
# VITE_APP_URL = https://wrj.shuixiongit.com/api
#VITE_APP_URL= http://192.168.1.168
# VITE_APP_URL= http://192.168.1.33
#VITE_APP_URL= http://192.168.1.204
VITE_APP_URL= http://192.168.1.33:81
# VITE_APP_URL= http://192.168.1.204
#新大屏地址
VITE_APP_DASHBOARD_URL = 'https://wrj.shuixiongit.com/command-center-dashboard/'
applications/drone-command/public/fonts/PANGMENZHENGDAOBIAOTITIMIANFEIBAN-2.TTF
Binary files differ
applications/drone-command/public/fonts/font.css
@@ -41,3 +41,14 @@
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "PangMen";
  src: url("PANGMENZHENGDAOBIAOTITIMIANFEIBAN-2.TTF") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
applications/drone-command/src/assets/images/aiNowFly/Frame.png
applications/drone-command/src/assets/images/aiNowFly/fly.png
applications/drone-command/src/assets/images/aiNowFly/flying.png
applications/drone-command/src/assets/images/aiNowFly/full-charge.svg
New file
@@ -0,0 +1,13 @@
<svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M16.5455 0C16.7126 0 16.8781 0.0326726 17.0325 0.0961522C17.1869 0.159632 17.3272 0.252675 17.4454 0.36997C17.5636 0.487265 17.6573 0.626515 17.7213 0.779768C17.7853 0.933022 17.8182 1.09728 17.8182 1.26316V2.52632H24.1818C25.1945 2.52632 26.1656 2.92556 26.8817 3.63623C27.5977 4.34689 28 5.31076 28 6.31579V44.2105C28 45.2156 27.5977 46.1794 26.8817 46.8901C26.1656 47.6008 25.1945 48 24.1818 48H3.81818C2.80554 48 1.83437 47.6008 1.11832 46.8901C0.402272 46.1794 0 45.2156 0 44.2105V6.31579C0 5.31076 0.402272 4.34689 1.11832 3.63623C1.83437 2.92556 2.80554 2.52632 3.81818 2.52632H10.1818V1.26316C10.1818 0.928148 10.3159 0.606858 10.5546 0.36997C10.7933 0.133082 11.117 0 11.4545 0H16.5455ZM24.1818 5.05263H3.81818C3.48063 5.05263 3.15691 5.18571 2.91823 5.4226C2.67954 5.65949 2.54545 5.98078 2.54545 6.31579V44.2105C2.54545 44.5455 2.67954 44.8668 2.91823 45.1037C3.15691 45.3406 3.48063 45.4737 3.81818 45.4737H24.1818C24.5194 45.4737 24.8431 45.3406 25.0818 45.1037C25.3205 44.8668 25.4545 44.5455 25.4545 44.2105V6.31579C25.4545 5.98078 25.3205 5.65949 25.0818 5.4226C24.8431 5.18571 24.5194 5.05263 24.1818 5.05263Z" fill="url(#paint0_linear_1519_1955)"/>
<path d="M22.909 42.9474V8.33691H5.09082V42.9474H22.909Z" fill="#40FF5C"/>
</g>
<defs>
<linearGradient id="paint0_linear_1519_1955" x1="14" y1="0" x2="14" y2="48" gradientUnits="userSpaceOnUse">
<stop offset="0.25" stop-color="#00FFF2"/>
<stop offset="0.514423" stop-color="#00FFF2"/>
<stop offset="0.721154" stop-color="#00FFF2"/>
</linearGradient>
</defs>
</svg>
applications/drone-command/src/assets/images/aiNowFly/low-battery.svg
New file
@@ -0,0 +1,6 @@
<svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 1321316190">
<path id="Vector" d="M16.3293 0C16.4942 0 16.6576 0.03249 16.81 0.0956148C16.9624 0.15874 17.1008 0.251263 17.2175 0.367902C17.3341 0.484542 17.4266 0.623013 17.4897 0.77541C17.5529 0.927807 17.5854 1.09114 17.5854 1.2561V2.5122H23.8659C24.8653 2.5122 25.8237 2.90921 26.5304 3.6159C27.2371 4.32259 27.6341 5.28107 27.6341 6.28049V43.9634C27.6341 44.9628 27.2371 45.9213 26.5304 46.628C25.8237 47.3347 24.8653 47.7317 23.8659 47.7317H3.76829C2.76888 47.7317 1.8104 47.3347 1.10371 46.628C0.397016 45.9213 0 44.9628 0 43.9634V6.28049C0 5.28107 0.397016 4.32259 1.10371 3.6159C1.8104 2.90921 2.76888 2.5122 3.76829 2.5122H10.0488V1.2561C10.0488 0.92296 10.1811 0.603466 10.4167 0.367902C10.6522 0.132338 10.9717 0 11.3049 0H16.3293ZM23.8659 5.02439H3.76829C3.43515 5.02439 3.11566 5.15673 2.8801 5.39229C2.64453 5.62786 2.5122 5.94735 2.5122 6.28049V43.9634C2.5122 44.2966 2.64453 44.616 2.8801 44.8516C3.11566 45.0872 3.43515 45.2195 3.76829 45.2195H23.8659C24.199 45.2195 24.5185 45.0872 24.754 44.8516C24.9896 44.616 25.122 44.2966 25.122 43.9634V6.28049C25.122 5.94735 24.9896 5.62786 24.754 5.39229C24.5185 5.15673 24.199 5.02439 23.8659 5.02439Z" fill="#00FFF2"/>
<path id="Vector_2" d="M22.6098 42.707V37.6826H5.02441V42.707H22.6098Z" fill="#F20303"/>
</g>
</svg>
applications/drone-command/src/assets/images/aiNowFly/medium-battery.svg
New file
@@ -0,0 +1,6 @@
<svg width="28" height="48" viewBox="0 0 28 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector">
<path d="M16.5455 0C16.7126 0 16.8781 0.0326726 17.0325 0.0961522C17.1869 0.159632 17.3272 0.252675 17.4454 0.36997C17.5636 0.487265 17.6573 0.626515 17.7213 0.779768C17.7853 0.933022 17.8182 1.09728 17.8182 1.26316V2.52632H24.1818C25.1945 2.52632 26.1656 2.92556 26.8817 3.63623C27.5977 4.34689 28 5.31076 28 6.31579V44.2105C28 45.2156 27.5977 46.1794 26.8817 46.8901C26.1656 47.6008 25.1945 48 24.1818 48H3.81818C2.80554 48 1.83437 47.6008 1.11832 46.8901C0.402272 46.1794 0 45.2156 0 44.2105V6.31579C0 5.31076 0.402272 4.34689 1.11832 3.63623C1.83437 2.92556 2.80554 2.52632 3.81818 2.52632H10.1818V1.26316C10.1818 0.928148 10.3159 0.606858 10.5546 0.36997C10.7933 0.133082 11.117 0 11.4545 0H16.5455ZM24.1818 5.05263H3.81818C3.48063 5.05263 3.15691 5.18571 2.91823 5.4226C2.67954 5.65949 2.54545 5.98078 2.54545 6.31579V44.2105C2.54545 44.5455 2.67954 44.8668 2.91823 45.1037C3.15691 45.3406 3.48063 45.4737 3.81818 45.4737H24.1818C24.5194 45.4737 24.8431 45.3406 25.0818 45.1037C25.3205 44.8668 25.4545 44.5455 25.4545 44.2105V6.31579C25.4545 5.98078 25.3205 5.65949 25.0818 5.4226C24.8431 5.18571 24.5194 5.05263 24.1818 5.05263Z" fill="#00FFF2"/>
<path d="M22.9091 42.9474V23.4947H5.09091V42.9474H22.9091Z" fill="#00EEFF"/>
</g>
</svg>
applications/drone-command/src/assets/images/aiNowFly/starting.png
applications/drone-command/src/assets/images/topContainer/logo.png
applications/drone-command/src/assets/images/topContainer/top-bg.png
applications/drone-command/src/components/basic-container/main.vue
@@ -2,8 +2,8 @@
 * @Author       : yuan
 * @Date         : 2025-06-14 15:19:16
 * @LastEditors  : yuan
 * @LastEditTime : 2025-06-27 14:34:02
 * @FilePath     : \src\components\basic-container\main.vue
 * @LastEditTime : 2026-01-07 09:28:08
 * @FilePath     : \applications\drone-command\src\components\basic-container\main.vue
 * @Description  : 
 * Copyright 2025 OBKoro1, All Rights Reserved. 
 * 2025-06-14 15:19:16
@@ -48,7 +48,7 @@
  height: 0;
  flex: 1;
  padding: 10px;
  padding: 30px;
  // box-sizing: border-box;
  // height: 100%;
@@ -80,10 +80,6 @@
  &__card {
    width: 100%;
  }
  &:first-child {
    padding-top: 0;
  }
}
</style>
applications/drone-command/src/config/website.js
@@ -1,13 +1,23 @@
/*
 * @Author       : yuan
 * @Date         : 2026-01-06 09:47:09
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-07 09:18:52
 * @FilePath     : \applications\drone-command\src\config\website.js
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 09:47:09
 */
/**
 * 全局配置文件
 */
export default {
  title: 'saber',
  logo: 'S',
  key: 'saber', //配置主键,目前用于存储
  key: 'command', //配置主键,目前用于存储
  indexTitle: '',
  clientId: 'drone', // 客户端id
  clientSecret: 'drone_secret', // 客户端密钥
  clientId: 'saber', // 客户端id
  clientSecret: 'saber_secret', // 客户端密钥
  tenantMode: true, // 是否开启租户模式
  tenantId: '000000', // 管理组租户编号
  captchaMode: true, // 是否开启验证码模式
@@ -32,7 +42,7 @@
    menu: true,
  },
  fistPage: {
    name: '个人工作台',
    name: '首页',
    path: '/wel/index',
    // path: '/tickets/ticket',
applications/drone-command/src/page/index/index.vue
@@ -1,26 +1,27 @@
<template>
  <div class="avue-contail" :class="{ 'avue--collapse': isCollapse }">
    <div class="avue-layout" :class="{ 'avue-layout--horizontal': isHorizontal }">
      <div class="avue-sidebar" v-show="validSidebar">
        <!-- 左侧导航栏 -->
        <logo />
        <sidebar />
      </div>
      <div class="avue-main">
        <!-- 顶部导航栏 -->
      <div class="top-container">
        <top ref="top" />
        <!-- 顶部标签卡 -->
        <div class="tags-box">
          <tags />
      </div>
      <div class="leaf-container">
        <div class="avue-sidebar" v-show="validSidebar">
          <!-- 左侧导航栏 -->
          <sidebar />
        </div>
        <search class="main-avue-view-container" v-show="isSearch"></search>
        <!-- 主体视图层 -->
        <div class="main-avue-view-container" v-show="!isSearch" v-if="isRefresh">
          <router-view #="{ Component }">
            <keep-alive :include="$store.getters.tagsKeep">
              <component :is="Component" />
            </keep-alive>
          </router-view>
      </div>
      <div class="right-container">
        <div class="avue-main">
          <!-- 主体视图层 -->
          <div class="main-avue-view-container" v-show="!isSearch" v-if="isRefresh">
            <router-view #="{ Component }">
              <keep-alive :include="$store.getters.tagsKeep">
                <component :is="Component" />
              </keep-alive>
            </router-view>
          </div>
        </div>
      </div>
    </div>
@@ -29,16 +30,16 @@
  </div>
</template>
<script>
import index from '@/mixins/index';
import wechat from './wechat.vue';
import index from '@/mixins/index'
import wechat from './wechat.vue'
//import { validatenull } from 'utils/validate';
import { mapGetters } from 'vuex';
import tags from './tags.vue';
import search from './search.vue';
import logo from './logo.vue';
import top from './top/index.vue';
import sidebar from './sidebar/index.vue';
import GlobalWS from '@/page/index/GlobalWS.vue';
import { mapGetters } from 'vuex'
import tags from './tags.vue'
import search from './search.vue'
import logo from './logo.vue'
import top from './top/index.vue'
import sidebar from './sidebar/index.vue'
import GlobalWS from '@/page/index/GlobalWS.vue'
export default {
  mixins: [index],
@@ -52,10 +53,10 @@
    wechat,
  },
  name: 'index',
  provide() {
  provide () {
    return {
      index: this,
    };
    }
  },
  computed: {
    ...mapGetters([
@@ -67,27 +68,27 @@
      'menu',
      'setting',
    ]),
    validSidebar() {
    validSidebar () {
      return !(
        (this.$route.meta || {}).menu === false || (this.$route.query || {}).menu === 'false'
      );
      )
    },
  },
  props: [],
  methods: {
    //打开菜单
    openMenu(item = {}) {
    openMenu (item = {}) {
      this.$store.dispatch('GetMenu', item.id).then(data => {
        if (data.length !== 0) {
          this.$router.$avueRouter.formatRoutes(data, true);
          this.$router.$avueRouter.formatRoutes(data, true)
          // 获取url里面的redirect
          const redirect = decodeURIComponent(this.$route.query.redirect || '');
          const redirect = decodeURIComponent(this.$route.query.redirect || '')
          if (redirect) {
            console.log('redirect', redirect);
            const [path, queryString] = redirect.split('?');
            const query = queryString ? Object.fromEntries(new URLSearchParams(queryString)) : {};
            this.$router.push({ path, query });
            console.log('redirect', redirect)
            const [path, queryString] = redirect.split('?')
            const query = queryString ? Object.fromEntries(new URLSearchParams(queryString)) : {}
            this.$router.push({ path, query })
          }
        }
        //当点击顶部菜单后默认打开第一个菜单
@@ -111,10 +112,10 @@
            }, itemActive.meta)
          });
        }*/
      });
      })
    },
  },
};
}
</script>
<style lang="scss" scoped>
applications/drone-command/src/page/index/sidebar/index.vue
@@ -1,3 +1,13 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-06 09:47:09
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-06 15:48:49
 * @FilePath     : \applications\drone-command\src\page\index\sidebar\index.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 09:47:09
-->
<template>
  <el-scrollbar class="avue-menu">
    <div v-if="menu && menu.length == 0 && !isHorizontal" class="avue-sidebar--tip">
@@ -42,10 +52,11 @@
.el-menu{
  :deep(){
    .el-menu-item.is-active{
      background: linear-gradient( 90deg, rgba(179,194,255,0.02) 0%, rgba(20,65,255,0.09) 100%) !important;
      border-radius: 16px 16px 16px 16px;
      background: linear-gradient( 90deg, #0300B8 0%, rgba(0,39,153,0) 100%) !important;
      border-radius: 0px 0px 0px 0px !important;
      i,span{
        color: rgba(20, 65, 255, 1) !important;
        color: #FFFFFF !important;
      }
    }
    .el-sub-menu i{
applications/drone-command/src/page/index/top/index.vue
@@ -1,24 +1,20 @@
<template>
  <div class="avue-top">
    <div class="top-bar__left">
      <div class="avue-breadcrumb" :class="[{ 'avue-breadcrumb--active': isCollapse }]"
        v-if="setting.collapse && !isHorizontal">
        <i class="icon-navicon" @click="setCollapse"></i>
      </div>
    </div>
    <div class="top-bar__title">
<!--<top-menu ref="topMenu" v-if="setting.menu"></top-menu>-->
      <top-search class="top-bar__item" v-if="setting.search"></top-search>
      <img :src="logoUrl" alt="">
      <span>低空飞行监管子系统</span>
    </div>
    <div class="top-bar__right">
      <div class="icon-box">
        <top-lock v-if="setting.lock"/>
        <top-lock v-if="setting.lock" />
        <top-qna />
        <top-full v-if="setting.fullscreen" title="全屏"/>
        <img class="gateway" @click="jumpMH" src="@/assets/images/mh.svg" alt="进入门户" title="进入门户" width="20" height="20">
        <top-full v-if="setting.fullscreen" title="全屏" />
        <img class="gateway" @click="jumpMH" src="@/assets/images/mh.svg" alt="进入门户" title="进入门户" width="20"
          height="20">
      </div>
      <div class="top-user">
        <img class="top-bar__img" :src="userInfo.avatar "  alt=""/>
        <img class="top-bar__img" :src="userInfo.avatar" alt="" />
        <el-dropdown>
          <span class="el-dropdown-link">
@@ -29,7 +25,7 @@
          </span>
          <template #dropdown>
            <el-dropdown-menu>
<!--              <el-dropdown-item>
              <!--              <el-dropdown-item>
                <router-link to="/">{{ $t('navbar.dashboard') }}</router-link>
              </el-dropdown-item>-->
              <el-dropdown-item>
@@ -47,6 +43,8 @@
</template>
<script>
import logo from '@/assets/images/topContainer/logo.png'
import { mapGetters } from 'vuex'
import topLock from './top-lock.vue'
import topMenu from './top-menu.vue'
@@ -74,7 +72,9 @@
  },
  name: 'top',
  data () {
    return {}
    return {
      logoUrl: logo,
    }
  },
  filters: {},
  created () { },
@@ -84,17 +84,12 @@
      'userInfo',
      'tagWel',
      'bsTagList',
      'isCollapse',
      'tag',
      'logsLen',
      'logsFlag',
      'isHorizontal',
    ]),
  },
  methods: {
    setCollapse () {
      this.$store.commit('SET_COLLAPSE')
    },
    logout () {
      this.$confirm(this.$t('logoutTip'), this.$t('提示'), {
        confirmButtonText: this.$t('submitText'),
@@ -102,9 +97,9 @@
        type: 'warning',
      }).then(() => {
        this.$store.dispatch('LogOut').then(() => {
        const env = import.meta.env.VITE_APP_ENV
          const env = import.meta.env.VITE_APP_ENV
          const adminUrl = import.meta.env.VITE_APP_DASHBOARD_URL
           env === 'development' ? this.$router.push({ path: '/login' }):window.location.replace(`${adminUrl}#/login`)
          env === 'development' ? this.$router.push({ path: '/login' }) : window.location.replace(`${adminUrl}#/login`)
        })
      })
    },
@@ -119,12 +114,9 @@
<style lang="scss" scoped>
.avue-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 15px;
  height: 50px;
  background-color: #fff;
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  height: 100%;
  background: url('@/assets/images/topContainer/top-bg.png') center no-repeat !important;
}
.top-bar__left {
@@ -132,10 +124,32 @@
}
.top-bar__title {
  margin-top: 16px;
  margin-left: 30px;
  flex: 1;
  display: flex;
  align-items: center;
  height: pxToVh(80);
  height: pxToVh(48);
  img {
    margin-right: 17px;
    width: 36.15px;
    height: 28px;
  }
  span {
    width: 353px;
    height: 43px;
    font-family: PangMen;
    font-weight: 400;
    font-size: 36px;
    color: #FFFFFF;
    letter-spacing: 3px;
    text-shadow: 3px 3px 0px rgba(0, 13, 42, 0.27);
    text-align: left;
    font-style: normal;
    text-transform: none;
  }
}
.top-bar__right {
@@ -144,15 +158,17 @@
  align-items: center;
  height: 100%;
  .icon-box{
  .icon-box {
    display: flex;
    align-items: center;
    gap: 0 20px;
    .gateway{
    .gateway {
      width: 25px;
      height: 25px;
    }
    >*{
    >* {
      cursor: pointer;
    }
  }
applications/drone-command/src/router/views/index.js
@@ -16,15 +16,6 @@
        },
        component: () => import(/* webpackChunkName: "views" */ '@/views/wel/index.vue'),
      },
      {
        path: 'dashboard',
        name: '控制台',
        meta: {
          i18n: 'dashboard',
          menu: false,
        },
        component: () => import(/* webpackChunkName: "views" */ '@/views/wel/dashboard.vue'),
      },
    ],
  },
  // 事件工单
applications/drone-command/src/styles/common.scss
@@ -26,10 +26,11 @@
}
.avue-layout {
  position: relative;
  display: flex;
  width: 100%;
  height: 100%;
  background: url("@/assets/images/layoutBg.png") no-repeat center / 100% 100%;
  background: #05050F;
  overflow: hidden;
  &--horizontal {
@@ -43,8 +44,8 @@
      .avue-menu,
      .el-menu-item,
      .el-sub-menu__title {
        height: $top_height;
        line-height: $top_height;
        height: $top_height !important;
        line-height: $top_height !important;
      }
      .is-active:before {
@@ -55,6 +56,37 @@
    .avue-logo {
      width: $sidebar_width
    }
  }
  > .top-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 97px;
    z-index: 9;
  }
  > .leaf-container {
    padding-top: 97px;
    width: 160px;
    height: 100%;
    .avue-sidebar {
      .el-menu-item,
      .el-sub-menu__title {
        margin-top: 26px;
        height: $top_height !important;
        line-height: $top_height !important;
      }
    }
  }
  > .right-container {
    width: 0;
    flex: 1;
    height: 100%;
  }
}
@@ -93,6 +125,7 @@
}
.main-avue-view-container {
  margin-top: 97px;
  height: 0;
  flex: 1;
  display: flex;
applications/drone-command/src/styles/sidebar.scss
@@ -10,6 +10,7 @@
  transition: width .2s;
  box-sizing: border-box;
  box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
  border-right: 1px solid #323241;
  .el-scrollbar__wrap {
    overflow-x: hidden;
@@ -60,7 +61,7 @@
        left: 0;
        bottom: 0;
        width: 4px;
        background: var(--el-color-primary);
        background: #0041FF;
        position: absolute;
      }
applications/drone-command/src/styles/theme/white.scss
@@ -81,18 +81,18 @@
  .avue-sidebar {
    box-shadow: 2px 0 6px rgba(0, 21, 41, 0.15);
    background-color: #fff;
    background-color: #05050F;
    .el-menu-item, .el-sub-menu__title {
      i, span {
        color: #666
        color: #8F8FA5;
      }
      &:hover {
        background: transparent;
        i, span {
          color: #333;
          color: #8F8FA5;
        }
      }
applications/drone-command/src/styles/top.scss
@@ -101,17 +101,6 @@
  }
}
.top-bar__title {
  height: 100%;
  padding-left: 50px;
  box-sizing: border-box;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: inherit;
  font-weight: 400;
}
.avue-logo {
  height: $top_height;
  line-height: $top_height;
applications/drone-command/src/styles/variables.scss
@@ -1,6 +1,6 @@
$sidebar_width: 230px;
$sidebar_width: 160px;
$sidebar_collapse: 60px;
$top_height: 50px;
$top_height: 40px;
@function pxToVh($px) {
  @return calc($px / 1080) * 100vh;
applications/drone-command/src/utils/auth.js
@@ -1,3 +1,13 @@
/*
 * @Author       : yuan
 * @Date         : 2026-01-06 09:47:09
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-06 11:03:04
 * @FilePath     : \applications\drone-command\src\utils\auth.js
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 09:47:09
 */
import Cookies from 'js-cookie';
const TokenKey = 'saber3-access-token';
applications/drone-command/src/utils/cesium/DrawPolygon.js
New file
@@ -0,0 +1,803 @@
import * as Cesium from 'cesium'
import * as turf from '@turf/turf'
import { boxTransformScale } from '@/utils/turfFunc'
import { ElMessage } from 'element-plus'
import { flyVisual, getPointPositionsHeight } from '@/utils/cesium/mapUtil'
/**
 * 多边形绘制与编辑工具类
 * 功能:
 *  - 绘制多边形
 *  - 拖动编辑端点
 *  - 删除端点、删除整个多边形
 *  - 多边形自交检查(避免非法几何)
 *  - 外部订阅/通知机制
 */
export class DrawPolygon {
    constructor() {
        // 图斑预览模式标记
        this.isPureSpotPreview = false
        // 是否删除测区
        this.isDeleteTheArea = true
        //是否可以编辑图斑
        this.isPreviewMode = true
        // Cesium 视图对象
        this.viewer = null
        // 当前绘制的多边形
        this.curPolygon = null
        // 绘制模式标识
        this.drawingMode = false
        // 编辑模式标识
        this.editingMode = false
        // 是否正在拖拽端点
        this.isDragging = false
        // 当前拖拽的点实体
        this.draggedEntity = null
        // 多边形实体
        this.polygonEntity = null
        // 存储端点的 DataSource
        this.editPolygonDataSource = null
        this.editPolygonPointDataSource = null
        // 存储中点(边中点)的 DataSource:用于编辑态插入新端点
        this.editPolygonMidPointDataSource = null
        // 鼠标事件处理器
        this.handler = null
        // 右键菜单 DOM
        this.menuPopup = null
        // 被右键选中的点
        this.delPolygonPoint = null
        // 是否显示警告提示(自交)
        this.isShowWaringTip = false
        // 当前拖拽点是否合法
        this.currentDragPointIsValid = false
        // 当前拖拽点的坐标
        this.currentDragPointPosition = null
        // 事件回调函数绑定 this
        this.handleLeftDown = this.handleLeftDown.bind(this)
        this.handleLeftUp = this.handleLeftUp.bind(this)
        this.handleMouseMove = this.handleMouseMove.bind(this)
        this.handleLeftClick = this.handleLeftClick.bind(this)
        this.handleRightClick = this.handleRightClick.bind(this)
        this.delPolygon = this.delPolygon.bind(this)
        this.delPoint = this.delPoint.bind(this)
        // 外部订阅者
        this.listeners = []
    }
    // 实体命名常量
    static ENTITY_NAMES = {
        POLYGON: '区域-面',
        POINT: '区域-端点',
        // 编辑态中点(用于快速插入新端点)
        MID_POINT: '区域-中点',
        POLYGON_ID: 'planar-route-polygon',
        POINT_ID_PREFIX: 'planar-route-point',
        MID_POINT_ID_PREFIX: 'planar-route-mid-point',
    }
    // 颜色常量
    static COLORS = {
        DEFAULT_POLYGON: Cesium.Color.fromBytes(45, 140, 240, 99), // 默认面颜色
        DEFAULT_LINE: Cesium.Color.fromBytes(45, 140, 240, 255), // 默认边界线颜色
        ERROR_POLYGON: Cesium.Color.fromCssColorString('rgba(255, 0, 0, .3)'), // 错误(自交)面颜色
        ERROR_LINE: Cesium.Color.fromCssColorString('rgba(255, 0, 0, 1)'), // 错误(自交)线颜色
    }
    // ============ 发布订阅机制 ============
    // 外部订阅数据变化
    subscribe (key, listener) {
        this.listeners.push({ key, listener })
    }
    // 通知订阅者
    notify (key, data) {
        this.listeners.filter(subscriber => subscriber.key === key).forEach(subscriber => subscriber.listener(data))
    }
    // ============ 绘制相关 ============
    // 编辑图斑
    editThePatch (data) {
        this.isPreviewMode = data
        // 关闭编辑能力时隐藏端点/中点,并清空中点,避免残留交互
        if (!this.isPreviewMode) {
            this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = false)
            this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false)
            this.editPolygonMidPointDataSource?.entities?.removeAll?.()
            return
        }
        if (this.editingMode) {
            this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true)
            this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
            this.rebuildEditPoints()
            this.rebuildMidPoints()
        }
    }
    // 删除测区
    deleteTheArea (data) {
        this.isDeleteTheArea = data
    }
    // 开始绘制
    startDrawing () {
        this.drawingMode = true
        this.curPolygon = new Cesium.PolygonHierarchy()
        // 如果还没有 DataSource,就新建一个
        if (!this.editPolygonDataSource) {
            this.editPolygonDataSource = new Cesium.CustomDataSource('editPolygonDataSource')
            this.viewer?.dataSources.add(this.editPolygonDataSource)
        }
        if (!this.editPolygonPointDataSource) {
            this.editPolygonPointDataSource = new Cesium.CustomDataSource('editPolygonPointDataSource')
            this.viewer?.dataSources.add(this.editPolygonPointDataSource)
        }
        // 中点数据源:编辑模式下用于“边上插点”
        if (!this.editPolygonMidPointDataSource) {
            this.editPolygonMidPointDataSource = new Cesium.CustomDataSource('editPolygonMidPointDataSource')
            this.viewer?.dataSources.add(this.editPolygonMidPointDataSource)
        }
        // 清空之前的点
        this.editPolygonDataSource?.entities.removeAll()
        this.editPolygonPointDataSource?.entities.removeAll()
        this.editPolygonMidPointDataSource?.entities.removeAll()
    }
    // 创建多边形实体(含边界线)
    createPolygonEntity () {
        this.polygonEntity = this.editPolygonDataSource.entities.add({
            name: DrawPolygon.ENTITY_NAMES.POLYGON,
            id: DrawPolygon.ENTITY_NAMES.POLYGON_ID,
            polygon: {
                hierarchy: new Cesium.CallbackProperty(() => this.curPolygon, false),
                material: DrawPolygon.COLORS.DEFAULT_POLYGON,
                outline: false,
                outlineWidth: 2,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
            },
            polyline: {
                width: 2,
                material: DrawPolygon.COLORS.DEFAULT_LINE,
                clampToGround: true,
                positions: new Cesium.CallbackProperty(
                    () => [...this.curPolygon.positions, this.curPolygon.positions[0]], // 闭合线
                    false
                ),
            },
        })
    }
    // 创建端点实体
    createPointEntity (position, isAdd) {
        const pointIndex = isAdd ? this.curPolygon.positions.length - 2 : this.curPolygon.positions.length - 1
        this.editPolygonPointDataSource.entities.add({
            name: DrawPolygon.ENTITY_NAMES.POINT,
            id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`,
            position: position.clone(),
            point: {
                pixelSize: 14,
                color: Cesium.Color.WHITE,
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customData: {
                ind: pointIndex, // 索引记录
            },
        })
    }
    createMidPointEntity (startInd, position) {
        // 使用 CallbackProperty 让中点位置随端点拖拽实时计算
        // startInd 表示该中点属于边:(startInd) -> (startInd + 1)
        const updatePosition = () => {
            const positions = this.curPolygon?.positions
            if (!positions || positions.length < 2) return position
            const n = positions.length
            const p1 = positions[startInd]
            const p2 = positions[(startInd + 1) % n]
            if (!p1 || !p2) return position
            return Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3())
        }
        this.editPolygonMidPointDataSource.entities.add({
            name: DrawPolygon.ENTITY_NAMES.MID_POINT,
            id: `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${startInd}`,
            position: new Cesium.CallbackProperty(updatePosition, false),
            point: {
                pixelSize: 10,
                color: Cesium.Color.fromCssColorString('rgba(255, 255, 255, .7)'),
                heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                disableDepthTestDistance: Number.POSITIVE_INFINITY,
            },
            customData: {
                startInd,
            },
        })
    }
    rebuildEditPoints () {
        // 插入/删除端点后:端点实体 id 与 customData.ind 需要全量重建,保证索引与 positions 一致
        if (!this.isPreviewMode) return
        if (!this.editPolygonPointDataSource || !this.curPolygon?.positions) return
        this.editPolygonPointDataSource.entities.removeAll()
        this.curPolygon.positions.forEach((position, index) => {
            this.editPolygonPointDataSource.entities.add({
                name: DrawPolygon.ENTITY_NAMES.POINT,
                id: `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${index}`,
                position: position.clone(),
                point: {
                    pixelSize: 14,
                    color: Cesium.Color.WHITE,
                    heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    ind: index,
                },
            })
        })
    }
    rebuildMidPoints () {
        // 仅在“顶点数量变化/切换编辑态”时维护中点实体数量
        // 拖拽过程中中点位置由 CallbackProperty 自动更新,不需要重建
        if (!this.isPreviewMode) return
        if (!this.editPolygonMidPointDataSource || !this.curPolygon?.positions) return
        const positions = this.curPolygon.positions
        const n = positions.length
        const entities = this.editPolygonMidPointDataSource.entities
        if (!this.editingMode || n < 2) {
            entities.removeAll()
            return
        }
        const neededIds = new Set()
        for (let i = 0; i < n; i += 1) {
            const id = `${DrawPolygon.ENTITY_NAMES.MID_POINT_ID_PREFIX}${i}`
            neededIds.add(id)
            // 缺少则补齐:保证每条边都有一个中点实体
            if (!entities.getById(id)) {
                const p1 = positions[i]
                const p2 = positions[(i + 1) % n]
                if (!p1 || !p2) continue
                const mid = Cesium.Cartesian3.midpoint(p1, p2, new Cesium.Cartesian3())
                this.createMidPointEntity(i, mid)
            }
        }
        entities.values.slice().forEach(entity => {
            if (entity?.name !== DrawPolygon.ENTITY_NAMES.MID_POINT) return
            // 多余则移除:例如删除端点导致边数量减少
            if (!neededIds.has(entity.id)) {
                entities.remove(entity)
            }
        })
    }
    insertPointFromMidPoint (midPointEntity) {
        // 点击中点:在对应边上插入一个新端点(白点),并立即进入拖拽态
        if (!this.isPreviewMode) return
        if (!this.editingMode) return
        if (!midPointEntity?.customData) return
        const startInd = midPointEntity.customData.startInd
        // 中点位置可能是 CallbackProperty,需要取当前时刻的值
        const positionProperty = midPointEntity.position
        const entityPosition = positionProperty?.getValue
            ? positionProperty.getValue(Cesium.JulianDate.now())
            : positionProperty
        if (!entityPosition) return
        // 插入到 startInd 与 startInd+1 之间
        const insertIndex = Math.min(Math.max(startInd + 1, 0), this.curPolygon.positions.length)
        this.curPolygon.positions.splice(insertIndex, 0, entityPosition.clone ? entityPosition.clone() : entityPosition)
        this.rebuildEditPoints()
        this.rebuildMidPoints()
        const newPointEntity = this.editPolygonPointDataSource?.entities?.getById(
            `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${insertIndex}`
        )
        if (newPointEntity) {
            this.isDragging = true
            this.draggedEntity = newPointEntity
            this.currentDragPointPosition = this.curPolygon.positions[insertIndex]
            this.disableMapControl()
        }
        this.notify('getPolygonPositions', this.curPolygon.positions)
    }
    // 清除不在范围内的点
    removeLastInvalidPoint () {
        const posLen = this.curPolygon.positions.length
        if (posLen === 0) return
        let targetPointId = ''
        let removeCount = 0
        if (posLen === 2) {
            removeCount = 2
            targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}0`
        } else if (posLen > 2) {
            removeCount = 1
            const pointIndex = posLen - 2
            targetPointId = `${DrawPolygon.ENTITY_NAMES.POINT_ID_PREFIX}${pointIndex}`
        }
        this.curPolygon.positions.splice(posLen - removeCount, removeCount)
        const invalidPoint = this.editPolygonPointDataSource.entities.getById(targetPointId)
        if (invalidPoint) {
            this.editPolygonPointDataSource.entities.remove(invalidPoint)
        }
        if (this.curPolygon.positions.length === 0 && this.polygonEntity) {
            this.editPolygonDataSource.entities.remove(this.polygonEntity)
            this.polygonEntity = null
        }
    }
    // 添加一个点
    addPosition (position, isAdd = true) {
        // 第一个点要重复压入一次,形成动态绘制效果
        if (this.curPolygon.positions.length === 0 && isAdd) {
            this.curPolygon.positions.push(position.clone())
        }
        this.curPolygon.positions.push(position.clone())
        // 如果没有实体则创建
        if (!this.polygonEntity) {
            this.createPolygonEntity()
        }
        // 创建端点实体
        if (this.isPreviewMode) {
            this.createPointEntity(position, isAdd)
        }
        this.notify('getPoints', position)
    }
    // ============ 鼠标事件 ============
    // 鼠标左键按下(选中端点拖动)
    handleLeftDown (movement) {
        if (!this.editingMode) return
        const pickedEntity = this.viewer.scene.pick(movement.position)?.id
        const isPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.POINT
        const isMidPoint = pickedEntity?.name === DrawPolygon.ENTITY_NAMES.MID_POINT
        if (pickedEntity && isPoint) {
            this.isDragging = true
            this.draggedEntity = pickedEntity
            this.currentDragPointPosition = this.curPolygon.positions[this.draggedEntity?.customData.ind]
            this.disableMapControl() // 禁止地图交互
            return
        }
        if (pickedEntity && isMidPoint) {
            this.insertPointFromMidPoint(pickedEntity)
        }
    }
    // 鼠标左键抬起(拖拽结束)
    handleLeftUp () {
        if (!(this.editingMode && this.curPolygon?.positions && this.draggedEntity)) return
        if (this.currentDragPointIsValid && this.isDragging) {
            // 更新点位置
            this.draggedEntity.position = this.currentDragPointPosition
            this.curPolygon.positions[this.draggedEntity?.customData.ind] = this.currentDragPointPosition
            // 校验多边形是否自交
            if (!this.curDragPointIsValid(this.curPolygon.positions)) {
                this.isShowWaringTip = true
                this.currentDragPointIsValid = true
                this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
            } else {
                this.isShowWaringTip = false
                this.currentDragPointIsValid = false
                this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
            }
            this.notify('getShowWaringTip', this.isShowWaringTip)
        }
        // 拖拽结束,恢复交互
        if (this.isDragging) {
            this.notify('getPolygonPositions', this.curPolygon.positions)
            this.isDragging = false
            this.draggedEntity = null
            this.enableMapControl()
            this.rebuildMidPoints()
        }
    }
    // 鼠标移动
    handleMouseMove (movement) {
        if (!this.drawingMode && !this.editingMode) return
        const cartesian = this.viewer.scene.pickPosition(movement.endPosition)
        if (!cartesian) return
        // 编辑模式下,拖拽点实时更新
        if (this.editingMode && this.draggedEntity?.customData) {
            this.draggedEntity.position = cartesian
            this.curPolygon.positions[this.draggedEntity?.customData.ind] = cartesian
        }
        // 绘制模式下,实时更新最后一个点
        if (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 1) {
            this.curPolygon.positions.pop()
            this.curPolygon.positions.push(cartesian)
        }
        // 实时检查是否自交
        if (
            (this.editingMode && this.draggedEntity?.customData) ||
            (this.drawingMode && this.polygonEntity && this.curPolygon.positions.length >= 3)
        ) {
            if (!this.curDragPointIsValid(this.curPolygon.positions)) {
                this.isShowWaringTip = true
                this.currentDragPointIsValid = true
                this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
            } else {
                this.isShowWaringTip = false
                this.currentDragPointIsValid = false
                this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
            }
            this.notify('getShowWaringTip', this.isShowWaringTip)
        }
    }
    // 鼠标左键点击
    handleLeftClick (click) {
        this.removeMenuPopup()
        const pickedAllEntity = this.viewer.scene.drillPick(click.position).filter(i => i.id)
        const isStartPoint = pickedAllEntity.find(i => i.id.name === '起飞点')
        const isPolygonPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
        const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
        if (isStartPoint) return
        // 如果不是绘制模式
        if (!this.drawingMode) {
            if (!isPolygon) {
                this.editingMode = false
                this.editPolygonPointDataSource.entities.show = false
                this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = false)
                return
            }
            if (isPolygon) {
                this.editingMode = true
                this.editPolygonPointDataSource.entities.show = true
                this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
                this.rebuildMidPoints()
            }
            return
        }
        // 点击闭合多边形
        if (this.curPolygon.positions.length < 4 && isPolygonPoint) {
            return
        }
        if (this.curPolygon.positions.length >= 4 && isPolygonPoint) {
            if (!this.drawingMode) return
            this.finishDrawing()
            return
        }
        // 添加新的点
        const cartesian = this.viewer.scene.pickPosition(click.position)
        if (!cartesian) return
        let arr = [...this.curPolygon.positions]
        arr.pop()
        // 校验多边形是否自交
        if (arr.length > 2 && !this.curDragPointIsValid([...arr, cartesian])) {
            ElMessage.warning('测区不支持交叉绘制,请重新绘制')
            return
        }
        this.addPosition(cartesian)
    }
    // 鼠标右键点击(弹出菜单)
    handleRightClick (click) {
        const that = this
        if (that.drawingMode) return
        that.removeMenuPopup()
        const pickedAllEntity = that.viewer.scene.drillPick(click.position).filter(i => i.id)
        const isPolygon = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POLYGON)
        const isEditPoint = pickedAllEntity.find(i => i.id.name === DrawPolygon.ENTITY_NAMES.POINT)
        const {
            position: { x, y },
        } = click
        let pickedEntity, tooltipEvent, menuType
        if (isEditPoint) {
            pickedEntity = isEditPoint
            tooltipEvent = that.delPoint
            menuType = 'edit-point'
        } else if (isPolygon) {
            pickedEntity = isPolygon
            tooltipEvent = that.delPolygon
            menuType = 'polygon'
        }
        if (pickedEntity && this.isDeleteTheArea) {
            that.delPolygonPoint = pickedEntity.id
            that.menuPopup = that.createMenuPopup(menuType)
            that.menuPopup.style.transform = `translate3d(${x - 10}px, ${y - 10}px, 0)`
            that.viewer.container.appendChild(that.menuPopup)
            that.menuPopup.addEventListener('click', tooltipEvent)
        }
    }
    // ============ 删除相关 ============
    // 删除所有实体
    removeEntities () {
        if (this.editPolygonDataSource) {
            this.editPolygonDataSource.entities.removeAll()
            this.editPolygonDataSource = null
        }
        if (this.editPolygonPointDataSource) {
            this.editPolygonPointDataSource.entities.removeAll()
            this.editPolygonPointDataSource = null
        }
        if (this.editPolygonMidPointDataSource) {
            this.editPolygonMidPointDataSource.entities.removeAll()
            this.editPolygonMidPointDataSource = null
        }
        this.editingMode = false
        this.polygonEntity = null
        this.curPolygon = null
    }
    // 完成绘制
    finishDrawing () {
        this.curPolygon.positions.pop()
        if (this.curPolygon.positions.length >= 3) {
            this.drawingMode = false
            this.editingMode = true
            this.editPolygonPointDataSource.entities.show = true
            this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
            this.rebuildMidPoints()
            if (!this.curDragPointIsValid(this.curPolygon.positions)) {
                this.isShowWaringTip = true
                this.currentDragPointIsValid = true
                this.updatePolygonAppearance(DrawPolygon.COLORS.ERROR_POLYGON, DrawPolygon.COLORS.ERROR_LINE)
            } else {
                this.isShowWaringTip = false
                this.currentDragPointIsValid = false
                this.updatePolygonAppearance(DrawPolygon.COLORS.DEFAULT_POLYGON, DrawPolygon.COLORS.DEFAULT_LINE)
            }
            this.notify('getShowWaringTip', this.isShowWaringTip)
            this.notify('getPolygonPositions', this.curPolygon.positions)
        }
    }
    // 删除多边形
    delPolygon () {
        this.removeEntities()
        this.removeMenuPopup()
        this.notify('getPolygonPositions', [])
        this.startDrawing()
    }
    // 删除图斑
    delSpot () {
        this.removeEntities()
        this.isPreviewMode = true
        this.startDrawing()
    }
    // 删除端点
    delPoint () {
        if (this.curPolygon.positions.length <= 3) {
            this.removeMenuPopup()
            return ElMessage.warning('端点不可少于3个')
        }
        if (!this.delPolygonPoint) return
        this.curPolygon.positions.splice(this.delPolygonPoint.customData.ind, 1)
        this.removeMenuPopup()
        this.rebuildEditPoints()
        this.rebuildMidPoints()
        this.notify('getPolygonPositions', this.curPolygon.positions)
    }
    // ============ 工具方法 ============
    // 创建右键菜单
    createMenuPopup (type = 'polygon') {
        const menuPopupVBox = document.createElement('div')
        menuPopupVBox.id = 'planarPolygonEdit'
        menuPopupVBox.className = 'planar-polygon-edit-tooltip'
        const menuPopup = document.createElement('div')
        menuPopup.id = 'planarPolygonEditMenu'
        menuPopup.className = 'planar-polygon-edit-menu'
        const menuItems =
            type === 'polygon'
                ? [{ title: '删除测区', class: 'del-planar-polygon' }]
                : [{ title: '删除端点', class: 'del-planar-point' }]
        menuItems.forEach(item => {
            const titleDiv = document.createElement('div')
            titleDiv.innerText = item.title
            titleDiv.className = item.class
            menuPopup.appendChild(titleDiv)
        })
        this.isPreviewMode = true
        menuPopupVBox.appendChild(menuPopup)
        return menuPopupVBox
    }
    // 移除菜单
    removeMenuPopup () {
        const that = this
        if (that.menuPopup) {
            that.menuPopup.removeEventListener('click', that.delPolygon)
            that.menuPopup.removeEventListener('click', that.delPoint)
            that.viewer.container.removeChild(that.menuPopup)
            that.menuPopup = null
        }
        that.delPolygonPoint = null
    }
    // 更新多边形样式(正常/错误)
    updatePolygonAppearance (polygonColor, lineColor) {
        this.polygonEntity.polygon.material = polygonColor
        this.polygonEntity.polyline.material = lineColor
    }
    // 禁用地图交互
    disableMapControl () {
        const controller = this.viewer.scene.screenSpaceCameraController
        controller.enableRotate = false
        controller.enableTranslate = false
        controller.enableZoom = false
    }
    // 启用地图交互
    enableMapControl () {
        const controller = this.viewer.scene.screenSpaceCameraController
        controller.enableRotate = true
        controller.enableTranslate = true
        controller.enableZoom = true
    }
    // 检查多边形是否自交
    curDragPointIsValid (positions) {
        if (positions.length < 3) return true
        const cartographics = Cesium.Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions)
        const latLngPoints = cartographics.map(cartographic => [
            Cesium.Math.toDegrees(cartographic.longitude),
            Cesium.Math.toDegrees(cartographic.latitude),
        ])
        // 用 turf.js 检查自交
        const poly = turf.polygon([[...latLngPoints, latLngPoints[0]]])
        const intersections = turf.kinks(poly)
        return intersections.features.length === 0
    }
    // 初始化已有多边形
    async initPolygon (viewer, positions, isPurePreview = false) {
        this.initHandler(viewer)
        this.isPureSpotPreview = isPurePreview
        this.startDrawing()
        let newPosition = positions.map(item => {
            return Cesium.Cartesian3.fromDegrees(Number(item.lng), Number(item.lat), Number(item?.height||0))
        })
        // 预览航线的时候调用
        if (!this.isPureSpotPreview) {
            this.notify('getPolygonPositions', newPosition)
        }
        newPosition.forEach(item => {
            this.addPosition(item, false)
        })
        // 视角飞入区域
        const newBox = boxTransformScale(
            positions.map(item => [item.lng, item.lat]),
            5
        )
        viewer.camera.flyTo({
            destination: Cesium.Rectangle.fromDegrees(...newBox),
            offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
            duration: 0.5,
        })
        const pointList = await getPointPositionsHeight(positions, viewer)
        flyVisual({ positionsData: pointList.map(item => [item.lng, item.lat, item.ASL]), viewer })
        this.drawingMode = false
        this.editingMode = true
        this.editPolygonPointDataSource && (this.editPolygonPointDataSource.entities.show = true)
        this.editPolygonMidPointDataSource && (this.editPolygonMidPointDataSource.entities.show = true)
        this.rebuildMidPoints()
    }
    // 初始化事件处理器
    initHandler (viewer) {
        this.viewer = viewer
        this.startDrawing()
        if (!this.handler) {
            this.handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
            // 注册鼠标事件
            const events = [
                [Cesium.ScreenSpaceEventType.LEFT_DOWN, this.handleLeftDown],
                [Cesium.ScreenSpaceEventType.LEFT_UP, this.handleLeftUp],
                [Cesium.ScreenSpaceEventType.MOUSE_MOVE, this.handleMouseMove],
                [Cesium.ScreenSpaceEventType.LEFT_CLICK, this.handleLeftClick],
                [Cesium.ScreenSpaceEventType.RIGHT_CLICK, this.handleRightClick],
            ]
            events.forEach(([type, handler]) => {
                this.handler.setInputAction(handler, type)
            })
        }
    }
    // 移除事件处理器
    removeHandler () {
        if (this.handler) {
            const eventTypes = [
                Cesium.ScreenSpaceEventType.LEFT_DOWN,
                Cesium.ScreenSpaceEventType.LEFT_UP,
                Cesium.ScreenSpaceEventType.MOUSE_MOVE,
                Cesium.ScreenSpaceEventType.LEFT_CLICK,
                Cesium.ScreenSpaceEventType.RIGHT_CLICK,
            ]
            eventTypes.forEach(type => {
                this.handler.removeInputAction(type)
            })
            this.handler = null
        }
    }
    /**
     * 销毁实例,释放资源
     */
    destroy () {
        if (!this.viewer) return
        this.removeMenuPopup()
        this.removeEntities()
        this.removeHandler()
        this.enableMapControl()
    }
}
applications/drone-command/src/utils/cesium/Material/index.js
@@ -54,6 +54,7 @@
    this._opacity = undefined
    this._alphaPower = undefined
    this._speed = undefined
    this._xRepeatCount = undefined
    this.color = options.color || Cesium.Color.fromBytes(0, 255, 255, 255)
    this.glowPower = .25
@@ -64,6 +65,9 @@
    this.opacity = options.opacity || 0.5
    this.alphaPower = options.alphaPower || 1.5
    this.speed = options.speed || 5
    this.xRepeatCount = options.xRepeatCount || 1
  }
  get isConstant () {
@@ -132,15 +136,17 @@
      color: new Cesium.Color(1.0, 1.0, 0.0, 0.7),
      bgColor: new Cesium.Color(0.0, 1.0, 0.0, 0.0),
      speed: 5,
      globalAlpha: 1.0
      globalAlpha: 1.0,
      xRepeatCount: 1.0
    },
    source: `
          uniform vec4 bgColor;
          uniform vec4 color;
          uniform float speed;
          uniform float globalAlpha;
          uniform float xRepeatCount; // 新增的 uniform 在着色器中
          czm_material czm_getMaterial(czm_materialInput materialInput) {
           czm_material czm_getMaterial(czm_materialInput materialInput) {
              czm_material material = czm_getDefaultMaterial(materialInput);
              vec2 st = materialInput.st;
              float time = fract(czm_frameNumber * speed / 1000.0);
@@ -150,7 +156,7 @@
                  colorMars3D = vec3(1.0);
              }
              material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s - time));
              material.alpha = color.a * 1.5 * smoothstep(0.0, 1.0, fract(st.s * xRepeatCount - time)); // 使用 xRepeatCount 来调整纹理重复
              material.diffuse = max(colorMars3D.rgb * material.alpha, colorMars3D.rgb);
              if (material.alpha < bgColor.a) {
@@ -182,6 +188,8 @@
    result.color = Cesium.Property.getValueOrUndefined(this._color, time)
    result.globalAlpha = Cesium.Property.getValueOrUndefined(this._opacity, time)
    result.speed = Cesium.Property.getValueOrUndefined(this._speed, time)
    result.xRepeatCount = Cesium.Property.getValueOrUndefined(this._xRepeatCount, time)
    return result
  }
@@ -191,7 +199,8 @@
      (other instanceof LineTrailMaterial &&
        Cesium.Property.equals(this._color, other._color) &&
        Cesium.Property.equals(this._opacity, other._opacity) &&
        Cesium.Property.equals(this._speed, other._speed))
        Cesium.Property.equals(this._speed, other._speed) &&
        Cesium.Property.equals(this._xRepeatCount, other._xRepeatCount))
    )
  }
}
@@ -200,6 +209,7 @@
  color: Cesium.createPropertyDescriptor('color'),
  opacity: Cesium.createPropertyDescriptor('opacity'),
  speed: Cesium.createPropertyDescriptor('speed'),
  xRepeatCount: Cesium.createPropertyDescriptor('xRepeatCount'),
})
applications/drone-command/src/utils/cesium/common.js
@@ -131,22 +131,22 @@
    let newLat = lat
    switch (direction) {
        case 'W': // 向前(heading 方向)
        case 'KeyW': // 向前(heading 方向)
            newLon += moveStep * Math.sin(headingRad)
            newLat += moveStep * Math.cos(headingRad)
            break
        case 'S': // 向后(反方向)
        case 'KeyS': // 向后(反方向)
            newLon -= moveStep * Math.sin(headingRad)
            newLat -= moveStep * Math.cos(headingRad)
            break
        case 'D': // 向右(heading + 90°)
        case 'KeyD': // 向右(heading + 90°)
            newLon += moveStep * Math.cos(headingRad)
            newLat -= moveStep * Math.sin(headingRad)
            break
        case 'A': // 向左(heading - 90°)
        case 'KeyA': // 向左(heading - 90°)
            newLon -= moveStep * Math.cos(headingRad)
            newLat += moveStep * Math.sin(headingRad)
            break
applications/drone-command/src/utils/cesium/compressImage.js
New file
@@ -0,0 +1,55 @@
/**
 * @description: 图片压缩
 * @param {*} src 图片地址
 * @param {*} quality 压缩质量
 * @param {*} maxWidth 最大宽度
 * @param {*} callback 回调函数
 * @return {*}
 */
function compressImage(src, quality, maxWidth, callback) {
    const img = new Image()
    img.setAttribute('crossOrigin', 'Anonymous')
    img.onload = function () {
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        let width = img.width
        let height = img.height
        if (width > maxWidth) {
            height *= maxWidth / width
            width = maxWidth
        }
        canvas.width = width
        canvas.height = height
        ctx.drawImage(img, 0, 0, width, height)
        const newData = canvas.toDataURL('image/jpeg', quality)
        // 使用atob()将base64转换为二进制字符串
        const binaryString = window.atob(newData.split(',')[1])
        const mimeString = newData.split(',')[0].split(':')[1].split(';')[0]
        // 创建Blob对象
        const array = []
        for (let i = 0; i < binaryString.length; i++) {
            array.push(binaryString.charCodeAt(i))
        }
        const blob = new Blob([new Uint8Array(array)], { type: mimeString })
        callback(blob)
    }
    img.src = src
    return src
}
export default function imageCompress(src, quality, maxWidth) {
    // 使用例子
    return compressImage(src, quality, maxWidth, function (blob) {
        // 创建一个新的图片URL
        const imageUrl = URL.createObjectURL(blob)
        // 可以在这里使用imageUrl,比如设置为img元素的src
        // document.getElementById('your-image-element').src = imageUrl;
        // 当不需要这个URL的时候,释放它
        URL.revokeObjectURL(imageUrl)
    })
}
applications/drone-command/src/utils/cesium/compressImage2.js
New file
@@ -0,0 +1,46 @@
/*
 * @Author: GuLiMmo 2820890765@qq.com
 * @Date: 2024-05-21 10:14:11
 * @LastEditors: GuLiMmo 2820890765@qq.com
 * @LastEditTime: 2024-05-21 10:20:34
 * @FilePath: /bigScreen/src/utils/cesium/compressImage2.js
 * @Description:
 * Copyright (c) 2024 by GuLiMmo, All Rights Reserved.
 */
export default function compressImage(
    src,
    maxWidth,
    maxHeight,
    outputFormat = 'image/jpeg',
    quality = 0.7
) {
    return new Promise((resolve, reject) => {
        const image = new Image()
        image.setAttribute('crossOrigin', 'Anonymous')
        image.src = src
        image.onload = () => {
            let width = image.width
            let height = image.height
            if (width > maxWidth) {
                height *= maxWidth / width
                width = maxWidth
            }
            if (height > maxHeight) {
                width *= maxHeight / height
                height = maxHeight
            }
            const canvas = document.createElement('canvas')
            canvas.width = width
            canvas.height = height
            const ctx = canvas.getContext('2d')
            ctx.drawImage(image, 0, 0, width, height)
            const newDataUrl = canvas.toDataURL(outputFormat, quality)
            resolve(newDataUrl)
        }
        image.onerror = reject
    })
}
applications/drone-command/src/utils/cesium/createRouteLine.js
@@ -2,17 +2,16 @@
import { Decimal } from 'decimal.js'
import _, { cloneDeep, throttle } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { render } from 'vue'
import { analysisPointLineKmz, handlePointListForKmz } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils'
import HistoryDronePopup from '@/components/DeviceJobDetails/HistoryDronePopup.vue'
import { getNewPolygonData } from '@/views/RoutePlan/PlanarAirLine/planarRouteUtils'
import {
    getNewPolygonData,
} from '@/views/newRoutePlan/routeUtils'
import { analyzeKmzFile, removeTextKey, XMLToJSON } from '@/utils/cesium/kmz'
import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial, } from '@/utils/cesium/Material'
import { ArrowLineMaterialProperty, LineTrailMaterial, PolylineGlowMaterial } from '@/utils/cesium/Material'
import CreateFrustum from '@/utils/cesium/frustum/CreateFrustum'
import { ElMessage } from 'element-plus'
@@ -23,61 +22,21 @@
import aircraftGltf from '@/assets/gltf/aircraft.gltf'
import newNumPoint from '@/assets/images/newStartPoint.png'
import {
    getPolyLine,
} from '@/views/RoutePlan/PointAirLine/pointWayLineUtils'
import { getPolyLine } from '@/views/newRoutePlan/Waypoint/pointWayLineUtils'
import store from '@/store/index'
import newStartPoint from '@/assets/images/newStartPoint.png'
import newStartPointMore from '@/assets/images/newStartPointMore.png'
import {
    arrowLineMaterialProperty, arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT,
    arrowLineMaterialPropertyGreen,
    arrowLineMaterialPropertyOrange,
} from '@/hooks/useMorePointLine/useMorePointLine'
let arrowLineMaterialProperty = new ArrowLineMaterialProperty({
    color: new Cesium.Color(128 / 255, 215 / 255, 255 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5
})
let arrowLineMaterialPropertyOrange = new ArrowLineMaterialProperty({
    color: new Cesium.Color(255 / 255, 185 / 255, 58 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5,
})
let arrowLineMaterialPropertyGreen = new ArrowLineMaterialProperty({
    color: new Cesium.Color(6 / 255, 217 / 255, 87 / 255, 1),
    directionColor: new Cesium.Color(1, 1, 1, 1),
    outlineColor: new Cesium.Color(1, 1, 1, 1),
    outlineWidth: 0,
    speed: 5,
})
// 记录index
let indexLog = 0
// 颜色
function getLineColor(lineNumber) {
    // 颜色顺序: 蓝(0), 黄(1), 绿(2)
    const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen]
    // 计算颜色索引 (从0开始)
    const colorIndex = (lineNumber - 1) % 3
    // 返回对应颜色
    return colors[colorIndex]
}
let runningLineMaterial = new LineTrailMaterial({
    color: Cesium.Color.fromCssColorString('#1FFF69'),
    opacity: 1,
    speed: 10,
})
let runningTextColor = Cesium.Color.fromCssColorString('#1FFF69')
let runningTextOffset = new Cesium.Cartesian2(0, -32)
let pendingLineMaterial = new PolylineGlowMaterial({
    color: Cesium.Color.fromCssColorString('#FFB81D'),
})
let pendingTextColor = Cesium.Color.fromCssColorString('#FFB81D')
let pendingTextOffset = new Cesium.Cartesian2(0, 32)
let MapPopUpBox = HistoryDronePopup
function getZoomFactor (camerasInfo, type) {
@@ -95,10 +54,108 @@
    return zoom_factor
}
// 记录index
let indexLog = 0
// 颜色
function getPendingExecutionLineColor (lineNumber) {
    // 颜色顺序: 蓝(灰), 更灰
    const colors = [arrowLineMaterialPropertyGray, arrowLineMaterialPropertyGrayT]
    // 计算颜色索引 (从0开始)
    const colorIndex = (lineNumber - 1) % 2
    // 返回对应颜色
    return colors[colorIndex]
}
function getLineColor (lineNumber) {
    // 颜色顺序: 蓝(0), 黄(1), 绿(2)
    const colors = [arrowLineMaterialProperty, arrowLineMaterialPropertyOrange, arrowLineMaterialPropertyGreen]
    // 计算颜色索引 (从0开始)
    const colorIndex = (lineNumber - 1) % 3
    // 返回对应颜色
    return colors[colorIndex]
}
function getBase64Image (imgUrl) {
    const img = new Image()
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    return new Promise(resolve => {
        img.onload = function () {
            canvas.width = img.width
            canvas.height = img.height
            ctx.drawImage(img, 0, 0)
            const base64 = canvas.toDataURL('image/png')
            resolve(base64)
        }
        img.src = imgUrl
    })
}
async function createCombinedSVG (index, billboardImageUrl) {
    const base64Image = await getBase64Image(billboardImageUrl)
    const svg = `
      <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
        <image href="${base64Image}" x="10" y="10" width="80" height="80"/>
        <text x="50" y="54" font-size="26" fill="white" text-anchor="middle" dominant-baseline="middle">
          ${index + 1}
        </text>
      </svg>
    `
    return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
}
function findHeightByCoordinates (data, targetLng, targetLat) {
    // 展平二维数组,然后查找第一个匹配的点
    const allPoints = data.flat()
    const point = allPoints.find(point =>
        point.longitude === targetLng && point.latitude === targetLat
    )
    return point ? point.height : null
}
function renderingMachineNest (options) {
    const { viewer, dockTransformPosition, dockPosition, data } = options
    viewer.entities.add({
        name: 'work-drone-route-point-dock',
        position: dockTransformPosition,
        billboard: {
            image: endingOnlineImg,
            width: 50,
            height: 50,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
        },
        customData: {
            data,
        },
    })
    viewer.entities.add({
        name: 'work-drone-route--point-polyline',
        position: dockTransformPosition,
        polyline: getPolyLine(viewer, { value: [dockPosition] }, 0),
    })
}
function renderingStartingPoint (options) {
    const { viewer, startPosition, data } = options
    viewer.entities.add({
        name: 'work-drone-route-point-start',
        position: startPosition,
        billboard: {
            image: rwqfdImg,
            width: 50,
            height: 50,
            disableDepthTestDistance: Number.POSITIVE_INFINITY,
        },
        customData: {
            data,
        },
    })
}
export default class CreateRouteLine {
    /**
     *
     *
     * @param {*} options 参数
     */
    constructor(options) {
@@ -119,10 +176,9 @@
    /**
     * 初始化viewer
     * @param {*} viewer
     * @param {*} viewer
     */
    initCreateRoute (viewer) {
        indexLog = 0
        this.viewer = viewer
    }
@@ -138,24 +194,27 @@
            const currentWaypointIndex = output?.ext['current_waypoint_index']
            this.updataMultiNestData(sn, {
                currentWaypointIndex
                currentWaypointIndex,
            })
        }
    }
    /**
     * 请求航线文件数据并解析处理
     * 请求航线文件数据并绘制解析处理
     * @param {*} data 包含文件url等信息
     * @param {*} dronePosition 无人机位置
     * @returns
     * @param {*} dockPosition 无人机位置
     * @returns
     */
    async parsingFiles (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) {
    async parsingFiles (data, dockPosition = null, isShowDock = false, isShowPointBillboard = true) {
        console.log(data,'888')
        const { url, wayline_type, device_sn } = data
        if (!url) return dronePosition != null ? {
            dronePosition: [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }]
        } : {}
        indexLog = 0
        if (!url)
            return dockPosition != null
                ? {
                    dronePosition: [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }],
                }
                : {}
        this.updataMultiNestData(device_sn)
        const res = await analyzeKmzFile(`${url}?_t=${new Date().getTime()}`)
@@ -178,16 +237,16 @@
        const missionConfig = removeTextKey(waylinesXmlJson.missionConfig)
        const waylinesXMLObj = removeTextKey(waylinesXmlJson.Folder)
        if (templateType === "mapping3d") {
        if (templateType === 'mapping3d') {
            if (!waylinesXMLObj[0].Placemark.length) return ElMessage.error('没有航线点位')
        } else {
            if (!waylinesXMLObj.Placemark.length) return ElMessage.error('没有航线点位')
        }
        if ([2, 4, 5, 6, 7, 10].includes(Number(wayline_type))) {
            if (templateType === "mapping3d") {
        if ([2, 4, 5, 7, 10].includes(Number(wayline_type))) {
            if (templateType === 'mapping3d') {
                let morePolygonData = waylinesXMLObj.map(i => ({
                    data: i.Placemark
                    data: i.Placemark,
                }))
                let newPolygonData = getNewPolygonData(morePolygonData)
@@ -214,7 +273,7 @@
                    waylinesXMLObj[0],
                    missionConfig,
                    templateXmlJson,
                    dronePosition,
                    dockPosition,
                    data,
                    startPoint,
                    isShowDock,
@@ -227,7 +286,7 @@
                waylinesXMLObj,
                missionConfig,
                templateXmlJson,
                dronePosition,
                dockPosition,
                data,
                startPoint,
                isShowDock,
@@ -238,7 +297,7 @@
            return this.drawPointRoute(
                waylinesXMLObj,
                missionConfig,
                dronePosition,
                dockPosition,
                data,
                startPoint,
                isShowDock,
@@ -248,50 +307,231 @@
        }
    }
    // 拆分航线
    async splitWayLine (data, dronePosition = null, isShowDock = false, isShowPointBillboard = true) {
        const {
            pointPlacemark,
            polygonList,
            pointList,
            templateType,
            startPoint,
            execute_height_mode,
            auto_flight_speed,
            take_off_security_height,
            buffer_distance_meters,
            missionConfig,
            waylinesXMLObjR,
        } = await analysisPointLineKmz(`${data.domain_url}${data.object_key}`)
        const { positionArray, filePositions } = this.disposeData(
            waylinesXMLObjR,
            missionConfig,
            null,
            data,
            startPoint,
            0
        )
        let bigPointList = handlePointListForKmz({
            placemark: pointPlacemark,
            startPoint: startPoint,
            execute_height_mode: execute_height_mode,
            auto_flight_speed: auto_flight_speed,
        })
        // 路径线
        let entityConfig
        bigPointList.shift()
        let flyPositions = bigPointList.map(i => [Number(i.longitude), Number(i.latitude), Number(i.height)])
        // 渲染多个点
        let firstTopPosition = null
        let resultPosotions = []
        await Promise.all(data.wayline_file_list.map(async (item, index) => {
            // 小航线渲染线
            const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key
            const { pointList } = await analysisPointLineKmz(kmzUrl)
            resultPosotions.push(pointList)
        })).then(res => {
            // 目的是区分大航线点属于哪条小航线
            bigPointList.forEach(bigObj => {
                let foundIndex = -1
                // 遍历 resultPositions 查找匹配项
                resultPosotions.some((subArray, subIndex) => {
                    const found = subArray.some(obj =>
                        obj.latitude === bigObj.latitude &&
                        obj.longitude === bigObj.longitude
                    )
                    if (found) {
                        foundIndex = subIndex
                        return true // 找到就停止搜索
                    }
                    return false
                })
                // 如果找到了,给 bigPointList 的对象添加标记
                if (foundIndex !== -1) {
                    bigObj.log = foundIndex // 或 bigObj.index = foundIndex
                }
            })
            console.log(bigPointList, '查看结果值') // 现在 bigPointList 会有 log 属性
            bigPointList.map(async (i, index) => {
                firstTopPosition = i
                const position = Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(findHeightByCoordinates(resultPosotions, i.longitude, i.latitude)))
                const combinedImage = await createCombinedSVG(index, i.log % 2 ? newStartPoint : newStartPointMore)
                entityConfig = {
                    name: 'work-drone-route-point-split',
                    position: position,
                    billboard: {
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                        image: combinedImage,
                        width: 50,
                        height: 50,
                        verticalOrigin: Cesium.VerticalOrigin.CENTER,
                        horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
                    }
                }
                // 只在第一个点添加 label
                if (index === 0) {
                    entityConfig.label = {
                        text: data.task_name || '',
                        font: 'bold 16px sans-serif',
                        fillColor: data.status === 2 ? runningTextColor : pendingTextColor,
                        pixelOffset: data.status === 2 ? runningTextOffset : pendingTextOffset,
                        style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                        outlineWidth: 2,
                        outlineColor: Cesium.Color.BLACK,
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    }
                }
                this.viewer.entities.add(entityConfig)
            })
        })
        // 渲染航线
        data.wayline_file_list.map(async (item, index) => {
            // 小航线渲染线
            const kmzUrl = import.meta.env.VITE_APP_AIRLINE_URL + item.object_key
            const { pointList } = await analysisPointLineKmz(kmzUrl)
            // if (curRouteLineData.value.droneData && splitLines.length === 1) {
            //     // 把pointList的第一个点替换为无人机位置
            //     pointList[0] = curRouteLineData.value.droneData[0]
            // }
            if (take_off_security_height + pointList[0].height > firstTopPosition.height) {
                // 安全高度+机巢高度 > 第一个点高度
                const positionTest = {
                    longitude: pointList[0].longitude,
                    latitude: pointList[0].latitude,
                    height: take_off_security_height + pointList[0].height,
                }
                const positionTest1 = {
                    longitude: pointList[1].longitude,
                    latitude: pointList[1].latitude,
                    height: take_off_security_height + pointList[0].height,
                }
                pointList.splice(1, 0, positionTest)
                // 往数组中插入元素
                pointList.splice(2, 0, positionTest1)
                // console.log(pointList, 'pointList')
            } else if (take_off_security_height + pointList[0].height < firstTopPosition.height) {
                const positionTest = {
                    longitude: pointList[0].longitude,
                    latitude: pointList[0].latitude,
                    height: firstTopPosition.height,
                }
                pointList.splice(1, 0, positionTest)
            }
            const routePositions = pointList.map(i =>
                Cesium.Cartesian3.fromDegrees(Number(i.longitude), Number(i.latitude), Number(i.height))
            )
            this.viewer.entities.add({
                name: 'work-drone-route-line-split',
                polyline: {
                    width: 4,
                    positions: routePositions,
                    material: data.status === 1 ? arrowLineMaterialPropertyGray : getLineColor(index + 1),
                    clampToGround: false,
                },
            })
        })
            // 落点线
            ; (bigPointList || [])?.forEach((item, index) => {
                const topPosition = Cesium.Cartesian3.fromDegrees(
                    Number(item.longitude),
                    Number(item.latitude),
                    Number(item.height)
                )
                let setting = {
                    name: 'work-drone-route--planar-polyline-split',
                    position: topPosition,
                    polyline: getPolyLine(this.viewer, { value: bigPointList }, index),
                }
                this.viewer.entities.add(setting)
            })
        return {
            entity: entityConfig,
            routeLinePositions: positionArray,
            filePositions:
                dronePosition != null
                    ? [{ lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height }, ...filePositions]
                    : filePositions,
        }
    }
    /**
     * 绘制面状航线
     * @param {*} lineObj 航线文件中的
     * @param {*} missionConfig 航线文件中的
     * @param {*} templateXmlJson 航线文件中的
     * @param {*} dronePosition 无人机位置
     * @param {*} data
     * @param {*} startPoint
     * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注
     * @returns
     * @param {*} dockPosition 无人机位置
     * @param {*} data
     * @param {*} startPoint
     * @returns
     */
    async drawPlanarRoute (lineObj, missionConfig, templateXmlJson, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) {
        const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type)
    async drawPlanarRoute (
        lineObj,
        missionConfig,
        templateXmlJson,
        dockPosition,
        data,
        startPoint,
        isShowDock,
        isShowPointBillboard,
        wayline_type
    ) {
        const { positionArray, filePositions } = this.disposeData(
            lineObj,
            missionConfig,
            dockPosition,
            data,
            startPoint,
            wayline_type
        )
        const { device_sn } = data
        const droneTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dronePosition.longitude),
            Number(dronePosition.latitude),
            Number(dronePosition.height),
        const dockTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dockPosition.longitude),
            Number(dockPosition.latitude),
            Number(dockPosition.height)
        )
        let coordArr = null
        const placemark = templateXmlJson.Folder?.Placemark
        // 取出点位
        const coordinates =
            placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.[
                '#text'
            ]?.split('\n') || []
        const coordinates = placemark.Polygon?.outerBoundaryIs.LinearRing.coordinates?.['#text']?.split('\n') || []
        // 数组转换
        coordArr = coordinates.map((coordinate) =>
        coordArr = coordinates.map(coordinate =>
            coordinate
                .replace(/\s+/g, '')
                .split(',')
                .map((v) => Number(v)),
                .map(v => Number(v))
        )
        // 获取当前经纬度绝对高度
        let newCoordArr = coordArr.map(item => ([item[0], item[1], 0]))
        let newCoordArr = coordArr.map(item => [item[0], item[1], 0])
        let planarRouteEntity = this.viewer.entities.add({
            name: 'work-drone-route-planar-polyline',
            polyline: {
                width: 3,
                positions: positionArray,
@@ -300,30 +540,30 @@
            },
            customData: {
                data
            }
                data,
            },
        })
        let droppointPositions = (filePositions || [])?.map(i => ({
            longitude: Number(i.lng),
            latitude: Number(i.lat),
            height: Number(i.alt),
        }));
        }))
        // 落点线
        (droppointPositions || [])?.forEach((item, index) => {
            const topPosition = Cesium.Cartesian3.fromDegrees(
                Number(item.longitude),
                Number(item.latitude),
                Number(item.height)
            )
            let setting = {
                name: 'work-drone-route-planar-polyline',
                position: topPosition,
                polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
            }
            this.viewer.entities.add(setting)
        })
            // 落点线
            ; (droppointPositions || [])?.forEach((item, index) => {
                const topPosition = Cesium.Cartesian3.fromDegrees(
                    Number(item.longitude),
                    Number(item.latitude),
                    Number(item.height)
                )
                let setting = {
                    name: 'work-drone-route--planar-polyline',
                    position: topPosition,
                    polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
                }
                this.viewer.entities.add(setting)
            })
        // 面状航线第一个点突出展示
        isShowPointBillboard && this.startingIncreasePoint(positionArray)
@@ -340,9 +580,7 @@
            name: 'work-drone-route-planar-border',
            polyline: {
                positions: Cesium.Cartesian3.fromDegreesArrayHeights(
                    cloneCoordArr.flat(),
                ),
                positions: Cesium.Cartesian3.fromDegreesArrayHeights(cloneCoordArr.flat()),
                width: 4,
                material: new Cesium.PolylineOutlineMaterialProperty({
                    color: new Cesium.Color.fromBytes(74, 138, 233),
@@ -357,11 +595,8 @@
        // 绘制面状地块--------------------
        this.viewer.entities.add({
            name: 'work-drone-route-planar-polygon',
            polygon: {
                hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(
                    newCoordArr.flat(),
                ),
                hierarchy: Cesium.Cartesian3.fromDegreesArrayHeights(newCoordArr.flat()),
                material: new Cesium.Color.fromBytes(75, 159, 221, 100),
                outline: true,
                outlineColor: new Cesium.Color.fromBytes(35, 85, 216, 255),
@@ -384,7 +619,7 @@
                label: {
                    text: data.job_name || '',
                    font: 'bold 16px YouSheBiaoTiHei',
                    font: 'bold 16px sans-serif',
                    fillColor: Cesium.Color.WHITE,
                    pixelOffset: new Cesium.Cartesian2(0, -50),
                    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
@@ -395,64 +630,31 @@
            })
        } else {
            // 显示机巢位置
            if (isShowDock) {
                // 起点
                this.viewer.entities.add({
                    name: 'work-drone-route-point-dock',
                    position: droneTransformPosition,
                    billboard: {
                        image: endingOnlineImg,
                        width: 50,
                        height: 50,
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    },
                    customData: {
                        data
                    }
                })
                this.viewer.entities.add({
                    name: 'work-drone-route--point-polyline',
                    position: droneTransformPosition,
                    polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0),
                })
            }
            const startPosition = Cesium.Cartesian3.fromDegrees(
                Number(filePositions[0].lng),
                Number(filePositions[0].lat),
                Number(filePositions[0].alt)
            )
            isShowDock && renderingMachineNest({
                viewer: this.viewer,
                dockTransformPosition,
                dockPosition,
                data
            })
            // 起点
            isShowPointBillboard && this.viewer.entities.add({
                name: 'work-drone-route-point-start',
                position: startPosition,
                billboard: {
                    image: rwqfdImg,
                    width: 50,
                    height: 50,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    data
                }
            isShowPointBillboard && renderingStartingPoint({
                viewer: this.viewer,
                startPosition: Cesium.Cartesian3.fromDegrees(
                    Number(filePositions[0].lng),
                    Number(filePositions[0].lat),
                    Number(filePositions[0].alt)
                ),
                data
            })
        }
        return {
            entity: planarRouteEntity,
            routeLinePositions: positionArray,
            filePositions: dronePosition != null ? [
                { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height },
                ...filePositions
            ] : filePositions
            filePositions:
                dockPosition != null
                    ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions]
                    : filePositions,
        }
    }
@@ -472,9 +674,7 @@
        let setting = {
            name: 'work-drone-route-planar-start',
            position: positions,
            billboard: {
                image: this.planarBillboard('#2D8CF0'),
                pixelOffset: new Cesium.Cartesian2(146, 72),
@@ -489,35 +689,48 @@
     * 绘制点航线
     * @param {*} lineObj 航线文件中的
     * @param {*} missionConfig 航线文件中的
     * @param {*} dronePosition 无人机位置
     * @param {*} dockPosition 无人机位置
     * @param {*} data 任务信息,航线地址等
     * @param {*} startPoint
     * @param {*} startPoint
     * @param {*} isShowDock 是否显示机巢位置
     * @param {*} isShowPointBillboard 是否显示,航线起飞点,终点等标注
     * @returns
     * @returns
     */
    async drawPointRoute (lineObj, missionConfig, dronePosition, data, startPoint, isShowDock, isShowPointBillboard, wayline_type) {
        const { positionArray, filePositions } = this.disposeData(lineObj, missionConfig, dronePosition, data, startPoint, wayline_type)
        const droneTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dronePosition.longitude),
            Number(dronePosition.latitude),
            Number(dronePosition.height),
    async drawPointRoute (
        lineObj,
        missionConfig,
        dockPosition,
        data,
        startPoint,
        isShowDock,
        isShowPointBillboard,
        wayline_type
    ) {
        const { positionArray, filePositions } = this.disposeData(
            lineObj,
            missionConfig,
            dockPosition,
            data,
            startPoint,
            wayline_type
        )
        const dockTransformPosition = Cesium.Cartesian3.fromDegrees(
            Number(dockPosition.longitude),
            Number(dockPosition.latitude),
            Number(dockPosition.height)
        )
        // 路径线
        let polyline
        indexLog = indexLog + 1
        if (this.type === 'clusterScheduling') {
            filePositions.forEach((item, index) => {
                let position = Cesium.Cartesian3.fromDegrees(item.lng, item.lat, item.alt)
                if (index === 0) {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-start',
                        position,
                        billboard: {
                            image: rwqfdImg,
                            outlineWidth: 0,
@@ -525,10 +738,9 @@
                            height: 50,
                            disableDepthTestDistance: Number.POSITIVE_INFINITY,
                        },
                        label: {
                            text: data.job_name || '',
                            font: 'bold 16px YouSheBiaoTiHei',
                            font: 'bold 16px sans-serif',
                            fillColor: data.type === 2 ? runningTextColor : pendingTextColor,
                            pixelOffset: data.type === 2 ? runningTextOffset : pendingTextOffset,
                            style: Cesium.LabelStyle.FILL_AND_OUTLINE,
@@ -540,9 +752,7 @@
                } else if (index === filePositions.length - 1) {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-end',
                        position,
                        billboard: {
                            image: newEndPointImg,
                            outlineWidth: 0,
@@ -556,9 +766,7 @@
                } else {
                    this.viewer.entities.add({
                        name: 'work-drone-route-point-approach',
                        position,
                        billboard: {
                            image: newNumPoint,
                            width: 50,
@@ -577,96 +785,56 @@
                    })
                }
            })
            polyline = this.viewer.entities.add({
                name: 'work-drone-route-point-polyline',
                polyline: {
                    width: 4,
                    positions: positionArray,
                    material: data.type === 2 ? runningLineMaterial : pendingLineMaterial,
                    material: data.type === 2 ? getLineColor(indexLog) : arrowLineMaterialPropertyGray,
                    clampToGround: false,
                }
                },
            })
        } else {
            // 显示机巢位置
            if (isShowDock) {
                // 起点
                this.viewer.entities.add({
                    name: 'work-drone-route-point-dock',
                    position: droneTransformPosition,
                    billboard: {
                        image: endingOnlineImg,
                        width: 50,
                        height: 50,
                        disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    },
                    customData: {
                        data
                    }
                })
                this.viewer.entities.add({
                    name: 'work-drone-route--point-polyline',
                    position: droneTransformPosition,
                    polyline: getPolyLine(this.viewer, { value: [dronePosition] }, 0),
                })
            }
            const startPosition = Cesium.Cartesian3.fromDegrees(
                Number(filePositions[0].lng),
                Number(filePositions[0].lat),
                Number(filePositions[0].alt)
            )
            isShowDock && renderingMachineNest({
                viewer: this.viewer,
                dockTransformPosition,
                dockPosition
            })
            // 起点
            isShowPointBillboard && this.viewer.entities.add({
                name: 'work-drone-route-point-start',
                position: startPosition,
                billboard: {
                    image: rwqfdImg,
                    width: 50,
                    height: 50,
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                },
                customData: {
                    data
                }
            isShowPointBillboard && renderingStartingPoint({
                viewer: this.viewer,
                startPosition: Cesium.Cartesian3.fromDegrees(
                    Number(filePositions[0].lng),
                    Number(filePositions[0].lat),
                    Number(filePositions[0].alt)
                ),
                data
            })
            // 终点
            isShowPointBillboard && this.viewer.entities.add({
                name: 'work-drone-route-point-end',
                position: positionArray[positionArray.length - 1],
                billboard: {
                    image: new Cesium.ConstantProperty(endPointImg),
                    width: 30,
                    height: 30,
                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐
                },
            })
            isShowPointBillboard &&
                this.viewer.entities.add({
                    name: 'work-drone-route-point-end',
                    position: positionArray[positionArray.length - 1],
                    billboard: {
                        image: new Cesium.ConstantProperty(endPointImg),
                        width: 30,
                        height: 30,
                        verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 底部对齐
                    },
                })
            console.log(data.status, '失败')
            polyline = this.viewer.entities.add({
                name: 'work-drone-route-point-polyline',
                polyline: {
                    width: 4,
                    positions: positionArray,
                    material: getLineColor(indexLog),
                    material: data.status === 5 ? arrowLineMaterialPropertyGray : getLineColor(indexLog),
                    clampToGround: false,
                },
                customData: {
                    data
                }
                    data,
                },
            })
        }
@@ -674,47 +842,45 @@
            longitude: Number(i.lng),
            latitude: Number(i.lat),
            height: Number(i.alt),
        }));
        }))
        // 落点线
        (droppointPositions || [])?.forEach((item, index) => {
            const topPosition = Cesium.Cartesian3.fromDegrees(
                Number(item.longitude),
                Number(item.latitude),
                Number(item.height)
            )
            // 落点线
            ; (droppointPositions || [])?.forEach((item, index) => {
                const topPosition = Cesium.Cartesian3.fromDegrees(
                    Number(item.longitude),
                    Number(item.latitude),
                    Number(item.height)
                )
            let setting = {
                name: 'work-drone-route-point-polyline',
                position: topPosition,
                polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
            }
            this.viewer.entities.add(setting)
        })
                let setting = {
                    name: 'work-drone-route--point-polyline',
                    position: topPosition,
                    polyline: getPolyLine(this.viewer, { value: droppointPositions }, index),
                }
                this.viewer.entities.add(setting)
            })
        return {
            entity: polyline,
            routeLinePositions: positionArray,
            filePositions: dronePosition != null ? [
                { lng: dronePosition.longitude, lat: dronePosition.latitude, alt: dronePosition.height },
                ...filePositions
            ] : filePositions,
            filePositions:
                dockPosition !== null
                    ? [{ lng: dockPosition.longitude, lat: dockPosition.latitude, alt: dockPosition.height }, ...filePositions]
                    : filePositions,
        }
    }
    /**
     * 通用处理数据
     * @param {*} lineObj
     * @param {*} missionConfig
     * @param {*} dronePosition
     * @param {*} startPoint
     * @param {*} lineObj
     * @param {*} missionConfig
     * @param {*} dronePosition
     * @param {*} startPoint
     * @returns {} {positionArray: 带拼接点位置, filePositions:光航线点位置}
     */
    disposeData (lineObj, missionConfig, dronePosition, data, startPoint, wayline_type) {
        indexLog = indexLog + 1
        const { device_sn } = data
        const executeHeightMode = lineObj.executeHeightMode === "WGS84"
        const executeHeightMode = lineObj.executeHeightMode === 'WGS84'
        let filePositions = lineObj.Placemark.map(item => {
            const [lng, lat] = item.Point.coordinates.split(',')
@@ -730,12 +896,14 @@
        let safetyHeight
        if (executeHeightMode) {
            let startHeight = new Decimal(missionConfig.takeOffSecurityHeight)
                .plus(dronePosition.height).toNumber()
            let startHeight = new Decimal(missionConfig.takeOffSecurityHeight).plus(dronePosition?.height || 0).toNumber()
            safetyHeight = startHeight < filePositions[0].alt ? filePositions[0].alt : startHeight
        } else {
            safetyHeight = missionConfig.takeOffSecurityHeight < filePositions[0].alt ? filePositions[0].alt : missionConfig.takeOffSecurityHeight
            safetyHeight =
                missionConfig.takeOffSecurityHeight < filePositions[0].alt
                    ? filePositions[0].alt
                    : missionConfig.takeOffSecurityHeight
        }
        let safetyPositions = []
@@ -773,28 +941,38 @@
        })
        this.updataMultiNestData(device_sn, {
            currentRoutePositions
            currentRoutePositions,
        })
        if ([0].includes(wayline_type)) {
            let startHeight = executeHeightMode ? filePositions[0].alt : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode ? filePositions[1].alt : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            let startHeight = executeHeightMode
                ? filePositions[0].alt
                : filePositions[0].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode
                ? filePositions[1].alt
                : filePositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            this.updataMultiNestData(device_sn, {
                showPopupPosition: Cesium.Cartesian3.midpoint(
                    Cesium.Cartesian3.fromDegrees(filePositions[0].lng, filePositions[0].lat, startHeight),
                    Cesium.Cartesian3.fromDegrees(filePositions[1].lng, filePositions[1].lat, endHeight),
                    new Cesium.Cartesian3())
                    new Cesium.Cartesian3()
                ),
            })
        } else {
            let startHeight = executeHeightMode ? safetyPositions[1].alt : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode ? safetyPositions[2].alt : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0)
            let startHeight = executeHeightMode
                ? safetyPositions[1].alt
                : safetyPositions[1].alt + (dronePosition != null ? dronePosition.height : 0)
            let endHeight = executeHeightMode
                ? safetyPositions[2].alt
                : safetyPositions[2].alt + (dronePosition != null ? dronePosition.height : 0)
            this.updataMultiNestData(device_sn, {
                showPopupPosition: Cesium.Cartesian3.midpoint(
                    Cesium.Cartesian3.fromDegrees(safetyPositions[1].lng, safetyPositions[1].lat, startHeight),
                    Cesium.Cartesian3.fromDegrees(safetyPositions[2].lng, safetyPositions[2].lat, endHeight),
                    new Cesium.Cartesian3())
                    new Cesium.Cartesian3()
                ),
            })
        }
@@ -805,9 +983,9 @@
                return {
                    ...item,
                    alt: Number(height)
                    alt: Number(height),
                }
            })
            }),
        }
    }
@@ -819,9 +997,10 @@
            return i?.name && i?.name.includes('work-drone-route-')
        })
        Array.isArray(entities) && entities.forEach(item => {
            this.viewer?.entities.remove(item)
        })
        Array.isArray(entities) &&
            entities.forEach(item => {
                this.viewer?.entities.remove(item)
            })
    }
    // 当前无人机视椎体相关
@@ -847,11 +1026,22 @@
                ['wide', 'ir', 'zoom'].includes(curDroneData?.camera_type)
            ) {
                if (curDroneData.camera_type === 'wide') {
                    fov = 60
                    fov = 65
                } else {
                    const diffFocal = zoomFactor.value[curDroneData.camera_type].max - getZoomFactor(host?.cameras?.[0], curDroneData.camera_type)
                    fov = (60 / zoomFactor.value[curDroneData.camera_type].max) * (diffFocal === 0 ? 1 : diffFocal)
                    const cameraMultiplier = getZoomFactor(host?.cameras?.[0], curDroneData.camera_type)
                    switch (cameraMultiplier) {
                        case 2: fov = 30; break
                        case 3: fov = 19; break
                        case 4: fov = 15; break
                        case 5: fov = 13; break
                        case 6: fov = 11; break
                        case 7: fov = 9; break
                        case 8: fov = 8.34; break
                        case 9: fov = 7.68; break
                        case 10: fov = 6.9; break
                        case 14: fov = 5; break
                        default: fov = 55.946 * Math.exp(-0.4504 * cameraMultiplier) + 6.323
                    }
                }
            }
@@ -871,8 +1061,8 @@
                    pitch: 0,
                    heading: attitude_head,
                    isShowVideoPlan: this.isShowVideoPlan
                })
                    isShowVideoPlan: this.isShowVideoPlan,
                }),
            })
        }
    }
@@ -882,7 +1072,7 @@
        if (device_sn && live_status) {
            this.updataMultiNestData(device_sn, {
                camera_type: live_status?.[0].video_type || 'wide'
                camera_type: live_status?.[0].video_type || 'wide',
            })
        }
    }
@@ -897,14 +1087,9 @@
        }
    }
    // 设置当前无人机视椎体信息面板
    setFrustumInfoDetails (data) {
        const {
            dock_sn,
            distance_to_airport,
            distance_to_next_point,
            estimated_time_to_next_point,
            plane_mileage
        } = data
        const { dock_sn, distance_to_airport, distance_to_next_point, estimated_time_to_next_point, plane_mileage } = data
        if (dock_sn) {
            const aircraftEntity = this.viewer?.entities.values.find(i => {
@@ -924,7 +1109,9 @@
                }
                aircraftEntity.label = new Cesium.LabelGraphics({
                    text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round(distance_to_next_point)}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`,
                    text: `离机场平面距离:${distance_to_airport}m\n离下个航点平面距离:${Math.round(
                        distance_to_next_point
                    )}m\n到达下个航点剩余时间:${arrivalTime}\n航线总平面里程:${Math.round(plane_mileage)}m`,
                    font: '13px monospace',
                    showBackground: true,
                    horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
@@ -936,7 +1123,6 @@
                return
            }
        }
    }
@@ -954,8 +1140,10 @@
        }
    }
    // 设置无人机模型
    setAircraftGltf (host) {
        const parent_sn = host?.parent_sn
        const attitude_head = Number(host?.payloads?.[0]?.gimbal_yaw) - 90 || 0
        if (parent_sn) {
            const aircraftEntity = this.viewer?.entities.values.find(i => {
@@ -963,9 +1151,18 @@
            })
            const position = Cesium.Cartesian3.fromDegrees(host?.longitude, host?.latitude, host?.height)
            // ✅ 新增:Heading / Pitch / Roll(单位:度 -> 弧度)
            const heading = Cesium.Math.toRadians(attitude_head) // 航向角
            const pitch = Cesium.Math.toRadians(0)     // 俯仰角
            const roll = Cesium.Math.toRadians(0)       // 横滚角
            // ✅ 新增:构造 HeadingPitchRoll 和 四元数
            const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll)
            const orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr)
            if (aircraftEntity) {
                aircraftEntity.position = new Cesium.ConstantPositionProperty(position)
                aircraftEntity.orientation = new Cesium.ConstantProperty(orientation)
                return
            }
@@ -974,18 +1171,35 @@
                this.viewer?.entities.add({
                    name: `aircraft-glf-${parent_sn}`,
                    position,
                    orientation,
                    label: {},
                    model: {
                        uri: aircraftGltf, // 或 .glb
                        scale: 1.0, // 缩放比例
                        scale: 64, // 缩放比例
                        minimumPixelSize: 64, // 最小像素尺寸(保证模型远处可见)
                        maximumScale: 128, // 最大缩放(可选)
                        maximumScale: 64, // 最大缩放(可选)
                    },
                })
            }
        }
    }
    // 移除无人机模型
    removeAircraftGltf (host) {
        const parent_sn = host?.parent_sn
        if (parent_sn) {
            const aircraftEntity = this.viewer?.entities.values.find(i => {
                return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`)
            })
            if (aircraftEntity) {
                this.viewer?.entities.remove(aircraftEntity)
            }
        }
    }
    // 移除所有无人机视椎体
    removeViewInfoFrustum () {
        this.multiNestData.forEach(item => {
            if ('viewInfoFrustum' in item && item.viewInfoFrustum) {
@@ -1010,9 +1224,10 @@
                return i?.name && i?.name.includes(`aircraft-glf-${parent_sn}`)
            })
            Array.isArray(entities) && entities.forEach(item => {
                this.viewer?.entities.remove(item)
            })
            Array.isArray(entities) &&
                entities.forEach(item => {
                    this.viewer?.entities.remove(item)
                })
        }
    }
@@ -1021,13 +1236,15 @@
            return i?.name && i?.name.includes(`aircraft-glf-`)
        })
        Array.isArray(entities) && entities.forEach(item => {
            this.viewer?.entities.remove(item)
        })
        Array.isArray(entities) &&
            entities.forEach(item => {
                this.viewer?.entities.remove(item)
            })
        this.multiNestData.length > 0 && (this.multiNestData.forEach(item => {
            item.viewInfoFrustum?.clear()
        }))
        this.multiNestData.length > 0 &&
            this.multiNestData.forEach(item => {
                item.viewInfoFrustum?.clear()
            })
    }
    /**
@@ -1043,19 +1260,19 @@
            this.multiNestData[isHaveInd] = {
                ...this.multiNestData[isHaveInd],
                ...data
                ...data,
            }
        } else {
            this.multiNestData.push({
                device_sn,
                ...data
                ...data,
            })
        }
    }
    /**
     * 根据机巢sn获取当前机巢对应的相关信息
     * @param {*} device_sn
     * @param {*} device_sn
     */
    getCurDroneData (device_sn) {
        const isHave = this.multiNestData.find(item => item.device_sn === device_sn)
@@ -1071,14 +1288,16 @@
            return {
                ...item,
                show: true,
                uuid: uuidv4()
                uuid: uuidv4(),
            }
        })
        this.viewer.scene.postRender.addEventListener(that.labelBoxRenderHandler)
    }
    showSingleDetailMapPopup (data) {
        this.detailsMapPopupData.forEach(i => i.job_id === data.job_id && i.device_sn === data.device_sn && (i.show = true))
        this.detailsMapPopupData.forEach(
            i => i?.job_id === data?.job_id && i?.device_sn === data?.device_sn && (i.show = true)
        )
    }
    getLabelDom (data) {
@@ -1090,28 +1309,30 @@
        tooltipContainer.style.position = 'absolute'
        tooltipContainer.style.transform = 'translate(-50%,10%)'
        tooltipContainer.style.pointerEvents = 'none'
        document.querySelector('.content-map-popups')?.append(tooltipContainer)
        document.querySelector('.content-map-popups').append(tooltipContainer)
        render(vNode, tooltipContainer)
        return tooltipContainer
    }
    labelBoxRender () {
        this.detailsMapPopupData.filter(i => i.show === true).forEach(item => {
            const { showPopupPosition } = this.getCurDroneData(item.device_sn)
        this.detailsMapPopupData
            .filter(i => i.show === true)
            .forEach(item => {
                const { showPopupPosition } = this.getCurDroneData(item.device_sn)
            let dom = document.querySelector(`[id="${item.uuid}"]`)
                let dom = document.querySelector(`[id="${item.uuid}"]`)
            if (!dom) {
                dom = this.getLabelDom(item)
            }
                if (!dom) {
                    dom = this.getLabelDom(item)
                }
            const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition)
            if (screenPosition) {
                dom.style.left = `${screenPosition.x}px`
                dom.style.top = `${screenPosition.y}px`
                dom.style.display = 'block'
            }
        })
                const screenPosition = this.viewer?.scene.cartesianToCanvasCoordinates(showPopupPosition)
                if (screenPosition) {
                    dom.style.left = `${screenPosition.x}px`
                    dom.style.top = `${screenPosition.y}px`
                    dom.style.display = 'block'
                }
            })
    }
    // 移除弹框标签
@@ -1137,4 +1358,4 @@
        const that = this
        that.viewer?.scene.postRender.removeEventListener(that.labelBoxRenderHandler)
    }
}
}
applications/drone-command/src/utils/cesium/eventBus.js
File was deleted
applications/drone-command/src/utils/cesium/frustum/CesiumVideoFrustum.js
New file
@@ -0,0 +1,712 @@
/*
 * @Author       : yuan
 * @Date         : 2025-08-16 17:01:46
 * @LastEditors  : yuan
 * @LastEditTime : 2025-09-26 16:27:20
 * @FilePath     : \src\utils\cesium\frustum\CesiumVideoFrustum.js
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-08-16 17:01:46
 */
import * as Cesium from 'cesium'
var videoShed3dShader = `
uniform float ztzf_frustum_opacity;
uniform sampler2D ztzf_frustum_videoTexture;
uniform sampler2D ztzf_frustum_maskTexture;
uniform vec4 ztzf_frustum_hiddenAreaColor;
uniform sampler2D shadowMap_texture;
uniform mat4 shadowMap_matrix;
uniform vec4 shadowMap_lightPositionEC;
uniform vec4 shadowMap_texelSizeDepthBias;
uniform vec4 shadowMap_normalOffsetScale;
uniform bool ztzf_frustum_flipx;
uniform bool ztzf_frustum_flipy;
// 新增边框参数
uniform vec4 ztzf_frustum_borderColor;  // 边框颜色(RGBA)
uniform float ztzf_frustum_borderWidth; // 边框宽度(0.0-1.0)
uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
in vec2 v_textureCoordinates;
vec4 toEye(in vec2 uv, in float depth) {
  vec2 xy = vec2((uv.x * 2.0 - 1.0), (uv.y * 2.0 - 1.0));
  vec4 posInCamera = czm_inverseProjection * vec4(xy, depth, 1.0);
  posInCamera = posInCamera / posInCamera.w;
  return posInCamera;
}
float getDepthMars3D(in vec4 depth) {
  float z_window = czm_unpackDepth(depth);
  z_window = czm_reverseLogDepth(z_window);
  float n_range = czm_depthRange.near;
  float f_range = czm_depthRange.far;
  return (2.0 * z_window - n_range - f_range) / (f_range - n_range);
}
float _czm_sampleShadowMap(sampler2D shadowMap, vec2 uv) {
  return texture(shadowMap, uv).r;
}
float _czm_shadowDepthCompare(sampler2D shadowMap, vec2 uv, float depth) {
  return step(depth, _czm_sampleShadowMap(shadowMap, uv));
}
float _czm_shadowVisibility(sampler2D shadowMap, czm_shadowParameters shadowParameters) {
  float depthBias = shadowParameters.depthBias;
  float depth = shadowParameters.depth;
  float nDotL = shadowParameters.nDotL;
  float normalShadingSmooth = shadowParameters.normalShadingSmooth;
  float darkness = shadowParameters.darkness;
  vec2 uv = shadowParameters.texCoords;
  depth -= depthBias;
  vec2 texelStepSize = shadowParameters.texelStepSize;
  float radius = 1.0;
  float dx0 = -texelStepSize.x * radius;
  float dy0 = -texelStepSize.y * radius;
  float dx1 = texelStepSize.x * radius;
  float dy1 = texelStepSize.y * radius;
  float visibility = (_czm_shadowDepthCompare(shadowMap, uv, depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, 0.0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, 0.0), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx0, dy1), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(0.0, dy1), depth) +
    _czm_shadowDepthCompare(shadowMap, uv + vec2(dx1, dy1), depth)) * (1.0 / 9.0);
  return visibility;
}
vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point) {
  vec3 v01 = point - planeOrigin;
  float d = dot(planeNormal, v01);
  return (point - planeNormal * d);
}
float ptm(vec3 pt) {
  return sqrt(pt.x * pt.x + pt.y * pt.y + pt.z * pt.z);
}
void main() {
  const float PI = 3.141592653589793;
  vec4 color = texture(colorTexture, v_textureCoordinates);
  vec4 currD = texture(depthTexture, v_textureCoordinates);
  float depth = getDepthMars3D(currD);
  vec4 positionEC = toEye(v_textureCoordinates, depth);
  vec3 normalEC = vec3(1.0);
  czm_shadowParameters shadowParameters;
  shadowParameters.texelStepSize = shadowMap_texelSizeDepthBias.xy;
  shadowParameters.depthBias = shadowMap_texelSizeDepthBias.z;
  shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBias.w;
  shadowParameters.darkness = shadowMap_normalOffsetScale.w;
  shadowParameters.depthBias *= max(depth * 0.01, 1.0);
  vec3 directionEC = normalize(positionEC.xyz - shadowMap_lightPositionEC.xyz);
  float nDotL = clamp(dot(normalEC, -directionEC), 0.0, 1.0);
  vec4 shadowPosition = shadowMap_matrix * positionEC;
  shadowPosition /= shadowPosition.w;
  if(any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) {
    out_FragColor = color;
    return;
  }
  shadowParameters.texCoords = shadowPosition.xy;
  shadowParameters.depth = shadowPosition.z;
  shadowParameters.nDotL = nDotL;
  float visibility = _czm_shadowVisibility(shadowMap_texture, shadowParameters);
  //视频投射
  if(visibility == 1.0) {
    if(ztzf_frustum_flipx){
      shadowPosition.x = shadowPosition.x + (0.5 - shadowPosition.x) * 2.0;
    }
    if(ztzf_frustum_flipy){
      shadowPosition.y = shadowPosition.y + (0.5 - shadowPosition.y) * 2.0;
    }
    vec4 videoColor = texture(ztzf_frustum_videoTexture, shadowPosition.xy);
    vec4 maskColor = texture(ztzf_frustum_maskTexture, shadowPosition.xy);
    videoColor *= maskColor;
    // 计算到边缘的距离
    float distToEdge = min(
        min(shadowPosition.x, 1.0 - shadowPosition.x),
        min(shadowPosition.y, 1.0 - shadowPosition.y)
    );
    // 混合视频颜色和背景
    vec4 finalColor = mix(color, vec4(videoColor.xyz, 1.0), ztzf_frustum_opacity * videoColor.a);
    // 添加边框效果
    if(distToEdge < ztzf_frustum_borderWidth && videoColor.a > 0.0) {
        float borderFactor = smoothstep(0.0, ztzf_frustum_borderWidth, distToEdge);
        finalColor = mix(ztzf_frustum_borderColor, finalColor, borderFactor);
    }
    out_FragColor = finalColor;
  } else {
    if(abs(shadowPosition.z - 0.0) < 0.01) {
      return;
    }
    out_FragColor = vec4(mix(color.rgb, ztzf_frustum_hiddenAreaColor.rgb, ztzf_frustum_hiddenAreaColor.a), ztzf_frustum_hiddenAreaColor.a);
  }
}
    `
class CesiumVideoFrustum {
    constructor(viewer, param) {
        this.param = param
        var option = this._initCameraParam()
        if (option || (option = {}), this.viewer = viewer, this._cameraPosition = option.cameraPosition, this._position = option.position,
            this.type = option.type, this._alpha = option.alpha || 0, this.element = option.element, this.color = option.color,
            this._debugFrustum = Cesium.defaultValue(option.debugFrustum, !0), this._aspectRatio = option.aspectRatio || this._getWinWidHei(),
            this._camerafov = option.fov || Cesium.Math.toDegrees(this.viewer.scene.camera.frustum.fov),
            this._isShadowMap = option.isShadowMap || false,
            this.texture = option.texture || new Cesium.Texture({
                context: this.viewer.scene.context,
                source: {
                    width: 1,
                    height: 1,
                    arrayBufferView: new Uint8Array([255, 255, 255, 255])
                },
                flipY: !1
            }), this.defaultShow = Cesium.defaultValue(option.show, !0), !this.cameraPosition || !this.position) return void console.log('初始化失败:请确认相机位置与视点位置正确!')
        this.activeVideo()
        this._createShadowMap()
        this._getOrientation()
        this._addCameraFrustum()
        this._addPostProcess()
        this.viewer.scene.primitives.add(this)
    }
    get alpha () {
        return this._alpha
    }
    set alpha (e) {
        this._alpha = e
        this._changeViewPos()
    }
    get aspectRatio () {
        return this._aspectRatio
    }
    set aspectRatio (e) {
        this._aspectRatio = e
        this._changeVideoWidHei()
    }
    get debugFrustum () {
        return this._debugFrustum
    }
    set debugFrustum (e) {
        this._debugFrustum = e
        this.cameraFrustum.show = e
        this.cameraFrustumOutline.show = e
    }
    get fov () {
        return this._camerafov
    }
    set fov (e) {
        this._camerafov = e
        this._changeCameraFov()
    }
    get cameraPosition () {
        return this._cameraPosition
    }
    set cameraPosition (e) {
        e && (this._cameraPosition = e, this._changeCameraPos())
    }
    get isShadowMap () {
        return this._isShadowMap
    }
    set isShadowMap (e) {
        this._isShadowMap = e
        this._changeCameraIsShadowMap()
    }
    get position () {
        return this._position
    }
    set position (e) {
        e && (this._position = e, this._changeViewPos())
    }
    get params () {
        var t = {}
        return t.element = this.element,
            t.position = this.position,
            t.cameraPosition = this.cameraPosition,
            t.fov = this.fov,
            t.isShadowMap = this.isShadowMap,
            t.aspectRatio = this.aspectRatio,
            t.alpha = this.alpha,
            t.debugFrustum = this.debugFrustum,
            t.show = this.show
    }
    get show () {
        return this.defaultShow
    }
    set show (e) {
        this.defaultShow = Boolean(e),
            this._switchShow()
    }
    isDestroyed () {
        return false
    }
    _initCameraParam () {
        var cameraPosition = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1)
        const hpr = Cesium.HeadingPitchRoll.fromDegrees(this.param.rotation.y * 1, this.param.rotation.x * 1, 0)
        const orientation = Cesium.Transforms.headingPitchRollQuaternion(cameraPosition, hpr)
        let matrix = Cesium.Matrix3.fromQuaternion(
            orientation,
            new Cesium.Matrix3()
        )
        let direction = Cesium.Matrix3.getColumn(matrix, 2, new Cesium.Cartesian3())
        let frontDirect = Cesium.Cartesian3.multiplyByScalar(
            direction,
            250,
            new Cesium.Cartesian3()
        )
        let position = Cesium.Cartesian3.add(cameraPosition, frontDirect, new Cesium.Cartesian3())
        return {
            cameraPosition: cameraPosition,
            position: position,
            alpha: this.param.alpha,
            near: this.param.near,
            fov: this.param.fov,
            isShadowMap: this.param.isShadowMap,
            debugFrustum: this.param.debugFrustum,
            aspectRatio: this.param.aspectRatio,
            element: this.param.element,
        }
    }
    _changeAlpha (e) {
        this.alpha = e
    }
    _changeAspectRatio (e) {
        this.aspectRatio = e
    }
    _changeRotation (e) {
        if (e) {
            this.param.rotation = e
            var option = this._initCameraParam()
            this.position = option.position
        }
    }
    _changeCameraPosition (e) {
        if (e) {
            this.param.position = e
            var option = this._initCameraParam()
            this.cameraPosition = option.cameraPosition
        }
    }
    _changeFov (e) {
        if (e) {
            this.param.fov = e
            var option = this._initCameraParam()
            this.fov = option.fov
        }
    }
    _changeIsShadowMap (e) {
        this.param.isShadowMap = e
        var option = this._initCameraParam()
        this.isShadowMap = option.isShadowMap
    }
    _changeFar (e) {
        if (e) {
            this.param.far = e
            var option = this._initCameraParam()
            this.position = option.position
        }
    }
    _changeNear (e) {
        if (e) {
            this.param.near = e
            this.near = this.param.near
            this._changeCameraPos()
        }
    }
    _getWinWidHei () {
        var viewer = this.viewer.scene
        return viewer.canvas.clientWidth / viewer.canvas.clientHeight
    }
    _changeCameraFov () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeCameraIsShadowMap () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewShadowMap && this.viewShadowMap.destroy()
        this.viewShadowMap = null
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeVideoWidHei () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeCameraPos () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewShadowMap && this.viewShadowMap.destroy()
        this.viewShadowMap = null
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            // this.cameraFrustum.destroy(),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _changeViewPos () {
        this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess)
        this.viewShadowMap && this.viewShadowMap.destroy()
        this.viewShadowMap = null
        this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            // this.cameraFrustum.destroy(),
            this._createShadowMap(this.cameraPosition, this.position),
            this._getOrientation(),
            this._addCameraFrustum(),
            this._addPostProcess()
    }
    _switchShow () {
        this.show ? !this.postProcess && this._addPostProcess() : (this.postProcess && this.viewer.scene.postProcessStages.remove(this.postProcess), delete this.postProcess, this.postProcess = null),
            this.cameraFrustum.show = this.show,
            this.cameraFrustumOutline.show = this.show
    }
    activeVideo () {
        var video = this.element,
            that = this
        if (video) {
            var viewer = this.viewer
            this.activeVideoListener || (this.activeVideoListener = function () {
                that.videoTexture && that.videoTexture.destroy(),
                    that.videoTexture = new Cesium.Texture({
                        context: viewer.scene.context,
                        source: video,
                        width: 1,
                        height: 1,
                        pixelFormat: Cesium.PixelFormat.RGBA,
                        pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE
                    })
            }),
                viewer.clock.onTick.addEventListener(this.activeVideoListener)
        }
    }
    locate () {
        var cameraPosition = Cesium.clone(this.cameraPosition),
            position = Cesium.clone(this.position)
        this.viewer.Camera.position = cameraPosition,
            this.viewer.camera.direction = Cesium.Cartesian3.subtract(position, cameraPosition, new Cesium.Cartesian3(0, 0, 0)),
            this.viewer.camera.up = Cesium.Cartesian3.normalize(cameraPosition, new Cesium.Cartesian3(0, 0, 0))
    }
    update (e) {
        this.viewShadowMap && this.viewer.scene.frameState.shadowMaps.push(this.viewShadowMap) // *重点* 多投影
    }
    _createShadowMap () {
        var e = Cesium.Cartesian3.fromDegrees(this.param.position.x * 1, this.param.position.y * 1, this.param.position.z * 1)
        var t = this.position
        var i = this.viewer.scene
        var a = new Cesium.Camera(i)
        a.position = e
        this.customFrustum = new Cesium.PerspectiveFrustum({
            fov: Cesium.Math.toRadians(this.fov),
            aspectRatio: this.aspectRatio,
            near: this.near,
            far: 250
        })
        a.frustum = new Cesium.PerspectiveFrustum({
            fov: Cesium.Math.toRadians(this.fov),
            aspectRatio: this.aspectRatio,
            near: this.near,
            far: 250
        })
        let curOrientation = {}
        const headingPitchRoll = new Cesium.HeadingPitchRoll(
            Cesium.Math.toRadians(this.param.rotation.y, 0),
            Cesium.Math.toRadians(this.param.rotation.x, 0),
            Cesium.Math.toRadians(0),
        )
        curOrientation['heading'] = headingPitchRoll['heading']
        curOrientation['pitch'] = headingPitchRoll['pitch']
        curOrientation['roll'] = headingPitchRoll['roll']
        let curDestination = {}
        curDestination.destination = e
        curDestination.orientation = curOrientation
        a.setView(curDestination)
        var n = Cesium.Cartesian3.distance(t, e)
        this.viewDis = n
        if (!this.isShadowMap) return
        this.viewShadowMap = new Cesium.ShadowMap({
            lightCamera: a,
            enable: !1,
            isPointLight: !1,
            isSpotLight: !0,
            cascadesEnabled: !1,
            context: i.context,
            pointLightRadius: n,
        })
    }
    _getOrientation () {
        const hpr = Cesium.HeadingPitchRoll.fromDegrees(180 + this.param.rotation.y * 1, 0, 90 - this.param.rotation.x * 1 || 0)
        const orientation = Cesium.Transforms.headingPitchRollQuaternion(this.cameraPosition, hpr)
        return this.orientation = orientation,
            orientation
    }
    _addCameraFrustum () {
        var e = this
        this.cameraFrustum = new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.FrustumGeometry({
                    origin: e.cameraPosition,
                    orientation: e.orientation,
                    frustum: e.customFrustum,
                    _drawNearPlane: !0
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromBytes(0, 213, 144, 20)
                    ),
                }
            }),
            releaseGeometryInstances: false,
            appearance: new Cesium.PerInstanceColorAppearance({
                translucent: true,
                flat: !0,
                closed: true,
            }),
            asynchronous: false,
            show: this.debugFrustum && this.show
        })
        this.cameraFrustumOutline = new Cesium.Primitive({
            geometryInstances: new Cesium.GeometryInstance({
                geometry: new Cesium.FrustumOutlineGeometry({
                    origin: e.cameraPosition,
                    orientation: e.orientation,
                    frustum: e.customFrustum,
                    _drawNearPlane: !0
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(
                        Cesium.Color.fromBytes(0, 213, 144, 255)
                    ),
                }
            }),
            appearance: new Cesium.PerInstanceColorAppearance({
                closed: true,
                flat: true,
                translucent: true
            }),
            asynchronous: false,
            show: this.debugFrustum && this.show
        })
        this.viewer.scene.primitives.add(this.cameraFrustum)
        this.viewer.scene.primitives.add(this.cameraFrustumOutline)
    }
    _addPostProcess () {
        if (!this.isShadowMap) return
        var e = this,
            t = videoShed3dShader
        const i = e.viewShadowMap['_primitiveBias']
        this.postProcess = new Cesium.PostProcessStage({
            fragmentShader: t,
            uniforms: {
                ztzf_frustum_videoTexture: function () {
                    return e.videoTexture
                },
                ztzf_frustum_maskTexture: function () {
                    return new Cesium['Texture']({
                        'context': e.viewer.scene.context,
                        'source': {
                            'width': 0x1,
                            'height': 0x1,
                            'arrayBufferView': new Uint8Array([255, 255, 255, 255])
                        },
                        'flipY': ![]
                    })
                },
                ztzf_frustum_opacity: function () {
                    return e.alpha
                },
                ztzf_frustum_hiddenAreaColor: () => {
                    return new Cesium.Color(0, 0, 0, 0.5)
                },
                shadowMap_texture: function () {
                    if (!e.viewShadowMap || !e.viewShadowMap['_shadowMapTexture'] || e.viewShadowMap['_shadowMapTexture']['isDestroyed']()) {
                        return new Cesium['Texture']({
                            'context': e.viewer.scene.context,
                            'source': {
                                'width': 0x1,
                                'height': 0x1,
                                'arrayBufferView': new Uint8Array([0x0, 0x0, 0x0, 0x0])
                            },
                            'flipY': ![]
                        })
                    }
                    return e.viewShadowMap._shadowMapTexture
                },
                shadowMap_matrix: function () {
                    return e.viewShadowMap._shadowMapMatrix
                },
                shadowMap_lightPositionEC: function () {
                    return e.viewShadowMap._lightPositionEC
                },
                shadowMap_texelSizeDepthBias: function () {
                    var t = new Cesium.Cartesian2
                    return t.x = 1 / e.viewShadowMap._textureSize.x,
                        t.y = 1 / e.viewShadowMap._textureSize.y,
                        Cesium.Cartesian4.fromElements(t.x, t.y, i.depthBias, i.normalShadingSmooth, this.combinedUniforms1)
                },
                shadowMap_normalOffsetScale: function () {
                    return Cesium.Cartesian4.fromElements(i.normalOffsetScale, e.viewShadowMap._distance, e.viewShadowMap.maximumDistance, e.viewShadowMap._darkness, this.combinedUniforms2)
                },
                ztzf_frustum_flipx: () => {
                    return false
                },
                ztzf_frustum_flipy: () => {
                    return false
                },
                ztzf_frustum_borderColor: () => {
                    return new Cesium.Color(0 / 255, 213 / 255, 144 / 255, 255 / 255)
                },
                ztzf_frustum_borderWidth: () => {
                    return 0.01
                },
            }
        }),
            this.viewer.scene.postProcessStages.add(this.postProcess)
    }
    destroy () {
        this.viewer.scene.postProcessStages.remove(this.postProcess),
            this.viewer.scene.primitives.remove(this.cameraFrustum),
            this.viewer.scene.primitives.remove(this.cameraFrustumOutline),
            //this._videoEle && this._videoEle.parentNode.removeChild(this._videoEle),
            this.activeVideoListener && this.viewer.clock.onTick.removeEventListener(this.activeVideoListener),
            this.activeVideoListener && delete this.activeVideoListener,
            delete this.postProcess,
            delete this.viewShadowMap,
            delete this.color,
            delete this.viewDis,
            delete this.cameraPosition,
            delete this.position,
            delete this.alpha,
            delete this._camerafov,
            delete this._isShadowMap,
            delete this._cameraPosition,
            delete this.videoTexture,
            delete this.cameraFrustum,
            delete this.cameraFrustumOutline,
            delete this._videoEle,
            delete this._debugFrustum,
            delete this._position,
            delete this._aspectRatio,
            delete this.element,
            delete this.orientation,
            delete this.texture,
            delete this.videoId,
            this.viewer.scene.primitives.remove(this),
            delete this.viewer
    }
}
export default CesiumVideoFrustum
applications/drone-command/src/utils/cesium/frustum/CoordinateTranslate.js
New file
@@ -0,0 +1,229 @@
/*
 * @Author       : yuan
 * @Date         : 2025-08-15 14:06:49
 * @LastEditors  : yuan
 * @LastEditTime : 2025-08-15 14:06:59
 * @FilePath     : \src\utils\cesium\frustum\CoordinateTranslate.js
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-08-15 14:06:49
 */
let ECEF = (function () {
    var _ = function () {
        this.PI = 3.141592653589793238
        this.a = 6378137.0
        this.b = 6356752.3142
        this.f = (this.a - this.b) / this.a
        this.e_sq = this.f * (2.0 - this.f)
        this.ee = 0.00669437999013
        this.WGSF = 1 / 298.257223563
        this.WGSe2 = this.WGSF * (2 - this.WGSF)
        this.WGSa = 6378137.00000
        this.EPSILON = 1.0e-12
    }
    _.prototype.CalculateCoordinates = function (point, azimuth, elevation, distance) {
        var vertical_height = distance * Math.sin(2 * this.PI / 360 * elevation)//垂直高度
        var horizontal_distance = distance * Math.cos(2 * this.PI / 360 * elevation)//水平距离
        if (azimuth > 360) azimuth = azimuth % 360
        if (azimuth < 0) azimuth = 360 + (azimuth % 360)
        var point1 = this.lonLat2WebMercator(point)
        var lnglat = null
        var x_length, y_length
        if (azimuth <= 90) {//第四象限
            x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * azimuth)
            y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * azimuth)
            lnglat = {
                x: point1.x + x_length,
                y: point1.y - y_length
            }
        } else if (azimuth > 90 && azimuth <= 180) {//第三象限
            x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 90))
            y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 90))
            lnglat = {
                x: point1.x - x_length,
                y: point1.y - y_length
            }
        } else if (azimuth > 180 && azimuth <= 270) {//第二象限
            x_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 180))
            y_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 180))
            lnglat = {
                x: point1.x - x_length,
                y: point1.y + y_length
            }
        } else {//第一象限
            x_length = horizontal_distance * Math.sin(2 * this.PI / 360 * (azimuth - 270))
            y_length = horizontal_distance * Math.cos(2 * this.PI / 360 * (azimuth - 270))
            lnglat = {
                x: point1.x + x_length,
                y: point1.y + y_length
            }
        }
        lnglat = this.webMercator2LonLat(lnglat)
        return {
            lng: lnglat.x,
            lat: lnglat.y,
            height: vertical_height
        }
    }
    /*
       *经纬度转Web墨卡托
       *@lonLat 经纬度
       */
    _.prototype.lonLat2WebMercator = function (lonLat) {
        let x = lonLat.x * this.a / 180
        let y = Math.log(Math.tan((90 + lonLat.y) * this.PI / 360)) / (this.PI / 180)
        y = y * this.a / 180
        return {
            x: x,
            y: y
        }
    }
    /*
       *Web墨卡托转经纬度
       *@mercator 平面坐标
       */
    _.prototype.webMercator2LonLat = function (mercator) {
        let x = mercator.x / this.a * 180
        let y = mercator.y / this.a * 180
        y = 180 / this.PI * (2 * (Math.exp(y * this.PI / 180)) - this.PI / 2)
        return {
            x: x,
            y: y
        }
    }
    _.prototype.get_atan = function (z, y) {
        let x
        if (z == 0) {
            x = this.PI / 2
        } else {
            if (y == 0) {
                x = this.PI
            } else {
                x = Math.atan(Math.abs(y / z))
                if ((y > 0) && (z < 0)) {
                    x = this.PI - x
                } else if ((y < 0) && (z < 0)) {
                    x = this.PI + x
                } else if ((y < 0) && (z > 0)) {
                    x = 2 * this.M_PI - x
                }
            }
        }
        return x
    }
    //WGS84转ECEF坐标系
    _.prototype.ConvertLLAToXYZ = function (LLACoor) {
        let lon = this.PI / 180 * LLACoor.longitude
        let lat = this.PI / 180 * LLACoor.latitude
        let H = LLACoor.altitude
        let N0 = this.a / Math.sqrt(1.0 - this.ee * Math.sin(lat) * Math.sin(lat))
        let x = (N0 + H) * Math.cos(lat) * Math.cos(lon)
        let y = (N0 + H) * Math.cos(lat) * Math.sin(lon)
        let z = (N0 * (1.0 - this.ee) + H) * Math.sin(lat)
        return {
            x: x,
            y: y,
            z: z
        }
    }
    //ECEF坐标系转WGS84
    _.prototype.ConvertXYZToLLA = function (XYZCoor) {
        let longitude = this.get_atan(XYZCoor.x, XYZCoor.y)
        if (longitude < 0) {
            longitude = longitude + this.PI
        }
        let latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), XYZCoor.z)
        let W = Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude))
        let N = this.WGSa / W
        let B1
        do {
            B1 = latitude
            W = Math.sqrt(1 - this.WGSe2 * Math.sin(B1) * Math.sin(B1))
            N = this.WGSa / W
            latitude = this.get_atan(Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y), (XYZCoor.z + N * this.WGSe2 * Math.sin(B1)))
        }
        while (Math.abs(latitude - B1) > this.EPSILON)
        var altitude = Math.sqrt(XYZCoor.x * XYZCoor.x + XYZCoor.y * XYZCoor.y) / Math.cos(latitude) - this.WGSa / Math.sqrt(1 - this.WGSe2 * Math.sin(latitude) * Math.sin(latitude))
        return {
            longitude: longitude * 180 / this.PI,
            latitude: latitude * 180 / this.PI,
            altitude: altitude
        }
    }
    /*北东天坐标系转WGS84
    @ a A点坐标
    @ p 相对参数,距离、方位角、仰角
    */
    //    俯视角pitch -elevation
    //航向角heading(yaw) -azimuth
    _.prototype.enu_to_ecef = function (a, p) {
        //距离
        let distance = p.distance
        //方位角
        let azimuth = p.azimuth
        //仰角
        let elevation = p.elevation
        let zUp = elevation >= 0 ? distance * Math.sin(this.PI / 180 * elevation) : (-1) * distance * Math.sin(this.PI / 180 * Math.abs(elevation))
        let d = distance * Math.cos(this.PI / 180 * Math.abs(elevation))
        let xEast
        let yNorth
        if (azimuth <= 90) {
            xEast = d * Math.sin(this.PI / 180 * azimuth)
            yNorth = d * Math.cos(this.PI / 180 * azimuth)
        } else if (azimuth > 90 && azimuth < 180) {
            xEast = d * Math.cos(this.PI / 180 * (azimuth - 90))
            yNorth = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 90))
        } else if (azimuth > 180 && azimuth < 270) {
            xEast = (-1) * d * Math.sin(this.PI / 180 * (azimuth - 180))
            yNorth = (-1) * d * Math.cos(this.PI / 180 * (azimuth - 180))
        } else {
            xEast = (-1) * d * Math.sin(this.PI / 180 * (360 - azimuth))
            yNorth = d * Math.cos(this.PI / 180 * (360 - azimuth))
        }
        let lamb = this.radians(a.latitude)
        let phi = this.radians(a.longitude)
        let h0 = a.altitude
        let s = Math.sin(lamb)
        let N = this.a / Math.sqrt(1.0 - this.e_sq * s * s)
        let sin_lambda = Math.sin(lamb)
        let cos_lambda = Math.cos(lamb)
        let sin_phi = Math.sin(phi)
        let cos_phi = Math.cos(phi)
        let x0 = (h0 + N) * cos_lambda * cos_phi
        let y0 = (h0 + N) * cos_lambda * sin_phi
        let z0 = (h0 + (1 - this.e_sq) * N) * sin_lambda
        let t = cos_lambda * zUp - sin_lambda * yNorth
        let zd = sin_lambda * zUp + cos_lambda * yNorth
        let xd = cos_phi * t - sin_phi * xEast
        let yd = sin_phi * t + cos_phi * xEast
        return this.ConvertXYZToLLA({
            x: xd + x0,
            y: yd + y0,
            z: zd + z0
        })
    }
    _.prototype.radians = function (degree) {
        return this.PI / 180 * degree
    }
    return _
})()
export default ECEF
applications/drone-command/src/utils/cesium/kmz.js
@@ -209,6 +209,11 @@
}
/**
 * 递归处理对象,移除 #text 键并返回其值,同时处理数值字符串到数字的转换
 * @param {any} obj - 需要处理的对象或值
 * @returns {any} 处理后的对象或值
 */
export function removeTextKey(obj) {
    if (typeof obj !== 'object' || obj === null) {
        // 如果是数值字符串,转换为数值
@@ -219,18 +224,21 @@
    }
    if (Array.isArray(obj)) {
        // 递归处理数组中的每个元素
        return obj.map(item => removeTextKey(item));
    }
    if (Object.prototype.hasOwnProperty.call(obj, '#text')) {
        // 如果 #text 的值是数值字符串,转换为数值
        // 如果存在 #text 属性,返回其值(如果是数值字符串则转换为数字)
        const textValue = obj['#text'];
        return typeof textValue === 'string' && /^-?\d+(\.\d+)?$/.test(textValue) ? Number(textValue) : textValue;
    }
    // 递归处理对象属性
    const newObj = {};
    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            // 如果属性值是空对象,则设为空字符串;否则递归处理
            newObj[key] = Object.keys(obj[key]).length === 0 ? '' : removeTextKey(obj[key]);
        }
    }
applications/drone-command/src/utils/cesium/mapUtil.js
@@ -1,7 +1,8 @@
import * as Cesium from 'cesium'
import * as turf from '@turf/turf'
const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env
import store from '@/store'
const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env
export const getLngLatDistance = (lat1, lng1, lat2, lng2) => {
    const radLat1 = (lat1 * Math.PI) / 180.0
    const radLat2 = (lat2 * Math.PI) / 180.0
@@ -189,6 +190,66 @@
    })
    const newArr = arr.sort((a, b) => a.distance - b.distance)
    return newArr[0]
}
// 获取多边形内部数据
export const samplePolygonInterior = (positions, samplingDistance) => {
    if (!positions.length) return []
    let minLng = Infinity,
        maxLng = -Infinity
    let minLat = Infinity,
        maxLat = -Infinity
    positions.forEach(pos => {
        minLng = Math.min(minLng, pos.lng)
        maxLng = Math.max(maxLng, pos.lng)
        minLat = Math.min(minLat, pos.lat)
        maxLat = Math.max(maxLat, pos.lat)
    })
    //  将 Cesium 多边形格式转为 Turf 格式
    const turfPolygonCoords = positions.map(pos => [pos.lng, pos.lat])
    turfPolygonCoords.push([positions[0].lng, positions[0].lat]) // 闭合多边形
    const turfPolygon = turf.polygon([turfPolygonCoords])
    //  生成网格点并判断是否在多边形内
    const sampledPoints = []
    for (let lng = minLng; lng <= maxLng; lng += samplingDistance) {
        for (let lat = minLat; lat <= maxLat; lat += samplingDistance) {
            // 创建 Turf 点
            const turfPoint = turf.point([lng, lat])
            // Turf 原生方法判断点是否在多边形内
            if (turf.booleanPointInPolygon(turfPoint, turfPolygon)) {
                sampledPoints.push({
                    lng: Number(lng.toFixed(6)),
                    lat: Number(lat.toFixed(6)),
                    height: 0,
                })
            }
        }
    }
    return sampledPoints
}
// 获取多边形面内的最高点(相对地形高度)
export const getPolygonHighestPoint = async (positions, viewer, samplingDistance = 0.0002) => {
    try {
        const interiorPoints = samplePolygonInterior(positions, samplingDistance, viewer)
        const allPoints = [...positions, ...interiorPoints]
        // 批量获取所有点的地形高度
        const pointsWithTerrain = await getPointPositionsHeight(allPoints, viewer)
        // console.log('所有点的地形高度', pointsWithTerrain)
        // 地形高度(ASL)最高的点
        const highestTerrainPoint = pointsWithTerrain.reduce(
            (max, curr) => (curr.ASL > max.ASL ? curr : max),
            pointsWithTerrain[0]
        )
        // console.log('地形最高点信息:', highestTerrainPoint)
        return highestTerrainPoint
    } catch (error) {
        console.error('获取地形最高点失败:', error)
        return null
    }
}
// 获取当前经纬度地形数据
@@ -410,11 +471,15 @@
    if (!Array.isArray(positionsData) || positionsData.length === 0) return
    // 如果是一个点,加两个点方便后续生成外包围盒
    if (positionsData.length === 1){
        const [lon,lat,height] = positionsData[0]
        positionsData = [[lon+0.001,lat+0.001,height],[lon-0.001,lat-0.001,height],[lon,lat,height]]
    if (positionsData.length === 1) {
        const [lon, lat, height = 0] = positionsData[0]
        positionsData = [
            [lon + 0.001, lat + 0.001, height],
            [lon - 0.001, lat - 0.001, height],
            [lon, lat, height],
        ]
    }
    const positions = positionsData.map(([lon, lat, height]) =>
    const positions = positionsData.map(([lon, lat, height = 0]) =>
        Cesium.Cartesian3.fromDegrees(Number(lon), Number(lat), Number(height || 0))
    )
@@ -439,7 +504,7 @@
            range: boundingSphere.radius * multiple,
        },
        complete: () => {
            // 飞行完成后检查相机高度是否小于最高点,如果小于直接飞到最高点
            // 飞行完成后检查相机高度是否小于最高点
            const cameraHeight = Cesium.Cartographic.fromCartesian(viewer.camera.position).height
            if (cameraHeight < maxHeight) {
                viewer.camera.flyTo({
@@ -529,7 +594,7 @@
// 辅助函数:创建旋转文字的画布
export function createRotatedTextCanvas(time) {
    const prefix = '预计'
    const prefix = '预计到达'
    const suffix = '分钟'
    const canvas = document.createElement('canvas')
    const context = canvas.getContext('2d')
@@ -537,7 +602,9 @@
    context.font = 'bold 16px Source Han Sans CN'
    // 测量各部分文本宽度
    const prefixWidth = context.measureText(prefix).width
    const timeWidth = context.measureText(time).width
    let timeWidth = context.measureText(time).width
    const timeWidthMore = `${time >= 10 ? 6 : 0}`
    timeWidth = timeWidth + Number(timeWidthMore)
    const suffixWidth = context.measureText(suffix).width
    const totalWidth = prefixWidth + timeWidth + suffixWidth
    const textHeight = 20 // 估算高度
@@ -550,17 +617,116 @@
    context.textAlign = 'left' // 改为左对齐,方便分段绘制
    // 清除画布
    context.clearRect(0, 0, canvas.width, canvas.height)
    // 绘制“预计”(白色)
    // 绘制“预计到达”(白色)
    context.fillStyle = '#FFFFFF'
    context.fillText(prefix, 10, 15)
    // 绘制时间(蓝色)
    context.font = 'bold 30px Source Han Sans CN'
    context.font = 'bold 24px Source Han Sans CN'
    context.fillStyle = '#1EE7E7' // 亮蓝色
    context.fillText(time, 10 + prefixWidth, 12)
    // 设置宽度
    context.textBaseline = 'middle'
    context.fillText(time, 10 + prefixWidth, 14)
    // 绘制“分钟”(白色)
    context.font = 'bold 16px Source Han Sans CN'
    context.fillStyle = '#FFFFFF'
    context.fillText(suffix, 20 + prefixWidth + timeWidth, 15)
    context.fillText(suffix, 14 + prefixWidth + timeWidth, 15)
    return canvas
}
export const saveCurrentCameraPosition = viewer => {
    const position = viewer?.camera.position.clone() // 相机位置(Cartesian3)
    const heading = viewer?.camera.heading // 航向角(弧度)
    const pitch = viewer?.camera.pitch // 俯仰角(弧度)
    const roll = viewer?.camera.roll // 翻滚角(弧度)
    store.commit('setCameraPosition', {
        duration: 0,
        destination: position,
        orientation: { heading, pitch, roll },
    })
}
// 连接地面线
export function getDockPolyLine(data, viewer) {
    if (!data.longitude || !data.latitude) return {}
    return {
        positions: new Cesium.CallbackProperty(() => {
            const pointPosition = Cesium.Cartesian3.fromDegrees(+data.longitude, +data.latitude, +data.height)
            if (!pointPosition) return []
            // 获取点的位置
            const cartographic = Cesium.Cartographic.fromCartesian(pointPosition)
            // 获取地形高度
            const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0
            // 创建地面点位置
            const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight)
            return [pointPosition, groundPosition]
        }, false),
        width: 0.5,
        material: Cesium.Color.WHITE,
    }
}
// 飞向一个机巢中心
export const flyDockCenter = (viewer, info) => {
    const { longitude = 115, latitude = 28, height = 0 } = info
    const position = Cesium.Cartesian3.fromDegrees(+longitude, +latitude, +height)
    const droneEntity = viewer?.entities.add({ position: position, point: { pixelSize: 0 } })
    viewer?.flyTo(droneEntity, {
        offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-60), 22000),
        duration: 0.5,
        complete: () => {
            viewer?.entities.remove(droneEntity)
        },
    })
}
// 在固定的经度上获得高度
export function getHeightOnFixedLonLat(ray, pointParams, ellipsoid = Cesium.Ellipsoid.WGS84) {
    const { longitude: lon, latitude: lat } = pointParams
    // 1. 地表点(高度=0)
    const p0 = Cesium.Cartesian3.fromDegrees(lon, lat, 0, ellipsoid)
    // 2. 该点处的法线方向(up 向量,单位向量)
    const up = ellipsoid.geodeticSurfaceNormal(p0, new Cesium.Cartesian3())
    // 3. 射线起点和方向
    const r0 = ray.origin // 相机位置
    const rd = ray.direction // 射线方向(已归一化?不一定,但不影响)
    // 4. 解:求直线 L(t) = p0 + t * up 与射线 R(s) = r0 + s * rd 的最近点
    // 使用向量公式求两条直线的最近点参数 t
    const w0 = Cesium.Cartesian3.subtract(p0, r0, new Cesium.Cartesian3())
    const a = Cesium.Cartesian3.dot(up, up) // = 1(因为 up 是单位向量)
    const b = Cesium.Cartesian3.dot(up, rd)
    const c = Cesium.Cartesian3.dot(rd, rd) // = |rd|^2
    const d = Cesium.Cartesian3.dot(up, w0)
    const e = Cesium.Cartesian3.dot(rd, w0)
    const denom = a * c - b * b
    let t
    if (Math.abs(denom) < Cesium.Math.EPSILON10) {
        // 两直线平行,取 t = 0
        t = 0
    } else {
        t = (b * e - c * d) / denom
    }
    // 5. 得到高度线上对应的点
    const resultPoint = Cesium.Cartesian3.add(
        p0,
        Cesium.Cartesian3.multiplyByScalar(up, t, new Cesium.Cartesian3()),
        new Cesium.Cartesian3()
    )
    // 6. 转回 Cartographic 获取精确高度
    const carto = Cesium.Cartographic.fromCartesian(resultPoint, ellipsoid)
    return {
        cartesian: resultPoint,
        height: carto.height,
        longitude: Cesium.Math.toDegrees(carto.longitude),
        latitude: Cesium.Math.toDegrees(carto.latitude),
    }
}
applications/drone-command/src/utils/cesium/publicCesium.js
@@ -70,6 +70,7 @@
 * @param {boolean} [options.flyToContour=false] - 是否飞行到轮廓,默认为 false
 * @param {number} [options.multiple=1.4] - 范围倍数
 * @param {number} [options.dockOptions={}] - 机巢显示
 * @param {array} [options.terrainLoadCallback] - 地形加载回调
 * @param {array} [options.boundaryChange] - 边界改变事件
 * @param {array} [options.boundaryColor] - 边界颜色
 * @param {array} [options.dockRangeType] - 机巢范围类型 1显示覆盖圆;2显示覆盖圈
@@ -92,15 +93,14 @@
            dom,
            flatMode = true,
            terrain = false,
            layerMode = 0,
            layerMode = 17,
            contour = true,
            flyToContour = false,
            multiple = 1.4,
            dockOptions = {},
            boundaryColor = '#7FFFD4',
            boundaryChange,
            terrainLoadCallback,
            boundaryColor = '#7FFFD4',
            dockRangeType = 1,
            useDockHeight = false
        } = options
@@ -120,6 +120,10 @@
            baseLayer: false,
            fullscreenButton: false,
        }
        // 尝试解决出现多个id相同的dom
        const domIdList = document.querySelectorAll(`#${dom}`)
        if (domIdList.length>1) domIdList[1].remove()
        this.viewer = new Viewer(dom, viewerOptions)
        this.viewerDom = dom
@@ -143,33 +147,47 @@
        this.viewer?.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) // 禁用双击
        if (terrain) {
            // 正则:第一位非0,第二位任意数字,后10位都是0
            const re = /^[1-9]\d0{10}$/
            let result = null
            // 1️⃣先判断 areaCode
            if (re.test(store.state.user.userInfo.detail.areaCode)) {
                result = store.state.user.userInfo.detail.areaCode.slice(0, 6)
            } else {
                // 2️⃣如果不满足,从 ancestors 里找
                const matchedItems = store.state.user.userInfo.detail.ancestors
                    .split(',')
                    .filter(item => re.test(item)) // 过滤出符合条件的
                // 取第一个符合条件的前6位(可以改成 last one 看需求)
                if (matchedItems.length > 0) {
                    result = matchedItems[0].slice(0, 6)
                }
            }
            try {
                Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, {
                    requestVertexNormals: true, // 启用地形法线增强立体感
                    requestWaterMask: true, // 启用水体遮罩效果
                }).then(terrainProvider => {
                    this.viewer.terrainProvider = terrainProvider
                })
                const noTerrainMechanism = ['1962779164650135554']
                if (noTerrainMechanism.includes(store.state.user.userInfo?.deptId)) {
                    // 使用Cesium World Terrain
                    Cesium.createWorldTerrainAsync({
                        requestWaterMask: false,    // 请求水域效果
                        requestVertexNormals: true // 请求光照和地形法线
                    }).then(terrainProvider => {
                        this.viewer.terrainProvider = terrainProvider
                        terrainLoadCallback?.()
                    })
                } else {
                    // 正则:第一位非0,第二位任意数字,后10位都是0
                    const re = /^[1-9]\d0{10}$/
                    let result = null
                    // 1️⃣先判断 areaCode
                    if (re.test(store.state.user.userInfo.detail.areaCode)) {
                        result = store.state.user.userInfo.detail.areaCode.slice(0, 6)
                    } else {
                        // 2️⃣如果不满足,从 ancestors 里找
                        const matchedItems = store.state.user.userInfo.detail.ancestors
                            .split(',')
                            .filter(item => re.test(item)) // 过滤出符合条件的
                        // 取第一个符合条件的前6位(可以改成 last one 看需求)
                        if (matchedItems.length > 0) {
                            result = matchedItems[0].slice(0, 6)
                        }
                    }
                    // 使用公司地形
                    Cesium.CesiumTerrainProvider.fromUrl(`${import.meta.env.VITE_APP_TERRAIN_URL}${result}`, {
                        requestVertexNormals: true, // 启用地形法线增强立体感
                        requestWaterMask: true, // 启用水体遮罩效果
                    }).then(terrainProvider => {
                        this.viewer.terrainProvider = terrainProvider
                        terrainLoadCallback?.()
                    })
                }
            } catch (error) {
                console.error('地形加载失败:', error)
            }
@@ -180,6 +198,12 @@
        this.switchFlatMode(flatMode)
        this.switchContour(contour)
        flyToContour && this.flyToContour(contour)
        if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) {
            let dpr = window.devicePixelRatio
            while (dpr >= 2.0) dpr /= 2.0 // 避免过高缩放导致模糊
            this.viewer.resolutionScale = dpr // 设置分辨率缩放比例
        }
    }
    getViewer () {
@@ -188,53 +212,69 @@
    // 销毁viewer
    viewerDestroy (removeDom = true) {
        if (!this.viewer) return
        // // 👇 关键:先停止所有动态行为
        // this.viewer.clock.shouldAnimate = false;
        // this.viewer.scene.requestRenderMode = false;
        // // 取消所有可能的 pending 渲染
        // if (this.viewer.cesiumWidget) {
        //     this.viewer.cesiumWidget.render = () => {}; // 空函数覆盖
        // }
        // // 清理实体和图层
        // this.viewer?.entities.removeAll();
        // this.viewer?.imageryLayers.removeAll();
        // // 清理数据源
        // try {
        //     const dataSources = this.viewer.dataSources;
        //     while (dataSources.length > 0) {
        //         dataSources.remove(dataSources.get(0));
        //     }
        // } catch (e) {
        //     console.warn('清理数据源时出错:', e);
        // }
        // // 清理场景中的图元
        // try {
        //     this.viewer?.scene.primitives.removeAll();
        // } catch (e) {
        //     console.warn('清理图元时出错:', e);
        // }
        // // 获取webgl上下文
        // let gl = this.viewer?.scene.context._originalGLContext
        // if (gl) {
        //     try {
        //         gl.canvas.width = 1
        //         gl.canvas.height = 1
        //         gl.getExtension("WEBGL_lose_context").loseContext()
        //     } catch (e) {
        //         console.warn('释放WebGL上下文时出错:', e)
        //     }
        //     gl = null
        // }
        this.viewer?.destroy() // 销毁Viewer实例
        this.viewer = null
        smallMpaCenterDistance = null
        if (this.viewer) {
            this.viewer?.entities.removeAll()
            this.viewer?.imageryLayers.removeAll()
            this.viewer?.dataSources.removeAll()
            // try {
            //     this.viewer?.scene?.primitives?.removeAll()
            //     this.viewer?.entities?.removeAll()
            // } catch (e) {
            //     console.warn('清理资源时出错:', e)
            //     console.log(this.viewer, 1111111111111)
            // }
            // 获取webgl上下文
            let gl = this.viewer?.scene.context._originalGLContext
            gl.canvas.width = 1
            gl.canvas.height = 1
            gl.getExtension("WEBGL_lose_context").loseContext()
            gl = null
            this.viewer?.destroy() // 销毁Viewer实例
            this.viewer = null
            var cesiumContainer = document.getElementById(this.viewerDom)
            if (cesiumContainer && removeDom) {
                cesiumContainer.remove() // 移除与地图相关的DOM元素
                this.viewerDom = null
            }
        var cesiumContainer = document.getElementById(this.viewerDom)
        if (cesiumContainer && removeDom) {
            cesiumContainer.remove() // 移除与地图相关的DOM元素
            this.viewerDom = null
        }
    }
    setShowDock (sns) {
        this.boundary?.setShowDock(sns)
    }
    setDockCoverColor (sns) {
        this.boundary?.setDockCoverColor(sns)
    }
    // 切换轮廓显示
    async switchContour (open) {
        if (open) {
            await this.boundary?.openContour()
        } else {
            this.boundary?.closeContour()
        }
    }
    // // 飞向轮廓居中
    // 飞向轮廓居中
    flyToContour () {
        this.boundary?.flyToBoundary()
    }
@@ -257,7 +297,7 @@
        this.globalBaseMapLayers?.forEach(item => {
            if (item.mapLayer) item.mapLayer.show = false
        })
        // store.state.common.mapSetting.mode = mode
        store.state.common.mapSetting.mode = mode
        // 标准地图(天地图矢量)加载
        if (mode === 0) {
            mapLayers.push(
@@ -324,9 +364,7 @@
            this.viewer?.imageryLayers.lowerToBottom(find.mapLayer)
        })
    }
    // 设置或重置滤镜
    setCurrentLayerFilter (data, mode) {
        if (mode === 17) {
            data.mapLayer.brightness = 0.6 // 亮度(默认值)
applications/drone-command/src/utils/cesium/use-kmz-tsa.js
New file
@@ -0,0 +1,675 @@
import _ from 'lodash'
import JSZIP from 'jszip'
import { JSONToXML } from './kmz'
import { getDateFromTimestamp } from '@/utils/date'
export default function kmzFile () {
    // 云台信息
    const droneModel = {
        52: '0',
        53: '1',
        66: '0',
        67: '1',
        80: '0',
        81: '1',
    }
    // 创建图斑航线
    const create = async (waylineBasicInfo, lnglats, spotList) => {
        try {
            const date = new Date()
            const username = window.localStorage.getItem('bs_username')
            const updateTime = date.getTime()
            const fileName = '图斑航线规划_' + date.getTime()
            // 无人机机型、云台型号、航线高度
            const { droneEnumValue, payloadEnumValue, height } = waylineBasicInfo
            const defaultHeight = height || 100
            // 起始点位 (参考点位-具体以机场为准)
            const startPoint = lnglats[0]
            const takeOffPoint = [startPoint[1], startPoint[0], 0].join(',')
            // template.xml对象模版
            const fileTemplate = {
                author: { '#text': username },
                createTime: { '#text': updateTime },
                updateTime: { '#text': updateTime },
                missionConfig: {
                    // 飞向首航点模式
                    flyToWaylineMode: { '#text': 'safely' },
                    // 航线结束动作
                    finishAction: { '#text': 'goHome' },
                    // 失控是否继续执行航线
                    exitOnRCLost: { '#text': 'goContinue' },
                    // 失控动作类型
                    executeRCLostAction: { '#text': 'goBack' },
                    // 安全起飞高度
                    takeOffSecurityHeight: { '#text': 20 },
                    // 参考起飞点
                    takeOffRefPoint: { '#text': takeOffPoint },
                    // 参考起飞点绝对高度
                    takeOffRefPointAGLHeight: { '#text': 0 },
                    // 全局航线过渡速度
                    globalTransitionalSpeed: { '#text': 10 },
                    // 全局返航高度
                    globalRTHHeight: { '#text': defaultHeight },
                    // 无人机参数
                    droneInfo: {
                        droneEnumValue: { '#text': droneEnumValue },
                        droneSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                    },
                    // 云台参数
                    payloadInfo: {
                        payloadEnumValue: { '#text': payloadEnumValue },
                        payloadSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                        payloadPositionIndex: { '#text': 0 },
                    },
                },
                Folder: {
                    // 预定义模板类型 - 航点飞行
                    templateType: { '#text': 'waypoint' },
                    // 是否使用全局航线过渡速度
                    useGlobalTransitionalSpeed: { '#text': 0 },
                    templateId: { '#text': 0 },
                    // 坐标系参数信息
                    waylineCoordinateSysParam: {
                        // 经纬度坐标系 - 固定值
                        coordinateMode: { '#text': 'WGS84' },
                        // 航点高程参考平面    - 相对高度
                        heightMode: { '#text': 'relativeToStartPoint' },
                    },
                    // 全局航线飞行速度
                    autoFlightSpeed: { '#text': 10 },
                    // 云台俯仰角控制模式
                    gimbalPitchMode: { '#text': 'usePointSetting' },
                    // 全局高度
                    globalHeight: { '#text': '100' },
                    // 全局偏航角模式参数 - 可使用当前默认值
                    globalWaypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 全局航点类型 - 直线飞行,飞行器到点停
                    globalWaypointTurnMode: {
                        '#text': 'toPointAndStopWithDiscontinuityCurvature',
                    },
                    // 全局航段轨迹是否尽量贴合直线
                    globalUseStraightLine: { '#text': 1 },
                    // 航点点位信息 - 位置、事件等
                    Placemark: [],
                    // payloadParam: {
                    //   payloadPositionIndex: { '#text': 0 },
                    //   focusMode: { '#text': 'firstPoint' },
                    //   meteringMode: { '#text': 'average' },
                    //   returnMode: { '#text': 'singleReturnFirst' },
                    //   samplingRate: { '#text': '240000' },
                    //   scanningMode: { '#text': 'repetitive' },
                    //   imageFormat: { '#text': 'wide,zoom' },
                    // },
                },
            }
            // 航点创建
            const polygonNo = {
                no: '',
                index: 0,
            }
            lnglats.forEach((lnglat, index) => {
                const obj = spotList.find(spot => spot.dkfw.find(item => _.isEqual(item, lnglat)))
                let fileSuffix = ''
                if (obj) {
                    if (polygonNo.no === obj.dkbh) {
                        polygonNo.index++
                    } else {
                        polygonNo.no = obj.dkbh
                        polygonNo.index = 1
                    }
                    fileSuffix = `图斑航线拍摄(${obj.dkbh}-${polygonNo.index})`
                } else {
                    fileSuffix = '图斑航线拍摄(起点)'
                }
                const placemark = {
                    Point: {
                        // 航点经纬度
                        coordinates: { '#text': `${lnglat[0]},${lnglat[1]}` },
                    },
                    // 航点index
                    index: { '#text': index },
                    // 全局航线高度(椭球高)
                    ellipsoidHeight: { '#text': defaultHeight },
                    // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度)
                    height: { '#text': defaultHeight },
                    // 航点飞行速度
                    waypointSpeed: { '#text': 10 },
                    // 沿航线方向 - 可使用当前默认值
                    waypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 航点类型
                    waypointTurnParam: {
                        waypointTurnMode: {
                            '#text': 'toPointAndStopWithDiscontinuityCurvature',
                        },
                        waypointTurnDampingDist: { '#text': 0.2 },
                    },
                    // 是否全局参数
                    useGlobalHeight: { '#text': 1 },
                    useGlobalSpeed: { '#text': 1 },
                    useGlobalHeadingParam: { '#text': 1 },
                    useGlobalTurnParam: { '#text': 1 },
                    useStraightLine: { '#text': 1 },
                    // 事件组
                    actionGroup: {
                        actionGroupId: { '#text': index },
                        actionGroupStartIndex: { '#text': index },
                        actionGroupEndIndex: { '#text': index },
                        actionGroupMode: { '#text': 'sequence' },
                        actionTrigger: {
                            actionTriggerType: { '#text': 'reachPoint' },
                        },
                        // 单个事件 - 如果是多个事件action可改成数组 action: [{}]
                        action: {
                            actionId: { '#text': 0 },
                            actionActuatorFunc: { '#text': 'takePhoto' },
                            actionActuatorFuncParam: {
                                fileSuffix: { '#text': fileSuffix },
                                payloadPositionIndex: { '#text': '0' },
                                useGlobalPayloadLensIndex: { '#text': '1' },
                            },
                        },
                    },
                }
                fileTemplate.Folder.Placemark.push(placemark)
            })
            const fileWaylines = waylinesContext(fileTemplate)
            return {
                fileName,
                template: fileTemplate,
                waylines: fileWaylines,
            }
        } catch (error) {
            console.log(error)
            return false
        }
    }
    // 创建正常航线
    const createNoramlWaylines = (waylineBasicInfo, lnglats) => {
        try {
            const date = new Date()
            const username = window.localStorage.getItem('bs_username')
            const updateTime = date.getTime()
            const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime)
            const fileName = '新建航线_' + year + month + day + hours + minutes
            // 无人机机型、云台型号、航线高度
            const { droneEnumValue, payloadEnumValue, height, waylineName } = waylineBasicInfo
            const defaultHeight = height || 100
            // 起始点位 (参考点位-具体以机场为准)
            const startPoint = lnglats[0]
            const takeOffPoint = [startPoint.latitude, startPoint.longitude, startPoint.height].join(',')
            // template.xml对象模版
            const fileTemplate = {
                author: { '#text': username },
                createTime: { '#text': updateTime },
                updateTime: { '#text': updateTime },
                missionConfig: {
                    // 飞向首航点模式
                    flyToWaylineMode: { '#text': 'safely' },
                    // 航线结束动作
                    finishAction: { '#text': 'goHome' },
                    // 失控是否继续执行航线
                    exitOnRCLost: { '#text': 'goContinue' },
                    // 失控动作类型
                    executeRCLostAction: { '#text': 'goBack' },
                    // 安全起飞高度
                    takeOffSecurityHeight: { '#text': 20 },
                    // 参考起飞点
                    takeOffRefPoint: { '#text': takeOffPoint },
                    // 参考起飞点绝对高度
                    takeOffRefPointAGLHeight: { '#text': 0 },
                    // 全局航线过渡速度
                    globalTransitionalSpeed: { '#text': 10 },
                    // 全局返航高度
                    globalRTHHeight: { '#text': defaultHeight },
                    // 无人机参数
                    droneInfo: {
                        droneEnumValue: { '#text': droneEnumValue },
                        droneSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                    },
                    // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞)
                    autoRerouteInfo: {
                        missionAutoRerouteMode: { '#text': 1 },
                        transitionalAutoRerouteMode: { '#text': 1 },
                    },
                    // 云台参数
                    payloadInfo: {
                        payloadEnumValue: { '#text': payloadEnumValue },
                        payloadSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                        payloadPositionIndex: { '#text': 0 },
                    },
                },
                Folder: {
                    // 预定义模板类型 - 航点飞行
                    templateType: { '#text': 'waypoint' },
                    // 是否使用全局航线过渡速度
                    useGlobalTransitionalSpeed: { '#text': 0 },
                    templateId: { '#text': 0 },
                    // 坐标系参数信息
                    waylineCoordinateSysParam: {
                        // 经纬度坐标系 - 固定值
                        coordinateMode: { '#text': 'WGS84' },
                        // 航点高程参考平面    - 相对高度
                        heightMode: { '#text': 'relativeToStartPoint' },
                    },
                    // 全局航线飞行速度
                    autoFlightSpeed: { '#text': 10 },
                    // 云台俯仰角控制模式
                    gimbalPitchMode: { '#text': 'usePointSetting' },
                    // 全局高度
                    globalHeight: { '#text': '100' },
                    // 全局偏航角模式参数 - 可使用当前默认值
                    globalWaypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 全局航点类型 - 直线飞行,飞行器到点停
                    globalWaypointTurnMode: {
                        '#text': 'toPointAndStopWithDiscontinuityCurvature',
                    },
                    // 全局航段轨迹是否尽量贴合直线
                    globalUseStraightLine: { '#text': 1 },
                    // 航点点位信息 - 位置、事件等
                    Placemark: [],
                    payloadParam: {
                        payloadPositionIndex: { '#text': 0 },
                        focusMode: { '#text': 'firstPoint' },
                        meteringMode: { '#text': 'average' },
                        returnMode: { '#text': 'singleReturnFirst' },
                        samplingRate: { '#text': '240000' },
                        scanningMode: { '#text': 'repetitive' },
                        imageFormat: { '#text': 'wide,zoom' },
                    },
                },
            }
            lnglats.forEach((lnglat, index) => {
                const placemark = {
                    Point: {
                        // 航点经纬度
                        coordinates: {
                            '#text': `${lnglat.longitude},${lnglat.latitude}`,
                        },
                    },
                    // 航点index
                    index: { '#text': index },
                    // 全局航线高度(椭球高)
                    ellipsoidHeight: { '#text': lnglat.height },
                    // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度)
                    height: { '#text': lnglat.height },
                    // 航点飞行速度
                    waypointSpeed: { '#text': lnglat?.speed || 10 },
                    // 沿航线方向 - 可使用当前默认值
                    waypointHeadingParam: {
                        waypointHeadingMode: { '#text': 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 航点类型
                    waypointTurnParam: {
                        waypointTurnMode: {
                            '#text': 'toPointAndStopWithDiscontinuityCurvature',
                        },
                        waypointTurnDampingDist: { '#text': 0.2 },
                    },
                    // 是否全局参数
                    useGlobalHeight: { '#text': 1 },
                    useGlobalSpeed: { '#text': 0 },
                    useGlobalHeadingParam: { '#text': 1 },
                    useGlobalTurnParam: { '#text': 1 },
                    useStraightLine: { '#text': 1 },
                }
                if (lnglat?.actions) {
                    const actionGroup = {
                        actionGroupId: { '#text': index },
                        actionGroupStartIndex: { '#text': index },
                        actionGroupEndIndex: { '#text': index },
                        actionGroupMode: { '#text': 'sequence' },
                        actionTrigger: {
                            actionTriggerType: { '#text': 'reachPoint' },
                        },
                        // 单个事件 - 如果是多个事件action可改成数组 action: [{}]
                        // action: [{
                        //   actionId: { '#text': 0 },
                        //   actionActuatorFunc: { '#text': '' },
                        //   actionActuatorFuncParam: {},
                        // }],
                        action: [],
                    }
                    lnglat?.actions.forEach((action, index) => {
                        const singleAction = {
                            actionId: { '#text': index },
                            actionActuatorFunc: { '#text': '' },
                            actionActuatorFuncParam: {},
                        }
                        // actionGroup.action['actionActuatorFunc'] = { '#text': action.key }
                        // action.params.forEach(p => {
                        //   if (p.value || p.value !== '') {
                        //     actionGroup.action['actionActuatorFuncParam'][p.key] = { '#text': p.value }
                        //   }
                        // })
                        singleAction['actionActuatorFunc'] = {
                            '#text': action.key,
                        }
                        action.params.forEach(p => {
                            if (p.value || p.value !== '') {
                                singleAction['actionActuatorFuncParam'][p.key] = {
                                    '#text': p.value,
                                }
                            }
                        })
                        if (lnglat?.actions.length > 1) {
                            actionGroup.action.push(singleAction)
                        } else {
                            actionGroup.action = singleAction
                        }
                    })
                    placemark.actionGroup = actionGroup
                }
                fileTemplate.Folder.Placemark.push(placemark)
            })
            const fileWaylines = waylinesContext(fileTemplate)
            return {
                fileName: waylineName || fileName,
                template: fileTemplate,
                waylines: fileWaylines,
            }
        } catch (error) {
            console.log(error)
            return error
        }
    }
    // 创建面状航线
    const createPlanarWayline = (waylineBasicInfo, lnglats) => { }
    // 创建点状航线
    const createPointWayLines = (waylineBasicInfo, points, settingInfo, dockPosition) => {
        try {
            const { droneEnumValue, payloadEnumValue, waylineName } = waylineBasicInfo
            const {
                imageFormat,
                heightMode,
                globalHeight,
                autoFlightSpeed,
                gimbalPitchMode,
                waypointTurnMode,
                waypointHeadingMode,
                finishAction,
                globalTransitionalSpeed,
            } = settingInfo
            const date = new Date()
            const username = window.localStorage.getItem('bs_username')
            const updateTime = date.getTime()
            const { year, month, day, hours, minutes } = getDateFromTimestamp(updateTime)
            const fileName = '新建航线_' + year + month + day + hours + minutes
            // 无人机机型、云台型号、航线高度
            const defaultHeight = 100
            // 起始点位 (参考点位-具体以机场为准)
            const takeOffPoint = [dockPosition.latitude, dockPosition.longitude, 0].join(',')
            // template.xml对象模版
            const fileTemplate = {
                author: { '#text': username },
                createTime: { '#text': updateTime },
                updateTime: { '#text': updateTime },
                missionConfig: {
                    // 飞向首航点模式
                    flyToWaylineMode: { '#text': 'safely' },
                    // 航线结束动作
                    finishAction: { '#text': finishAction || 'goHome' },
                    // 失控是否继续执行航线
                    exitOnRCLost: { '#text': 'executeLostAction' },
                    // 失控动作类型
                    executeRCLostAction: { '#text': 'goBack' },
                    // 安全起飞高度
                    takeOffSecurityHeight: { '#text': 120 },
                    // 参考起飞点
                    takeOffRefPoint: { '#text': takeOffPoint },
                    // 参考起飞点绝对高度
                    takeOffRefPointAGLHeight: { '#text': 0 },
                    // 全局航线过渡速度
                    globalTransitionalSpeed: { '#text': globalTransitionalSpeed || 10 },
                    // 全局返航高度
                    globalRTHHeight: { '#text': defaultHeight },
                    // 无人机参数
                    droneInfo: {
                        droneEnumValue: { '#text': droneEnumValue },
                        droneSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                    },
                    // 是否自动绕行(暂未验证M30T机型使用此参数后会不会无法起飞)
                    autoRerouteInfo: {
                        missionAutoRerouteMode: { '#text': 1 },
                        transitionalAutoRerouteMode: { '#text': 1 },
                    },
                    // 云台参数
                    payloadInfo: {
                        payloadEnumValue: { '#text': payloadEnumValue },
                        payloadSubEnumValue: {
                            '#text': droneModel[payloadEnumValue],
                        },
                        payloadPositionIndex: { '#text': 0 },
                    },
                },
                Folder: {
                    // 预定义模板类型 - 航点飞行
                    templateType: { '#text': 'waypoint' },
                    // 是否使用全局航线过渡速度
                    useGlobalTransitionalSpeed: { '#text': 0 },
                    templateId: { '#text': 0 },
                    // 坐标系参数信息
                    waylineCoordinateSysParam: {
                        // 经纬度坐标系 - 固定值
                        coordinateMode: { '#text': 'WGS84' },
                        // 航点高程参考平面    - 相对高度
                        heightMode: heightMode,
                    },
                    // 全局航线飞行速度
                    autoFlightSpeed: { '#text': autoFlightSpeed || 10 },
                    // 云台俯仰角控制模式
                    gimbalPitchMode: { '#text': gimbalPitchMode || 'manual' },
                    // 全局高度
                    globalHeight: { '#text': globalHeight || '100' },
                    // 全局偏航角模式参数 - 可使用当前默认值
                    globalWaypointHeadingParam: {
                        waypointHeadingMode: { '#text': waypointHeadingMode || 'followWayline' },
                        waypointHeadingAngle: { '#text': 0 },
                        waypointPoiPoint: {
                            '#text': '0.000000,0.000000,0.000000',
                        },
                        waypointHeadingPathMode: { '#text': 'followBadArc' },
                        waypointHeadingPoiIndex: { '#text': 0 },
                    },
                    // 全局航点类型 - 直线飞行,飞行器到点停
                    globalWaypointTurnMode: {
                        '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature',
                    },
                    // 全局航段轨迹是否尽量贴合直线
                    globalUseStraightLine: { '#text': 1 },
                    // 航点点位信息 - 位置、事件等
                    Placemark: [],
                    payloadParam: {
                        payloadPositionIndex: { '#text': 0 },
                        focusMode: { '#text': 'firstPoint' },
                        meteringMode: { '#text': 'average' },
                        returnMode: { '#text': 'singleReturnFirst' },
                        samplingRate: { '#text': '240000' },
                        scanningMode: { '#text': 'repetitive' },
                        imageFormat: imageFormat,
                    },
                },
            }
            points.length &&
                points.forEach((detail, ind) => {
                    const { Point, index, height, ellipsoidHeight, actionGroup, waypointSpeed } = detail
                    const placemark = {
                        Point,
                        // 航点index
                        index,
                        // 全局航线高度(椭球高)
                        ellipsoidHeight,
                        // 全局航线高度 (EGM96海拔高/相对起飞点高度/AGL相对地面高度)
                        height,
                        // 航点飞行速度
                        waypointSpeed: { '#text': waypointSpeed || 10 },
                        // 沿航线方向 - 可使用当前默认值
                        waypointHeadingParam: {
                            waypointHeadingMode: {
                                '#text': waypointHeadingMode || 'followWayline',
                            },
                            waypointHeadingAngle: { '#text': 0 },
                            waypointPoiPoint: {
                                '#text': '0.000000,0.000000,0.000000',
                            },
                            waypointHeadingPathMode: { '#text': 'followBadArc' },
                            waypointHeadingPoiIndex: { '#text': 0 },
                        },
                        // 航点类型
                        waypointTurnParam: {
                            waypointTurnMode: {
                                '#text': waypointTurnMode || 'toPointAndStopWithDiscontinuityCurvature',
                            },
                            waypointTurnDampingDist: { '#text': 0.2 },
                        },
                        // 是否全局参数
                        useGlobalHeight: { '#text': 1 },
                        useGlobalSpeed: { '#text': 0 },
                        useGlobalHeadingParam: { '#text': 1 },
                        useGlobalTurnParam: { '#text': 1 },
                        useStraightLine: { '#text': 1 },
                    }
                    if (actionGroup) {
                        placemark.actionGroup = actionGroup
                    }
                    fileTemplate.Folder.Placemark.push(placemark)
                })
            const fileWaylines = waylinesContext(fileTemplate)
            return {
                fileName: waylineName || fileName,
                template: fileTemplate,
                waylines: fileWaylines,
            }
        } catch (error) {
            console.log(error)
            return error
        }
    }
    const save = async fileInfo => {
        const JsZip = new JSZIP()
        const { fileName, template, waylines } = fileInfo
        const templateXml = JSONToXML(template, '', true)
        const waylinesWpml = JSONToXML(waylines, '', true)
        JsZip.file('wpmz/template.kml', templateXml)
        JsZip.file('wpmz/waylines.wpml', waylinesWpml)
        const fileBlob = await JsZip.generateAsync({ type: 'blob' })
        // saveAs(kmzFile, `${fileName}.kmz`)
        return fileBlob
    }
    const waylinesContext = templateJson => {
        const waylinesObj = {
            // 标准模版
            missionConfig: {
                flyToWaylineMode: null,
                finishAction: null,
                exitOnRCLost: null,
                executeRCLostAction: null,
                takeOffSecurityHeight: null,
                globalTransitionalSpeed: null,
                globalRTHHeight: null,
                droneInfo: null,
                payloadInfo: null,
            },
            Folder: {
                templateId: null,
                executeHeightMode: null,
                waylineId: null,
                autoFlightSpeed: null,
                /*
          waylines.wpml和template.xml的Placemark通用
          不同点:
            waylineId = templateId
            ellipsoidHeight、 height 替换成 executeHeight
        */
                Placemark: null,
            },
        }
        Object.keys(waylinesObj).forEach(key => {
            if (Object.prototype.toString.call(waylinesObj[key]) === '[object Object]') {
                Object.keys(waylinesObj[key]).forEach(item => {
                    waylinesObj[key][item] = templateJson[key][item]
                    if (item === 'executeHeightMode') {
                        waylinesObj[key][item] = templateJson[key].waylineCoordinateSysParam.heightMode
                    }
                    if (item === 'waylineId') {
                        waylinesObj[key][item] = templateJson[key].templateId
                    }
                    if (item === 'Placemark') {
                        const placemarks = _.cloneDeep(templateJson[key][item])
                        placemarks.forEach(placemark => {
                            placemark.executeHeight = {
                                '#text': placemark.ellipsoidHeight?.['#text'] || '',
                            }
                            delete placemark.ellipsoidHeight
                            delete placemark.height
                        })
                        waylinesObj[key][item] = placemarks
                    }
                })
            } else {
                waylinesObj[key] = templateJson[key]
            }
        })
        return waylinesObj
    }
    return {
        create,
        createNoramlWaylines,
        createPlanarWayline,
        createPointWayLines,
        save,
    }
}
applications/drone-command/src/utils/cesium/useBoundary.js
@@ -6,19 +6,17 @@
import { useStore } from 'vuex'
import { getDeviceRegion, getDeviceRegionCount } from '@/api/home/aggregation'
import userStore from '@/store/modules/user'
import { areaCodeToArr, getContourByCode } from '@/utils/cesium/mapUtil'
// import { getEllipse } from '@/hooks/useSingleDroneMap/useSingleDroneMap'
import { areaCodeToArr, getContourByCode, getDockPolyLine, getLnglatAltitude } from '@/utils/cesium/mapUtil'
import getBaseConfig from '@/buildConfig/config'
import { getDroneStatusImage } from '@/utils/stateToImageMap/drone'
import {
    GroundCirclePrimitiveManager
} from '@/utils/mapUtils'
import { GroundCirclePrimitiveManager } from '@/utils/mapUtils'
import lowBattery from '@/assets/images/aiNowFly/low-battery.svg'
import mediumBattery from '@/assets/images/aiNowFly/medium-battery.svg'
import fullBattery from '@/assets/images/aiNowFly/full-charge.svg'
const { VITE_APP_BASE, VITE_APP_ENV, VITE_APP_REGION_URL } = import.meta.env
// const { singleDockSystem } = getBaseConfig()
const { singleDockSystem } = getBaseConfig()
/**
 * 使用通用的边界
@@ -26,8 +24,15 @@
 * @param options
 */
export const useBoundary = (viewer, options = {}) => {
    const { multiple = 1.4, dockOptions = {}, boundaryChange, boundaryColor, dockRangeType, useDockHeight = false } = options
    const { scrollShowDock = false, showDock = false } = dockOptions
    const {
        multiple = 1.4,
        dockOptions = {},
        boundaryChange,
        boundaryColor,
        dockRangeType,
        useDockHeight = false,
    } = options
    const { scrollShowDock = false, showDock = false, showDockNameAndBattery = false } = dockOptions
    const manager = new GroundCirclePrimitiveManager()
    manager.init(viewer)
@@ -56,7 +61,7 @@
    }
    // 获取设备列表
    async function getDeviceRegionFun () {
    async function getDeviceRegionFun() {
        const res = await getDeviceRegion({ areaCode: selectedAreaCode })
        return res?.data?.data || []
    }
@@ -100,60 +105,127 @@
        return await res.json()
    }
    function getBase64Image(imgUrl) {
        const img = new Image()
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        return new Promise(resolve => {
            img.onload = function () {
                canvas.width = img.width
                canvas.height = img.height
                ctx.drawImage(img, 0, 0)
                const base64 = canvas.toDataURL('image/png')
                resolve(base64)
            }
            img.src = imgUrl
        })
    }
    function batteryLevelSvg(batteryNum) {
        if (batteryNum <= 30) {
            return lowBattery
        } else if (batteryNum > 30 && batteryNum <= 60) {
            return mediumBattery
        } else {
            return fullBattery
        }
    }
    async function nestSVG(nickname, batteryImgUrl, batteryNum) {
        let txt = nickname.length < 4 ? 130 : nickname.length * 38
        let tranx = nickname.length < 4 ? 86 : nickname.length * 26 + 3
        const batteryBase64Image = await getBase64Image(batteryImgUrl)
        const svg = `
          <svg width="${txt}" height="100" xmlns="http://www.w3.org/2000/svg">
            <g transform="translate(${tranx}, 10)">
                <!-- 电池图标 -->
                <image href="${batteryBase64Image}" x="0" y="2" width="12" height="13"/>
                <!-- 电量值 -->
                <text x="12" y="10" font-size="14" fill="${
                    batteryNum <= 30 ? '#FF604B' : batteryNum >= 60 ? '#40FF5C' : '#00FFF2'
                }"  text-anchor="start" dominant-baseline="middle" font-weight="bolder" font-family="Source Han Sans CN" text-rendering="geometricPrecision">
                    ${batteryNum}%
                </text>
            </g>
          </svg>
        `
        return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
    }
    // 无人机散点渲染
    const droneSplashed = () => {
        let showDockList = showDockSnList === null
            ? _.cloneDeep(initDockList)
            : initDockList.filter(item => showDockSnList.includes(item.device_sn))
        manager.removeAll()
        let showDockList =
            showDockSnList === null
                ? _.cloneDeep(initDockList)
                : initDockList.filter(item => showDockSnList.includes(item.device_sn))
        showDockList.forEach((item, index) => {
        showDockList.forEach(async (item, index) => {
            if (item.status === 'OFFLINE') return
            let polyline = {}
            if (dockRangeType === 2) {
                manager.addCircleOutline({
                    data: {
                        lng: item.longitude,
                        lat: item.latitude
                        lat: item.latitude,
                    },
                    frameColor: Cesium.Color.fromCssColorString('#7FFFD4')
                    frameColor: Cesium.Color.fromCssColorString('#7FFFD4'),
                })
            } else {
                manager.addCircle({
                    data: {
                        lng: item.longitude,
                        lat: item.latitude
                        lat: item.latitude,
                    },
                    materialColor: Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3)
                    materialColor: item.color ? item.color : Cesium.Color.CORNFLOWERBLUE.withAlpha(0.3),
                })
            }
            if (useDockHeight) {
                polyline = {
                    positions: new Cesium.CallbackProperty(() => {
                        const pointPosition = Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, +item.height)
                        if (!pointPosition) return []
                        // 获取点的位置
                        const cartographic = Cesium.Cartographic.fromCartesian(pointPosition)
                        // 获取地形高度
                        const terrainHeight = viewer.scene.globe.getHeight(cartographic) || 0
                        // 创建地面点位置
                        const groundPosition = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, terrainHeight)
                        return [pointPosition, groundPosition]
                    }, false),
                    width: 0.5,
                    material: Cesium.Color.WHITE,
                }
                polyline = getDockPolyLine(item, viewer)
            }
            const batteryImgUrl = batteryLevelSvg(item.capacity_percent)
            const combinedImage = await nestSVG(item.nickname, batteryImgUrl, item.capacity_percent)
            let pointObj = {}
            try {
                pointObj = await getLnglatAltitude(item.longitude, item.latitude, viewer)
            } catch (e) {}
            const position = Cesium.Cartesian3.fromDegrees(
                +item.longitude,
                +item.latitude,
                useDockHeight ? +item.height : pointObj?.height || 0
            )
            if (showDockNameAndBattery) {
                // 带电量
                dockSource.entities.add({
                    position,
                    billboard: {
                        // new Cesium.ConstantProperty(pointData.status === 'OFFLINE' ? endingImg : unSelectedOnline),
                        image: combinedImage,
                    },
                })
            }
            dockSource.entities.add({
                position: Cesium.Cartesian3.fromDegrees(+item.longitude, +item.latitude, !useDockHeight ? 0 : +item.height),
                label: {
                    text: item.nickname,
                    font: 'bold 16px Source Han Sans CN',
                    fillColor: Cesium.Color.WHITE,
                    outlineColor: Cesium.Color.BLACK,
                    outlineWidth: 2,
                    style: Cesium.LabelStyle.FILL_AND_OUTLINE,
                    verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
                    pixelOffset: new Cesium.Cartesian2(0, -24),
                },
                position,
                polyline,
                billboard: {
                    image: getDroneStatusImage(item.status),
                    width: 60,
                    height: 60,
                    eyeOffset: new Cesium.Cartesian3(0, 0, -5)
                    disableDepthTestDistance: Number.POSITIVE_INFINITY,
                    eyeOffset: new Cesium.Cartesian3(0, 0, -5),
                },
            })
        })
@@ -186,7 +258,7 @@
                            positions: positions,
                            width: 5, // 直接设置宽度
                            clampToGround: true,
                            material: Cesium.Color.fromCssColorString(boundaryColor)
                            material: Cesium.Color.fromCssColorString(boundaryColor),
                        },
                        polygon,
                    })
@@ -205,7 +277,7 @@
                        positions: positions,
                        width: 1, // 直接设置宽度
                        clampToGround: true,
                        material: Cesium.Color.fromCssColorString(boundaryColor)
                        material: Cesium.Color.fromCssColorString(boundaryColor),
                    },
                })
            })
@@ -217,7 +289,6 @@
    // 打开边界
    const openContour = async () => {
        const areaCode = selectedAreaCode || userAreaCode
        viewer.scene.postRender.removeEventListener(determineScaling)
        if (!areaCode) return
        const hierarchy = areaCodeToArr(areaCode.slice(0, 6))
@@ -258,25 +329,29 @@
            const outlineGJson = await getContourByCode(areaCode.slice(0, 6))
            scalingJudgment.forEach(item => item.show && (item.outline = outlineGJson))
            renderOutline(scalingJudgment[(hierarchy.length - 3) * -1])
        } catch (e) { }
        viewer.scene.postRender.addEventListener(determineScaling)
        } catch (e) {}
        try {
            viewer?.scene?.postRender?.addEventListener(determineScaling)
        }catch (e) {
        }
    }
    // 关闭边界
    function closeContour () {
    function closeContour() {
        removeAllEntities()
        viewer.scene.postRender.removeEventListener(determineScaling)
    }
    // 飞向边界
    async function flyToBoundary () {
    async function flyToBoundary() {
        // 单机巢系统
        if (singleDockSystem) {
            if (!initDockList.length) {
                initDockList = await getDeviceRegionFun()
            }
            viewer?.camera.flyTo({
                destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 24000),
                destination: Cesium.Cartesian3.fromDegrees(initDockList[0].longitude, initDockList[0].latitude, 22000),
                duration: 0,
            })
            return
@@ -286,7 +361,8 @@
        const dataSource = await Cesium.GeoJsonDataSource.load(gJson)
        if (!dataSource) return
        viewer.dataSources.add(dataSource)
        // 获取多边形边界所有点
        let positionList = []
        dataSource.entities.values.forEach(function (entity) {
            if (entity.polygon) {
                // 获取多边形的边界球
@@ -295,18 +371,16 @@
                    let cartographic = Cesium.Cartographic.fromCartesian(item)
                    let lng = Cesium.Math.toDegrees(cartographic.longitude) // 经度
                    let lat = Cesium.Math.toDegrees(cartographic.latitude) // 纬度
                    return [_.round(lng, 6), _.round(lat, 6)]
                })
                const newBox = boxTransformScale(curPolygonPosition, multiple)
                viewer.camera.flyTo({
                    destination: Cesium.Rectangle.fromDegrees(...newBox),
                    offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
                    duration: 0.5,
                })
                positionList = positionList.concat(curPolygonPosition)
            }
        })
        const newBox = boxTransformScale(positionList, multiple)
        viewer.camera.flyTo({
            destination: Cesium.Rectangle.fromDegrees(...newBox),
            offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90), 0),
            duration: 0.5,
        })
        dataSource.entities.values.forEach(entity => {
@@ -320,7 +394,7 @@
     * 设置要显示的机巢列表
     * @param dockSns - 要显示的机巢设备序列号数组。如果为 undefined,则显示所有已初始化的机巢;如果为空数组,则不显示任何机巢。
     */
    function setShowDock (dockSns) {
    function setShowDock(dockSns) {
        if (dockSns === undefined) {
            showDockSnList = null
        } else if (dockSns.length === 0) {
@@ -328,21 +402,31 @@
        } else {
            showDockSnList = dockSns
        }
        removeDockCover()
        if (active === '县') droneSplashed()
        if (!showDock) return
        if (scrollShowDock ? active === '县' : true) {
            droneSplashed()
        }
    }
    function setDockCoverColor(arr) {
        initDockList.forEach(item => {
            arr.forEach(dock => {
                item.color = dock.device_sn === item.device_sn ? dock.color : null
            })
        })
        if (scrollShowDock ? active === '县' : true) {
            droneSplashed()
        }
    }
    onBeforeUnmount(() => {
        manager.destroy()
    })
    return {
        openContour,
        closeContour,
        flyToBoundary,
        setShowDock
        setShowDock,
        setDockCoverColor,
    }
}
applications/drone-command/src/views/README.md
New file
@@ -0,0 +1,56 @@
# 新增视图目录与页面说明
以下为本次在 `src/views` 目录下新增的一级菜单目录及其二级菜单页面说明,命名遵循项目现有英文目录风格,并尽量避免与现有目录和页面产生冲突。
## 一级菜单与目录
- 数据驾驶舱:`dataCockpit`
  - 页面:`dataCockpit/index.vue`
- 侦测反制:`detectionCountermeasure`
  - 页面:`detectionCountermeasure/index.vue`
- 基础管理:`basicManage`
  - 页面:`basicManage/index.vue`
- 权限管理:`permissionManage`
  - 页面:`permissionManage/index.vue`
- 区域管理:`areaManage`
  - 页面:`areaManage/index.vue`
- 记录管理:`recordManage`
  - 页面:`recordManage/index.vue`
## 二级菜单与页面文件
- 侦测反制(`detectionCountermeasure`)
  - 设备应用配置:`deviceAppConfig.vue`
  - 侦测范围管理:`detectionRange.vue`
  - 任务调度:`taskSchedule.vue`
  - 反制效果评估:`countermeasureEvaluation.vue`
- 基础管理(`basicManage`)
  - 设备出入库管理:`deviceStock.vue`
  - 维护记录管理:`maintainRecord.vue`
  - 设备报废管理:`deviceScrap.vue`
- 权限管理(`permissionManage`)
  - 用户管理:`permissionUser.vue`
  - 部门管理:`permissionDept.vue`
  - 角色管理:`permissionRole.vue`
  - 系统操作日志:`operationLog.vue`
- 区域管理(`areaManage`)
  - 区域划分:`partition.vue`
  - 派出所信息管理:`precinctInfo.vue`
  - 场景配置管理:`sceneConfig.vue`
  - 区域数据统计:`areaStatistics.vue`
  - 防区管理:`defenseZone.vue`
- 记录管理(`recordManage`)
  - 报警记录:`alarmRecords.vue`
  - 历史轨迹:`historyTracks.vue`
## 命名与冲突规避
- 目录名使用英文:与现有 `layerManagement`、`dataCenter` 等保持一致。
- 权限相关目录使用 `permissionManage`,避免与现有 `authority`、`system` 产生语义或路由上的冲突。
- 区域相关目录使用 `areaManage`,避免与现有 `base/region` 路由与页面混淆。
- 新页面均为基础骨架,使用 Element Plus 的 `el-card` 作为占位,便于后续快速填充业务。
## 后续接入建议
- 路由接入:可在 `src/router/views/index.js` 中为上述页面配置静态路由,或保持现有“后端菜单驱动”的模式,在后端返回菜单时指向对应 `src/views` 路径。
- 菜单图标与权限:按现有规则由后端下发,前端通过 `AvueRouter.formatRoutes` 与 `GetButtons` 自动生成与控制。
applications/drone-command/src/views/areaManage/areaStatistics.vue
New file
@@ -0,0 +1,18 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-06 16:37:57
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-07 10:57:46
 * @FilePath     : \applications\drone-command\src\views\areaManage\areaStatistics.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 16:37:57
-->
<template>
  <basic-container>
    区域数据统计
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/defenseZone.vue
New file
@@ -0,0 +1,8 @@
<template>
  <basic-container>
    防区管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss"></style>
applications/drone-command/src/views/areaManage/index.vue
New file
@@ -0,0 +1,9 @@
<template>
   <basic-container>
    区域数据
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/areaManage/partition.vue
New file
@@ -0,0 +1,9 @@
<template>
   <basic-container>
    区域划分
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/areaManage/precinctInfo.vue
New file
@@ -0,0 +1,9 @@
<template>
   <basic-container>
    派出所信息管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/areaManage/sceneConfig.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    场景配置管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/basicManage/deviceScrap.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    设备报废管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/basicManage/deviceStock.vue
New file
@@ -0,0 +1,19 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-06 16:36:40
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-07 10:59:40
 * @FilePath     : \applications\drone-command\src\views\basicManage\deviceStock.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 16:36:40
-->
<template>
  <basic-container>
    设备出入库管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/basicManage/index.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    基础管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/basicManage/maintainRecord.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    维护记录管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/dataCockpit/components/MapContainer.vue
New file
@@ -0,0 +1,13 @@
<template>
    <div>
    </div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>
applications/drone-command/src/views/dataCockpit/index.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    维护记录管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/detectionCountermeasure/countermeasureEvaluation.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    反制效果评估
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/detectionCountermeasure/detectionRange.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    侦测范围管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/detectionCountermeasure/deviceAppConfig.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    设备应用配置
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/detectionCountermeasure/index.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    侦测反制
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/detectionCountermeasure/taskSchedule.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    任务调度
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/permissionManage/index.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    权限管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/permissionManage/operationLog.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    系统操作日志
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/permissionManage/permissionDept.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    部门管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/permissionManage/permissionRole.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    角色管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/permissionManage/permissionUser.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    用户管理
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/recordManage/alarmRecords.vue
New file
@@ -0,0 +1,9 @@
<template>
  <basic-container>
    报警记录
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/recordManage/historyTracks.vue
New file
@@ -0,0 +1,19 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-06 16:38:25
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-07 11:04:23
 * @FilePath     : \applications\drone-command\src\views\recordManage\historyTracks.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 16:38:25
-->
<template>
  <basic-container>
    历史轨迹
  </basic-container>
</template>
<script setup>
</script>
<style scoped lang="scss">
</style>
applications/drone-command/src/views/recordManage/index.vue
New file
@@ -0,0 +1,12 @@
<template>
  <div class="page">
    <el-card header="记录管理"></el-card>
  </div>
</template>
<script setup>
</script>
<style scoped lang="scss">
.page {
  padding: 16px;
}
</style>
applications/drone-command/src/views/wel/components/backlog.vue
File was deleted
applications/drone-command/src/views/wel/components/calendarBox.vue
File was deleted
applications/drone-command/src/views/wel/components/flightStatistics.vue
File was deleted
applications/drone-command/src/views/wel/components/flyratio.vue
File was deleted
applications/drone-command/src/views/wel/components/proportionStatic.vue
File was deleted
applications/drone-command/src/views/wel/components/statistics.vue
File was deleted
applications/drone-command/src/views/wel/components/taskOutcome.vue
File was deleted
applications/drone-command/src/views/wel/dashboard.vue
File was deleted
applications/drone-command/src/views/wel/index.vue
@@ -1,217 +1,23 @@
<!--
 * @Author       : yuan
 * @Date         : 2026-01-06 09:47:09
 * @LastEditors  : yuan
 * @LastEditTime : 2026-01-07 09:20:47
 * @FilePath     : \applications\drone-command\src\views\wel\index.vue
 * @Description  :
 * Copyright 2026 OBKoro1, All Rights Reserved.
 * 2026-01-06 09:47:09
-->
<template>
  <wel-container>
    <div class="workbench" v-if="display">
      <div class="workleft">
        <!-- 设备统计  -->
        <statistics></statistics>
        <!-- 综合统计分析 -->
        <div class="comprehensiveCon">
          <div class="comprehensive">
  <div>
            <div class="center">
              <div class="centerLeft">
                <!-- 工单统计 -->
                <proportionStatic></proportionStatic>
                <!-- 飞行统计 -->
                <flightStatistics></flightStatistics>
              </div>
              <div class="centerRight">
                <!-- 机巢工单数量排名(件) -->
                <flyratio></flyratio>
                <!-- 任务成果 -->
                <taskOutcome></taskOutcome>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="workright">
        <Bocklog></Bocklog>
        <CalenBox></CalenBox>
      </div>
    </div>
  </wel-container>
  </div>
</template>
<script setup>
import taskOutcome from './components/taskOutcome.vue'
import flightStatistics from './components/flightStatistics.vue'
import proportionStatic from './components/proportionStatic.vue'
import flyratio from './components/flyratio.vue'
import Bocklog from './components/backlog.vue'
import CalenBox from './components/calendarBox.vue'
import * as echarts from 'echarts'
import useEchartsResize from '@/hooks/useEchartsResize'
import { mapGetters } from 'vuex'
import { getJobEventByStatus, getJobEventTotal, getFly, getFlyTime } from '@/api/home/index'
import overviewImg1 from '@/assets/images/workbench/tc1.png'
import fy1 from '@/assets/images/workbench/fy1.png'
import statistics from './components/statistics.vue'
import { ElMessage } from 'element-plus'
let checked = ref('CURRENT_YEAR')
const display = ref(false)
let timeListStr = ['本周', '本月', '本年']
let timeListEnum = ['CURRENT_WEEK', 'CURRENT_MONTH', 'CURRENT_YEAR']
const params = ref({
  date_enum: 'CURRENT_YEAR',
  device_sn: '',
  end_date: undefined,
  start_date: undefined,
})
const dateSelect = ref('CURRENT_YEAR')
let timeClick = (item, index) => {
  checked.value = item
  params.value.date_enum = item
  dateSelect.value = item
}
const refresh = () => {
  params.value.date_enum = 'CURRENT_YEAR'
  checked.value = 'CURRENT_YEAR'
  dateSelect.value = 'CURRENT_YEAR'
}
// 跳转
const jumpshebei = () => {
  ElMessage.warning('加急开发中...')
}
const eventTotal = ref(0)
const data = ref([])
onMounted(() => {
  getJobEventTotal().then(res => {
    eventTotal.value = res?.data?.data || 0
  })
  setTimeout(() => {
    display.value = true
  },100)
})
</script>
<style>
.el-font-size {
  font-size: 14px;
}
</style>
<style scoped lang="scss">
.workbench {
  height: 100%;
  flex: 1;
  // padding: 0px 20px 0 10px;
  display: flex;
  justify-content: space-between;
}
<style lang="scss" scoped>
.workleft {
  width: 68%;
  margin-right: 10px;
  height: 100%;
  .comprehensiveCon {
    // background: #ffffff !important;
    height: pxToVh(776);
    border-radius: 8px 8px 8px 8px;
    .comprehensive {
      // padding: 14px 14px 0 21px;
      .title {
        display: flex;
        justify-content: space-between;
        align-items: center;
        .name {
          display: flex;
          align-items: center;
          font-weight: bold;
          font-size: 16px;
          color: #363636;
          font-family: 'Source Han Sans CN';
          span {
            margin-right: 4px;
          }
          img {
            cursor: pointer;
          }
        }
        .arrow {
          cursor: pointer;
        }
        .time-card {
          text-align: center;
          // height: 30px;
          height: pxToVh(30);
          background: #ffffff;
          border-radius: 4px 0px 0px 4px;
          border: 1px solid #e5e5e5;
          font-weight: 400;
          font-size: 14px;
          color: #7c8091;
          display: flex;
          width: 282px;
          margin-left: 15px;
          .card-item {
            width: 94px;
            height: 100%;
            line-height: 28px;
            cursor: pointer;
            font-family: 'Source Han Sans CN';
            font-weight: 400;
            font-size: 14px;
            color: #7c8091;
          }
          .card-item:first-child {
            border-right: 1px solid #e5e5e5;
          }
          .card-item:nth-child(2) {
            border-right: 1px solid #e5e5e5;
          }
          .card-item.active {
            color: #1441ff;
            border: 1px solid #1c5cff;
          }
        }
      }
      // 工、单
      .center {
        display: flex;
        .centerLeft {
          width: 50%;
        }
        .centerRight {
          width: 50%;
        }
      }
    }
  }
}
.workright {
  width: 0;
  flex: 1;
  height: 100%;
  // display: flex;
  // flex-direction: column;
}
</style>
</style>
pnpm-lock.yaml
Diff too large