From 31ad252341e3614daab677a9cf2e37f62489484e Mon Sep 17 00:00:00 2001
From: sean.zhou <sean.zhou@dji.com>
Date: Fri, 18 Nov 2022 18:35:12 +0800
Subject: [PATCH] initial v1.3.0

---
 src/components/GMap.vue                                    |   26 
 src/components/task/TaskPanel.vue                          |  249 +++++
 src/components/task/use-format-task.ts                     |   35 
 src/types/wayline.ts                                       |   29 
 src/components/g-map/DockControlPanel.vue                  |  100 +
 src/api/device-cmd/index.ts                                |   12 
 src/components/g-map/DeviceSettingPopover.vue              |  106 ++
 src/pages/page-web/projects/dock.vue                       |    2 
 src/components/g-map/useDeviceSetting.ts                   |   56 +
 src/types/device.ts                                        |   19 
 src/pages/page-web/projects/task.vue                       |   28 
 src/components/devices/device-log/DeviceLogDetailModal.vue |    4 
 src/pages/page-pilot/pilot-home.vue                        |   12 
 src/types/enums.ts                                         |    2 
 src/utils/error-code/index.ts                              |  310 ++++++
 src/api/wayline.ts                                         |   74 +
 src/components/livestream-agora.vue                        |  176 ++-
 src/utils/device-setting.ts                                |  193 ++++
 src/components/task/use-task-progress-event.ts             |   19 
 src/pages/page-web/projects/workspace.vue                  |   57 
 src/components/livestream-others.vue                       |  175 ++-
 src/types/device-setting.ts                                |  148 +++
 src/router/index.ts                                        |    2 
 src/utils/device-cmd.ts                                    |  126 ++
 src/types/airport-tsa.ts                                   |   24 
 src/hooks/use-g-map-tsa.ts                                 |   25 
 src/types/task.ts                                          |   97 ++
 src/event-bus/index.ts                                     |    5 
 src/pages/page-web/projects/wayline.vue                    |   78 +
 src/components/g-map/useDockControl.ts                     |   16 
 src/api/device-setting/index.ts                            |   24 
 src/store/index.ts                                         |   37 
 /dev/null                                                  |  123 --
 src/components/g-map/DeviceSettingBox.vue                  |  242 +++++
 src/components/task/CreatePlan.vue                         |  135 ++
 src/api/manage.ts                                          |    6 
 package.json                                               |    3 
 src/types/device-cmd.ts                                    |   63 +
 src/App.vue                                                |   10 
 39 files changed, 2,384 insertions(+), 464 deletions(-)

diff --git a/package.json b/package.json
index 2cf085e..2b5640f 100644
--- a/package.json
+++ b/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",
diff --git a/src/App.vue b/src/App.vue
index ef1496f..453e2a5 100644
--- a/src/App.vue
+++ b/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>
diff --git a/src/api/device-cmd/index.ts b/src/api/device-cmd/index.ts
index f4ea817..8e48e53 100644
--- a/src/api/device-cmd/index.ts
+++ b/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
 }
diff --git a/src/api/device-setting/index.ts b/src/api/device-setting/index.ts
new file mode 100644
index 0000000..6df9d2d
--- /dev/null
+++ b/src/api/device-setting/index.ts
@@ -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
+}
diff --git a/src/api/manage.ts b/src/api/manage.ts
index 559d12b..fbdcb48 100644
--- a/src/api/manage.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/api/wayline.ts b/src/api/wayline.ts
index 2efa822..dfbeef3 100644
--- a/src/api/wayline.ts
+++ b/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
 }
diff --git a/src/components/GMap.vue b/src/components/GMap.vue
index 411b7ed..d7a836b 100644
--- a/src/components/GMap.vue
+++ b/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;
diff --git a/src/components/TaskPanel.vue b/src/components/TaskPanel.vue
deleted file mode 100644
index 084626f..0000000
--- a/src/components/TaskPanel.vue
+++ /dev/null
@@ -1,182 +0,0 @@
-<template>
-  <div class="header">Task Plan Library</div>
-  <div class="plan-panel-wrapper">
-    <!-- <router-link :to=" '/' + ERouterName.CREATE_PLAN">
-      <a-button type="primary">Create Plan</a-button>
-    </router-link> -->
-    <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 #status="{ record }">
-        <span v-if="taskProgressMap[record.bid]">
-          <a-progress type="line" :percent="taskProgressMap[record.bid]?.progress?.percent"
-            :status="taskProgressMap[record.bid]?.status.indexOf(ETaskStatus.FAILED) != -1 ? 'exception' : taskProgressMap[record.bid]?.status.indexOf(ETaskStatus.OK) != -1 ? 'success' : 'normal'">
-            <template #format="percent">
-              <a-tooltip :title="taskProgressMap[record.bid]?.status">
-                <div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;">
-                  {{ percent }}% {{ taskProgressMap[record.bid]?.status }}
-                </div>
-              </a-tooltip>
-            </template>
-          </a-progress>
-        </span>
-      </template>
-      <template #action="{ record }">
-        <span class="action-area">
-          <a-popconfirm
-            title="Are you sure execute this task?"
-            ok-text="Yes"
-            cancel-text="No"
-            @confirm="executePlan(record.job_id)"
-          >
-            <a-button type="primary" size="small">Execute</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 { computed, onMounted, watch } from 'vue'
-import { IPage } from '../api/http/type'
-import { executeWaylineJobs, getWaylineJobs } from '../api/wayline'
-import { getRoot } from '../root'
-import { useMyStore } from '../store'
-import { ELocalStorageKey, ERouterName } from '../types/enums'
-import router from '/@/router'
-import { ETaskStatus } from '/@/types/wayline'
-
-const store = useMyStore()
-
-const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
-
-const root = getRoot()
-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: 'Plan Name',
-    dataIndex: 'job_name'
-  },
-  {
-    title: 'Flight Route Name',
-    dataIndex: 'file_name',
-    ellipsis: true
-  },
-  {
-    title: 'Dock Name',
-    dataIndex: 'dock_name',
-    ellipsis: true
-  },
-  {
-    title: 'Creator',
-    dataIndex: 'username',
-  },
-  {
-    title: 'Updated',
-    dataIndex: 'update_time'
-  },
-  {
-    title: 'Status',
-    key: 'status',
-    width: 200,
-    slots: { customRender: 'status' }
-  },
-
-  {
-    title: 'Action',
-    slots: { customRender: 'action' }
-  }
-]
-type Pagination = TableState['pagination']
-
-interface TaskPlan {
-  bid: string,
-  job_id: string,
-  job_name: string,
-  file_name: string,
-  dock_name: string,
-  username: string,
-  create_time: string,
-}
-
-const plansData = reactive({
-  data: [] as TaskPlan[]
-})
-
-function createPlan () {
-  root.$router.push('/' + ERouterName.CREATE_PLAN)
-}
-
-const taskProgressMap = computed(() => store.state.taskProgressInfo)
-
-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()
-}
-
-function executePlan (jobId: string) {
-  executeWaylineJobs(workspaceId, jobId).then(res => {
-    if (res.code === 0) {
-      message.success('Executed 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;
-  }
-}
-.header {
-  width: 100%;
-  height: 60px;
-  background: #fff;
-  padding: 16px;
-  font-size: 20px;
-  font-weight: bold;
-  text-align: start;
-  color: #000;
-}
-</style>
diff --git a/src/components/devices/device-log/DeviceLogDetailModal.vue b/src/components/devices/device-log/DeviceLogDetailModal.vue
index c203ce3..9f36302 100644
--- a/src/components/devices/device-log/DeviceLogDetailModal.vue
+++ b/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"
diff --git a/src/components/g-map/DeviceSettingBox.vue b/src/components/g-map/DeviceSettingBox.vue
new file mode 100644
index 0000000..0f7d508
--- /dev/null
+++ b/src/components/g-map/DeviceSettingBox.vue
@@ -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>
diff --git a/src/components/g-map/DeviceSettingPopover.vue b/src/components/g-map/DeviceSettingPopover.vue
new file mode 100644
index 0000000..72e0b0f
--- /dev/null
+++ b/src/components/g-map/DeviceSettingPopover.vue
@@ -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>
diff --git a/src/components/g-map/DockControlPanel.vue b/src/components/g-map/DockControlPanel.vue
index 284d9ca..645ee20 100644
--- a/src/components/g-map/DockControlPanel.vue
+++ b/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;
+          }
         }
       }
     }
diff --git a/src/components/g-map/useDeviceSetting.ts b/src/components/g-map/useDeviceSetting.ts
new file mode 100644
index 0000000..e2cea30
--- /dev/null
+++ b/src/components/g-map/useDeviceSetting.ts
@@ -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
+  }
+}
diff --git a/src/components/g-map/useDockControl.ts b/src/components/g-map/useDockControl.ts
index 94e302f..5e8c38c 100644
--- a/src/components/g-map/useDockControl.ts
+++ b/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
diff --git a/src/components/livestream-agora.vue b/src/components/livestream-agora.vue
index 41463f3..b1c0d05 100644
--- a/src/components/livestream-agora.vue
+++ b/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>
diff --git a/src/components/livestream-others.vue b/src/components/livestream-others.vue
index a77bcc0..16b0bb3 100644
--- a/src/components/livestream-others.vue
+++ b/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>
 
diff --git a/src/pages/page-web/projects/create-plan.vue b/src/components/task/CreatePlan.vue
similarity index 63%
rename from src/pages/page-web/projects/create-plan.vue
rename to src/components/task/CreatePlan.vue
index 3891f2a..e48144a 100644
--- a/src/pages/page-web/projects/create-plan.vue
+++ b/src/components/task/CreatePlan.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;
diff --git a/src/components/task/TaskPanel.vue b/src/components/task/TaskPanel.vue
new file mode 100644
index 0000000..339c189
--- /dev/null
+++ b/src/components/task/TaskPanel.vue
@@ -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>
diff --git a/src/components/task/use-format-task.ts b/src/components/task/use-format-task.ts
new file mode 100644
index 0000000..167a581
--- /dev/null
+++ b/src/components/task/use-format-task.ts
@@ -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,
+  }
+}
diff --git a/src/components/task/use-task-progress-event.ts b/src/components/task/use-task-progress-event.ts
new file mode 100644
index 0000000..54d4551
--- /dev/null
+++ b/src/components/task/use-task-progress-event.ts
@@ -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)
+  })
+}
diff --git a/src/components/wayline-panel.vue b/src/components/wayline-panel.vue
deleted file mode 100644
index aad0a49..0000000
--- a/src/components/wayline-panel.vue
+++ /dev/null
@@ -1,123 +0,0 @@
-<template>
-  <div class="panel-wrapper" draggable="true">
-    <div class="header">Wayline Library</div>
-    <a-button type="primary" style="margin-top:20px" @click="onRefresh"
-      >Refresh</a-button
-    >
-    <a-table class="table" :columns="columns" :data-source="data">
-      <template #name="{ text, record }">
-        <a :href="record.preview_url">{{ text }}</a>
-      </template>
-      <template #action>
-        <span class="action-area">
-          action
-        </span>
-      </template>
-    </a-table>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref } from '@vue/reactivity'
-import { onMounted } from 'vue'
-import { ELocalStorageKey } from '../types/enums'
-import { getWaylineFiles } from '/@/api/wayline'
-const columns = [
-  {
-    title: 'FileName',
-    dataIndex: 'name',
-    key: 'name',
-    slots: { customRender: 'name' }
-  },
-  {
-    title: 'TemplateType',
-    dataIndex: 'template_type',
-    key: 'template_type'
-  },
-  {
-    title: 'Favorited',
-    dataIndex: 'favorite',
-    key: 'favorite'
-  },
-  {
-    title: 'DroneType',
-    dataIndex: 'drone_type',
-    key: 'drone_type'
-  },
-  {
-    title: 'PayloadType',
-    dataIndex: 'payload_type',
-    key: 'payload_type'
-  },
-  {
-    title: 'User',
-    dataIndex: 'user',
-    key: 'user'
-  },
-  {
-    title: 'Action',
-    key: 'action',
-    slots: { customRender: 'action' }
-  }
-]
-const data = ref([
-  {
-    key: '1',
-    name: 'name1',
-    template_type: '0',
-    drone_type: '0-60-0',
-    payload_type: 'PM320_DUAL',
-    user: 'pilot',
-    favorited: 'true'
-  }
-])
-onMounted(() => {
-  onRefresh()
-})
-const onRefresh = async () => {
-  const wid: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)
-  data.value = []
-  const index = 1
-  const res = await getWaylineFiles(wid, {
-    page: 1, // 页数
-    page_size: 9, // 每页大小
-    order_by: 'update_time desc' // 排序, xxx_column_desc, xxx_column_asc,  xxx_column(default asc)
-  })
-  console.log(res)
-  res.data.list.forEach(ele => {
-    data.value.push({
-      key: index.toString(),
-      name: ele.name,
-      template_type: ele.template_types[0],
-      drone_type: ele.drone_model_key,
-      payload_type: ele.payload_model_keys[0],
-      user: ele.user_name,
-      favorite: ele.favorited.toString()
-    })
-  })
-  console.log('wayline files:', data.value)
-}
-</script>
-
-<style lang="scss" scoped>
-.panel-wrapper {
-  width: 100%;
-  .table {
-    background: #fff;
-    margin-top: 32px;
-  }
-  .header {
-    width: 100%;
-    height: 60px;
-    background: #fff;
-    padding: 16px 24px;
-    font-size: 20px;
-    text-align: start;
-    color: #000;
-  }
-  .action-area {
-    color: $primary;
-    cursor: pointer;
-  }
-}
-</style>
diff --git a/src/event-bus/index.ts b/src/event-bus/index.ts
index 8024981..d4d24b3 100644
--- a/src/event-bus/index.ts
+++ b/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>()
diff --git a/src/hooks/use-g-map-tsa.ts b/src/hooks/use-g-map-tsa.ts
index 6db59d8..fe3a398 100644
--- a/src/hooks/use-g-map-tsa.ts
+++ b/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) {
diff --git a/src/pages/page-pilot/pilot-home.vue b/src/pages/page-pilot/pilot-home.vue
index 1a1d0c4..a897a57 100644
--- a/src/pages/page-pilot/pilot-home.vue
+++ b/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) => {
diff --git a/src/pages/page-web/projects/dock.vue b/src/pages/page-web/projects/dock.vue
index 72fd32f..0575267 100644
--- a/src/pages/page-web/projects/dock.vue
+++ b/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>
diff --git a/src/pages/page-web/projects/task.vue b/src/pages/page-web/projects/task.vue
index e594a8d..3e83954 100644
--- a/src/pages/page-web/projects/task.vue
+++ b/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>
diff --git a/src/pages/page-web/projects/wayline.vue b/src/pages/page-web/projects/wayline.vue
index b64e8e8..da7db6b 100644
--- a/src/pages/page-web/projects/wayline.vue
+++ b/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>
diff --git a/src/pages/page-web/projects/workspace.vue b/src/pages/page-web/projects/workspace.vue
index 4fc4dd3..4d07a56 100644
--- a/src/pages/page-web/projects/workspace.vue
+++ b/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>
diff --git a/src/router/index.ts b/src/router/index.ts
index 8b20a35..9e5918d 100644
--- a/src/router/index.ts
+++ b/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'
diff --git a/src/store/index.ts b/src/store/index.ts
index 36a7725..00a49fa 100644
--- a/src/store/index.ts
+++ b/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]
diff --git a/src/types/airport-tsa.ts b/src/types/airport-tsa.ts
index c672f36..9de449e 100644
--- a/src/types/airport-tsa.ts
+++ b/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, // 开启
+}
diff --git a/src/types/device-cmd.ts b/src/types/device-cmd.ts
index 1230881..621fd25 100644
--- a/src/types/device-cmd.ts
+++ b/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 消息状态
diff --git a/src/types/device-setting.ts b/src/types/device-setting.ts
new file mode 100644
index 0000000..25acf21
--- /dev/null
+++ b/src/types/device-setting.ts
@@ -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
diff --git a/src/types/device.ts b/src/types/device.ts
index 08c285a..733ef3f 100644
--- a/src/types/device.ts
+++ b/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 {
diff --git a/src/types/enums.ts b/src/types/enums.ts
index 00a298c..e0e1995 100644
--- a/src/types/enums.ts
+++ b/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',
 
     // 设备指令
diff --git a/src/types/task.ts b/src/types/task.ts
new file mode 100644
index 0000000..86d91d3
--- /dev/null
+++ b/src/types/task.ts
@@ -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,
+}
diff --git a/src/types/wayline.ts b/src/types/wayline.ts
index 69746e6..dfe7f91 100644
--- a/src/types/wayline.ts
+++ b/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'
 }
diff --git a/src/utils/device-cmd.ts b/src/utils/device-cmd.ts
index b6c80dd..3ffe75f 100644
--- a/src/utils/device-cmd.ts
+++ b/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
+          }
+        }
       }
     }
   })
diff --git a/src/utils/device-setting.ts b/src/utils/device-setting.ts
new file mode 100644
index 0000000..bd02a2a
--- /dev/null
+++ b/src/utils/device-setting.ts
@@ -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
+}
diff --git a/src/utils/error-code/index.ts b/src/utils/error-code/index.ts
new file mode 100644
index 0000000..e4c2679
--- /dev/null
+++ b/src/utils/error-code/index.ts
@@ -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',
+  },
+]

--
Gitblit v1.9.3