sean.zhou
2022-11-18 31ad252341e3614daab677a9cf2e37f62489484e
initial v1.3.0
26 files modified
1 files renamed
11 files added
2 files deleted
3030 ■■■■ changed files
package.json 3 ●●●●● patch | view | raw | blame | history
src/App.vue 10 ●●●●● patch | view | raw | blame | history
src/api/device-cmd/index.ts 12 ●●●● patch | view | raw | blame | history
src/api/device-setting/index.ts 24 ●●●●● patch | view | raw | blame | history
src/api/manage.ts 6 ●●●●● patch | view | raw | blame | history
src/api/wayline.ts 74 ●●●● patch | view | raw | blame | history
src/components/GMap.vue 26 ●●●● patch | view | raw | blame | history
src/components/TaskPanel.vue 182 ●●●●● patch | view | raw | blame | history
src/components/devices/device-log/DeviceLogDetailModal.vue 4 ●●●● patch | view | raw | blame | history
src/components/g-map/DeviceSettingBox.vue 242 ●●●●● patch | view | raw | blame | history
src/components/g-map/DeviceSettingPopover.vue 106 ●●●●● patch | view | raw | blame | history
src/components/g-map/DockControlPanel.vue 100 ●●●● patch | view | raw | blame | history
src/components/g-map/useDeviceSetting.ts 56 ●●●●● patch | view | raw | blame | history
src/components/g-map/useDockControl.ts 16 ●●●● patch | view | raw | blame | history
src/components/livestream-agora.vue 176 ●●●●● patch | view | raw | blame | history
src/components/livestream-others.vue 175 ●●●●● patch | view | raw | blame | history
src/components/task/CreatePlan.vue 135 ●●●● patch | view | raw | blame | history
src/components/task/TaskPanel.vue 249 ●●●●● patch | view | raw | blame | history
src/components/task/use-format-task.ts 35 ●●●●● patch | view | raw | blame | history
src/components/task/use-task-progress-event.ts 19 ●●●●● patch | view | raw | blame | history
src/components/wayline-panel.vue 123 ●●●●● patch | view | raw | blame | history
src/event-bus/index.ts 5 ●●●●● patch | view | raw | blame | history
src/hooks/use-g-map-tsa.ts 25 ●●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-home.vue 12 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/dock.vue 2 ●●● patch | view | raw | blame | history
src/pages/page-web/projects/task.vue 28 ●●●●● patch | view | raw | blame | history
src/pages/page-web/projects/wayline.vue 78 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/workspace.vue 57 ●●●● patch | view | raw | blame | history
src/router/index.ts 2 ●●● patch | view | raw | blame | history
src/store/index.ts 37 ●●●● patch | view | raw | blame | history
src/types/airport-tsa.ts 24 ●●●●● patch | view | raw | blame | history
src/types/device-cmd.ts 63 ●●●●● patch | view | raw | blame | history
src/types/device-setting.ts 148 ●●●●● patch | view | raw | blame | history
src/types/device.ts 19 ●●●● patch | view | raw | blame | history
src/types/enums.ts 2 ●●● patch | view | raw | blame | history
src/types/task.ts 97 ●●●●● patch | view | raw | blame | history
src/types/wayline.ts 29 ●●●● patch | view | raw | blame | history
src/utils/device-cmd.ts 126 ●●●●● patch | view | raw | blame | history
src/utils/device-setting.ts 193 ●●●●● patch | view | raw | blame | history
src/utils/error-code/index.ts 310 ●●●●● patch | view | raw | blame | history
package.json
@@ -75,6 +75,7 @@
        "ant-design-vue/es/empty/style/css",
        "ant-design-vue/es/form/style/css",
        "ant-design-vue/es/image/style/css",
        "ant-design-vue/es/input-number/style/css",
        "ant-design-vue/es/input/style/css",
        "ant-design-vue/es/layout/style/css",
        "ant-design-vue/es/menu/style/css",
@@ -94,7 +95,9 @@
        "ant-design-vue/es/tag/style/css",
        "ant-design-vue/es/tooltip/style/css",
        "ant-design-vue/es/tree/style/css",
        "ant-design-vue/es/upload/style/css",
        "axios",
        "lodash",
        "mitt",
        "moment",
        "reconnecting-websocket",
src/App.vue
@@ -1,5 +1,5 @@
<template>
  <div id="demo-app" class="demo-app">
  <div class="demo-app">
    <router-view />
    <!-- <div class="map-wrapper">
      <GMap/>
@@ -26,9 +26,17 @@
.demo-app {
  width: 100%;
  height: 100%;
  .map-wrapper {
    height: 100%;
    width: 100%;
  }
}
</style>
<style lang="scss">
#demo-app {
  width: 100%;
  height: 100%
}
</style>
src/api/device-cmd/index.ts
@@ -1,5 +1,5 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { DeviceCmd } from '/@/types/device-cmd'
import { DeviceCmd, DeviceCmdItemAction } from '/@/types/device-cmd'
const CMD_API_PREFIX = '/control/api/v1'
@@ -8,13 +8,19 @@
  device_cmd: DeviceCmd // 指令
}
export interface PostSendCmdBody {
  action: DeviceCmdItemAction
}
/**
 * 发送机场控制指令
 * @param params
 * @returns
 */
// /control/api/v1/devices/{dock_sn}/jobs/{service_identifier}
export async function postSendCmd (params: SendCmdParams): Promise<IWorkspaceResponse<{}>> {
  const resp = await request.post(`${CMD_API_PREFIX}/devices/${params.dock_sn}/jobs/${params.device_cmd}`)
export async function postSendCmd (params: SendCmdParams, body?: PostSendCmdBody): Promise<IWorkspaceResponse<{}>> {
  const postBody = body || {}
  const resp = await request.post(`${CMD_API_PREFIX}/devices/${params.dock_sn}/jobs/${params.device_cmd}`, {
    ...postBody
  })
  return resp.data
}
src/api/device-setting/index.ts
New file
@@ -0,0 +1,24 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { ELocalStorageKey } from '/@/types'
import { NightLightsStateEnum, DistanceLimitStatus, ObstacleAvoidance } from '/@/types/device-setting'
const MNG_API_PREFIX = '/manage/api/v1'
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
export interface PutDevicePropsBody {
  night_lights_state?: NightLightsStateEnum;// 夜航灯开关
  height_limit?: number;// 限高设置
  distance_limit_status?: DistanceLimitStatus;// 限远开关
  obstacle_avoidance?: ObstacleAvoidance;// 飞行器避障开关设置
}
/**
 * 设置设备属性
 * @param params
 * @returns
 */
//  /manage/api/v1/devices/{{workspace_id}}/devices/{{device_sn}}/property
export async function putDeviceProps (deviceSn: string, body: PutDevicePropsBody): Promise<IWorkspaceResponse<{}>> {
  const resp = await request.put(`${MNG_API_PREFIX}/devices/${workspaceId}/devices/${deviceSn}/property`, body)
  return resp.data
}
src/api/manage.ts
@@ -159,4 +159,10 @@
  })
  const result = await request.get(url)
  return result.data
}
export const changeLivestreamLens = async function (body: {}): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/live/streams/switch`
  const result = await request.post(url, body)
  return result.data
}
src/api/wayline.ts
@@ -1,14 +1,9 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1'
import request, { IPage, IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
import { TaskType, TaskStatus, OutOfControlAction } from '/@/types/task'
import { WaylineType } from '/@/types/wayline'
export interface CreatePlan {
  name: string,
  file_id: string,
  dock_sn: string,
  immediate: boolean,
  type: string,
}
const HTTP_PREFIX = '/wayline/api/v1'
// Get Wayline Files
export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> {
@@ -24,12 +19,11 @@
  if (result.data.type === 'application/json') {
    const reader = new FileReader()
    reader.onload = function (e) {
      let text = reader.result as string
      const text = reader.result as string
      const result = JSON.parse(text)
      message.error(result.message)
    }
    reader.readAsText(result.data, 'utf-8')
    return
  } else {
    return result.data
  }
@@ -42,6 +36,17 @@
  return result.data
}
export interface CreatePlan {
  name: string,
  file_id: string,
  dock_sn: string,
  task_type: TaskType, // 任务类型
  wayline_type: WaylineType, // 航线类型
  execute_time?: number // 执行时间(毫秒)
  rth_altitude: number // 相对机场返航高度 20 - 500
  out_of_control_action: OutOfControlAction // 失控动作
}
// Create Wayline Job
export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks`
@@ -49,16 +54,53 @@
  return result.data
}
export interface Task {
  job_id: string,
  job_name: string,
  task_type: TaskType, // 任务类型
  file_id: string, // 航线文件id
  file_name: string, // 航线名称
  wayline_type: WaylineType, // 航线类型
  dock_sn: string,
  dock_name: string,
  workspace_id: string,
  username: string,
  execute_time: string,
  end_time: string,
  status: TaskStatus, // 任务状态
  progress: number, // 执行进度
  code: number, // 错误码
  rth_altitude: number // 相对机场返航高度 20 - 500
  out_of_control_action: OutOfControlAction // 失控动作
}
// Get Wayline Jobs
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IWorkspaceResponse<any>> {
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IListWorkspaceResponse<Task>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}`
  const result = await request.get(url)
  return result.data
}
// Execute Wayline Job
export const executeWaylineJobs = async function (workspaceId: string, plan_id: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${plan_id}`
  const result = await request.post(url)
export interface DeleteTaskParams {
  job_id: string
}
// 取消机场任务
export async function deleteTask (workspaceId: string, params: DeleteTaskParams): Promise<IWorkspaceResponse<{}>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs`
  const result = await request.delete(url, {
    params: params
  })
  return result.data
}
// Upload Wayline file
export const importKmzFile = async function (workspaceId: string, file: {}): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/file/upload`
  const result = await request.post(url, file, {
    headers: {
      'Content-Type': 'multipart/form-data',
    }
  })
  return result.data
}
src/components/GMap.vue
@@ -182,12 +182,12 @@
            <a-row>
              <a-col span="12">
                <a-tooltip title="Network State">
                  <span :style="deviceInfo.dock.network_state.quality === 2 ? 'color: #00ee8b' :
                    deviceInfo.dock.network_state.quality === 1 ? 'color: yellow' : 'color: red'">
                    <span v-if="deviceInfo.dock.network_state.type === 1"><SignalFilled /></span>
                  <span :style="deviceInfo.dock.network_state?.quality === 2 ? 'color: #00ee8b' :
                    deviceInfo.dock.network_state?.quality === 1 ? 'color: yellow' : 'color: red'">
                    <span v-if="deviceInfo.dock.network_state?.type === 1"><SignalFilled /></span>
                    <span v-else><GlobalOutlined /></span>
                  </span>
                  <span class="ml10" >{{ deviceInfo.dock.network_state.rate }} KB/S</span>
                  <span class="ml10" >{{ deviceInfo.dock.network_state?.rate }} KB/S</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
@@ -199,13 +199,13 @@
              <a-col span="6">
                <a-tooltip>
                  <template #title>
                    <p>total: {{ deviceInfo.dock.storage.total }}</p>
                    <p>used: {{ deviceInfo.dock.storage.used  }}</p>
                    <p>total: {{ deviceInfo.dock.storage?.total }}</p>
                    <p>used: {{ deviceInfo.dock.storage?.used  }}</p>
                  </template>
                  <span><FolderOpenOutlined /></span>
                  <span class="ml10" v-if="deviceInfo.dock.storage.total > 0">
                    <a-progress type="circle" :width="20" :percent="deviceInfo.dock.storage.used * 100/ deviceInfo.dock.storage.total"
                      :strokeWidth="20" :showInfo="false" :strokeColor="deviceInfo.dock.storage.used * 100 / deviceInfo.dock.storage.total > 80 ? 'red' : '#00ee8b' "/>
                  <span class="ml10" v-if="deviceInfo.dock.storage?.total > 0">
                    <a-progress type="circle" :width="20" :percent="deviceInfo.dock.storage?.used * 100/ deviceInfo.dock.storage?.total"
                      :strokeWidth="20" :showInfo="false" :strokeColor="deviceInfo.dock.storage?.used * 100 / deviceInfo.dock.storage?.total > 80 ? 'red' : '#00ee8b' "/>
                  </span>
                </a-tooltip>
              </a-col>
@@ -264,8 +264,8 @@
            </a-row>
            <a-row class="p5">
              <a-col span="24">
                <a-button type="primary" :disabled="controlPanelVisible" size="small" @click="dockDebugOnOff(osdVisible.gateway_sn, true)">
                  远程调试
                <a-button type="primary" :disabled="controlPanelVisible" size="small" @click="setControlPanelVisible(true)">
                  设备操作
                </a-button>
              </a-col>
            </a-row>
@@ -589,6 +589,7 @@
        }
      }
      if (data.currentType === EDeviceTypeName.Dock && data.dockInfo[data.currentSn]) {
        deviceTsaUpdateHook.value.initMarker(EDeviceTypeName.Dock, EDeviceTypeName.Dock, data.currentSn, data.dockInfo[data.currentSn].longitude, data.dockInfo[data.currentSn].latitude)
        if (osdVisible.value.visible && osdVisible.value.is_dock && osdVisible.value.gateway_sn !== '') {
          deviceInfo.dock = data.dockInfo[osdVisible.value.gateway_sn]
          deviceInfo.device = data.deviceInfo[deviceInfo.dock.sub_device?.device_sn]
@@ -839,6 +840,7 @@
      EDockModeCode,
      controlPanelVisible,
      dockDebugOnOff,
      setControlPanelVisible,
    }
  }
})
@@ -883,7 +885,7 @@
}
.osd-panel {
  position: absolute;
  left: 350px;
  left: 10px;
  top: 10px;
  width: 480px;
  background: black;
src/components/TaskPanel.vue
File was deleted
src/components/devices/device-log/DeviceLogDetailModal.vue
@@ -8,7 +8,7 @@
    <div class="device-log-detail-wrap">
      <div class="device-log-list">
        <div class="log-list-item">
          <a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id"  size="small" @click="onDownloadLog(airportTableLogState.logList.file_id)">
          <a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id || !airportTableLogState.logList?.object_key"  size="small" @click="onDownloadLog(airportTableLogState.logList.file_id)">
             下载机场日志
          </a-button>
          <a-table  :columns="airportLogColumns"
@@ -26,7 +26,7 @@
          </a-table>
        </div>
        <div class="log-list-item">
          <a-button type="primary"  class="download-btn" :disabled="!droneTableLogState.logList?.file_id" size="small" @click="onDownloadLog(droneTableLogState.logList.file_id)">
          <a-button type="primary"  class="download-btn" :disabled="!droneTableLogState.logList?.file_id || !droneTableLogState.logList?.object_key" size="small" @click="onDownloadLog(droneTableLogState.logList.file_id)">
             下载飞行器日志
          </a-button>
          <a-table  :columns="droneLogColumns"
src/components/g-map/DeviceSettingBox.vue
New file
@@ -0,0 +1,242 @@
<template>
  <div class="device-setting-wrapper">
    <div class="device-setting-header">设备属性设置</div>
    <div class="device-setting-box">
      <!-- 飞行器夜航灯 -->
      <div class="control-setting-item">
        <div class="control-setting-item-left">
          <div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].label }}</div>
          <div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value }}</div>
        </div>
        <div class="control-setting-item-right">
          <DeviceSettingPopover
            :visible="deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].popConfirm.visible"
            :loading="deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].popConfirm.loading"
            @confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)"
            @cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)"
          >
            <template #formContent>
              <div class="form-content">
                <span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].label }}:</span>
                <a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.nightLightsState" />
              </div>
            </template>
            <a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)">Edit</a>
          </DeviceSettingPopover>
        </div>
      </div>
      <!-- 限高 -->
      <div class="control-setting-item">
        <div class="control-setting-item-left">
          <div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].label }}</div>
          <div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].value }}</div>
        </div>
        <div class="control-setting-item-right">
          <DeviceSettingPopover
            :visible="deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].popConfirm.visible"
            :loading="deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].popConfirm.loading"
            @confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)"
            @cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)"
          >
            <template #formContent>
              <div class="form-content">
                <span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].label }}:</span>
                <a-input-number v-model:value="deviceSettingFormModel.heightLimit" :min="20" :max="1500" />
                m
              </div>
            </template>
            <a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)">Edit</a>
          </DeviceSettingPopover>
        </div>
      </div>
      <!-- 限远 -->
      <div class="control-setting-item">
        <div class="control-setting-item-left">
          <div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].label }}</div>
          <div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value }}</div>
        </div>
        <div class="control-setting-item-right">
          <DeviceSettingPopover
            :visible="deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].popConfirm.visible"
            :loading="deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].popConfirm.loading"
            @confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)"
            @cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)"
          >
            <template #formContent>
              <div class="form-content">
                <span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].label }}:</span>
                <a-switch style="margin-right: 10px;" checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.distanceLimitStatus.state" />
                <a-input-number v-model:value="deviceSettingFormModel.distanceLimitStatus.distanceLimit" :min="15" :max="8000" />
                m
              </div>
            </template>
            <a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)">Edit</a>
          </DeviceSettingPopover>
        </div>
      </div>
      <!-- 水平避障 -->
      <div class="control-setting-item">
        <div class="control-setting-item-left">
          <div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].label }}</div>
          <div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value }}</div>
        </div>
        <div class="control-setting-item-right">
          <DeviceSettingPopover
            :visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].popConfirm.visible"
            :loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].popConfirm.loading"
            @confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)"
            @cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)"
          >
            <template #formContent>
              <div class="form-content">
                <span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].label }}:</span>
                <a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.obstacleAvoidanceHorizon" />
              </div>
            </template>
            <a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)">Edit</a>
          </DeviceSettingPopover>
        </div>
      </div>
      <!-- 上视避障 -->
      <div class="control-setting-item">
        <div class="control-setting-item-left">
          <div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].label }}</div>
          <div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value }}</div>
        </div>
        <div class="control-setting-item-right">
          <DeviceSettingPopover
            :visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].popConfirm.visible"
            :loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].popConfirm.loading"
            @confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)"
            @cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)"
          >
            <template #formContent>
              <div class="form-content">
                <span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].label }}:</span>
                <a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.obstacleAvoidanceUpside" />
              </div>
            </template>
            <a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)">Edit</a>
          </DeviceSettingPopover>
        </div>
      </div>
      <!-- 下视避障 -->
      <div class="control-setting-item">
        <div class="control-setting-item-left">
          <div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].label }}</div>
          <div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value }}</div>
        </div>
        <div class="control-setting-item-right">
          <DeviceSettingPopover
            :visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].popConfirm.visible"
            :loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].popConfirm.loading"
            @confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)"
            @cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)"
          >
            <template #formContent>
              <div class="form-content">
                <span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].label }}:</span>
                <a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.obstacleAvoidanceDownside" />
              </div>
            </template>
            <a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)">Edit</a>
          </DeviceSettingPopover>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue'
import { DeviceInfoType } from '/@/types/device'
import { useMyStore } from '/@/store'
import { cloneDeep } from 'lodash'
import { initDeviceSetting, initDeviceSettingFormModel, DeviceSettingKeyEnum } from '/@/types/device-setting'
import { updateDeviceSettingInfoByOsd, updateDeviceSettingFormModelByOsd } from '/@/utils/device-setting'
import { useDeviceSetting } from './useDeviceSetting'
import DeviceSettingPopover from './DeviceSettingPopover.vue'
const props = defineProps<{
  sn: string,
  deviceInfo: DeviceInfoType,
}>()
const store = useMyStore()
const deviceSetting = ref(cloneDeep(initDeviceSetting))
const deviceSettingFormModelFromOsd = ref(cloneDeep(initDeviceSettingFormModel))
const deviceSettingFormModel = ref(cloneDeep(initDeviceSettingFormModel)) // 真实使用的formModel
// 根据设备osd信息更新信息
watch(() => props.deviceInfo, (value) => {
  updateDeviceSettingInfoByOsd(deviceSetting.value, value)
  updateDeviceSettingFormModelByOsd(deviceSettingFormModelFromOsd.value, value)
  // console.log('deviceInfo', value)
}, {
  immediate: true,
  deep: true
})
function onShowPopConfirm (settingKey: DeviceSettingKeyEnum) {
  deviceSetting.value[settingKey].popConfirm.visible = true
  deviceSettingFormModel.value = cloneDeep(deviceSettingFormModelFromOsd.value)
}
function onCancel (settingKey: DeviceSettingKeyEnum) {
  deviceSetting.value[settingKey].popConfirm.visible = false
}
async function onConfirm (settingKey: DeviceSettingKeyEnum) {
  deviceSetting.value[settingKey].popConfirm.loading = true
  const body = genDevicePropsBySettingKey(settingKey, deviceSettingFormModel.value)
  await setDeviceProps(props.sn, body)
  deviceSetting.value[settingKey].popConfirm.loading = false
  deviceSetting.value[settingKey].popConfirm.visible = false
}
// 更新设备属性
const {
  genDevicePropsBySettingKey,
  setDeviceProps,
} = useDeviceSetting()
</script>
<style lang='scss' scoped>
.device-setting-wrapper{
  border-bottom: 1px solid #515151;
  .device-setting-header{
    font-size: 14px;
    font-weight: 600;
    padding: 10px 10px 0px;
  }
  .device-setting-box{
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    padding: 4px 10px;
    .control-setting-item{
      width: 220px;
      height: 58px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      border: 1px solid #666;
      margin: 4px 0;
      padding: 0 8px;
      .control-setting-item-left{
        display: flex;
        flex-direction: column;
        .item-label{
          font-weight: 700;
        }
      }
    }
  }
}
</style>
src/components/g-map/DeviceSettingPopover.vue
New file
@@ -0,0 +1,106 @@
<template>
  <a-popover :visible="state.sVisible"
             trigger="click"
             v-bind="$attrs"
             :overlay-class-name="overlayClassName"
             placement="bottom"
             @visibleChange=";"
             v-on="$attrs">
    <template #content>
      <div class="title-content">
      </div>
      <slot name="formContent" />
      <div class="uranus-popconfirm-btns">
        <a-button size="sm"
           @click="onCancel">
           {{ cancelText || '取消'}}
        </a-button>
        <a-button size="sm"
          :loading="loading"
          type="primary"
          class="confirm-btn"
          @click="onConfirm">
          {{ okText || '确定' }}
        </a-button>
      </div>
    </template>
    <template v-if="$slots.default">
      <slot></slot>
    </template>
  </a-popover>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, reactive, watch, computed } from 'vue'
const props = defineProps<{
    visible?: boolean,
    loading?: Boolean,
    disabled?: Boolean,
    title?: String,
    okText?: String,
    cancelText?: String,
    width?: Number,
}>()
const emit = defineEmits(['cancel', 'confirm'])
const state = reactive({
  sVisible: false,
  loading: false,
})
watch(() => props.visible, (val) => {
  state.sVisible = val || false
})
const loading = computed(() => {
  return props.loading
})
const okLabel = computed(() => {
  return props.loading ? '' : '确定'
})
const overlayClassName = computed(() => {
  const classList = ['device-setting-popconfirm']
  return classList.join(' ')
})
function onConfirm (e: Event) {
  if (props.disabled) {
    return
  }
  emit('confirm', e)
}
function onCancel (e: Event) {
  state.sVisible = false
  emit('cancel', e)
}
</script>
<style lang="scss">
.device-setting-popconfirm {
  min-width: 300px;
  .uranus-popconfirm-btns{
    display: flex;
    padding: 10px 0px;
    justify-content: flex-end;
    .confirm-btn{
      margin-left: 10px;
    }
  }
  .form-content{
    display: inline-flex;
    align-items: center;
    .form-label{
      padding-right: 10px;
    }
  }
}
</style>
src/components/g-map/DockControlPanel.vue
@@ -2,24 +2,32 @@
<div class="dock-control-panel">
  <!-- title -->
  <div class="dock-control-panel-header fz16 pl5 pr5 flex-align-center flex-row flex-justify-between">
    <span>远程调试 {{ props.sn}}</span>
    <span>设备操作 {{ props.sn}}</span>
    <span @click="closeControlPanel">
    <CloseOutlined />
    </span>
  </div>
  <!-- setting -->
  <DeviceSettingBox :sn="props.sn" :deviceInfo="props.deviceInfo"></DeviceSettingBox>
  <!-- cmd -->
  <div class="control-cmd-wrapper">
    <div v-for="(cmdItem, index) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
     <div class="control-cmd-item-left">
        <div class="item-label">{{ cmdItem.label }}</div>
        <div class="item-status">{{ cmdItem.status }}</div>
     </div>
     <div class="control-cmd-item-right">
        <a-button :loading="cmdItem.loading" size="small" type="primary" @click="sendControlCmd(cmdItem, index)">
        {{ cmdItem.operateText }}
        </a-button>
     </div>
   </div>
    <div class="control-cmd-header">
      远程调试
      <a-switch class="debug-btn" checked-children="开" un-checked-children="关" v-model:checked="debugStatus" @change="onDeviceStatusChange"/>
    </div>
    <div class="control-cmd-box">
      <div v-for="(cmdItem, index) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
        <div class="control-cmd-item-left">
            <div class="item-label">{{ cmdItem.label }}</div>
            <div class="item-status">{{ cmdItem.status }}</div>
        </div>
        <div class="control-cmd-item-right">
            <a-button :disabled="!debugStatus || cmdItem.disabled" :loading="cmdItem.loading" size="small" type="primary" @click="sendControlCmd(cmdItem, index)">
            {{ cmdItem.operateText }}
            </a-button>
        </div>
      </div>
    </div>
 </div>
</div>
@@ -35,6 +43,7 @@
import { cmdList as baseCmdList, DeviceCmdItem } from '/@/types/device-cmd'
import { useMyStore } from '/@/store'
import { updateDeviceCmdInfoByOsd, updateDeviceCmdInfoByExecuteInfo } from '/@/utils/device-cmd'
import DeviceSettingBox from './DeviceSettingBox.vue'
const props = defineProps<{
  sn: string,
@@ -71,14 +80,34 @@
}
// dock 控制指令
const debugStatus = ref(false)
async function onDeviceStatusChange (status: boolean) {
  let result = false
  if (status) {
    result = await dockDebugOnOff(props.sn, true)
  } else {
    result = await dockDebugOnOff(props.sn, false)
  }
  if (!result) {
    if (status) {
      debugStatus.value = false
    } else {
      debugStatus.value = true
    }
  }
}
const {
  sendDockControlCmd,
  dockDebugOnOff
} = useDockControl()
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
  const success = await sendDockControlCmd({
    sn: props.sn,
    cmd: cmdItem.cmdKey
    cmd: cmdItem.cmdKey,
    action: cmdItem.action
  }, true)
  if (success) {
    // updateDeviceSingleCmdInfo(cmdList.value[index])
@@ -103,26 +132,39 @@
  }
  .control-cmd-wrapper{
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    padding: 4px 10px;
    .control-cmd-item{
      width: 220px;
      height: 58px;
    .control-cmd-header{
      font-size: 14px;
      font-weight: 600;
      padding: 10px 10px 0px;
      .debug-btn{
        margin-left: 10px;
        border:1px solid #585858;
      }
    }
    .control-cmd-box{
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      justify-content: space-between;
      border: 1px solid #666;
      margin: 4px 0;
      padding: 0 8px;
      .control-cmd-item-left{
      padding: 4px 10px;
      .control-cmd-item{
        width: 220px;
        height: 58px;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-between;
        border: 1px solid #666;
        margin: 4px 0;
        padding: 0 8px;
        .item-label{
          font-weight: 700;
        .control-cmd-item-left{
          display: flex;
          flex-direction: column;
          .item-label{
            font-weight: 700;
          }
        }
      }
    }
src/components/g-map/useDeviceSetting.ts
New file
@@ -0,0 +1,56 @@
import { message } from 'ant-design-vue'
import { putDeviceProps, PutDevicePropsBody } from '/@/api/device-setting'
import { DeviceSettingKeyEnum, DeviceSettingFormModel, ObstacleAvoidanceStatusEnum, NightLightsStateEnum, DistanceLimitStatusEnum } from '/@/types/device-setting'
export function useDeviceSetting () {
  // 生成参数
  function genDevicePropsBySettingKey (key: DeviceSettingKeyEnum, fromModel: DeviceSettingFormModel) {
    const body = {} as PutDevicePropsBody
    if (key === DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET) {
      body.night_lights_state = fromModel.nightLightsState ? NightLightsStateEnum.OPEN : NightLightsStateEnum.CLOSE
    } else if (key === DeviceSettingKeyEnum.HEIGHT_LIMIT_SET) {
      body.height_limit = fromModel.heightLimit
    } else if (key === DeviceSettingKeyEnum.DISTANCE_LIMIT_SET) {
      body.distance_limit_status = {}
      if (fromModel.distanceLimitStatus.state) {
        body.distance_limit_status.state = DistanceLimitStatusEnum.SET
        body.distance_limit_status.distance_limit = fromModel.distanceLimitStatus.distanceLimit
      } else {
        body.distance_limit_status.state = DistanceLimitStatusEnum.UNSET
      }
    } else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON) {
      body.obstacle_avoidance = {
        horizon: fromModel.obstacleAvoidanceHorizon ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
      }
    } else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE) {
      body.obstacle_avoidance = {
        upside: fromModel.obstacleAvoidanceUpside ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
      }
    } else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE) {
      body.obstacle_avoidance = {
        downside: fromModel.obstacleAvoidanceDownside ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
      }
    }
    return body
  }
  // 设置设备属性
  async function setDeviceProps (sn: string, body: PutDevicePropsBody) {
    try {
      const { code, message: msg } = await putDeviceProps(sn, body)
      if (code === 0) {
        // message.success('指令发送成功')
        return true
      }
      throw (msg)
    } catch (e) {
      message.error('设备属性设置失败')
      return false
    }
  }
  return {
    genDevicePropsBySettingKey,
    setDeviceProps
  }
}
src/components/g-map/useDockControl.ts
@@ -1,7 +1,7 @@
import { message } from 'ant-design-vue'
import { ref } from 'vue'
import { postSendCmd } from '/@/api/device-cmd'
import { DeviceCmd } from '/@/types/device-cmd'
import { DeviceCmd, DeviceCmdItemAction } from '/@/types/device-cmd'
export function useDockControl () {
  const controlPanelVisible = ref(false)
@@ -12,22 +12,30 @@
  // 远程调试开关
  async function dockDebugOnOff (sn: string, on: boolean) {
    const success = await sendDockControlCmd({
    const result = await sendDockControlCmd({
      sn: sn,
      cmd: on ? DeviceCmd.DebugModeOpen : DeviceCmd.DebugModeClose
    }, false)
    if (success) {
    if (result) {
      setControlPanelVisible(on)
    }
    return result
  }
  // 发送指令
  async function sendDockControlCmd (params: {
    sn: string,
    cmd: DeviceCmd
    action?: DeviceCmdItemAction
  }, tip = true) {
    try {
      const { code, message: msg } = await postSendCmd({ dock_sn: params.sn, device_cmd: params.cmd })
      let body = undefined as any
      if (params.action !== undefined) {
        body = {
          action: params.action
        }
      }
      const { code, message: msg } = await postSendCmd({ dock_sn: params.sn, device_cmd: params.cmd }, body)
      if (code === 0) {
        tip && message.success('指令发送成功')
        return true
src/components/livestream-agora.vue
@@ -3,15 +3,23 @@
    <div id="player" style="width: 720px; height: 420px; border: 1px solid"></div>
    <p class="fz24">Live streaming source selection</p>
    <div class="flex-row flex-justify-center flex-align-center mt10">
      <template v-if="livePara.liveState && dronePara.isDockLive">
        <span class="mr10">Lens:</span>
        <a-radio-group v-model:value="dronePara.lensSelected" button-style="solid">
          <a-radio-button v-for="lens in dronePara.lensList" :key="lens" :value="lens">{{lens}}</a-radio-button>
        </a-radio-group>
      </template>
      <template v-else>
      <a-select
        style="width:150px"
        placeholder="Select Drone"
        @select="onDroneSelect"
        v-model:value="dronePara.droneSelected"
      >
        <a-select-option
          v-for="item in dronePara.droneList"
          :key="item.value"
          :value="item.value"
          @click="onDroneSelect(item)"
          >{{ item.label }}</a-select-option
        >
      </a-select>
@@ -19,12 +27,13 @@
        class="ml10"
        style="width:150px"
        placeholder="Select Camera"
        @select="onCameraSelect"
        v-model:value="dronePara.cameraSelected"
      >
        <a-select-option
          v-for="item in dronePara.cameraList"
          :key="item.value"
          :value="item.value"
          @click="onCameraSelect(item)"
          >{{ item.label }}</a-select-option
        >
      </a-select>
@@ -35,13 +44,13 @@
        @select="onVideoSelect"
      >
        <a-select-option
          class="ml10"
          v-for="item in dronePara.videoList"
          :key="item.value"
          :value="item.value"
          >{{ item.label }}</a-select-option
        >
      </a-select> -->
      </template>
      <a-select
        class="ml10"
        style="width:150px"
@@ -60,12 +69,16 @@
      Note: Obtain The Following Parameters From https://console.agora.io
    </p>
    <div class="flex-row flex-justify-center flex-align-center">
      <span class="mr10">AppId:</span>
      <a-input v-model:value="agoraPara.appid" placeholder="APP ID"></a-input>
      <span class="ml10">Token:</span>
      <a-input
        class="ml10"
        v-model:value="agoraPara.token"
        placeholder="Token"
        @change="encodeToken"
      ></a-input>
      <span class="ml10">Channel:</span>
      <a-input
        class="ml10"
        v-model:value="agoraPara.channel"
@@ -73,14 +86,15 @@
      ></a-input>
    </div>
    <div class="mt20 flex-row flex-justify-center flex-align-center">
      <a-button type="primary" large @click="onStart">Play</a-button>
      <a-button v-if="livePara.liveState && dronePara.isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
      <a-button v-else type="primary" large @click="onStart">Play</a-button>
      <a-button class="ml20" type="primary" large @click="onStop"
        >Stop</a-button
      >
      <a-button class="ml20" type="primary" large @click="onUpdateQuality"
        >Update Clarity</a-button
      >
      <a-button class="ml20" type="primary" large @click="onRefresh"
      <a-button v-if="!livePara.liveState || !dronePara.isDockLive" class="ml20" type="primary" large @click="onRefresh"
        >Refresh Live Capacity</a-button
      >
    </div>
@@ -91,8 +105,9 @@
import AgoraRTC, { IAgoraRTCClient, IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
import { message } from 'ant-design-vue'
import { onMounted, reactive } from 'vue'
import { uuidv4 } from '../utils/uuid'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
const root = getRoot()
@@ -120,6 +135,12 @@
  }
]
interface SelectOption {
  value: any,
  label: string,
  more?: any
}
let agoraClient = {} as IAgoraRTCClient
const agoraPara = reactive({
  appid: config.agoraAPPID,
@@ -130,13 +151,16 @@
})
const dronePara = reactive({
  livestreamSource: [],
  droneList: [] as any[],
  cameraList: [] as any[],
  videoList: [] as any[],
  droneSelected: '',
  cameraSelected: '',
  videoSelected: '',
  claritySelected: 0
  droneList: [] as SelectOption[],
  cameraList: [] as SelectOption[],
  videoList: [] as SelectOption[],
  droneSelected: undefined as string | undefined,
  cameraSelected: undefined as string | undefined,
  videoSelected: undefined as string | undefined,
  claritySelected: 0,
  lensList: [] as string[],
  lensSelected: undefined as string | undefined,
  isDockLive: false
})
const livePara = reactive({
  url: '',
@@ -144,14 +168,15 @@
  videoId: '',
  liveState: false
})
const nonSwitchable = 'normal'
const onRefresh = async () => {
  dronePara.droneList = []
  dronePara.cameraList = []
  dronePara.videoList = []
  dronePara.droneSelected = ''
  dronePara.cameraSelected = ''
  dronePara.videoSelected = ''
  dronePara.droneSelected = undefined
  dronePara.cameraSelected = undefined
  dronePara.videoSelected = undefined
  await getLiveCapacity({})
    .then(res => {
      if (res.code === 0) {
@@ -166,18 +191,20 @@
        if (dronePara.livestreamSource) {
          dronePara.livestreamSource.forEach((ele: any) => {
            dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn })
            dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
          })
        }
      }
    })
    .catch(error => {
      message.error(error)
      console.error(error)
    })
}
onMounted(() => {
  onRefresh()
  agoraPara.token = encodeURIComponent(agoraPara.token)
  agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'vp8' })
  // Subscribe when a remote user publishes a stream
  agoraClient.on('user-joined', async (user: IAgoraRTCRemoteUser) => {
@@ -191,13 +218,17 @@
      const remoteVideoTrack = user.videoTrack!
      // Dynamically create a container in the form of a DIV element for playing the remote video track.
      const remotePlayerContainer: any = document.getElementById('player')
      // remotePlayerContainer.id = agoraPara.uid
      remotePlayerContainer.id = user.uid.toString()
      remoteVideoTrack.play(remotePlayerContainer)
    }
  })
  agoraClient.on('user-unpublished', async (user: any) => {
    console.log('unpublish live:', user)
    message.info('unpublish live')
  })
  agoraClient.on('exception', async (e: any) => {
    console.log(e)
    message.error(e.msg)
  })
})
@@ -207,7 +238,9 @@
const handleJoinChannel = (uid: any) => {
  agoraPara.uid = uid
}
const encodeToken = (e: any) => {
  agoraPara.token = encodeURIComponent(agoraPara.token)
}
const onStart = async () => {
  const that = this
  console.log(
@@ -222,13 +255,12 @@
  if (
    dronePara.droneSelected == null ||
    dronePara.cameraSelected == null ||
    dronePara.videoSelected == null ||
    dronePara.claritySelected == null
  ) {
    message.warn('waring: not select live para!!!')
    return
  }
  agoraClient.setClientRole('audience', { level: 1 })
  agoraClient.setClientRole('audience', { level: 2 })
  if (agoraClient.connectionState === 'DISCONNECTED') {
    agoraClient
      .join(agoraPara.appid, agoraPara.channel, agoraPara.token)
@@ -236,11 +268,8 @@
  livePara.videoId =
    dronePara.droneSelected +
    '/' +
    dronePara.cameraSelected +
    '/' +
    dronePara.videoSelected
    dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
  console.log(agoraPara)
  agoraPara.token = encodeURIComponent(agoraPara.token)
  livePara.url =
    'channel=' +
@@ -259,6 +288,9 @@
    video_quality: dronePara.claritySelected
  })
    .then(res => {
      if (res.code !== 0) {
        return
      }
      livePara.liveState = true
    })
    .catch(err => {
@@ -269,9 +301,8 @@
  livePara.videoId =
    dronePara.droneSelected +
    '/' +
    dronePara.cameraSelected +
    '/' +
    dronePara.videoSelected
    dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
  stopLivestream({
    video_id: livePara.videoId
  }).then(res => {
@@ -279,52 +310,52 @@
      message.success(res.message)
    }
    livePara.liveState = false
    dronePara.lensSelected = ''
    console.log('stop play livestream')
  })
}
const onDroneSelect = (val: any) => {
  dronePara.droneSelected = val
  if (dronePara.droneSelected) {
    const droneTemp = dronePara.livestreamSource
    dronePara.cameraList = []
const onDroneSelect = (val: SelectOption) => {
  dronePara.cameraList = []
  dronePara.videoList = []
  dronePara.lensList = []
    droneTemp.forEach((ele: any) => {
      const drone = ele
      if (drone.cameras_list && drone.sn === dronePara.droneSelected) {
        const cameraListTemp = drone.cameras_list
        cameraListTemp.forEach((ele: any) => {
          dronePara.cameraList.push({ label: ele.name, value: ele.index })
        })
      }
    })
  dronePara.cameraSelected = undefined
  dronePara.videoSelected = undefined
  dronePara.lensSelected = undefined
  dronePara.droneSelected = val.value
  if (!val.more) {
    return
  }
  val.more.forEach((ele: any) => {
    dronePara.cameraList.push({ label: ele.name, value: ele.index, more: ele.videos_list })
  })
}
const onCameraSelect = (val: any) => {
  dronePara.cameraSelected = val
const onCameraSelect = (val: SelectOption) => {
  dronePara.cameraSelected = val.value
  dronePara.videoSelected = undefined
  dronePara.lensSelected = undefined
  dronePara.videoList = []
  dronePara.lensList = []
  if (!val.more) {
    return
  }
  if (dronePara.cameraSelected) {
    const droneTemp = dronePara.livestreamSource
    droneTemp.forEach((ele: any) => {
      const drone = ele
      if (drone.sn === dronePara.droneSelected) {
        const cameraListTemp = drone.cameras_list
        cameraListTemp.forEach((ele: any) => {
          const camera = ele
          if (camera.index === dronePara.cameraSelected) {
            const videoListTemp = camera.videos_list
            dronePara.videoList = []
            videoListTemp.forEach((ele: any) => {
              dronePara.videoList.push({ label: ele.type, value: ele.index })
            })
            dronePara.videoSelected = dronePara.videoList[0]?.value
          }
        })
      }
    })
  val.more.forEach((ele: any) => {
    dronePara.videoList.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
  })
  if (dronePara.videoList.length === 0) {
    return
  }
  const firstVideo: SelectOption = dronePara.videoList[0]
  dronePara.videoSelected = firstVideo.value
  dronePara.lensList = firstVideo.more
  dronePara.lensSelected = firstVideo.label
  dronePara.isDockLive = dronePara.lensList.length > 0
}
const onVideoSelect = (val: any) => {
  dronePara.videoSelected = val
const onVideoSelect = (val: SelectOption) => {
  dronePara.videoSelected = val.value
  dronePara.lensList = val.more
  dronePara.lensSelected = val.label
}
const onClaritySelect = (val: any) => {
  dronePara.claritySelected = val
@@ -343,6 +374,21 @@
    }
  })
}
const onSwitch = () => {
  if (dronePara.lensSelected === undefined || dronePara.lensSelected === nonSwitchable) {
    message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
    return
  }
  changeLivestreamLens({
    video_id: livePara.videoId,
    video_type: dronePara.lensSelected
  }).then(res => {
    if (res.code === 0) {
      message.success('Switching live camera successfully.')
    }
  })
}
</script>
<style lang="scss" scoped>
src/components/livestream-others.vue
@@ -8,11 +8,20 @@
      class="mt20"
    ></video>
    <p class="fz24">Live streaming source selection</p>
    <div class="flex-row flex-justify-center flex-align-center mt10">
      <template v-if="liveState && isDockLive">
        <span class="mr10">Lens:</span>
        <a-radio-group v-model:value="lensSelected" button-style="solid">
          <a-radio-button v-for="lens in lensList" :key="lens" :value="lens">{{lens}}</a-radio-button>
        </a-radio-group>
      </template>
      <template v-else>
      <a-select
        style="width: 150px"
        placeholder="Select Live Type"
        @select="onLiveTypeSelect"
        v-model:value="livetypeSelected"
      >
        <a-select-option
          v-for="item in liveTypeList"
@@ -26,12 +35,13 @@
        class="ml10"
        style="width:150px"
        placeholder="Select Drone"
        @select="onDroneSelect"
        v-model:value="droneSelected"
      >
        <a-select-option
          v-for="item in droneList"
          :key="item.value"
          :value="item.value"
          @click="onDroneSelect(item)"
          >{{ item.label }}</a-select-option
        >
      </a-select>
@@ -39,12 +49,13 @@
        class="ml10"
        style="width:150px"
        placeholder="Select Camera"
        @select="onCameraSelect"
        v-model:value="cameraSelected"
      >
        <a-select-option
          v-for="item in cameraList"
          :key="item.value"
          :value="item.value"
          @click="onCameraSelect(item)"
          >{{ item.label }}</a-select-option
        >
      </a-select>
@@ -52,21 +63,23 @@
        class="ml10"
        style="width:150px"
        placeholder="Select Lens"
        @select="onVideoSelect"
        v-model:value="videoSelected"
      >
        <a-select-option
          class="ml10"
          v-for="item in videoList"
          :key="item.value"
          :value="item.value"
          @click="onVideoSelect(item)"
          >{{ item.label }}</a-select-option
        >
      </a-select> -->
      </template>
      <a-select
        class="ml10"
        style="width:150px"
        placeholder="Select Clarity"
        @select="onClaritySelect"
        v-model:value="claritySelected"
      >
        <a-select-option
          v-for="item in clarityList"
@@ -85,14 +98,15 @@
      </p>
    </div>
    <div class="mt10 flex-row flex-justify-center flex-align-center">
      <a-button type="primary" large @click="onStart">Play</a-button>
      <a-button v-if="liveState && isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
      <a-button v-else type="primary" large @click="onStart">Play</a-button>
      <a-button class="ml20" type="primary" large @click="onStop"
        >Stop</a-button
      >
      <a-button class="ml20" type="primary" large @click="onUpdateQuality"
        >Update Clarity</a-button
      >
      <a-button class="ml20" type="primary" large @click="onRefresh"
      <a-button v-if="!liveState || !isDockLive" class="ml20" type="primary" large @click="onRefresh"
        >Refresh Live Capacity</a-button
      >
    </div>
@@ -103,12 +117,18 @@
import { message } from 'ant-design-vue'
import { onMounted, reactive, ref } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
import jswebrtc from '/@/vendors/jswebrtc.min.js'
const root = getRoot()
const liveTypeList = [
interface SelectOption {
  value: any,
  label: string,
  more?: any
}
const liveTypeList: SelectOption[] = [
  {
    value: 1,
    label: 'RTMP'
@@ -122,7 +142,7 @@
    label: 'GB28181'
  }
]
const clarityList = [
const clarityList: SelectOption[] = [
  {
    value: 0,
    label: 'Adaptive'
@@ -152,12 +172,16 @@
const videoList = ref()
const droneSelected = ref()
const cameraSelected = ref()
const videoSeleted = ref()
const claritySeleted = ref()
const videoSelected = ref()
const claritySelected = ref()
const videoId = ref()
const liveState = ref<boolean>(false)
const livetypeSelected = ref()
const rtspData = ref()
const lensList = ref<string[]>([])
const lensSelected = ref<String>()
const isDockLive = ref(false)
const nonSwitchable = 'normal'
const onRefresh = async () => {
  droneList.value = []
@@ -165,7 +189,7 @@
  videoList.value = []
  droneSelected.value = null
  cameraSelected.value = null
  videoSeleted.value = null
  videoSelected.value = null
  await getLiveCapacity({})
    .then(res => {
      console.log(res)
@@ -178,16 +202,17 @@
        console.log('live_capacity:', resData)
        livestreamSource.value = resData
        const temp: Array<{}> = []
        const temp: Array<SelectOption> = []
        if (livestreamSource.value) {
          livestreamSource.value.forEach((ele: any) => {
            temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn })
            temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
          })
          droneList.value = temp
        }
      }
    })
    .catch(error => {
      message.error(error)
      console.error(error)
    })
}
@@ -201,22 +226,22 @@
    livetypeSelected.value,
    droneSelected.value,
    cameraSelected.value,
    videoSeleted.value,
    claritySeleted.value
    videoSelected.value,
    claritySelected.value
  )
  const timestamp = new Date().getTime().toString()
  if (
    livetypeSelected.value == null ||
    droneSelected.value == null ||
    cameraSelected.value == null ||
    videoSeleted.value == null ||
    claritySeleted.value == null
    claritySelected.value == null
  ) {
    message.warn('waring: not select live para!!!')
    return
  }
  videoId.value =
    droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
    droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
  let liveURL = ''
  switch (livetypeSelected.value) {
    case 1: {
@@ -241,9 +266,12 @@
    url: liveURL,
    video_id: videoId.value,
    url_type: livetypeSelected.value,
    video_quality: claritySeleted.value
    video_quality: claritySelected.value
  })
    .then(res => {
      if (res.code !== 0) {
        return
      }
      if (livetypeSelected.value === 3) {
        const url = res.data.url
        const videoElement = videowebrtc.value
@@ -259,7 +287,6 @@
                console.log('start play livestream')
              }
            })
            liveState.value = true
          }
        })
      } else if (livetypeSelected.value === 2) {
@@ -281,10 +308,10 @@
          autoplay: true,
          onPlay: (obj: any) => {
            console.log('start play livestream')
            liveState.value = true
          }
        })
      }
      liveState.value = true
    })
    .catch(err => {
      console.error(err)
@@ -292,13 +319,15 @@
}
const onStop = () => {
  videoId.value =
    droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
    droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
  stopLivestream({
    video_id: videoId.value
  }).then(res => {
    if (res.code === 0) {
      message.info(res.message)
      message.success(res.message)
      liveState.value = false
      lensSelected.value = undefined
      console.log('stop play livestream')
    }
  })
@@ -311,10 +340,10 @@
  }
  setLivestreamQuality({
    video_id: videoId.value,
    video_quality: claritySeleted.value
    video_quality: claritySelected.value
  }).then(res => {
    if (res.code === 0) {
      message.success('Set the clarity to ' + clarityList[claritySeleted.value].label)
      message.success('Set the clarity to ' + clarityList[claritySelected.value].label)
    }
  })
}
@@ -322,54 +351,66 @@
const onLiveTypeSelect = (val: any) => {
  livetypeSelected.value = val
}
const onDroneSelect = (val: any) => {
  droneSelected.value = val
  const temp: Array<{}> = []
const onDroneSelect = (val: SelectOption) => {
  droneSelected.value = val.value
  const temp: Array<SelectOption> = []
  cameraList.value = []
  if (droneSelected.value) {
    const droneTemp = livestreamSource.value
    droneTemp.forEach((ele: any) => {
      const drone = ele
      if (drone.cameras_list && drone.sn === droneSelected.value) {
        const cameraListTemp = drone.cameras_list
        console.info(ele)
        cameraListTemp.forEach((ele: any) => {
          temp.push({ label: ele.name, value: ele.index })
        })
        cameraList.value = temp
      }
    })
  cameraSelected.value = undefined
  videoSelected.value = undefined
  videoList.value = []
  lensList.value = []
  if (!val.more) {
    return
  }
  val.more.forEach((ele: any) => {
    temp.push({ label: ele.name, value: ele.index, more: ele.videos_list })
  })
  cameraList.value = temp
}
const onCameraSelect = (val: any) => {
  cameraSelected.value = val
  const result: Array<{}> = []
  if (cameraSelected.value) {
    const droneTemp = livestreamSource.value
    droneTemp.forEach((ele: any) => {
      const drone = ele
      if (drone.sn === droneSelected.value) {
        const cameraListTemp = drone.cameras_list
        cameraListTemp.forEach((ele: any) => {
          const camera = ele
          if (camera.index === cameraSelected.value) {
            const videoListTemp = camera.videos_list
            videoListTemp.forEach((ele: any) => {
              result.push({ label: ele.type, value: ele.index })
            })
            videoList.value = result
            videoSeleted.value = videoList.value[0]?.value
          }
        })
      }
    })
const onCameraSelect = (val: SelectOption) => {
  cameraSelected.value = val.value
  const result: Array<SelectOption> = []
  videoSelected.value = undefined
  videoList.value = []
  lensList.value = []
  if (!val.more) {
    return
  }
  val.more.forEach((ele: any) => {
    result.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
  })
  videoList.value = result
  if (videoList.value.length === 0) {
    return
  }
  const firstVideo: SelectOption = videoList.value[0]
  videoSelected.value = firstVideo.value
  lensList.value = firstVideo.more
  lensSelected.value = firstVideo.label
  isDockLive.value = lensList.value.length > 0
}
const onVideoSelect = (val: any) => {
  videoSeleted.value = val
const onVideoSelect = (val: SelectOption) => {
  videoSelected.value = val.value
  lensList.value = val.more
  lensSelected.value = val.label
}
const onClaritySelect = (val: any) => {
  claritySeleted.value = val
  claritySelected.value = val
}
const onSwitch = () => {
  if (lensSelected.value === undefined || lensSelected.value === nonSwitchable) {
    message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
    return
  }
  changeLivestreamLens({
    video_id: videoId.value,
    video_type: lensSelected.value
  }).then(res => {
    if (res.code === 0) {
      message.success('Switching live camera successfully.')
    }
  })
}
</script>
src/components/task/CreatePlan.vue
File was renamed from src/pages/page-web/projects/create-plan.vue
@@ -1,5 +1,5 @@
<template>
  <div class="plan">
  <div class="create-plan-wrapper">
    <div class="header">
      Create Plan
    </div>
@@ -8,6 +8,7 @@
        <a-form-item label="Plan Name" name="name" :labelCol="{span: 24}">
          <a-input style="background: black;"  placeholder="Please enter plan name" v-model:value="planBody.name"/>
        </a-form-item>
        <!-- 航线 -->
        <a-form-item label="Flight Route" :wrapperCol="{offset: 7}" name="file_id">
          <router-link
            :to="{name: 'select-plan'}"
@@ -40,6 +41,7 @@
            </div>
          </div>
        </a-form-item>
        <!-- 设备 -->
        <a-form-item label="Device" :wrapperCol="{offset: 10}" v-model:value="planBody.dock_sn" name="dock_sn">
          <router-link
            :to="{name: 'select-plan'}"
@@ -59,13 +61,41 @@
            </div>
          </div>
        </a-form-item>
        <a-form-item label="Immediate">
          <a-switch v-model:checked="planBody.immediate">
            <template #checkedChildren><CheckOutlined /></template>
            <template #unCheckedChildren><CloseOutlined /></template>
          </a-switch>
        <!-- 任务类型 -->
        <a-form-item label="Plan Timer" class="plan-timer-form-item">
          <div style="white-space: nowrap;">
            <a-radio-group v-model:value="planBody.task_type" button-style="solid">
              <a-radio-button :value="TaskType.Immediate">Immediate</a-radio-button>
              <a-radio-button :value="TaskType.Single">Timed&One-Time</a-radio-button>
            </a-radio-group>
          </div>
        </a-form-item>
        <a-form-item style="position: absolute; bottom: 0px; margin-bottom: 0; margin-left: -10px; width: 280px;">
        <!-- 执行时间 -->
        <a-form-item label="Start Time" v-if="planBody.task_type === TaskType.Single" name="select_execute_time">
          <a-date-picker
            v-model:value="planBody.select_execute_time"
            format="YYYY-MM-DD HH:mm:ss"
            show-time
            placeholder="Select Time"
            style="width: 280px;"
          />
        </a-form-item>
        <!-- RTH Altitude Relative to Dock -->
        <a-form-item label="RTH Altitude Relative to Dock (m)" :labelCol="{span: 24}" name="rth_altitude">
          <a-input v-model:value="planBody.rth_altitude" style="background: black !important;">
          </a-input>
        </a-form-item>
        <!-- Lost Action -->
        <a-form-item label="Lost Action" :labelCol="{span: 24}" name="out_of_control_action">
          <div style="white-space: nowrap;">
            <a-radio-group v-model:value="planBody.out_of_control_action" button-style="solid">
              <a-radio-button v-for="action in OutOfControlActionOptions" :value="action.value" :key="action.value">
                {{ action.label }}
              </a-radio-button>
            </a-radio-group>
          </div>
        </a-form-item>
        <a-form-item style="width: 280px;">
          <div class="footer">
            <a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel
            </a-button>
@@ -88,14 +118,16 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, UnwrapRef } from 'vue'
import { CheckOutlined, CloseOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { CloseOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { ELocalStorageKey, ERouterName } from '/@/types'
import { useMyStore } from '/@/store'
import { WaylineFile } from '/@/types/wayline'
import { WaylineType, WaylineFile } from '/@/types/wayline'
import { Device, EDeviceType } from '/@/types/device'
import { createPlan, CreatePlan } from '/@/api/wayline'
import { getRoot } from '/@/root'
import { TaskType, OutOfControlActionOptions, OutOfControlAction } from '/@/types/task'
import moment, { Moment } from 'moment'
import { RuleObject } from 'ant-design-vue/es/form/interface'
const root = getRoot()
const store = useMyStore()
@@ -113,13 +145,16 @@
const disabled = ref(false)
const routeName = ref('')
const planBody: UnwrapRef<CreatePlan> = reactive({
const planBody = reactive({
  name: '',
  file_id: computed(() => store.state.waylineInfo.id),
  dock_sn: computed(() => store.state.dockInfo.device_sn),
  immediate: false,
  type: 'wayline'
  task_type: TaskType.Immediate,
  select_execute_time: undefined as Moment| undefined,
  rth_altitude: '',
  out_of_control_action: OutOfControlAction.ReturnToHome,
})
const drawerVisible = ref(false)
const valueRef = ref()
const rules = {
@@ -128,21 +163,42 @@
    { max: 20, message: 'Length should be 1 to 20', trigger: 'blur' }
  ],
  file_id: [{ required: true, message: 'Select Route' }],
  dock_sn: [{ required: true, message: 'Select Device' }]
  dock_sn: [{ required: true, message: 'Select Device' }],
  select_execute_time: [{ required: true, message: 'Select start time' }],
  rth_altitude: [
    {
      validator: async (rule: RuleObject, value: string) => {
        if (!/^[0-9]{1,}$/.test(value)) {
          throw new Error('RTH Altitude Relative Require number')
        }
      },
    }
  ],
  out_of_control_action: [{ required: true, message: 'Select Lost Action' }],
}
function onSubmit () {
  valueRef.value.validate().then(() => {
    disabled.value = true
    createPlan(workspaceId, planBody)
    const createPlanBody = { ...planBody } as unknown as CreatePlan
    if (planBody.select_execute_time) {
      createPlanBody.execute_time = moment(planBody.select_execute_time).valueOf()
    }
    createPlanBody.rth_altitude = Number(createPlanBody.rth_altitude)
    if (wayline.value && wayline.value.template_types && wayline.value.template_types.length > 0) {
      createPlanBody.wayline_type = wayline.value.template_types[0]
    }
    // console.log('planBody', createPlanBody)
    createPlan(workspaceId, createPlanBody)
      .then(res => {
        message.success('Saved Successfully')
        setTimeout(() => {
          disabled.value = false
        }, 1500)
      }).finally(() => {
        closePlan()
      })
  }).catch((e: any) => {
    console.log('validate err', e)
  })
}
@@ -167,16 +223,16 @@
</script>
<style lang="scss">
.plan {
.create-plan-wrapper {
  background-color: #232323;
  color: white;
  color: fff;
  padding-bottom: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  .header {
    height: 53px;
    height: 52px;
    border-bottom: 1px solid #4f4f4f;
    font-weight: 700;
    font-size: 16px;
@@ -184,30 +240,50 @@
    display: flex;
    align-items: center;
  }
  .content {
    height: 100%;
    height: calc(100% - 54px);
    overflow-y: auto;
    form {
      margin: 10px;
    }
    form label, input {
      color: white;
      background-color: #232323;
      color: #fff;
    }
    .ant-input-suffix {
      color: #fff;
    }
    .plan-timer-form-item {
      // flex-direction: column;
      .ant-radio-button-wrapper{
        background-color: #232323;
        color: #fff;
        &.ant-radio-button-wrapper-checked{
          background-color: #1890ff;
        }
      }
    }
  }
  .footer {
    display: flex;
    align-items: center;
    justify-content: center;
    border-top: 1px solid #4f4f4f;
    min-height: 65px;
    margin-bottom: 0;
    padding-bottom: 0;
    padding:10px 0;
    button {
      width: 45%;
      color: white;
      color: #fff ;
      border: 0;
    }
  }
}
.wayline-panel {
  background: #3c3c3c;
  margin-left: auto;
@@ -228,6 +304,7 @@
    margin: 0px 10px 0 10px;
  }
}
.panel {
  background: #3c3c3c;
  margin-left: auto;
src/components/task/TaskPanel.vue
New file
@@ -0,0 +1,249 @@
<template>
  <div class="header">Task Plan Library</div>
  <div class="plan-panel-wrapper">
    <a-table class="plan-table" :columns="columns" :data-source="plansData.data" row-key="job_id"
      :pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
      <!-- 执行时间 -->
      <template #duration="{ record }">
        <div>
          <div>{{ formatTaskTime(record.execute_time) }}</div>
          <div>{{ formatTaskTime(record.end_time) }}</div>
        </div>
      </template>
      <!-- 任务类型 -->
      <template #taskType="{ record }">
        <div>{{ formatTaskType(record) }}</div>
      </template>
      <!-- 失控动作 -->
      <template #lostAction="{ record }">
        <div>{{ formatLostAction(record) }}</div>
      </template>
      <!-- 状态 -->
      <template #status="{ record }">
        <div>
          <div class="flex-display flex-align-center">
            <span class="circle-icon" :style="{backgroundColor: formatTaskStatus(record).color}"></span>
            {{ formatTaskStatus(record).text }}
            <a-tooltip v-if="!!record.code" placement="bottom" arrow-point-at-center >
              <template #title>
              <div>{{ getCodeMessage(record.code) }}</div>
              </template>
              <exclamation-circle-outlined class="ml5" :style="{color: commonColor.WARN, fontSize: '16px' }"/>
            </a-tooltip>
          </div>
          <div v-if="record.status === TaskStatus.Carrying">
            <a-progress :percent="record.progress || 0" />
          </div>
        </div>
      </template>
      <!-- 操作 -->
      <template #action="{ record }">
        <span class="action-area">
          <a-popconfirm
            v-if="record.status === TaskStatus.Wait"
            title="Are you sure you want to delete flight task?"
            ok-text="Yes"
            cancel-text="No"
            @confirm="onDeleteTask(record.job_id)"
          >
            <a-button type="primary" size="small">Delete</a-button>
          </a-popconfirm>
        </span>
      </template>
    </a-table>
  </div>
</template>
<script setup lang="ts">
import { reactive, ref } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { TableState } from 'ant-design-vue/lib/table/interface'
import { onMounted } from 'vue'
import { IPage } from '/@/api/http/type'
import { deleteTask, getWaylineJobs, Task } from '/@/api/wayline'
import { useMyStore } from '/@/store'
import { ELocalStorageKey } from '/@/types/enums'
import { useFormatTask } from './use-format-task'
import { TaskStatus, TaskProgressInfo, TaskProgressStatus, TaskProgressWsStatusMap } from '/@/types/task'
import { useTaskProgressEvent } from './use-task-progress-event'
import { getErrorMessage } from '/@/utils/error-code/index'
import { commonColor } from '/@/utils/color'
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
const store = useMyStore()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const body: IPage = {
  page: 1,
  total: 0,
  page_size: 50
}
const paginationProp = reactive({
  pageSizeOptions: ['20', '50', '100'],
  showQuickJumper: true,
  showSizeChanger: true,
  pageSize: 50,
  current: 1,
  total: 0
})
const columns = [
  {
    title: 'Planned/Actual Time',
    dataIndex: 'duration',
    width: 180,
    slots: { customRender: 'duration' },
  },
  {
    title: 'Plan Name',
    dataIndex: 'job_name',
    width: 150,
    ellipsis: true
  },
  {
    title: 'Type',
    dataIndex: 'taskType',
    width: 120,
    slots: { customRender: 'taskType' },
  },
  {
    title: 'Flight Route Name',
    dataIndex: 'file_name',
    width: 150,
    ellipsis: true
  },
  {
    title: 'Dock Name',
    dataIndex: 'dock_name',
    width: 120,
    ellipsis: true
  },
  {
    title: 'RTH Altitude Relative to Dock (m)',
    dataIndex: 'rth_altitude',
    width: 120,
    ellipsis: true
  },
  {
    title: 'Lost Action',
    dataIndex: 'out_of_control_action',
    width: 120,
    slots: { customRender: 'lostAction' },
  },
  {
    title: 'Creator',
    dataIndex: 'username',
    width: 120,
  },
  {
    title: 'Status',
    key: 'status',
    width: 200,
    slots: { customRender: 'status' }
  },
  {
    title: 'Action',
    width: 120,
    slots: { customRender: 'action' }
  }
]
type Pagination = TableState['pagination']
const plansData = reactive({
  data: [] as Task[]
})
const { formatTaskType, formatTaskTime, formatLostAction, formatTaskStatus } = useFormatTask()
// 设备任务执行进度更新
function onTaskProgressWs (data: TaskProgressInfo) {
  const { bid, output } = data
  if (output) {
    const { status, progress } = output || {}
    const taskItem = plansData.data.find(task => task.job_id === bid)
    if (!taskItem) return
    if (status) {
      taskItem.status = TaskProgressWsStatusMap[status]
      // 执行中,更新进度
      if (status === TaskProgressStatus.Sent || status === TaskProgressStatus.inProgress) {
        taskItem.progress = progress?.percent || 0
      } else if ([TaskProgressStatus.Rejected, TaskProgressStatus.Canceled, TaskProgressStatus.Timeout, TaskProgressStatus.Failed, TaskProgressStatus.OK].includes(status)) {
        getPlans()
      }
    }
  }
}
function getCodeMessage (code: number) {
  return getErrorMessage(code) + `(code: ${code})`
}
useTaskProgressEvent(onTaskProgressWs)
onMounted(() => {
  getPlans()
})
function getPlans () {
  getWaylineJobs(workspaceId, body).then(res => {
    if (res.code !== 0) {
      return
    }
    plansData.data = res.data.list
    paginationProp.total = res.data.pagination.total
    paginationProp.current = res.data.pagination.page
  })
}
function refreshData (page: Pagination) {
  body.page = page?.current!
  body.page_size = page?.pageSize!
  getPlans()
}
// 删除任务
async function onDeleteTask (jobId: string) {
  const { code } = await deleteTask(workspaceId, {
    job_id: jobId
  })
  if (code === 0) {
    message.success('Deleted successfully')
    getPlans()
  }
}
</script>
<style lang="scss" scoped>
.plan-panel-wrapper {
  width: 100%;
  padding: 16px;
  .plan-table {
    background: #fff;
    margin-top: 10px;
  }
  .action-area {
    color: $primary;
    cursor: pointer;
  }
  .circle-icon {
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-right: 3px;
    border-radius: 50%;
    vertical-align: middle;
    flex-shrink: 0;
  }
}
.header {
  width: 100%;
  height: 60px;
  background: #fff;
  padding: 16px;
  font-size: 20px;
  font-weight: bold;
  text-align: start;
  color: #000;
}
</style>
src/components/task/use-format-task.ts
New file
@@ -0,0 +1,35 @@
import { DEFAULT_PLACEHOLDER } from '/@/utils/constants'
import { Task } from '/@/api/wayline'
import { TaskStatusColor, TaskStatusMap, TaskTypeMap, OutOfControlActionMap } from '/@/types/task'
export function useFormatTask () {
  function formatTaskType (task: Task) {
    return TaskTypeMap[task.task_type] || DEFAULT_PLACEHOLDER
  }
  function formatTaskTime (time: string) {
    return time || DEFAULT_PLACEHOLDER
  }
  function formatLostAction (task: Task) {
    return OutOfControlActionMap[task.out_of_control_action] || DEFAULT_PLACEHOLDER
  }
  function formatTaskStatus (task: Task) {
    const statusObj = {
      text: '',
      color: ''
    }
    const { status } = task
    statusObj.text = TaskStatusMap[status]
    statusObj.color = TaskStatusColor[status]
    return statusObj
  }
  return {
    formatTaskType,
    formatTaskTime,
    formatLostAction,
    formatTaskStatus,
  }
}
src/components/task/use-task-progress-event.ts
New file
@@ -0,0 +1,19 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { TaskProgressInfo } from '/@/types/task'
export function useTaskProgressEvent (onTaskProgressWs: (data: TaskProgressInfo) => void): void {
  function handleTaskProgress (payload: any) {
    onTaskProgressWs(payload.data)
    // eslint-disable-next-line no-unused-expressions
    // console.log('payload', payload.data)
  }
  onMounted(() => {
    EventBus.on('deviceTaskProgress', handleTaskProgress)
  })
  onBeforeUnmount(() => {
    EventBus.off('deviceTaskProgress', handleTaskProgress)
  })
}
src/components/wayline-panel.vue
File was deleted
src/event-bus/index.ts
@@ -1,8 +1,9 @@
import mitt, { Emitter } from 'mitt'
type Events = {
  deviceUpgrade: any;
  deviceLogUploadProgress: any
  deviceUpgrade: any; // 设备升级
  deviceLogUploadProgress: any // 设备日志上传
  deviceTaskProgress: any // 设备任务进度
};
const emitter: Emitter<Events> = mitt<Events>()
src/hooks/use-g-map-tsa.ts
@@ -3,18 +3,19 @@
import { ELocalStorageKey } from '/@/types'
import { getDeviceBySn } from '/@/api/manage'
import { message } from 'ant-design-vue'
import dockIcon from '/@/assets/icons/dock.png'
import rcIcon from '/@/assets/icons/rc.png'
import droneIcon from '/@/assets/icons/drone.png'
export function deviceTsaUpdate () {
  const root = getRoot()
  const AMap = root.$aMap
  const icons: {
    [key: string]: string
  } = {
    'sub-device': '/@/assets/icons/drone.png',
    'gateway': '/@/assets/icons/rc.png',
    'dock': '/@/assets/icons/dock.png'
  }
  const icons = new Map([
    ['sub-device', droneIcon],
    ['gateway', rcIcon],
    ['dock', dockIcon]
  ])
  const markers = store.state.markerInfo.coverMap
  const paths = store.state.markerInfo.pathMap
@@ -33,12 +34,17 @@
  function initIcon (type: string) {
    return new AMap.Icon({
      image: icons[type],
      imageSize: new AMap.Size(40, 40)
      image: icons.get(type),
      imageSize: new AMap.Size(40, 40),
      size: new AMap.Size(40, 40)
    })
  }
  function initMarker (type: string, name: string, sn: string, lng?: number, lat?: number) {
    if (AMap === undefined) {
      location.reload()
      return
    }
    if (markers[sn]) {
      return
    }
@@ -50,7 +56,6 @@
      offset: [0, -20],
    })
    root.$map.add(markers[sn])
    // markers[sn].on('moving', function (e: any) {
    //   let path = paths[sn]
    //   if (!path) {
src/pages/page-pilot/pilot-home.vue
@@ -252,6 +252,12 @@
  apiPilot.onBackClickReg()
  apiPilot.onStopPlatform()
  window.connectCallback = arg => {
    connectCallback(arg)
  }
  window.wsConnectCallback = arg => {
    wsConnectCallback(arg)
  }
  device.data.gateway_sn = apiPilot.getRemoteControllerSN()
  if (device.data.gateway_sn === EStatusValue.DISCONNECT.toString()) {
    message.warn('Data is not available, please restart the remote control.')
@@ -296,12 +302,6 @@
    bindParam.user_id = res.data.user_id
    bindParam.workspace_id = res.data.workspace_id
  })
  window.connectCallback = arg => {
    connectCallback(arg)
  }
  window.wsConnectCallback = arg => {
    wsConnectCallback(arg)
  }
})
const connectCallback = async (arg: any) => {
src/pages/page-web/projects/dock.vue
@@ -9,7 +9,7 @@
    </div>
    <div v-if="docksData.data.length !== 0">
      <div v-for="dock in docksData.data" :key="dock.device_sn">
        <div v-if="dock.children" class="panel" style="padding-top: 5px;" @click="selectDock(dock)">
        <div v-if="dock?.children" class="panel" style="padding-top: 5px;" @click="selectDock(dock)">
          <div class="title">
            <a-tooltip :title="dock.nickname">
              <div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
src/pages/page-web/projects/task.vue
@@ -5,34 +5,42 @@
        <a-col :span="1"></a-col>
        <a-col :span="20">Task Plan Library</a-col>
        <a-col :span="2">
          <span v-if="!createPlanTip">
            <router-link :to="{ name: 'create-plan'}">
              <PlusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = true"/>
          <span v-if="taskRoute">
            <router-link :to="{ name: ERouterName.CREATE_PLAN}">
              <PlusOutlined class="route-icon"/>
            </router-link>
          </span>
          <span v-else>
            <router-link :to="{ name: 'task'}">
              <MinusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = false"/>
            <router-link :to="{ name: ERouterName.TASK}">
              <MinusOutlined class="route-icon"/>
            </router-link>
          </span>
        </a-col>
        <a-col :span="1"></a-col>
      </a-row>
    </div>
    <div v-if="createPlanTip">
      <router-view />
    <div v-if="!taskRoute">
      <router-view/>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ERouterName } from '/@/types/enums'
const createPlanTip = ref(false)
const route = useRoute()
const taskRoute = computed(() => {
  return route.name === ERouterName.TASK
})
</script>
<style lang="scss">
.route-icon {
  color: #fff;
  font-size: 16px;
}
</style>
src/pages/page-web/projects/wayline.vue
@@ -3,12 +3,24 @@
    <div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;">
      <a-row>
        <a-col :span="1"></a-col>
        <a-col :span="22">Flight Route Library</a-col>
        <a-col :span="1"></a-col>
        <a-col :span="15">Flight Route Library</a-col>
        <a-col :span="8" v-if="importVisible" class="flex-row flex-justify-end flex-align-center">
          <a-upload
            name="file"
            :multiple="false"
            :before-upload="beforeUpload"
            :show-upload-list="false"
            :customRequest="uploadFile"
          >
            <a-button type="text" style="color: white;">
              <SelectOutlined />
            </a-button>
          </a-upload>
        </a-col>
      </a-row>
    </div>
    <div class="height-100">
    <a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
    <a-spin :spinning="loading" :delay="300" tip="downloading" size="large">
      <div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
        <div v-for="wayline in waylinesData.data" :key="wayline.id">
          <div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)">
@@ -72,14 +84,17 @@
import { reactive } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { onMounted, onUpdated, ref } from 'vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline'
import { ELocalStorageKey } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles, importKmzFile } from '/@/api/wayline'
import { ELocalStorageKey, ERouterName } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined, SelectOutlined } from '@ant-design/icons-vue'
import { EDeviceType } from '/@/types/device'
import { useMyStore } from '/@/store'
import { WaylineFile } from '/@/types/wayline'
import { downloadFile } from '/@/utils/common'
import { IPage } from '/@/api/http/type'
import { CURRENT_CONFIG } from '/@/api/http/config'
import { load } from '@amap/amap-jsapi-loader'
import { getRoot } from '/@/root'
const loading = ref(false)
const store = useMyStore()
@@ -93,19 +108,28 @@
  data: [] as WaylineFile[]
})
const root = getRoot()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const deleteTip = ref(false)
const deleteWaylineId = ref<string>('')
const canRefresh = ref(true)
const importVisible = ref<boolean>(root.$router.currentRoute.value.name === ERouterName.WAYLINE)
onMounted(() => {
  getWaylines()
  setTimeout(() => {
    const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
    const parent = element?.parentNode as HTMLDivElement
    console.info(element, parent)
    // console.info(element.scrollHeight, parent.clientHeight)
  }, 1000)
})
onUpdated(() => {
  const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
  const parent = element?.parentNode as HTMLDivElement
  setTimeout(() => {
    console.info(element, parent)
    if (element?.scrollHeight < parent?.clientHeight && pagination.total > waylinesData.data.length) {
      if (canRefresh.value) {
        pagination.page++
@@ -130,6 +154,7 @@
    if (res.code !== 0) {
      return
    }
    waylinesData.data = []
    res.data.list.forEach((wayline: WaylineFile) => waylinesData.data.push(wayline))
    pagination.total = res.data.pagination.total
    pagination.page = res.data.pagination.page
@@ -151,8 +176,7 @@
    deleteWaylineId.value = ''
    deleteTip.value = false
    pagination.total--
    waylinesData.data = []
    setTimeout(getWaylines, 500)
    getWaylines()
  })
}
@@ -175,12 +199,50 @@
function onScroll (e: any) {
  const element = e.srcElement
  console.info(element)
  if (element.scrollTop + element.clientHeight === element.scrollHeight && Math.ceil(pagination.total / pagination.page_size) > pagination.page && canRefresh.value) {
    pagination.page++
    getWaylines()
  }
}
interface FileItem {
  uid: string;
  name?: string;
  status?: string;
  response?: string;
  url?: string;
}
interface FileInfo {
  file: FileItem;
  fileList: FileItem[];
}
const fileList = ref<FileItem[]>([])
function beforeUpload (file: FileItem) {
  fileList.value = [file]
  loading.value = true
  return true
}
const uploadFile = async () => {
  console.info(loading.value)
  fileList.value.forEach(async (file: FileItem) => {
    const fileData = new FormData()
    fileData.append('file', file, file.name)
    await importKmzFile(workspaceId, fileData).then((res) => {
      if (res.code === 0) {
        message.success(`${file.name} file uploaded successfully`)
        canRefresh.value = true
        getWaylines()
      }
    }).finally(() => {
      loading.value = false
      fileList.value = []
    })
  })
}
</script>
<style lang="scss" scoped>
src/pages/page-web/projects/workspace.vue
@@ -13,22 +13,22 @@
      <div class="media-wrapper" v-if="root.$route.name === ERouterName.MEDIA">
        <MediaPanel />
      </div>
      <div class="media-wrapper" v-if="root.$route.name === ERouterName.TASK">
      <div class="task-wrapper" v-if="root.$route.name === ERouterName.TASK">
        <TaskPanel />
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import Sidebar from '/@/components/common/sidebar.vue'
import MediaPanel from '/@/components/MediaPanel.vue'
import TaskPanel from '/@/components/TaskPanel.vue'
import TaskPanel from '/@/components/task/TaskPanel.vue'
import GMap from '/@/components/GMap.vue'
import { EBizCode, ERouterName } from '/@/types'
import { getRoot } from '/@/root'
import { useMyStore } from '/@/store'
import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
import EventBus from '/@/event-bus'
const root = getRoot()
const store = useMyStore()
@@ -72,7 +72,7 @@
      break
    }
    case EBizCode.FlightTaskProgress: {
      store.commit('SET_FLIGHT_TASK_PROGRESS', payload.data)
      EventBus.emit('deviceTaskProgress', payload)
      break
    }
    case EBizCode.DeviceHms: {
@@ -112,42 +112,41 @@
.project-app-wrapper {
  display: flex;
  position: absolute;
  transition: width 0.2s ease;
  height: 100%;
  width: 100%;
  .left {
    width: 400px;
    display: flex;
    width: 335px;
    flex: 0 0 335px;
    background-color: #232323;
    float: left;
    .main-content {
      flex: 1;
      color: $text-white-basic;
    }
  }
  .right {
    width: 100%;
    height: 100%;
    .map-wrapper {
    flex-grow: 1;
    position: relative;
    .map-wrapper{
      width: 100%;
      height: 100%;
    }
  }
  .main-content {
    flex: 1;
    color: $text-white-basic;
  }
  .media-wrapper {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: 100;
    background: #f6f8fa;
  }
  .wayline-wrapper {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: 100;
    background: #f6f8fa;
    padding: 16px;
    .media-wrapper,
    .task-wrapper {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      z-index: 100;
      background: #f6f8fa;
    }
  }
}
</style>
src/router/index.ts
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { ERouterName } from '/@/types/index'
import CreatePlan from '../pages/page-web/projects/create-plan.vue'
import CreatePlan from '/@/components/task/CreatePlan.vue'
import WaylinePanel from '/@/pages/page-web/projects/wayline.vue'
import DockPanel from '/@/pages/page-web/projects/dock.vue'
import LiveAgora from '/@/components/livestream-agora.vue'
src/store/index.ts
@@ -4,7 +4,7 @@
import { Device, DeviceHms, DeviceOsd, DeviceStatus, DockOsd, GatewayOsd, OSDVisible } from '../types/device'
import { getLayers } from '/@/api/layer'
import { LayerType } from '/@/types/mapLayer'
import { ETaskStatus, TaskInfo, WaylineFile } from '/@/types/wayline'
import { WaylineFile } from '/@/types/wayline'
import { DevicesCmdExecuteInfo } from '/@/types/device-cmd'
const initStateFunc = () => ({
@@ -81,11 +81,6 @@
  dockInfo: {
  } as Device,
  taskProgressInfo: {
  } as {
    [bid: string]: TaskInfo
  },
  hmsInfo: {} as {
    [sn: string]: DeviceHms[]
  },
@@ -113,16 +108,29 @@
    state.deviceState.currentType = EDeviceTypeName.Gateway
  },
  SET_DOCK_INFO (state, info) {
    if (Object.keys(info.host).length === 0) {
      return
    }
    if (!state.deviceState.dockInfo[info.sn]) {
      state.deviceState.dockInfo[info.sn] = info.host
      return
    }
    state.deviceState.currentSn = info.sn
    state.deviceState.currentType = EDeviceTypeName.Dock
    const dock = state.deviceState.dockInfo[info.sn]
    if (info.host.sdr && state.deviceState.dockInfo[info.sn]) {
    if (info.host.sdr) {
      dock.sdr = info.host.sdr
      dock.media_file_detail = info.host.media_file_detail
      return
    }
    const sdr = dock?.sdr
    const mediaFileDetail = dock?.media_file_detail
    if (info.host.job_number !== undefined) {
      if (info.host.drone_battery_maintenance_info) {
        dock.drone_battery_maintenance_info = info.host.drone_battery_maintenance_info
      }
      return
    }
    const sdr = dock.sdr
    const mediaFileDetail = dock.media_file_detail
    state.deviceState.dockInfo[info.sn] = info.host
    state.deviceState.dockInfo[info.sn].sdr = sdr
    state.deviceState.dockInfo[info.sn].media_file_detail = mediaFileDetail
@@ -159,17 +167,6 @@
  },
  SET_SELECT_DOCK_INFO (state, info) {
    state.dockInfo = info
  },
  SET_FLIGHT_TASK_PROGRESS (state, info) {
    const taskInfo: TaskInfo = info.output
    if (taskInfo.status === ETaskStatus.OK || taskInfo.status === ETaskStatus.FAILED) {
      taskInfo.status = taskInfo.status.concat('(Code:').concat(info.result).concat(')')
      setTimeout(() => {
        delete state.taskProgressInfo[info.bid]
      }, 60000)
    }
    state.taskProgressInfo[info.bid] = info.output
  },
  SET_DEVICE_HMS_INFO (state, info) {
    const hmsList: Array<DeviceHms> = state.hmsInfo[info.sn]
src/types/airport-tsa.ts
@@ -36,3 +36,27 @@
  Close = 0, // 关闭
  Open = 1, // 打开
}
// 机场声光报警状态
export enum AlarmModeEnum {
  CLOSE = 0, // 关闭
  OPEN = 1, // 开启
}
// 电池保养
export enum BatteryStoreModeEnum {
  BATTERY_PLAN_STORE = 1, // 电池计划存储策略
  BATTERY_EMERGENCY_STORE = 2, // 电池应急存储策略
}
// 飞行器电池保养
export enum DroneBatteryStateEnum {
  NoMaintenanceRequired = 0, // 0-无需保养
  MaintenanceRequired = 1, // 1-待保养
  MaintenanceInProgress = 2, // 2-正在保养
}
export enum DroneBatteryModeEnum {
  CLOSE = 0, // 关闭
  OPEN = 1, // 开启
}
src/types/device-cmd.ts
@@ -1,3 +1,4 @@
import { AlarmModeEnum, BatteryStoreModeEnum, DroneBatteryModeEnum } from '/@/types/airport-tsa';
// 机场指令集
export enum DeviceCmd {
  // 简单指令
@@ -19,7 +20,13 @@
  PutterClose = 'putter_close', // 推杆闭合
  ChargeOpen = 'charge_open', // 打开充电
  ChargeClose = 'charge_close', // 关闭充电
  AlarmStateSwitch = 'alarm_state_switch', // 机场声光报警
  BatteryStoreModeSwitch = 'battery_store_mode_switch', // 电池保养
  DroneBatteryModeSwitch = 'battery_maintenance_switch', // 飞行器电池保养
}
export type DeviceCmdItemAction = AlarmModeEnum | BatteryStoreModeEnum | DroneBatteryModeEnum
export interface DeviceCmdItem{
  label: string, // 标题
@@ -27,8 +34,10 @@
  operateText: string, // 按钮文字
  cmdKey: DeviceCmd, // 请求指令
  oppositeCmdKey?: DeviceCmd, // 相反状态指令
  action?: DeviceCmdItemAction, // 参数
  func: string, // 处理函数
  loading: boolean // 按钮loading
  disabled?: boolean // 按钮disabled
}
// 机场指令
@@ -114,6 +123,34 @@
    func: 'supplementLightStatus',
    loading: false,
  },
  {
    label: '机场声光报警',
    status: '关',
    operateText: '打开',
    cmdKey: DeviceCmd.AlarmStateSwitch,
    action: AlarmModeEnum.OPEN,
    func: 'alarmState',
    loading: false,
  },
  {
    label: '机场电池存储模式',
    status: '计划',
    operateText: '应急',
    cmdKey: DeviceCmd.BatteryStoreModeSwitch,
    action: BatteryStoreModeEnum.BATTERY_EMERGENCY_STORE,
    func: 'batteryStoreMode',
    loading: false,
  },
  {
    label: '飞机电池保养',
    status: '--',
    operateText: '保养',
    cmdKey: DeviceCmd.DroneBatteryModeSwitch,
    action: DroneBatteryModeEnum.OPEN,
    func: 'droneBatteryMode',
    loading: false,
    disabled: true,
  },
]
export enum DeviceCmdStatusText {
@@ -176,6 +213,32 @@
  DeviceSupplementLightCloseText = '关闭中...',
  DeviceSupplementLightCloseFailedText = '开',
  DeviceSupplementLightCloseBtnText = '打开',
  AlarmStateOpenNormalText = '开',
  AlarmStateOpenText = '开启中...',
  AlarmStateOpenFailedText = '关',
  AlarmStateOpenBtnText = '关闭',
  AlarmStateCloseNormalText = '关',
  AlarmStateCloseText = '关闭中...',
  AlarmStateCloseFailedText = '开',
  AlarmStateCloseBtnText = '打开',
  BatteryStoreModePlanNormalText = '计划',
  BatteryStoreModePlanText = '切换中...',
  BatteryStoreModePlanFailedText = '应急',
  BatteryStoreModePlanBtnText = '应急',
  BatteryStoreModeEmergencyNormalText = '应急',
  BatteryStoreModeEmergencyText = '切换中...',
  BatteryStoreModeEmergencyFailedText = '计划',
  BatteryStoreModeEmergencyBtnText = '计划',
  DroneBatteryModeMaintenanceInProgressText = '保养中',
  DroneBatteryModeMaintenanceNotNeedText = '无需保养',
  DroneBatteryModeMaintenanceNeedText = '需保养',
  DroneBatteryModeOpenBtnText = '保养',
  DroneBatteryModeCloseBtnText = '关闭保养',
}
// cmd ws 消息状态
src/types/device-setting.ts
New file
@@ -0,0 +1,148 @@
// 夜航灯开关
export enum NightLightsStateEnum {
  CLOSE = 0, // 0-关闭
  OPEN = 1, // 1-打开
}
// 限远开关
export enum DistanceLimitStatusEnum {
  UNSET = 0, // 0-未设置
  SET = 1, // 1-已设置
}
export interface DistanceLimitStatus {
  state?: DistanceLimitStatusEnum;
  distance_limit?: number; // 限远
}
// 避障
export enum ObstacleAvoidanceStatusEnum {
  CLOSE = 0, // 0-关闭
  OPEN = 1, // 1-开启
}
export interface ObstacleAvoidance {
  horizon?: ObstacleAvoidanceStatusEnum;// 水平避障开关
  upside?: ObstacleAvoidanceStatusEnum;// 上行方向避障开关
  downside?: ObstacleAvoidanceStatusEnum;// 下行方向避障开关
}
// 设备管理设置key
export enum DeviceSettingKeyEnum {
  NIGHT_LIGHTS_MODE_SET = 'night_lights_state', // 夜航灯开关
  HEIGHT_LIMIT_SET = 'height_limit', // 限高设置
  DISTANCE_LIMIT_SET = 'distance_limit_status', // 限远开关
  OBSTACLE_AVOIDANCE_HORIZON = 'obstacle_avoidance_horizon', // 水平避障状态
  OBSTACLE_AVOIDANCE_UPSIDE = 'obstacle_avoidance_upside', // 上视避障状态
  OBSTACLE_AVOIDANCE_DOWNSIDE = 'obstacle_avoidance_downside', // 下视避障状态
}
export type DeviceSettingType = Record<DeviceSettingKeyEnum, any>
export const initDeviceSetting = {
  [DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET]:
  {
    label: '飞行器夜航灯',
    value: '',
    trueValue: NightLightsStateEnum.CLOSE,
    editable: false,
    popConfirm: {
      visible: false,
      loading: false,
      // content: '为保证飞行器的作业安全,建议打开夜航灯',
      label: '飞行器夜航灯',
    },
    settingKey: DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET,
  },
  [DeviceSettingKeyEnum.HEIGHT_LIMIT_SET]:
  {
    label: '限高',
    value: '',
    trueValue: 120,
    editable: false,
    popConfirm: {
      visible: false,
      loading: false,
      // content: '限高:20 - 1500m',
      // info: '修改限高会影响当前机场的所有作业任务,建议确认作业情况后再进行修改',
      label: '限高',
    },
    settingKey: DeviceSettingKeyEnum.HEIGHT_LIMIT_SET,
  },
  [DeviceSettingKeyEnum.DISTANCE_LIMIT_SET]:
  {
    label: '限远',
    value: '',
    trueValue: DistanceLimitStatusEnum.UNSET,
    // info: '限远(15 - 8000m)是约束飞行器相对机场的最大作业距离',
    editable: false,
    popConfirm: {
      visible: false,
      loading: false,
      // content: '限远 (15- 8000m) 是约束飞行器相对机场的最大作业距离',
      // info: '修改限远会影响当前机场的所有作业任务,建议确认作业情况后再进行修改',
      label: '限远',
    },
    settingKey: DeviceSettingKeyEnum.DISTANCE_LIMIT_SET,
  },
  [DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON]:
  {
    label: '水平避障',
    value: '',
    trueValue: ObstacleAvoidanceStatusEnum.CLOSE,
    // info: '飞行器的避障工作状态显示,可以快速开启/关闭飞行器避障,如需进一步设置请在设备运维页面设置',
    editable: false,
    popConfirm: {
      visible: false,
      loading: false,
      // content: '飞行器避障是保障飞行作业安全的基础功能,建议保持飞行器避障开启',
      label: '水平避障',
    },
    settingKey: DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON,
  },
  [DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE]:
  {
    label: '上视避障',
    value: '',
    trueValue: ObstacleAvoidanceStatusEnum.CLOSE,
    // info: '飞行器的避障工作状态显示,可以快速开启/关闭飞行器避障,如需进一步设置请在设备运维页面设置',
    editable: false,
    popConfirm: {
      visible: false,
      loading: false,
      // content: '飞行器避障是保障飞行作业安全的基础功能,建议保持飞行器避障开启',
      label: '上视避障',
    },
    settingKey: DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE,
  },
  [DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE]:
  {
    label: '下视避障',
    value: '',
    trueValue: ObstacleAvoidanceStatusEnum.CLOSE,
    // info: '飞行器的避障工作状态显示,可以快速开启/关闭飞行器避障,如需进一步设置请在设备运维页面设置',
    editable: false,
    popConfirm: {
      visible: false,
      loading: false,
      // content: '飞行器避障是保障飞行作业安全的基础功能,建议保持飞行器避障开启',
      label: '下视避障',
    },
    settingKey: DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE,
  },
} as DeviceSettingType
export const initDeviceSettingFormModel = {
  nightLightsState: false, // 夜航灯开关
  heightLimit: 20, // 限高设置
  distanceLimitStatus: { state: false, distanceLimit: 15 }, // 限远开关
  obstacleAvoidanceHorizon: false, // 飞行器避障-水平开关设置
  obstacleAvoidanceUpside: false, // 飞行器避障-上视开关设置
  obstacleAvoidanceDownside: false, // 飞行器避障-下视开关设置
}
export type DeviceSettingFormModel = typeof initDeviceSettingFormModel
src/types/device.ts
@@ -1,5 +1,6 @@
import { commonColor } from '/@/utils/color'
import { NightLightsStateEnum, DistanceLimitStatus, ObstacleAvoidance } from './device-setting'
import { AlarmModeEnum, BatteryStoreModeEnum, DroneBatteryStateEnum } from './airport-tsa'
export interface DeviceValue {
  key: string; // 'domain-type-subtype'
  domain: string; // 表示一个领域,作为一个命名空间,暂时分 飞机类-0, 负载类-1,RC类-2,机场类-3 4种
@@ -235,7 +236,11 @@
    landing_power: string,
    remain_flight_time: number,
    return_home_power: string,
  }
  },
  night_lights_state?: NightLightsStateEnum;// 夜航灯开关
  height_limit?: number;// 限高设置
  distance_limit_status?: DistanceLimitStatus;// 限远开关
  obstacle_avoidance?: ObstacleAvoidance;// 飞行器避障开关设置
}
export interface DockOsd {
@@ -294,6 +299,12 @@
    device_online_status: number,
    device_paired: number,
  },
  alarm_state?: AlarmModeEnum; // 机场声光报警状态
  battery_store_mode?: BatteryStoreModeEnum; // 电池保养(存储)模式
  drone_battery_maintenance_info?: { // 飞行器电池保养信息
    maintenance_state: DroneBatteryStateEnum, // 保养状态
    maintenance_time_left: number, // 电池保养剩余时间(小时)
  }
}
export enum EModeCode {
@@ -343,6 +354,10 @@
  H20N = '1-61-0' as any,
  DJI_Dock_Camera = '1-165-0' as any,
  L1 = '1-90742-0' as any,
  M3E = '0-77-0' as any,
  M3D = '0-77-1' as any,
  M3E_Camera = '1-66-0' as any,
  M3T_Camera = '1-67-0' as any,
}
export enum EDockModeCode {
src/types/enums.ts
@@ -90,7 +90,7 @@
    MapElementDelete = 'map_element_delete',
    DeviceOnline = 'device_online',
    DeviceOffline = 'device_offline',
    FlightTaskProgress = 'flighttask_progress',
    FlightTaskProgress = 'flighttask_progress', // 机场任务执行进度
    DeviceHms = 'device_hms',
    // 设备指令
src/types/task.ts
New file
@@ -0,0 +1,97 @@
import { commonColor } from '/@/utils/color'
// 任务类型
export enum TaskType {
  Immediate = 0, // 立即执行
  Single = 1, // 单次定时任务
}
export const TaskTypeMap = {
  [TaskType.Immediate]: 'Immediate',
  [TaskType.Single]: 'Timed & One-Time',
}
// 失控动作
export enum OutOfControlAction {
  ReturnToHome = 0,
  Hover = 1,
  Land = 2,
}
export const OutOfControlActionMap = {
  [OutOfControlAction.ReturnToHome]: 'Return to Home',
  [OutOfControlAction.Hover]: 'Hover',
  [OutOfControlAction.Land]: 'Land',
}
export const OutOfControlActionOptions = [
  { value: OutOfControlAction.ReturnToHome, label: OutOfControlActionMap[OutOfControlAction.ReturnToHome] },
  { value: OutOfControlAction.Hover, label: OutOfControlActionMap[OutOfControlAction.Hover] },
  { value: OutOfControlAction.Land, label: OutOfControlActionMap[OutOfControlAction.Land] },
]
// 任务状态
export enum TaskStatus {
  Wait = 1, //  待执行
  Carrying = 2, // 执行中
  Success = 3, // 完成
  CanCel = 4, // 取消
  Fail = 5, // 失败
}
export const TaskStatusMap = {
  [TaskStatus.Wait]: 'To be performed',
  [TaskStatus.Carrying]: 'In progress',
  [TaskStatus.Success]: 'Task completed',
  [TaskStatus.CanCel]: 'Task canceled',
  [TaskStatus.Fail]: 'Task failed',
}
export const TaskStatusColor = {
  [TaskStatus.Wait]: commonColor.BLUE,
  [TaskStatus.Carrying]: commonColor.BLUE,
  [TaskStatus.Success]: commonColor.NORMAL,
  [TaskStatus.CanCel]: commonColor.FAIL,
  [TaskStatus.Fail]: commonColor.FAIL,
}
// 任务执行 ws 消息状态
export enum TaskProgressStatus {
  Sent = 'sent', // 已下发
  inProgress = 'in_progress', // 执行中
  Paused = 'paused', // 暂停
  Rejected = 'rejected', // 拒绝
  Canceled = 'canceled', // 取消或终止
  Timeout = 'timeout', // 超时
  Failed = 'failed', // 失败
  OK = 'ok', // 上传成功
}
// 任务进度消息
export interface TaskProgressInfo {
  bid: string,
  output:{
    ext: {
      current_waypoint_index: number,
      media_count: number // 媒体文件
    },
    progress:{
      current_step: number,
      percent: number
    },
    status: TaskProgressStatus
  },
  result: number,
}
// ws status => log status
export const TaskProgressWsStatusMap = {
  [TaskProgressStatus.Sent]: TaskStatus.Carrying,
  [TaskProgressStatus.inProgress]: TaskStatus.Carrying,
  [TaskProgressStatus.Rejected]: TaskStatus.Fail,
  [TaskProgressStatus.OK]: TaskStatus.Success,
  [TaskProgressStatus.Failed]: TaskStatus.Fail,
  [TaskProgressStatus.Canceled]: TaskStatus.CanCel,
  [TaskProgressStatus.Timeout]: TaskStatus.Fail,
  [TaskProgressStatus.Paused]: TaskStatus.Wait,
}
src/types/wayline.ts
@@ -1,30 +1,15 @@
// 航线类型
export enum WaylineType {
  NormalWaypointWayline = 0, // 普通航点航线
  AccurateReshootingWayline = 1 // 精准复拍航线
}
export interface WaylineFile {
  id: string,
  name: string,
  drone_model_key: any,
  payload_model_keys: string[],
  template_types: number[],
  template_types: WaylineType[],
  update_time: number,
  user_name: string,
}
export interface TaskExt {
  current_waypoint_index: number,
  media_count: number,
}
export interface TaskProgress {
  current_step: number,
  percent: number,
}
export interface TaskInfo {
  status: string,
  progress: TaskProgress,
  ext: TaskExt,
}
export enum ETaskStatus {
  OK = 'ok',
  FAILED = 'failed'
}
src/utils/device-cmd.ts
@@ -1,6 +1,7 @@
import { DroneBatteryModeEnum, DroneBatteryStateEnum } from './../types/airport-tsa';
import { DeviceInfoType } from '/@/types/device'
import { DeviceCmd, DeviceCmdItem, DeviceCmdExecuteInfo, DeviceCmdStatusText, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
import { AirportStorage, CoverStateEnum, PutterStateEnum, ChargeStateEnum, SupplementLightStateEnum } from '/@/types/airport-tsa'
import { AirportStorage, CoverStateEnum, PutterStateEnum, ChargeStateEnum, SupplementLightStateEnum, AlarmModeEnum, BatteryStoreModeEnum } from '/@/types/airport-tsa'
import { getBytesObject } from './bytes'
import { DEFAULT_PLACEHOLDER } from './constants'
@@ -35,6 +36,12 @@
      droneFormat(cmdItem, device)
    } else if (cmdItem.cmdKey === DeviceCmd.SupplementLightOpen || cmdItem.cmdKey === DeviceCmd.SupplementLightClose) { // 补光灯开关
      getSupplementLightState(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.AlarmStateSwitch) { // 声光报警
      getAlarmState(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.BatteryStoreModeSwitch) { // 电池保养
      getBatteryStoreMode(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.DroneBatteryModeSwitch) { // 飞行器电池保养
      getDroneBatteryMode(cmdItem, dock)
    }
  })
}
@@ -163,6 +170,55 @@
    if (cmdItem.cmdKey !== DeviceCmd.SupplementLightClose) {
      exchangeDeviceCmd(cmdItem)
    }
  }
}
// 声光报警
function getAlarmState (cmdItem: DeviceCmdItem, airportProperties: any) {
  const alarmState = airportProperties?.alarm_state
  if (alarmState === AlarmModeEnum.CLOSE) {
    cmdItem.operateText = DeviceCmdStatusText.AlarmStateCloseBtnText
    cmdItem.status = DeviceCmdStatusText.AlarmStateCloseNormalText
    cmdItem.action = AlarmModeEnum.OPEN
  } else if (alarmState === AlarmModeEnum.OPEN) {
    cmdItem.operateText = DeviceCmdStatusText.AlarmStateOpenBtnText
    cmdItem.status = DeviceCmdStatusText.AlarmStateOpenNormalText
    cmdItem.action = AlarmModeEnum.CLOSE
  }
}
// 机场电池模式
function getBatteryStoreMode (cmdItem: DeviceCmdItem, airportProperties: any) {
  const batteryStoreMode = airportProperties?.battery_store_mode
  if (batteryStoreMode === BatteryStoreModeEnum.BATTERY_PLAN_STORE) {
    cmdItem.operateText = DeviceCmdStatusText.BatteryStoreModePlanBtnText
    cmdItem.status = DeviceCmdStatusText.BatteryStoreModePlanNormalText
    cmdItem.action = BatteryStoreModeEnum.BATTERY_EMERGENCY_STORE
  } else if (batteryStoreMode === BatteryStoreModeEnum.BATTERY_EMERGENCY_STORE) {
    cmdItem.operateText = DeviceCmdStatusText.BatteryStoreModeEmergencyBtnText
    cmdItem.status = DeviceCmdStatusText.BatteryStoreModeEmergencyNormalText
    cmdItem.action = BatteryStoreModeEnum.BATTERY_PLAN_STORE
  }
}
// 飞行器电池保养
function getDroneBatteryMode (cmdItem: DeviceCmdItem, airportProperties: any) {
  const maintenanceState = airportProperties?.drone_battery_maintenance_info?.maintenance_state
  if (maintenanceState === DroneBatteryStateEnum.MaintenanceInProgress) {
    cmdItem.operateText = DeviceCmdStatusText.DroneBatteryModeCloseBtnText
    cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceInProgressText
    cmdItem.action = DroneBatteryModeEnum.CLOSE
    cmdItem.disabled = false
  } else if (maintenanceState === DroneBatteryStateEnum.NoMaintenanceRequired) {
    cmdItem.operateText = DeviceCmdStatusText.DroneBatteryModeOpenBtnText
    cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceNeedText
    cmdItem.action = DroneBatteryModeEnum.OPEN
    cmdItem.disabled = true
  } else if (maintenanceState === DroneBatteryStateEnum.MaintenanceRequired) {
    cmdItem.operateText = DeviceCmdStatusText.DroneBatteryModeOpenBtnText
    cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceNotNeedText
    cmdItem.action = DroneBatteryModeEnum.OPEN
    cmdItem.disabled = false
  }
}
@@ -344,6 +400,74 @@
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.AlarmStateSwitch) { // 机场声光报警
        if (cmdItem.action === AlarmModeEnum.CLOSE) {
          if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
            cmdItem.status = DeviceCmdStatusText.AlarmStateCloseText
            cmdItem.loading = true
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
            cmdItem.status = DeviceCmdStatusText.AlarmStateCloseFailedText
            cmdItem.loading = false
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
            cmdItem.loading = false
          }
        } else if (cmdItem.action === AlarmModeEnum.OPEN) {
          if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
            cmdItem.status = DeviceCmdStatusText.AlarmStateOpenText
            cmdItem.loading = true
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
            cmdItem.status = DeviceCmdStatusText.AlarmStateOpenFailedText
            cmdItem.loading = false
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
            cmdItem.loading = false
          }
        }
      } else if (cmdItem.cmdKey === DeviceCmd.BatteryStoreModeSwitch) { // 电池保养
        if (cmdItem.action === BatteryStoreModeEnum.BATTERY_PLAN_STORE) {
          if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
            cmdItem.status = DeviceCmdStatusText.BatteryStoreModePlanText
            cmdItem.loading = true
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
            cmdItem.status = DeviceCmdStatusText.BatteryStoreModePlanFailedText
            cmdItem.loading = false
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
            cmdItem.loading = false
          }
        } else if (cmdItem.action === BatteryStoreModeEnum.BATTERY_EMERGENCY_STORE) {
          if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
            cmdItem.status = DeviceCmdStatusText.BatteryStoreModeEmergencyText
            cmdItem.loading = true
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
            cmdItem.status = DeviceCmdStatusText.BatteryStoreModeEmergencyFailedText
            cmdItem.loading = false
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
            cmdItem.loading = false
          }
        }
      } else if (cmdItem.cmdKey === DeviceCmd.DroneBatteryModeSwitch) { // 飞行器电池保养
        if (cmdItem.action === DroneBatteryModeEnum.OPEN) {
          if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
            cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceInProgressText
            // cmdItem.loading = true
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
            cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceNeedText
            // cmdItem.loading = false
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
            cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceNotNeedText
            // cmdItem.loading = false
          }
        } else if (cmdItem.action === DroneBatteryModeEnum.CLOSE) {
          if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
            cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceInProgressText
            cmdItem.loading = true
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
            cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceInProgressText
            cmdItem.loading = false
          } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
            cmdItem.status = DeviceCmdStatusText.DroneBatteryModeMaintenanceNeedText
            cmdItem.loading = false
          }
        }
      }
    }
  })
src/utils/device-setting.ts
New file
@@ -0,0 +1,193 @@
import { DeviceInfoType } from '/@/types/device'
import { DeviceSettingType, DeviceSettingKeyEnum, DistanceLimitStatusEnum, ObstacleAvoidanceStatusEnum, DeviceSettingFormModel, NightLightsStateEnum } from '/@/types/device-setting'
import { DEFAULT_PLACEHOLDER } from './constants'
import { isNil } from 'lodash'
const Unit_M = ' m'
/**
 * 根据osd 更新信息
 * @param deviceSetting
 * @param deviceInfo
 * @returns
 */
export function updateDeviceSettingInfoByOsd (deviceSetting: DeviceSettingType, deviceInfo: DeviceInfoType) {
  const { device, dock, gateway } = deviceInfo || {}
  if (!deviceSetting) {
    return
  }
  // 夜航灯
  let nightLightsState = '' as any
  if (isNil(device?.night_lights_state)) {
    deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].editable = false
    deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value = DEFAULT_PLACEHOLDER
    nightLightsState = DEFAULT_PLACEHOLDER
  } else {
    deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].editable = true
    nightLightsState = device?.night_lights_state
    if (nightLightsState === NightLightsStateEnum.CLOSE) {
      deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value = '关闭'
    } else if (nightLightsState === NightLightsStateEnum.OPEN) {
      deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value = '开启'
    } else {
      deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value = DEFAULT_PLACEHOLDER
    }
  }
  deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].trueValue = nightLightsState
  // 限高
  let heightLimit = device?.height_limit as any
  if (isNil(heightLimit) || heightLimit === 0) {
    heightLimit = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].editable = false
  } else {
    deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].editable = true
  }
  deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].trueValue = heightLimit
  deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].value = heightLimit + Unit_M
  // 限远
  let distanceLimitStatus = '' as any
  if (isNil(device?.distance_limit_status?.state)) {
    distanceLimitStatus = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].editable = false
    deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value = DEFAULT_PLACEHOLDER
  } else {
    deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].editable = true
    distanceLimitStatus = device?.distance_limit_status?.state
    if (distanceLimitStatus === DistanceLimitStatusEnum.UNSET) {
      deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value = '关闭'
    } else if (distanceLimitStatus === DistanceLimitStatusEnum.SET) {
      const distanceLimit = device?.distance_limit_status?.distance_limit
      if (distanceLimit) {
        deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value = distanceLimit + Unit_M
      } else {
        deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value = DEFAULT_PLACEHOLDER
      }
    } else {
      deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value = DEFAULT_PLACEHOLDER
    }
  }
  deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].trueValue = distanceLimitStatus
  // 避障
  if (isNil(device?.obstacle_avoidance)) {
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].editable = false
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].trueValue = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].editable = false
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].trueValue = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].editable = false
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value = DEFAULT_PLACEHOLDER
    deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].trueValue = DEFAULT_PLACEHOLDER
  } else {
    const { horizon, upside, downside } = device.obstacle_avoidance || {}
    if (isNil(horizon)) {
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].editable = false
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value = DEFAULT_PLACEHOLDER
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].trueValue = DEFAULT_PLACEHOLDER
    } else {
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].editable = false
      if (horizon === ObstacleAvoidanceStatusEnum.CLOSE) {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value = '关闭'
      } else if (horizon === ObstacleAvoidanceStatusEnum.OPEN) {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value = '开启'
      } else {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value = DEFAULT_PLACEHOLDER
      }
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].trueValue = horizon
    }
    if (isNil(upside)) {
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].editable = false
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value = DEFAULT_PLACEHOLDER
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].trueValue = DEFAULT_PLACEHOLDER
    } else {
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].editable = false
      if (upside === ObstacleAvoidanceStatusEnum.CLOSE) {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value = '关闭'
      } else if (upside === ObstacleAvoidanceStatusEnum.OPEN) {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value = '开启'
      } else {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value = DEFAULT_PLACEHOLDER
      }
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].trueValue = upside
    }
    if (isNil(downside)) {
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].editable = false
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value = DEFAULT_PLACEHOLDER
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].trueValue = DEFAULT_PLACEHOLDER
    } else {
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].editable = false
      if (downside === ObstacleAvoidanceStatusEnum.CLOSE) {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value = '关闭'
      } else if (downside === ObstacleAvoidanceStatusEnum.OPEN) {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value = '开启'
      } else {
        deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value = DEFAULT_PLACEHOLDER
      }
      deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].trueValue = downside
    }
  }
  return deviceSetting
}
// 更新formModel
export function updateDeviceSettingFormModelByOsd (deviceSettingFormModelFromOsd: DeviceSettingFormModel, deviceInfo: DeviceInfoType) {
  const { device, dock, gateway } = deviceInfo || {}
  if (!deviceSettingFormModelFromOsd) {
    return
  }
  // 夜航灯
  const nightLightsState = device?.night_lights_state as any
  if (!isNil(nightLightsState) && nightLightsState === NightLightsStateEnum.OPEN) {
    deviceSettingFormModelFromOsd.nightLightsState = true
  } else {
    deviceSettingFormModelFromOsd.nightLightsState = false
  }
  // 限高
  const heightLimit = device?.height_limit as any
  if (isNil(heightLimit) || heightLimit === 0) {
    deviceSettingFormModelFromOsd.heightLimit = 20
  } else {
    deviceSettingFormModelFromOsd.heightLimit = heightLimit
  }
  // 限远
  const distanceLimitStatus = device?.distance_limit_status?.state as any
  if (!isNil(distanceLimitStatus) && distanceLimitStatus === DistanceLimitStatusEnum.SET) {
    deviceSettingFormModelFromOsd.distanceLimitStatus.state = true
    deviceSettingFormModelFromOsd.distanceLimitStatus.distanceLimit = device?.distance_limit_status?.distance_limit || 15
  } else {
    deviceSettingFormModelFromOsd.distanceLimitStatus.state = false
    deviceSettingFormModelFromOsd.distanceLimitStatus.distanceLimit = 15
  }
  // 避障
  if (isNil(device?.obstacle_avoidance)) {
    deviceSettingFormModelFromOsd.obstacleAvoidanceHorizon = false
    deviceSettingFormModelFromOsd.obstacleAvoidanceUpside = false
    deviceSettingFormModelFromOsd.obstacleAvoidanceDownside = false
  } else {
    const { horizon, upside, downside } = device.obstacle_avoidance || {}
    if (!isNil(horizon) && horizon === ObstacleAvoidanceStatusEnum.OPEN) {
      deviceSettingFormModelFromOsd.obstacleAvoidanceHorizon = true
    } else {
      deviceSettingFormModelFromOsd.obstacleAvoidanceHorizon = false
    }
    if (!isNil(upside) && upside === ObstacleAvoidanceStatusEnum.OPEN) {
      deviceSettingFormModelFromOsd.obstacleAvoidanceUpside = true
    } else {
      deviceSettingFormModelFromOsd.obstacleAvoidanceUpside = false
    }
    if (!isNil(downside) && downside === ObstacleAvoidanceStatusEnum.OPEN) {
      deviceSettingFormModelFromOsd.obstacleAvoidanceDownside = true
    } else {
      deviceSettingFormModelFromOsd.obstacleAvoidanceDownside = false
    }
  }
  return deviceSettingFormModelFromOsd
}
src/utils/error-code/index.ts
New file
@@ -0,0 +1,310 @@
export interface ErrorCode {
  code: number;
  msg: string;
}
/**
 * 根据错误码翻译错误信息
 * @param code
 * @param errorMsg
 * @returns
 */
export function getErrorMessage (code: number, errorMsg?: string): string {
  const errorInfo = ERROR_CODE.find((item: ErrorCode) => item.code === code)
  return errorInfo ? errorInfo.msg : errorMsg || 'Server error'
}
// 暂时只添加航线错误
export const ERROR_CODE = [
  {
    code: 314001,
    msg: 'The issued route task url is empty',
  },
  {
    code: 314002,
    msg: 'The issued route task md5 is empty',
  },
  {
    code: 314003,
    msg: 'MissionID is invalid',
  },
  {
    code: 314004,
    msg: 'Failed to send flight route task from cloud',
  },
  {
    code: 314005,
    msg: 'Route md5 check failed',
  },
  {
    code: 314006,
    msg: 'Timeout waiting for aircraft to upload route (waiting for gs_state)',
  },
  {
    code: 314007,
    msg: 'Failed to upload route to aircraft',
  },
  {
    code: 314008,
    msg: 'Timeout waiting for the aircraft to enter the route executable state',
  },
  {
    code: 314009,
    msg: 'Failed to open route mission',
  },
  {
    code: 314010,
    msg: 'Route execution failed',
  },
  {
    code: 316001,
    msg: 'Failed to set alternate point',
  },
  {
    code: 316002,
    msg: 'Alternate safety transfer altitude equipment failed',
  },
  {
    code: 316003,
    msg: 'Failed to set takeoff altitude. Remarks: The default safe takeoff height of the aircraft set by the current DJI Dock is: 1.8',
  },
  {
    code: 316004,
    msg: 'Failed to set runaway behavior',
  },
  {
    code: 316005,
    msg: 'Aircraft RTK convergence failed',
  },
  {
    code: 316013,
    msg: 'DJI Dock Moved',
  },
  {
    code: 316015,
    msg: 'The aircraft RTK convergence position is too far from the DJI Dock',
  },
  {
    code: 316007,
    msg: 'Set parameter timeout while waiting for aircraft to be ready',
  },
  {
    code: 316008,
    msg: 'Failed to gain control of aircraft',
  },
  {
    code: 316009,
    msg: 'Aircraft power is low',
  },
  {
    code: 316010,
    msg: 'After power on, the aircraft is not connected for more than 2 minutes (flight control OSD reception timeout)',
  },
  {
    code: 316011,
    msg: 'Landing Position Offset',
  },
  {
    code: 317001,
    msg: 'Failed to get the number of media files',
  },
  {
    code: 319001,
    msg: 'The task center is not currently idle',
  },
  {
    code: 319002,
    msg: 'dronenest communication timeout',
  },
  {
    code: 319999,
    msg: 'Unknown error, e.g. restart after crash',
  },
  {
    code: 321000,
    msg: 'Route execution failed, unknown error',
  },
  {
    code: 321257,
    msg: 'The route has already started and cannot be started again',
  },
  {
    code: 321258,
    msg: 'The route cannot be interrupted in this state',
  },
  {
    code: 321259,
    msg: 'The route has not started and cannot end the route',
  },
  {
    code: 321513,
    msg: 'Reach the height limit',
  },
  {
    code: 321514,
    msg: 'Reach the limit',
  },
  {
    code: 321515,
    msg: 'Crossing the restricted flight zone',
  },
  {
    code: 321516,
    msg: 'Low limit',
  },
  {
    code: 321517,
    msg: 'Obstacle Avoidance',
  },
  {
    code: 321769,
    msg: 'Weak GPS signal',
  },
  {
    code: 321770,
    msg: 'The current gear state cannot be executed, B control seizes the control, and the gear is switched',
  },
  {
    code: 321771,
    msg: 'The home point is not refreshed',
  },
  {
    code: 321772,
    msg: 'The current battery is too low to start the task',
  },
  {
    code: 321773,
    msg: 'Low battery return',
  },
  {
    code: 321776,
    msg: 'RTK not ready',
  },
  {
    code: 321778,
    msg: 'The aircraft is idling on the ground and is not allowed to start the route, thinking that the user is not ready.',
  },
  {
    code: 322282,
    msg: 'User interrupt (B control takeover)',
  },
  {
    code: 514100,
    msg: 'Command not supported',
  },
  {
    code: 514101,
    msg: 'Failed to close putter',
  },
  {
    code: 514102,
    msg: 'Failed to release putter',
  },
  {
    code: 514103,
    msg: 'Aircraft battery is low',
  },
  {
    code: 514104,
    msg: 'Failed to start charging',
  },
  {
    code: 514105,
    msg: 'Failed to stop charging',
  },
  {
    code: 514106,
    msg: 'Failed to restart the aircraft',
  },
  {
    code: 514107,
    msg: 'Failed to open hatch',
  },
  {
    code: 514108,
    msg: 'Failed to close hatch',
  },
  {
    code: 514109,
    msg: 'Failed to open the plane',
  },
  {
    code: 514110,
    msg: 'Failed to close the plane',
  },
  {
    code: 514111,
    msg: 'The aircraft failed to turn on the slow-rotating propeller in the cabin',
  },
  {
    code: 514112,
    msg: 'The aircraft failed to stop the slow-rotating propeller in the cabin',
  },
  {
    code: 514113,
    msg: 'Failed to establish wired connection with aircraft',
  },
  {
    code: 514114,
    msg: 'Get aircraft power status, command timed out, or return code is not 0',
  },
  {
    code: 514116,
    msg: 'The DJI Dock is busy and other control orders are being executed at the DJI Dock',
  },
  {
    code: 514117,
    msg: 'Check hatch status failed',
  },
  {
    code: 514118,
    msg: 'Check putter status failed',
  },
  {
    code: 514120,
    msg: 'DJI Dock and aircraft SDR connection failed',
  },
  {
    code: 514121,
    msg: 'Emergency stop state',
  },
  {
    code: 514122,
    msg: 'Failed to get the charging status of the aircraft (Failed to get the charging status, the flight mission can be executed, affecting charging and remote troubleshooting)',
  },
  {
    code: 514123,
    msg: 'Unable to power on due to low battery',
  },
  {
    code: 514124,
    msg: 'Failed to get battery information',
  },
  {
    code: 514125,
    msg: 'The battery is fully charged and cannot be charged',
  },
  {
    code: 514145,
    msg: 'Can not work while debugging on site',
  },
  {
    code: 514146,
    msg: 'Unable to work in remote debugging',
  },
  {
    code: 514147,
    msg: 'Unable to work in upgrade state',
  },
  {
    code: 514148,
    msg: 'Unable to execute new tasks in job state',
  },
  {
    code: 514150,
    msg: 'DJI Dock is automatically restarting',
  },
]