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