Compare commits
16 Commits
1.3.0-beta
...
main
Author | SHA1 | Date |
---|---|---|
zhangxu | 9d8e1c6eae | 6 days ago |
jessie.huo@dji.com | b26ef22ab6 | 10 months ago |
jessie.huo@dji.com | ae62a2c5ef | 11 months ago |
sean.zhou | 42854e4db7 | 1 year ago |
sean.zhou | 3ef74070df | 1 year ago |
sean.zhou | 91f0f86730 | 1 year ago |
sean.zhou | 07ff9ab4cc | 1 year ago |
sean.zhou | 763e39bd71 | 2 years ago |
sean.zhou | 91fbc1342f | 2 years ago |
sean.zhou | a6d5406418 | 2 years ago |
sean.zhou | f3e92fa131 | 2 years ago |
sean.zhou | b09ed762ed | 2 years ago |
sean.zhou | 9650dd9f1d | 2 years ago |
sean.zhou | 20e5f4cd98 | 2 years ago |
sean.zhou | 83d4c07a67 | 2 years ago |
sean.zhou | 31ad252341 | 2 years ago |
111 changed files with 18504 additions and 5460 deletions
@ -1 +1 @@
@@ -1 +1 @@
|
||||
registry=https://registry.npm.taobao.org/ |
||||
registry=https://registry.npmmirror.com/ |
@ -0,0 +1,24 @@
@@ -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 |
||||
} |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
import request, { IWorkspaceResponse } from '/@/api/http/request' |
||||
import { ELocalStorageKey } from '/@/types' |
||||
|
||||
// DRC 链路
|
||||
const DRC_API_PREFIX = '/control/api/v1' |
||||
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '' |
||||
|
||||
export interface PostDrcBody { |
||||
client_id?: string // token过期时,用于续期则必填
|
||||
expire_sec?: number // 过期时间,单位秒,默认3600
|
||||
} |
||||
|
||||
export interface DrcParams { |
||||
address: string |
||||
username: string |
||||
password: string |
||||
client_id: string |
||||
expire_time: number // 过期时间
|
||||
enable_tls: boolean // 是否开启tls
|
||||
} |
||||
|
||||
// 获取 mqtt 连接认证
|
||||
export async function postDrc (body: PostDrcBody): Promise<IWorkspaceResponse<DrcParams>> { |
||||
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/connect`, body) |
||||
return resp.data |
||||
} |
||||
|
||||
export interface DrcEnterBody { |
||||
client_id: string |
||||
dock_sn: string |
||||
expire_sec?: number // 过期时间,单位秒,默认3600
|
||||
device_info?: { |
||||
osd_frequency?: number |
||||
hsi_frequency?: number |
||||
} |
||||
} |
||||
|
||||
export interface DrcEnterResp { |
||||
sub: string[] // 需要订阅接收的topic
|
||||
pub: string[] // 推送的topic地址
|
||||
} |
||||
|
||||
// 进入飞行控制 (建立drc连接&获取云控控制权)
|
||||
export async function postDrcEnter (body: DrcEnterBody): Promise<IWorkspaceResponse<DrcEnterResp>> { |
||||
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body) |
||||
return resp.data |
||||
} |
||||
|
||||
export interface DrcExitBody { |
||||
client_id: string |
||||
dock_sn: string |
||||
} |
||||
|
||||
// 退出飞行控制 (退出drc连接&退出云控控制权)
|
||||
export async function postDrcExit (body: DrcExitBody): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/exit`, body) |
||||
return resp.data |
||||
} |
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
import request, { IWorkspaceResponse } from '/@/api/http/request' |
||||
// import { ELocalStorageKey } from '/@/types'
|
||||
|
||||
const API_PREFIX = '/control/api/v1' |
||||
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
|
||||
|
||||
// 获取飞行控制权
|
||||
export async function postFlightAuth (sn: string): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/flight`) |
||||
return resp.data |
||||
} |
||||
export enum WaylineLostControlActionInCommandFlight { |
||||
CONTINUE = 0, |
||||
EXEC_LOST_ACTION = 1 |
||||
} |
||||
export enum LostControlActionInCommandFLight { |
||||
HOVER = 0, // 悬停
|
||||
Land = 1, // 着陆
|
||||
RETURN_HOME = 2, // 返航
|
||||
} |
||||
export enum ERthMode { |
||||
SMART = 0, |
||||
SETTING = 1 |
||||
} |
||||
export enum ECommanderModeLostAction { |
||||
CONTINUE = 0, |
||||
EXEC_LOST_ACTION = 1 |
||||
} |
||||
export enum ECommanderFlightMode { |
||||
SMART = 0, |
||||
SETTING = 1 |
||||
} |
||||
export interface PointBody { |
||||
latitude: number; |
||||
longitude: number; |
||||
height: number; |
||||
} |
||||
export interface PostFlyToPointBody { |
||||
max_speed: number, |
||||
points: PointBody[] |
||||
} |
||||
|
||||
// 飞向目标点
|
||||
export async function postFlyToPoint (sn: string, body: PostFlyToPointBody): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`, body) |
||||
return resp.data |
||||
} |
||||
|
||||
// 停止飞向目标点
|
||||
export async function deleteFlyToPoint (sn: string): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.delete(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`) |
||||
return resp.data |
||||
} |
||||
|
||||
export interface PostTakeoffToPointBody{ |
||||
target_height: number; |
||||
target_latitude: number; |
||||
target_longitude: number; |
||||
security_takeoff_height: number; // 安全起飞高
|
||||
max_speed: number; // flyto过程中能达到的最大速度, 单位m/s 跟飞机档位有关
|
||||
rc_lost_action: LostControlActionInCommandFLight; // 失控行为
|
||||
rth_altitude: number; // 返航高度
|
||||
exit_wayline_when_rc_lost: WaylineLostControlActionInCommandFlight; |
||||
rth_mode: ERthMode; |
||||
commander_mode_lost_action: ECommanderModeLostAction; |
||||
commander_flight_mode: ECommanderFlightMode; |
||||
commander_flight_height: number; |
||||
} |
||||
|
||||
// 一键起飞
|
||||
export async function postTakeoffToPoint (sn: string, body: PostTakeoffToPointBody): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/takeoff-to-point`, body) |
||||
return resp.data |
||||
} |
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
import request, { IWorkspaceResponse } from '/@/api/http/request' |
||||
import { CameraType, CameraMode } from '/@/types/live-stream' |
||||
import { GimbalResetMode } from '/@/types/drone-control' |
||||
// import { ELocalStorageKey } from '/@/types'
|
||||
|
||||
const API_PREFIX = '/control/api/v1' |
||||
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
|
||||
|
||||
export interface PostPayloadAuthBody { |
||||
payload_index: string |
||||
} |
||||
|
||||
// 获取负载控制权
|
||||
export async function postPayloadAuth (sn: string, body: PostPayloadAuthBody): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/payload`, body) |
||||
return resp.data |
||||
} |
||||
|
||||
// TODO: 画面拖动控制
|
||||
export enum PayloadCommandsEnum { |
||||
CameraModeSwitch = 'camera_mode_switch', |
||||
CameraPhotoTake = 'camera_photo_take', |
||||
CameraRecordingStart = 'camera_recording_start', |
||||
CameraRecordingStop = 'camera_recording_stop', |
||||
CameraFocalLengthSet = 'camera_focal_length_set', |
||||
GimbalReset = 'gimbal_reset', |
||||
CameraAim = 'camera_aim' |
||||
} |
||||
|
||||
export interface PostCameraModeBody { |
||||
payload_index: string |
||||
camera_mode: CameraMode |
||||
} |
||||
|
||||
export interface PostCameraPhotoBody { |
||||
payload_index: string |
||||
} |
||||
|
||||
export interface PostCameraRecordingBody { |
||||
payload_index: string |
||||
} |
||||
|
||||
export interface DeleteCameraRecordingParams { |
||||
payload_index: string |
||||
} |
||||
|
||||
export interface PostCameraFocalLengthBody { |
||||
payload_index: string, |
||||
camera_type: CameraType, |
||||
zoom_factor: number |
||||
} |
||||
|
||||
export interface PostGimbalResetBody{ |
||||
payload_index: string, |
||||
reset_mode: GimbalResetMode, |
||||
} |
||||
|
||||
export interface PostCameraAimBody{ |
||||
payload_index: string, |
||||
camera_type: CameraType, |
||||
locked: boolean, |
||||
x: number, |
||||
y: number, |
||||
} |
||||
|
||||
export type PostPayloadCommandsBody = { |
||||
cmd: PayloadCommandsEnum.CameraModeSwitch, |
||||
data: PostCameraModeBody |
||||
} | { |
||||
cmd: PayloadCommandsEnum.CameraPhotoTake, |
||||
data: PostCameraPhotoBody |
||||
} | { |
||||
cmd: PayloadCommandsEnum.CameraRecordingStart, |
||||
data: PostCameraRecordingBody |
||||
} | { |
||||
cmd: PayloadCommandsEnum.CameraRecordingStop, |
||||
data: DeleteCameraRecordingParams |
||||
} | { |
||||
cmd: PayloadCommandsEnum.CameraFocalLengthSet, |
||||
data: PostCameraFocalLengthBody |
||||
} | { |
||||
cmd: PayloadCommandsEnum.GimbalReset, |
||||
data: PostGimbalResetBody |
||||
} | { |
||||
cmd: PayloadCommandsEnum.CameraAim, |
||||
data: PostCameraAimBody |
||||
} |
||||
|
||||
// 发送负载名称
|
||||
export async function postPayloadCommands (sn: string, body: PostPayloadCommandsBody): Promise<IWorkspaceResponse<null>> { |
||||
const resp = await request.post(`${API_PREFIX}/devices/${sn}/payload/commands`, body) |
||||
return resp.data |
||||
} |
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
import request from '../http/request' |
||||
import { IWorkspaceResponse } from '../http/type' |
||||
import { EFlightAreaType, ESyncStatus, FlightAreaContent } from './../../types/flight-area' |
||||
import { ELocalStorageKey } from '/@/types/enums' |
||||
import { GeojsonCoordinate } from '/@/utils/genjson' |
||||
|
||||
export interface GetFlightArea { |
||||
area_id: string, |
||||
name: string, |
||||
type: EFlightAreaType, |
||||
content: FlightAreaContent, |
||||
status: boolean, |
||||
username: string, |
||||
create_time: number, |
||||
update_time: number, |
||||
} |
||||
|
||||
export interface PostFlightAreaBody { |
||||
id: string, |
||||
name: string, |
||||
type: EFlightAreaType, |
||||
content: { |
||||
properties: { |
||||
color: string, |
||||
clampToGround: boolean, |
||||
}, |
||||
geometry: { |
||||
type: string, |
||||
coordinates: GeojsonCoordinate | GeojsonCoordinate[][], |
||||
radius?: number, |
||||
} |
||||
} |
||||
} |
||||
|
||||
export interface FlightAreaStatus { |
||||
sync_code: number, |
||||
sync_status: ESyncStatus, |
||||
sync_msg: string, |
||||
|
||||
} |
||||
export interface GetDeviceStatus { |
||||
device_sn: string, |
||||
nickname?: string, |
||||
device_name?: string, |
||||
online?: boolean, |
||||
flight_area_status: FlightAreaStatus, |
||||
} |
||||
|
||||
const MAP_API_PREFIX = '/map/api/v1' |
||||
|
||||
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '' |
||||
|
||||
export async function getFlightAreaList (): Promise<IWorkspaceResponse<GetFlightArea[]>> { |
||||
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-areas`) |
||||
return resp.data |
||||
} |
||||
|
||||
export async function changeFlightAreaStatus (area_id: string, status: boolean): Promise<IWorkspaceResponse<any>> { |
||||
const resp = await request.put(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`, { status }) |
||||
return resp.data |
||||
} |
||||
|
||||
export async function saveFlightArea (body: PostFlightAreaBody): Promise<IWorkspaceResponse<any>> { |
||||
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area`, body) |
||||
return resp.data |
||||
} |
||||
|
||||
export async function deleteFlightArea (area_id: string): Promise<IWorkspaceResponse<any>> { |
||||
const resp = await request.delete(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`) |
||||
return resp.data |
||||
} |
||||
|
||||
export async function syncFlightArea (device_sn: string[]): Promise<IWorkspaceResponse<any>> { |
||||
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/sync`, { device_sn }) |
||||
return resp.data |
||||
} |
||||
|
||||
export async function getDeviceStatus (): Promise<IWorkspaceResponse<GetDeviceStatus[]>> { |
||||
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/device-status`) |
||||
return resp.data |
||||
} |
@ -1,182 +0,0 @@
@@ -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> |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
<template> |
||||
<div> |
||||
<span class="status-tag pointer"> |
||||
<a-popconfirm |
||||
:title="getTitle()" |
||||
ok-text="Yes" |
||||
cancel-text="No" |
||||
placement="left" |
||||
@confirm="onFirmwareStatusClick(firmware)" |
||||
> |
||||
<a-tag :color="firmware.firmware_status ? commonColor.NORMAL : commonColor.FAIL" |
||||
:class="firmware.firmware_status ? 'border-corner ' : 'status-disable border-corner'"> |
||||
{{ getText(firmware.firmware_status) }} |
||||
</a-tag> |
||||
</a-popconfirm> |
||||
</span> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { defineProps, defineEmits, ref, watch, computed } from 'vue' |
||||
import { changeFirmareStatus } from '/@/api/manage' |
||||
import { ELocalStorageKey } from '/@/types' |
||||
import { Firmware, FirmwareStatusEnum } from '/@/types/device-firmware' |
||||
import { commonColor } from '/@/utils/color' |
||||
|
||||
const props = defineProps<{ |
||||
firmware: Firmware |
||||
}>() |
||||
|
||||
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||
|
||||
function getTitle () { |
||||
return `Are you sure to set this firmware to ${getText(!props.firmware.firmware_status)}?` |
||||
} |
||||
|
||||
function getText (status: boolean) { |
||||
return status ? FirmwareStatusEnum.TRUE : FirmwareStatusEnum.FALSE |
||||
} |
||||
|
||||
function onFirmwareStatusClick (record: Firmware) { |
||||
changeFirmareStatus(workspaceId, record.firmware_id, { status: !record.firmware_status }).then((res) => { |
||||
if (res.code === 0) { |
||||
record.firmware_status = !record.firmware_status |
||||
} |
||||
}) |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.status-disable{ |
||||
opacity: 0.4; |
||||
} |
||||
.border-corner { |
||||
border-radius: 3px; |
||||
} |
||||
.pointer { |
||||
cursor: pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
<template> |
||||
<div @click="selectCurrent"> |
||||
<a-dropdown class="height-100 width-100 icon-panel"> |
||||
<FlightAreaIcon :type="actionMap[selectedKey].type" :is-circle="actionMap[selectedKey].isCircle" :hide-title="true"/> |
||||
<template #overlay> |
||||
<a-menu @click="selectAction" mode="vertical-right" :selectedKeys="[selectedKey]"> |
||||
<a-menu-item v-for="(v, k) in actionMap" :key="k"> |
||||
<FlightAreaIcon :type="v.type" :is-circle="v.isCircle"/> |
||||
</a-menu-item> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { ref, defineEmits } from 'vue' |
||||
import { EFlightAreaType } from '../../types/flight-area' |
||||
import FlightAreaIcon from './FlightAreaIcon.vue' |
||||
|
||||
const emit = defineEmits(['select-action', 'click']) |
||||
|
||||
const actionMap: Record<string, { type: EFlightAreaType, isCircle: boolean}> = { |
||||
1: { |
||||
type: EFlightAreaType.DFENCE, |
||||
isCircle: true, |
||||
}, |
||||
2: { |
||||
type: EFlightAreaType.DFENCE, |
||||
isCircle: false, |
||||
}, |
||||
3: { |
||||
type: EFlightAreaType.NFZ, |
||||
isCircle: true, |
||||
}, |
||||
4: { |
||||
type: EFlightAreaType.NFZ, |
||||
isCircle: false, |
||||
}, |
||||
|
||||
} |
||||
|
||||
const selectedKey = ref<string>('1') |
||||
const selectAction = (item: any) => { |
||||
selectedKey.value = item.key |
||||
emit('select-action', actionMap[item.key]) |
||||
} |
||||
const selectCurrent = () => { |
||||
emit('click', actionMap[selectedKey.value]) |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.icon-panel { |
||||
align-items: center; |
||||
justify-content: center; |
||||
cursor: pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,197 @@
@@ -0,0 +1,197 @@
|
||||
<template> |
||||
<div class="flight-area-device-panel"> |
||||
<Title title="Choose Synchronous Devices"> |
||||
<div style="position: absolute; right: 10px;"> |
||||
<a style="color: white;" @click="closePanel"><CloseOutlined /></a> |
||||
</div> |
||||
</Title> |
||||
<div class="scrollbar"> |
||||
<div id="data" v-if="data.length !== 0"> |
||||
<div v-for="dock in data" :key="dock.device_sn"> |
||||
<div class="pt5 panel flex-row" @click="selectDock(dock)" :style="{opacity: selectedDocksMap[dock.device_sn] ? 1 : 0.5 }"> |
||||
<div style="width: 88%"> |
||||
<div class="title"> |
||||
<RobotFilled class="fz20"/> |
||||
<a-tooltip :title="dock.nickname"> |
||||
<div class="pr10 ml5" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="ml10 mr10 pr5 pl5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> |
||||
<div> |
||||
Custom Flight Area |
||||
</div> |
||||
<div> |
||||
<div v-if="!dock.status"> |
||||
<a-tooltip title="Dock offline"> |
||||
<ApiOutlined /> |
||||
</a-tooltip> |
||||
</div> |
||||
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZED"> |
||||
<a-tooltip title="Data synced"> |
||||
<CheckCircleTwoTone twoToneColor="#28d445"/> |
||||
</a-tooltip> |
||||
</div> |
||||
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING |
||||
|| deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC"> |
||||
<a-tooltip title="To be synced"> |
||||
<SyncOutlined spin /> |
||||
</a-tooltip> |
||||
</div> |
||||
<div v-else> |
||||
<a-tooltip :title="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_msg || 'No synchronization'"> |
||||
<ExclamationCircleTwoTone twoToneColor="#e70102" /> |
||||
</a-tooltip> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="box" v-if="selectedDocksMap[dock.device_sn]"> |
||||
<CheckOutlined /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<DividerLine style="position: absolute; bottom: 68px;" /> |
||||
<div class="flex-row flex-justify-between footer"> |
||||
<a-button class="mr10" @click="closePanel">Cancel |
||||
</a-button> |
||||
<a-button type="primary" :disabled="confirmDisabled" @click="syncDeviceFlightArea">Sync |
||||
</a-button> |
||||
</div> |
||||
</div> |
||||
<div v-else> |
||||
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { CloseOutlined, RobotFilled, CheckOutlined, ApiOutlined, CheckCircleTwoTone, SyncOutlined, ExclamationCircleTwoTone } from '@ant-design/icons-vue' |
||||
import Title from '/@/components/workspace/Title.vue' |
||||
import { defineEmits, onMounted, ref, defineProps, computed } from 'vue' |
||||
import { getBindingDevices } from '/@/api/manage' |
||||
import { EDeviceTypeName, ELocalStorageKey } from '/@/types' |
||||
import { IPage } from '/@/api/http/type' |
||||
import { Device } from '/@/types/device' |
||||
import DividerLine from '../workspace/DividerLine.vue' |
||||
import { message } from 'ant-design-vue' |
||||
import { GetDeviceStatus, syncFlightArea } from '/@/api/flight-area' |
||||
import { ESyncStatus } from '/@/types/flight-area' |
||||
|
||||
const props = defineProps<{ |
||||
data: GetDeviceStatus[] |
||||
}>() |
||||
const emit = defineEmits(['closePanel']) |
||||
const closePanel = () => { |
||||
emit('closePanel', false) |
||||
} |
||||
|
||||
const confirmDisabled = ref(false) |
||||
|
||||
const deviceStatusMap = computed(() => props.data.reduce((obj: Record<string, GetDeviceStatus>, val: GetDeviceStatus) => { |
||||
obj[val.device_sn] = val |
||||
return obj |
||||
}, {} as Record<string, GetDeviceStatus>)) |
||||
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '' |
||||
const body: IPage = { |
||||
page: 1, |
||||
total: 0, |
||||
page_size: 10, |
||||
} |
||||
const data = ref<Device[]>([]) |
||||
const selectedDocksMap = ref<Record<string, boolean>>({}) |
||||
|
||||
const getDocks = async () => { |
||||
await getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => { |
||||
if (res.code !== 0) { |
||||
return |
||||
} |
||||
data.value.push(...res.data.list) |
||||
body.page = res.data.pagination.page |
||||
body.page_size = res.data.pagination.page_size |
||||
body.total = res.data.pagination.total |
||||
}) |
||||
} |
||||
|
||||
const selectDock = (dock: Device) => { |
||||
if (!dock.status) { |
||||
message.info(`Dock(${dock.nickname}) is offline.`) |
||||
return |
||||
} |
||||
if (deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING || |
||||
deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC) { |
||||
message.info('The dock is synchronizing.') |
||||
return |
||||
} |
||||
selectedDocksMap.value[dock.device_sn] = !selectedDocksMap.value[dock.device_sn] |
||||
} |
||||
|
||||
onMounted(() => { |
||||
getDocks() |
||||
const key = setInterval(() => { |
||||
if (body.total === 0 || Math.ceil(body.total / body.page_size) <= body.page) { |
||||
clearInterval(key) |
||||
return |
||||
} |
||||
body.page++ |
||||
getDocks() |
||||
}, 1000) |
||||
}) |
||||
|
||||
const syncDeviceFlightArea = () => { |
||||
const keys = Object.keys(selectedDocksMap.value) |
||||
if (keys.length === 0) { |
||||
message.warn('Please select the docks that need to be synchronized.') |
||||
return |
||||
} |
||||
confirmDisabled.value = true |
||||
Object.keys(selectedDocksMap.value).forEach(k => { |
||||
const device = deviceStatusMap.value[k] |
||||
if (device) { |
||||
device.flight_area_status = { sync_code: 0, sync_status: ESyncStatus.WAIT_SYNC, sync_msg: '' } |
||||
} |
||||
}) |
||||
syncFlightArea(keys).then(res => { |
||||
if (res.code === 0) { |
||||
message.success('The devices are synchronizing...') |
||||
selectedDocksMap.value = {} |
||||
} |
||||
}).finally(() => setTimeout(() => { |
||||
confirmDisabled.value = false |
||||
}, 3000)) |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.flight-area-device-panel { |
||||
position: absolute; |
||||
left: 285px; |
||||
width: 280px; |
||||
height: 100vh; |
||||
float: right; |
||||
top: 0; |
||||
z-index: 1000; |
||||
color: white; |
||||
background: #282828; |
||||
.footer { |
||||
position: absolute; |
||||
width: 100%; |
||||
bottom: 10px; |
||||
padding: 10px; |
||||
button { |
||||
width: 45%; |
||||
border: 0; |
||||
} |
||||
} |
||||
.scrollbar { |
||||
overflow-y: auto; |
||||
height: calc(100vh - 150px); |
||||
} |
||||
.box { |
||||
font-size: 22px; |
||||
line-height: 60px; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
<template> |
||||
<div class="flex-row flex-align-center"> |
||||
<div class="shape" :class="type" :style="isCircle ? 'border-radius: 50%;' : ''"></div> |
||||
<div class="ml5" v-if="!hideTitle">{{ FlightAreaTypeTitleMap[type][isCircle ? EGeometryType.CIRCLE : EGeometryType.POLYGON] }}</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { defineProps } from 'vue' |
||||
import { EFlightAreaType, EGeometryType, FlightAreaTypeTitleMap } from '../../types/flight-area' |
||||
|
||||
const props = defineProps<{ |
||||
type: EFlightAreaType, |
||||
isCircle: boolean, |
||||
hideTitle?: boolean |
||||
}>() |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.nfz { |
||||
border-color: red; |
||||
} |
||||
.dfence { |
||||
border-color: $tag-green; |
||||
} |
||||
.shape { |
||||
width: 16px; |
||||
height: 16px; |
||||
border-width: 3px; |
||||
border-style: solid; |
||||
} |
||||
</style> |
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
<template> |
||||
<div class="panel" style="padding-top: 5px;" :class="{disable: !flightArea.status}"> |
||||
<div class="title"> |
||||
<a-tooltip :title="flightArea.name"> |
||||
<div class="pr10" style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ flightArea.name }}</div> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||
<span class="mr10">Update at {{ formatDateTime(flightArea.update_time).toLocaleString() }}</span> |
||||
</div> |
||||
<div class="flex-row flex-justify-between flex-align-center ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||
<FlightAreaIcon :type="flightArea.type" :isCircle="EGeometryType.CIRCLE === flightArea.content.geometry.type"/> |
||||
<div class="mr10 operate"> |
||||
<a-popconfirm v-if="flightArea.status" title="Is it determined to disable the current area?" okText="Disable" @confirm="changeAreaStatus(false)"> |
||||
<stop-outlined /> |
||||
</a-popconfirm> |
||||
<a-popconfirm v-else @confirm="changeAreaStatus(true)" title="Is it determined to enable the current area?" okText="Enable" > |
||||
<check-circle-outlined /> |
||||
</a-popconfirm> |
||||
<EnvironmentFilled class="ml10" @click="clickLocation"/> |
||||
<a-popconfirm title="Is it determined to delete the current area?" okText="Delete" okType="danger" @confirm="deleteArea"> |
||||
<delete-outlined class="ml10" /> |
||||
</a-popconfirm> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { defineProps, reactive, defineEmits, computed } from 'vue' |
||||
import { GetFlightArea, changeFlightAreaStatus } from '../../api/flight-area' |
||||
import FlightAreaIcon from './FlightAreaIcon.vue' |
||||
import { formatDateTime } from '../../utils/time' |
||||
import { EGeometryType } from '../../types/flight-area' |
||||
import { StopOutlined, CheckCircleOutlined, DeleteOutlined, EnvironmentFilled } from '@ant-design/icons-vue' |
||||
|
||||
const props = defineProps<{ |
||||
data: GetFlightArea |
||||
}>() |
||||
const emit = defineEmits(['delete', 'update', 'location']) |
||||
|
||||
const flightArea = computed(() => props.data) |
||||
const changeAreaStatus = (status: boolean) => { |
||||
changeFlightAreaStatus(props.data.area_id, status).then(res => { |
||||
if (res.code === 0) { |
||||
flightArea.value.status = status |
||||
emit('update', flightArea) |
||||
} |
||||
}) |
||||
} |
||||
const deleteArea = () => { |
||||
emit('delete', flightArea.value.area_id) |
||||
} |
||||
const clickLocation = () => { |
||||
emit('location', flightArea.value.area_id) |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.panel { |
||||
background: #3c3c3c; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
margin-top: 10px; |
||||
height: 90px; |
||||
width: 95%; |
||||
font-size: 13px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
|
||||
.title { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
height: 30px; |
||||
font-weight: bold; |
||||
margin: 0px 10px 0 10px; |
||||
} |
||||
.operate > *{ |
||||
font-size: 16px; |
||||
} |
||||
} |
||||
|
||||
.disable { |
||||
opacity: 50%; |
||||
} |
||||
|
||||
</style> |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
<template> |
||||
<div class="flight-area-panel"> |
||||
<div v-if="data.length === 0"> |
||||
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||
</div> |
||||
<div v-else v-for="area in flightAreaList" :key="area.area_id"> |
||||
<FlightAreaItem :data="area" @delete="deleteArea" @update="updateArea" @location="clickLocation(area)"/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { defineProps, defineEmits, ref, computed } from 'vue' |
||||
import FlightAreaItem from './FlightAreaItem.vue' |
||||
import { GetFlightArea } from '/@/api/flight-area' |
||||
|
||||
const emit = defineEmits(['deleteArea', 'updateArea', 'locationArea']) |
||||
|
||||
const props = defineProps<{ |
||||
data: GetFlightArea[] |
||||
}>() |
||||
|
||||
const flightAreaList = computed(() => props.data) |
||||
|
||||
const deleteArea = (areaId: string) => { |
||||
emit('deleteArea', areaId) |
||||
} |
||||
|
||||
const updateArea = (area: GetFlightArea) => { |
||||
emit('updateArea', area) |
||||
} |
||||
|
||||
const clickLocation = (area: GetFlightArea) => { |
||||
emit('locationArea', area) |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.flight-area-panel { |
||||
overflow-y: auto; |
||||
height: calc(100vh - 150px); |
||||
} |
||||
</style> |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
<template> |
||||
<div class="flight-area-sync-panel p10 flex-row flex-align-center" > |
||||
<RobotFilled class="fz30" twoToneColor="red" fill="#00ff00"/> |
||||
<div class="ml20 mr10 flex-column" @click="switchPanel"> |
||||
<div class="fz18">Sync Across Devices</div> |
||||
<div v-if="syncDevicesCount > 0"><a-spin /> Syncing to {{ syncDevicesCount }} devices</div> |
||||
</div> |
||||
<RightOutlined class="fz18" @click="switchPanel"/> |
||||
<FlightAreaDevicePanel v-if="visible" @close-panel="closePanel" :data="syncDevices"/> |
||||
</div> |
||||
|
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { RobotFilled, RightOutlined } from '@ant-design/icons-vue' |
||||
import FlightAreaDevicePanel from '/@/components/flight-area/FlightAreaDevicePanel.vue' |
||||
import { computed, onMounted, ref, watch } from 'vue' |
||||
import { GetDeviceStatus, getDeviceStatus } from '/@/api/flight-area' |
||||
import { ESyncStatus, FlightAreaSyncProgress } from '/@/types/flight-area' |
||||
import { useFlightAreaSyncProgressEvent } from './use-flight-area-sync-progress-event' |
||||
|
||||
const visible = ref(false) |
||||
const syncDevices = ref<GetDeviceStatus[]>([]) |
||||
const syncDevicesCount = computed(() => syncDevices.value.filter(device => |
||||
device.flight_area_status.sync_status === ESyncStatus.SYNCHRONIZING || device.flight_area_status.sync_status === ESyncStatus.WAIT_SYNC).length) |
||||
const getAllDeviceStatus = () => { |
||||
getDeviceStatus().then(res => { |
||||
if (res.code === 0) { |
||||
syncDevices.value = res.data |
||||
} |
||||
}) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
getAllDeviceStatus() |
||||
}) |
||||
const switchPanel = () => { |
||||
visible.value = !visible.value |
||||
} |
||||
const closePanel = (val: boolean) => { |
||||
visible.value = val |
||||
} |
||||
|
||||
const handleSyncProgress = (data: FlightAreaSyncProgress) => { |
||||
let has = false |
||||
const status = { sync_code: data.result, sync_status: data.status, sync_msg: data.message } |
||||
syncDevices.value.forEach(device => { |
||||
if (data.sn === device.device_sn) { |
||||
device.flight_area_status = status |
||||
has = true |
||||
} |
||||
}) |
||||
if (!has) { |
||||
syncDevices.value.push({ device_sn: data.sn, flight_area_status: status }) |
||||
} |
||||
} |
||||
useFlightAreaSyncProgressEvent(handleSyncProgress) |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.flight-area-sync-panel { |
||||
height: 70px; |
||||
cursor: pointer; |
||||
} |
||||
</style> |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
import { FlightAreasDroneLocation } from '/@/types/flight-area' |
||||
import { CommonHostWs } from '/@/websocket' |
||||
import EventBus from '/@/event-bus/' |
||||
import { onMounted, onBeforeUnmount } from 'vue' |
||||
|
||||
export function useFlightAreaDroneLocationEvent (onFlightAreaDroneLocationWs: (data: CommonHostWs<FlightAreasDroneLocation>) => void): void { |
||||
function handleDroneLocationEvent (data: any) { |
||||
onFlightAreaDroneLocationWs(data.data) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
EventBus.on('flightAreasDroneLocationWs', handleDroneLocationEvent) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
EventBus.off('flightAreasDroneLocationWs', handleDroneLocationEvent) |
||||
}) |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
import EventBus from '/@/event-bus/' |
||||
import { onMounted, onBeforeUnmount } from 'vue' |
||||
import { FlightAreaSyncProgress } from '/@/types/flight-area' |
||||
|
||||
export function useFlightAreaSyncProgressEvent (onFlightAreaSyncProgressWs: (data: FlightAreaSyncProgress) => void): void { |
||||
function handleSyncProgressEvent (data: FlightAreaSyncProgress) { |
||||
onFlightAreaSyncProgressWs(data) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
EventBus.on('flightAreasSyncProgressWs', handleSyncProgressEvent) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
EventBus.off('flightAreasSyncProgressWs', handleSyncProgressEvent) |
||||
}) |
||||
} |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
import { EFlightAreaUpdate, FlightAreaUpdate, FlightAreasDroneLocation } from '/@/types/flight-area' |
||||
import { CommonHostWs } from '/@/websocket' |
||||
import EventBus from '/@/event-bus/' |
||||
import { onMounted, onBeforeUnmount } from 'vue' |
||||
|
||||
function doNothing (data: FlightAreaUpdate) { |
||||
} |
||||
export function useFlightAreaUpdateEvent (addFunc = doNothing, deleteFunc = doNothing, updateFunc = doNothing): void { |
||||
function handleDroneLocationEvent (data: FlightAreaUpdate) { |
||||
switch (data.operation) { |
||||
case EFlightAreaUpdate.ADD: |
||||
addFunc(data) |
||||
break |
||||
case EFlightAreaUpdate.UPDATE: |
||||
updateFunc(data) |
||||
break |
||||
case EFlightAreaUpdate.DELETE: |
||||
deleteFunc(data) |
||||
break |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
EventBus.on('flightAreasUpdateWs', handleDroneLocationEvent) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
EventBus.off('flightAreasUpdateWs', handleDroneLocationEvent) |
||||
}) |
||||
} |
@ -0,0 +1,155 @@
@@ -0,0 +1,155 @@
|
||||
import { message, notification } from 'ant-design-vue' |
||||
import { MapDoodleEnum } from '/@/types/map-enum' |
||||
import { getRoot } from '/@/root' |
||||
import { PostFlightAreaBody, saveFlightArea } from '/@/api/flight-area' |
||||
import { generateCircleContent, generatePolyContent } from '/@/utils/map-layer-utils' |
||||
import { GeojsonCoordinate } from '/@/utils/genjson' |
||||
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform.js' |
||||
import { uuidv4 } from '/@/utils/uuid' |
||||
import { CommonHostWs } from '/@/websocket' |
||||
import { FlightAreasDroneLocation } from '/@/types/flight-area' |
||||
import rootStore from '/@/store' |
||||
import { h } from 'vue' |
||||
import { useGMapCover } from '/@/hooks/use-g-map-cover' |
||||
import moment from 'moment' |
||||
import { DATE_FORMAT } from '/@/utils/constants' |
||||
|
||||
export function useFlightArea () { |
||||
const root = getRoot() |
||||
const store = rootStore |
||||
const coverMap = store.state.coverMap |
||||
|
||||
let useGMapCoverHook = useGMapCover() |
||||
|
||||
const MIN_RADIUS = 10 |
||||
function checkCircle (obj: any): boolean { |
||||
if (obj.getRadius() < MIN_RADIUS) { |
||||
message.error(`The radius must be greater than ${MIN_RADIUS}m.`) |
||||
root.$map.remove(obj) |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
function checkPolygon (obj: any): boolean { |
||||
const path: any[][] = obj.getPath() |
||||
if (path.length < 3) { |
||||
message.error('The path of the polygon cannot be crossed.') |
||||
root.$map.remove(obj) |
||||
return false |
||||
} |
||||
// root.$aMap.GeometryUtil.doesLineLineIntersect()
|
||||
return true |
||||
} |
||||
|
||||
function setExtData (obj: any) { |
||||
let ext = obj.getExtData() |
||||
const id = uuidv4() |
||||
const name = `${ext.type}-${moment().format(DATE_FORMAT)}` |
||||
ext = Object.assign({}, ext, { id, name }) |
||||
obj.setExtData(ext) |
||||
return ext |
||||
} |
||||
function createFlightArea (obj: any) { |
||||
const ext = obj.getExtData() |
||||
const data = { |
||||
id: ext.id, |
||||
type: ext.type, |
||||
name: ext.name, |
||||
} |
||||
let coordinates: GeojsonCoordinate | GeojsonCoordinate[][] |
||||
let content |
||||
switch (ext.mapType) { |
||||
case 'circle': |
||||
content = generateCircleContent(obj.getCenter(), obj.getRadius()) |
||||
coordinates = getWgs84(content.geometry.coordinates as GeojsonCoordinate) |
||||
break |
||||
case 'polygon': |
||||
content = generatePolyContent(obj.getPath()).content |
||||
coordinates = [getWgs84(content.geometry.coordinates[0] as GeojsonCoordinate[])] |
||||
break |
||||
default: |
||||
message.error(`Invalid type: ${obj.mapType}`) |
||||
root.$map.remove(obj) |
||||
return |
||||
} |
||||
content.geometry.coordinates = coordinates |
||||
|
||||
saveFlightArea(Object.assign({}, data, { content }) as PostFlightAreaBody).then(res => { |
||||
if (res.code !== 0) { |
||||
useGMapCoverHook.removeCoverFromMap(ext.id) |
||||
} |
||||
}).finally(() => root.$map.remove(obj)) |
||||
} |
||||
|
||||
function getDrawFlightAreaCallback (obj: any) { |
||||
useGMapCoverHook = useGMapCover() |
||||
const ext = setExtData(obj) |
||||
switch (ext.mapType) { |
||||
case MapDoodleEnum.CIRCLE: |
||||
if (!checkCircle(obj)) { |
||||
return |
||||
} |
||||
break |
||||
case MapDoodleEnum.POLYGON: |
||||
if (!checkPolygon(obj)) { |
||||
return |
||||
} |
||||
break |
||||
default: |
||||
break |
||||
} |
||||
createFlightArea(obj) |
||||
} |
||||
|
||||
const getWgs84 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => { |
||||
if (coordinate[0] instanceof Array) { |
||||
return (coordinate as GeojsonCoordinate[]).map(c => gcj02towgs84(c[0], c[1])) as T |
||||
} |
||||
return gcj02towgs84(coordinate[0], coordinate[1]) |
||||
} |
||||
|
||||
const getGcj02 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => { |
||||
if (coordinate[0] instanceof Array) { |
||||
return (coordinate as GeojsonCoordinate[]).map(c => wgs84togcj02(c[0], c[1])) as T |
||||
} |
||||
return wgs84togcj02(coordinate[0], coordinate[1]) |
||||
} |
||||
|
||||
const onFlightAreaDroneLocationWs = (data: CommonHostWs<FlightAreasDroneLocation>) => { |
||||
const nearArea = data.host.drone_locations.filter(val => !val.is_in_area) |
||||
const inArea = data.host.drone_locations.filter(val => val.is_in_area) |
||||
notification.warning({ |
||||
key: `flight-area-${data.sn}`, |
||||
message: `Drone(${data.sn}) flight area information`, |
||||
description: h('div', |
||||
[ |
||||
h('div', [ |
||||
h('span', { class: 'fz18' }, 'In the flight area: '), |
||||
h('ul', [ |
||||
...inArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`)) |
||||
]) |
||||
]), |
||||
h('div', [ |
||||
h('span', { class: 'fz18' }, 'Near the flight area: '), |
||||
h('ul', [ |
||||
...nearArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`)) |
||||
]) |
||||
]) |
||||
]), |
||||
duration: null, |
||||
style: { |
||||
width: '420px', |
||||
marginTop: '-8px', |
||||
marginLeft: '-28px', |
||||
} |
||||
}) |
||||
} |
||||
|
||||
return { |
||||
getDrawFlightAreaCallback, |
||||
getGcj02, |
||||
getWgs84, |
||||
onFlightAreaDroneLocationWs, |
||||
} |
||||
} |
@ -0,0 +1,242 @@
@@ -0,0 +1,242 @@
|
||||
<template> |
||||
<div class="device-setting-wrapper"> |
||||
<div class="device-setting-header">Device Property Set</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 './use-device-setting' |
||||
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> |
@ -0,0 +1,106 @@
@@ -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> |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
<template> |
||||
<div class="drone-control-info-wrap"> |
||||
<a-textarea v-model:value="info" placeholder="drc info" :rows="5" disabled/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { ref, defineProps, watch } from 'vue' |
||||
|
||||
const props = defineProps<{ |
||||
message?: string, |
||||
}>() |
||||
|
||||
const info = ref('') |
||||
watch(() => props.message, message => { |
||||
info.value = message || '' |
||||
}, { |
||||
immediate: true |
||||
}) |
||||
|
||||
// const emit = defineEmits(['cancel', 'confirm']) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.drone-control-info-wrap { |
||||
&::v-deep{ |
||||
textarea.ant-input { |
||||
background-color: #000; |
||||
color: #fff; |
||||
white-space: pre-wrap; |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,837 @@
@@ -0,0 +1,837 @@
|
||||
<template> |
||||
<div class="drone-control-wrapper"> |
||||
<div class="drone-control-header">Drone Flight Control</div> |
||||
<div class="drone-control-box"> |
||||
<div class="box"> |
||||
<div class="row"> |
||||
<div class="drone-control"><Button :ghost="!flightController" size="small" @click="onClickFightControl">{{ flightController ? 'Exit Remote Control' : 'Enter Remote Control'}}</Button></div> |
||||
</div> |
||||
<div class="row"> |
||||
<div class="drone-control-direction"> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_Q)" @onmouseup="onMouseUp"> |
||||
<template #icon><UndoOutlined /></template><span class="word">Q</span> |
||||
</Button> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_W)" @onmouseup="onMouseUp"> |
||||
<template #icon><UpOutlined/></template><span class="word">W</span> |
||||
</Button> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_E)" @onmouseup="onMouseUp"> |
||||
<template #icon><RedoOutlined /></template><span class="word">E</span> |
||||
</Button> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_UP)" @onmouseup="onMouseUp"> |
||||
<template #icon><ArrowUpOutlined /></template> |
||||
</Button> |
||||
<br /> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_A)" @onmouseup="onMouseUp"> |
||||
<template #icon><LeftOutlined/></template><span class="word">A</span> |
||||
</Button> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_S)" @onmouseup="onMouseUp"> |
||||
<template #icon><DownOutlined/></template><span class="word">S</span> |
||||
</Button> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_D)" @onmouseup="onMouseUp"> |
||||
<template #icon><RightOutlined/></template><span class="word">D</span> |
||||
</Button> |
||||
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_DOWN)" @onmouseup="onMouseUp"> |
||||
<template #icon><ArrowDownOutlined /></template> |
||||
</Button> |
||||
</div> |
||||
<Button type="primary" size="small" danger ghost @click="handleEmergencyStop" > |
||||
<template #icon><PauseCircleOutlined/></template><span>Break</span> |
||||
</Button> |
||||
</div> |
||||
<div class="row"> |
||||
<DroneControlPopover |
||||
:visible="flyToPointPopoverData.visible" |
||||
:loading="flyToPointPopoverData.loading" |
||||
@confirm="($event) => onFlyToConfirm(true)" |
||||
@cancel="($event) =>onFlyToConfirm(false)" |
||||
> |
||||
<template #formContent> |
||||
<div class="form-content"> |
||||
<div> |
||||
<span class="form-label">latitude:</span> |
||||
<a-input-number v-model:value="flyToPointPopoverData.latitude"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">longitude:</span> |
||||
<a-input-number v-model:value="flyToPointPopoverData.longitude"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">height(m):</span> |
||||
<a-input-number v-model:value="flyToPointPopoverData.height"/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<Button size="small" ghost @click="onShowFlyToPopover" > |
||||
<span>Fly to</span> |
||||
</Button> |
||||
</DroneControlPopover> |
||||
<Button size="small" ghost @click="onStopFlyToPoint" > |
||||
<span>Stop Fly to</span> |
||||
</Button> |
||||
<DroneControlPopover |
||||
:visible="takeoffToPointPopoverData.visible" |
||||
:loading="takeoffToPointPopoverData.loading" |
||||
@confirm="($event) => onTakeoffToPointConfirm(true)" |
||||
@cancel="($event) =>onTakeoffToPointConfirm(false)" |
||||
> |
||||
<template #formContent> |
||||
<div class="form-content"> |
||||
<div> |
||||
<span class="form-label">latitude:</span> |
||||
<a-input-number v-model:value="takeoffToPointPopoverData.latitude"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">longitude:</span> |
||||
<a-input-number v-model:value="takeoffToPointPopoverData.longitude"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">height(m):</span> |
||||
<a-input-number v-model:value="takeoffToPointPopoverData.height"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Safe Takeoff Altitude(m):</span> |
||||
<a-input-number v-model:value="takeoffToPointPopoverData.securityTakeoffHeight"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Return-to-Home Altitude(m):</span> |
||||
<a-input-number v-model:value="takeoffToPointPopoverData.rthAltitude"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Lost Action:</span> |
||||
<a-select |
||||
v-model:value="takeoffToPointPopoverData.rcLostAction" |
||||
style="width: 120px" |
||||
:options="LostControlActionInCommandFLightOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Wayline Lost Action:</span> |
||||
<a-select |
||||
v-model:value="takeoffToPointPopoverData.exitWaylineWhenRcLost" |
||||
style="width: 120px" |
||||
:options="WaylineLostControlActionInCommandFlightOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Return-to-Home Mode:</span> |
||||
<a-select |
||||
v-model:value="takeoffToPointPopoverData.rthMode" |
||||
style="width: 120px" |
||||
:options="RthModeInCommandFlightOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Commander Mode Lost Action:</span> |
||||
<a-select |
||||
v-model:value="takeoffToPointPopoverData.commanderModeLostAction" |
||||
style="width: 120px" |
||||
:options="CommanderModeLostActionInCommandFlightOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Commander Flight Mode:</span> |
||||
<a-select |
||||
v-model:value="takeoffToPointPopoverData.commanderFlightMode" |
||||
style="width: 120px" |
||||
:options="CommanderFlightModeInCommandFlightOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">Commander Flight Height(m):</span> |
||||
<a-input-number v-model:value="takeoffToPointPopoverData.commanderFlightHeight"/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<Button size="small" ghost @click="onShowTakeoffToPointPopover" > |
||||
<span>Take off</span> |
||||
</Button> |
||||
<div v-for="(cmdItem) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item"> |
||||
<Button :loading="cmdItem.loading" size="small" ghost @click="sendControlCmd(cmdItem, 0)"> |
||||
{{ cmdItem.operateText }} |
||||
</Button> |
||||
</div> |
||||
<div> |
||||
<Button size="small" ghost @click="openLivestreamAgora" > |
||||
<span>Agora Live</span> |
||||
</Button> |
||||
<Button size="small" ghost @click="openLivestreamOthers" > |
||||
<span>RTMP/GB28181 Live</span> |
||||
</Button> |
||||
</div> |
||||
</DroneControlPopover> |
||||
</div> |
||||
</div> |
||||
<div class="box"> |
||||
<div class="row"> |
||||
<Select v-model:value="payloadSelectInfo.value" style="width: 110px; marginRight: 5px" :options="payloadSelectInfo.options" @change="handlePayloadChange"/> |
||||
<div class="drone-control"> |
||||
<Button type="primary" size="small" @click="onAuthPayload">Payload Control</Button> |
||||
</div> |
||||
</div> |
||||
<div class="row"> |
||||
<DroneControlPopover |
||||
:visible="gimbalResetPopoverData.visible" |
||||
:loading="gimbalResetPopoverData.loading" |
||||
@confirm="($event) => onGimbalResetConfirm(true)" |
||||
@cancel="($event) =>onGimbalResetConfirm(false)" |
||||
> |
||||
<template #formContent> |
||||
<div class="form-content"> |
||||
<div> |
||||
<span class="form-label">reset mode:</span> |
||||
<a-select |
||||
v-model:value="gimbalResetPopoverData.resetMode" |
||||
style="width: 180px" |
||||
:options="GimbalResetModeOptions" |
||||
></a-select> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<Button size="small" ghost @click="onShowGimbalResetPopover"> |
||||
<span>Gimbal Reset</span> |
||||
</Button> |
||||
</DroneControlPopover> |
||||
<Button size="small" ghost @click="onSwitchCameraMode"> |
||||
<span>Camera Mode Switch</span> |
||||
</Button> |
||||
</div> |
||||
<div class="row"> |
||||
<Button size="small" ghost @click="onStartCameraRecording"> |
||||
<span>Start Recording</span> |
||||
</Button> |
||||
<Button size="small" ghost @click="onStopCameraRecording"> |
||||
<span>Stop Recording</span> |
||||
</Button> |
||||
</div> |
||||
<div class="row"> |
||||
<Button size="small" ghost @click="onTakeCameraPhoto"> |
||||
<span>Take Photo</span> |
||||
</Button> |
||||
<DroneControlPopover |
||||
:visible="zoomFactorPopoverData.visible" |
||||
:loading="zoomFactorPopoverData.loading" |
||||
@confirm="($event) => onZoomFactorConfirm(true)" |
||||
@cancel="($event) =>onZoomFactorConfirm(false)" |
||||
> |
||||
<template #formContent> |
||||
<div class="form-content"> |
||||
<div> |
||||
<span class="form-label">camera type:</span> |
||||
<a-select |
||||
v-model:value="zoomFactorPopoverData.cameraType" |
||||
style="width: 120px" |
||||
:options="ZoomCameraTypeOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">zoom factor:</span> |
||||
<a-input-number v-model:value="zoomFactorPopoverData.zoomFactor" :min="2" :max="200" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<Button size="small" ghost @click="($event) => onShowZoomFactorPopover()"> |
||||
<span class="word" @click=";">Zoom</span> |
||||
</Button> |
||||
</DroneControlPopover> |
||||
<DroneControlPopover |
||||
:visible="cameraAimPopoverData.visible" |
||||
:loading="cameraAimPopoverData.loading" |
||||
@confirm="($event) => onCameraAimConfirm(true)" |
||||
@cancel="($event) =>onCameraAimConfirm(false)" |
||||
> |
||||
<template #formContent> |
||||
<div class="form-content"> |
||||
<div> |
||||
<span class="form-label">camera type:</span> |
||||
<a-select |
||||
v-model:value="cameraAimPopoverData.cameraType" |
||||
style="width: 120px" |
||||
:options="CameraTypeOptions" |
||||
></a-select> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">locked:</span> |
||||
<a-switch v-model:checked="cameraAimPopoverData.locked"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">x:</span> |
||||
<a-input-number v-model:value="cameraAimPopoverData.x" :min="0" :max="1"/> |
||||
</div> |
||||
<div> |
||||
<span class="form-label">y:</span> |
||||
<a-input-number v-model:value="cameraAimPopoverData.y" :min="0" :max="1"/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<Button size="small" ghost @click="($event) => onShowCameraAimPopover()"> |
||||
<span class="word" @click=";">AIM</span> |
||||
</Button> |
||||
</DroneControlPopover> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<!-- 信息提示 --> |
||||
<DroneControlInfoPanel :message="drcInfo"></DroneControlInfoPanel> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, reactive, ref, watch, computed, onMounted, watchEffect } from 'vue' |
||||
import { Select, message, Button } from 'ant-design-vue' |
||||
import { PayloadInfo, DeviceInfoType, ControlSource, DeviceOsdCamera, DrcStateEnum } from '/@/types/device' |
||||
import { useMyStore } from '/@/store' |
||||
import { postDrcEnter, postDrcExit } from '/@/api/drc' |
||||
import { useMqtt, DeviceTopicInfo } from './use-mqtt' |
||||
import { DownOutlined, UpOutlined, LeftOutlined, RightOutlined, PauseCircleOutlined, UndoOutlined, RedoOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue' |
||||
import { useManualControl, KeyCode } from './use-manual-control' |
||||
import { usePayloadControl } from './use-payload-control' |
||||
import { CameraMode, CameraType, CameraTypeOptions, ZoomCameraTypeOptions, CameraListItem } from '/@/types/live-stream' |
||||
import { useDroneControlWsEvent } from './use-drone-control-ws-event' |
||||
import { useDroneControlMqttEvent } from './use-drone-control-mqtt-event' |
||||
import { |
||||
postFlightAuth, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight, ERthMode, |
||||
ECommanderModeLostAction, ECommanderFlightMode |
||||
} from '/@/api/drone-control/drone' |
||||
import { useDroneControl } from './use-drone-control' |
||||
import { |
||||
GimbalResetMode, GimbalResetModeOptions, LostControlActionInCommandFLightOptions, WaylineLostControlActionInCommandFlightOptions, |
||||
RthModeInCommandFlightOptions, CommanderModeLostActionInCommandFlightOptions, CommanderFlightModeInCommandFlightOptions |
||||
} from '/@/types/drone-control' |
||||
import DroneControlPopover from './DroneControlPopover.vue' |
||||
import DroneControlInfoPanel from './DroneControlInfoPanel.vue' |
||||
import { noDebugCmdList as baseCmdList, DeviceCmdItem, DeviceCmd } from '/@/types/device-cmd' |
||||
import { useDockControl } from './use-dock-control' |
||||
|
||||
const props = defineProps<{ |
||||
sn: string, |
||||
deviceInfo: DeviceInfoType, |
||||
payloads: null | PayloadInfo[] |
||||
}>() |
||||
|
||||
const store = useMyStore() |
||||
const clientId = computed(() => { |
||||
return store.state.clientId |
||||
}) |
||||
|
||||
const initCmdList = baseCmdList.map(cmdItem => Object.assign({}, cmdItem)) |
||||
const cmdList = ref(initCmdList) |
||||
|
||||
const { |
||||
sendDockControlCmd |
||||
} = useDockControl() |
||||
|
||||
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) { |
||||
cmdItem.loading = true |
||||
const result = await sendDockControlCmd({ |
||||
sn: props.sn, |
||||
cmd: cmdItem.cmdKey, |
||||
action: cmdItem.action |
||||
}, false) |
||||
if (result) { |
||||
message.success('Return home successful') |
||||
if (flightController.value) { |
||||
exitFlightCOntrol() |
||||
} |
||||
} else { |
||||
message.error('Failed to return home') |
||||
} |
||||
cmdItem.loading = false |
||||
} |
||||
|
||||
const { flyToPoint, stopFlyToPoint, takeoffToPoint } = useDroneControl() |
||||
const MAX_SPEED = 14 |
||||
|
||||
const flyToPointPopoverData = reactive({ |
||||
visible: false, |
||||
loading: false, |
||||
latitude: null as null | number, |
||||
longitude: null as null | number, |
||||
height: null as null | number, |
||||
maxSpeed: MAX_SPEED, |
||||
}) |
||||
|
||||
function onShowFlyToPopover () { |
||||
flyToPointPopoverData.visible = !flyToPointPopoverData.visible |
||||
flyToPointPopoverData.loading = false |
||||
flyToPointPopoverData.latitude = null |
||||
flyToPointPopoverData.longitude = null |
||||
flyToPointPopoverData.height = null |
||||
} |
||||
|
||||
async function onFlyToConfirm (confirm: boolean) { |
||||
if (confirm) { |
||||
if (!flyToPointPopoverData.height || !flyToPointPopoverData.latitude || !flyToPointPopoverData.longitude) { |
||||
message.error('Input error') |
||||
return |
||||
} |
||||
try { |
||||
await flyToPoint(props.sn, { |
||||
max_speed: flyToPointPopoverData.maxSpeed, |
||||
points: [ |
||||
{ |
||||
latitude: flyToPointPopoverData.latitude, |
||||
longitude: flyToPointPopoverData.longitude, |
||||
height: flyToPointPopoverData.height |
||||
} |
||||
] |
||||
}) |
||||
} catch (error) { |
||||
} |
||||
} |
||||
flyToPointPopoverData.visible = false |
||||
} |
||||
|
||||
async function onStopFlyToPoint () { |
||||
await stopFlyToPoint(props.sn) |
||||
} |
||||
|
||||
const takeoffToPointPopoverData = reactive({ |
||||
visible: false, |
||||
loading: false, |
||||
latitude: null as null | number, |
||||
longitude: null as null | number, |
||||
height: null as null | number, |
||||
securityTakeoffHeight: null as null | number, |
||||
maxSpeed: MAX_SPEED, |
||||
rthAltitude: null as null | number, |
||||
rcLostAction: LostControlActionInCommandFLight.RETURN_HOME, |
||||
exitWaylineWhenRcLost: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION, |
||||
rthMode: ERthMode.SETTING, |
||||
commanderModeLostAction: ECommanderModeLostAction.CONTINUE, |
||||
commanderFlightMode: ECommanderFlightMode.SETTING, |
||||
commanderFlightHeight: null as null | number, |
||||
}) |
||||
|
||||
function onShowTakeoffToPointPopover () { |
||||
takeoffToPointPopoverData.visible = !takeoffToPointPopoverData.visible |
||||
takeoffToPointPopoverData.loading = false |
||||
takeoffToPointPopoverData.latitude = null |
||||
takeoffToPointPopoverData.longitude = null |
||||
takeoffToPointPopoverData.securityTakeoffHeight = null |
||||
takeoffToPointPopoverData.rthAltitude = null |
||||
takeoffToPointPopoverData.rcLostAction = LostControlActionInCommandFLight.RETURN_HOME |
||||
takeoffToPointPopoverData.exitWaylineWhenRcLost = WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION |
||||
takeoffToPointPopoverData.rthMode = ERthMode.SETTING |
||||
takeoffToPointPopoverData.commanderModeLostAction = ECommanderModeLostAction.CONTINUE |
||||
takeoffToPointPopoverData.commanderFlightMode = ECommanderFlightMode.SETTING |
||||
takeoffToPointPopoverData.commanderFlightHeight = null |
||||
} |
||||
|
||||
async function onTakeoffToPointConfirm (confirm: boolean) { |
||||
if (confirm) { |
||||
if (!takeoffToPointPopoverData.height || |
||||
!takeoffToPointPopoverData.latitude || |
||||
!takeoffToPointPopoverData.longitude || |
||||
!takeoffToPointPopoverData.securityTakeoffHeight || |
||||
!takeoffToPointPopoverData.rthAltitude || |
||||
!takeoffToPointPopoverData.commanderFlightHeight) { |
||||
message.error('Input error') |
||||
return |
||||
} |
||||
try { |
||||
await takeoffToPoint(props.sn, { |
||||
target_latitude: takeoffToPointPopoverData.latitude, |
||||
target_longitude: takeoffToPointPopoverData.longitude, |
||||
target_height: takeoffToPointPopoverData.height, |
||||
security_takeoff_height: takeoffToPointPopoverData.securityTakeoffHeight, |
||||
rth_altitude: takeoffToPointPopoverData.rthAltitude, |
||||
max_speed: takeoffToPointPopoverData.maxSpeed, |
||||
rc_lost_action: takeoffToPointPopoverData.rcLostAction, |
||||
exit_wayline_when_rc_lost: takeoffToPointPopoverData.exitWaylineWhenRcLost, |
||||
rth_mode: takeoffToPointPopoverData.rthMode, |
||||
commander_mode_lost_action: takeoffToPointPopoverData.commanderModeLostAction, |
||||
commander_flight_mode: takeoffToPointPopoverData.commanderFlightMode, |
||||
commander_flight_height: takeoffToPointPopoverData.commanderFlightHeight, |
||||
}) |
||||
} catch (error) { |
||||
} |
||||
} |
||||
takeoffToPointPopoverData.visible = false |
||||
} |
||||
|
||||
const deviceTopicInfo: DeviceTopicInfo = reactive({ |
||||
sn: props.sn, |
||||
pubTopic: '', |
||||
subTopic: '' |
||||
}) |
||||
|
||||
useMqtt(deviceTopicInfo) |
||||
|
||||
// 飞行控制 |
||||
// const drcState = computed(() => { |
||||
// return store.state.deviceState?.dockInfo[props.sn]?.link_osd?.drc_state === DrcStateEnum.CONNECTED |
||||
// }) |
||||
const flightController = ref(false) |
||||
|
||||
async function onClickFightControl () { |
||||
if (flightController.value) { |
||||
exitFlightCOntrol() |
||||
return |
||||
} |
||||
enterFlightControl() |
||||
} |
||||
|
||||
// 进入飞行控制 |
||||
async function enterFlightControl () { |
||||
try { |
||||
const { code, data } = await postDrcEnter({ |
||||
client_id: clientId.value, |
||||
dock_sn: props.sn, |
||||
}) |
||||
if (code === 0) { |
||||
flightController.value = true |
||||
if (data.sub && data.sub.length > 0) { |
||||
deviceTopicInfo.subTopic = data.sub[0] |
||||
} |
||||
if (data.pub && data.pub.length > 0) { |
||||
deviceTopicInfo.pubTopic = data.pub[0] |
||||
} |
||||
// 获取飞行控制权 |
||||
if (droneControlSource.value !== ControlSource.A) { |
||||
await postFlightAuth(props.sn) |
||||
} |
||||
message.success('Get flight control successfully') |
||||
} |
||||
} catch (error: any) { |
||||
} |
||||
} |
||||
|
||||
// 退出飞行控制 |
||||
async function exitFlightCOntrol () { |
||||
try { |
||||
const { code } = await postDrcExit({ |
||||
client_id: clientId.value, |
||||
dock_sn: props.sn, |
||||
}) |
||||
if (code === 0) { |
||||
flightController.value = false |
||||
deviceTopicInfo.subTopic = '' |
||||
deviceTopicInfo.pubTopic = '' |
||||
message.success('Exit flight control') |
||||
} |
||||
} catch (error: any) { |
||||
} |
||||
} |
||||
|
||||
// drc mqtt message |
||||
const { drcInfo, errorInfo } = useDroneControlMqttEvent(props.sn) |
||||
|
||||
const { |
||||
handleKeyup, |
||||
handleEmergencyStop, |
||||
resetControlState, |
||||
} = useManualControl(deviceTopicInfo, flightController) |
||||
|
||||
function onMouseDown (type: KeyCode) { |
||||
handleKeyup(type) |
||||
} |
||||
|
||||
function onMouseUp () { |
||||
resetControlState() |
||||
} |
||||
|
||||
// 负载控制 |
||||
const payloadSelectInfo = { |
||||
value: null as any, |
||||
controlSource: undefined as undefined | ControlSource, |
||||
options: [] as any, |
||||
payloadIndex: '' as string, |
||||
camera: undefined as undefined | DeviceOsdCamera // 当前负载osd信息 |
||||
} |
||||
|
||||
const handlePayloadChange = (value: string) => { |
||||
const payload = props.payloads?.find(item => item.payload_sn === value) |
||||
if (payload) { |
||||
payloadSelectInfo.payloadIndex = payload.payload_index || '' |
||||
payloadSelectInfo.controlSource = payload.control_source |
||||
payloadSelectInfo.camera = undefined |
||||
} |
||||
} |
||||
|
||||
// function getCurrentCamera (cameraList: CameraListItem[], cameraIndex?: string):CameraListItem | null { |
||||
// let camera = null |
||||
// cameraList.forEach(item => { |
||||
// if (item.camera_index === cameraIndex) { |
||||
// camera = item |
||||
// } |
||||
// }) |
||||
// return camera |
||||
// } |
||||
|
||||
// const currentCamera = computed(() => { |
||||
// return getCurrentCamera(props.deviceInfo.dock.basic_osd.live_capacity?.device_list[0]?.camera_list as CameraListItem[], camera_index) |
||||
// }) |
||||
// 更新负载信息 |
||||
watch(() => props.payloads, (payloads) => { |
||||
if (payloads && payloads.length > 0) { |
||||
payloadSelectInfo.value = payloads[0].payload_sn |
||||
payloadSelectInfo.controlSource = payloads[0].control_source || ControlSource.B |
||||
payloadSelectInfo.payloadIndex = payloads[0].payload_index || '' |
||||
payloadSelectInfo.options = payloads.map(item => ({ label: item.payload_name, value: item.payload_sn })) |
||||
payloadSelectInfo.camera = undefined |
||||
} else { |
||||
payloadSelectInfo.value = null |
||||
payloadSelectInfo.controlSource = undefined |
||||
payloadSelectInfo.options = [] |
||||
payloadSelectInfo.payloadIndex = '' |
||||
payloadSelectInfo.camera = undefined |
||||
} |
||||
}, { |
||||
immediate: true, |
||||
deep: true |
||||
}) |
||||
watch(() => props.deviceInfo.device, (droneOsd) => { |
||||
if (droneOsd && droneOsd.cameras) { |
||||
payloadSelectInfo.camera = droneOsd.cameras.find(item => item.payload_index === payloadSelectInfo.payloadIndex) |
||||
} else { |
||||
payloadSelectInfo.camera = undefined |
||||
} |
||||
}, { |
||||
immediate: true, |
||||
deep: true |
||||
}) |
||||
|
||||
// ws 消息通知 |
||||
const { droneControlSource, payloadControlSource } = useDroneControlWsEvent(props.sn, payloadSelectInfo.value) |
||||
watch(() => payloadControlSource, (controlSource) => { |
||||
payloadSelectInfo.controlSource = controlSource.value |
||||
}, { |
||||
immediate: true, |
||||
deep: true |
||||
}) |
||||
const { |
||||
checkPayloadAuth, |
||||
authPayload, |
||||
resetGimbal, |
||||
switchCameraMode, |
||||
takeCameraPhoto, |
||||
startCameraRecording, |
||||
stopCameraRecording, |
||||
changeCameraFocalLength, |
||||
cameraAim, |
||||
} = usePayloadControl() |
||||
|
||||
async function onAuthPayload () { |
||||
const result = await authPayload(props.sn, payloadSelectInfo.payloadIndex) |
||||
if (result) { |
||||
payloadControlSource.value = ControlSource.A |
||||
} |
||||
} |
||||
|
||||
const gimbalResetPopoverData = reactive({ |
||||
visible: false, |
||||
loading: false, |
||||
resetMode: null as null | GimbalResetMode, |
||||
}) |
||||
|
||||
function onShowGimbalResetPopover () { |
||||
gimbalResetPopoverData.visible = !gimbalResetPopoverData.visible |
||||
gimbalResetPopoverData.loading = false |
||||
gimbalResetPopoverData.resetMode = null |
||||
} |
||||
|
||||
async function onGimbalResetConfirm (confirm: boolean) { |
||||
if (confirm) { |
||||
if (gimbalResetPopoverData.resetMode === null) { |
||||
message.error('Please select reset mode') |
||||
return |
||||
} |
||||
gimbalResetPopoverData.loading = true |
||||
try { |
||||
await resetGimbal(props.sn, { |
||||
payload_index: payloadSelectInfo.payloadIndex, |
||||
reset_mode: gimbalResetPopoverData.resetMode |
||||
}) |
||||
} catch (err) { |
||||
} |
||||
} |
||||
gimbalResetPopoverData.visible = false |
||||
} |
||||
|
||||
async function onSwitchCameraMode () { |
||||
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||
return |
||||
} |
||||
const currentCameraMode = payloadSelectInfo.camera?.camera_mode |
||||
await switchCameraMode(props.sn, { |
||||
payload_index: payloadSelectInfo.payloadIndex, |
||||
camera_mode: currentCameraMode === CameraMode.Photo ? CameraMode.Video : CameraMode.Photo |
||||
}) |
||||
} |
||||
|
||||
async function onTakeCameraPhoto () { |
||||
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||
return |
||||
} |
||||
await takeCameraPhoto(props.sn, payloadSelectInfo.payloadIndex) |
||||
} |
||||
|
||||
async function onStartCameraRecording () { |
||||
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||
return |
||||
} |
||||
await startCameraRecording(props.sn, payloadSelectInfo.payloadIndex) |
||||
} |
||||
|
||||
async function onStopCameraRecording () { |
||||
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||
return |
||||
} |
||||
await stopCameraRecording(props.sn, payloadSelectInfo.payloadIndex) |
||||
} |
||||
|
||||
const zoomFactorPopoverData = reactive({ |
||||
visible: false, |
||||
loading: false, |
||||
cameraType: null as null | CameraType, |
||||
zoomFactor: null as null | number, |
||||
}) |
||||
|
||||
function onShowZoomFactorPopover () { |
||||
zoomFactorPopoverData.visible = !zoomFactorPopoverData.visible |
||||
zoomFactorPopoverData.loading = false |
||||
zoomFactorPopoverData.cameraType = null |
||||
zoomFactorPopoverData.zoomFactor = null |
||||
} |
||||
|
||||
async function onZoomFactorConfirm (confirm: boolean) { |
||||
if (confirm) { |
||||
if (!zoomFactorPopoverData.zoomFactor || zoomFactorPopoverData.cameraType === null) { |
||||
message.error('Please input Zoom Factor') |
||||
return |
||||
} |
||||
zoomFactorPopoverData.loading = true |
||||
try { |
||||
await changeCameraFocalLength(props.sn, { |
||||
payload_index: payloadSelectInfo.payloadIndex, |
||||
camera_type: zoomFactorPopoverData.cameraType, |
||||
zoom_factor: zoomFactorPopoverData.zoomFactor |
||||
}) |
||||
} catch (err) { |
||||
} |
||||
} |
||||
zoomFactorPopoverData.visible = false |
||||
} |
||||
|
||||
const cameraAimPopoverData = reactive({ |
||||
visible: false, |
||||
loading: false, |
||||
cameraType: null as null | CameraType, |
||||
locked: false, |
||||
x: null as null | number, |
||||
y: null as null | number, |
||||
}) |
||||
|
||||
function onShowCameraAimPopover () { |
||||
cameraAimPopoverData.visible = !cameraAimPopoverData.visible |
||||
cameraAimPopoverData.loading = false |
||||
cameraAimPopoverData.cameraType = null |
||||
cameraAimPopoverData.locked = false |
||||
cameraAimPopoverData.x = null |
||||
cameraAimPopoverData.y = null |
||||
} |
||||
|
||||
function openLivestreamOthers () { |
||||
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', true) |
||||
} |
||||
|
||||
function openLivestreamAgora () { |
||||
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', true) |
||||
} |
||||
|
||||
async function onCameraAimConfirm (confirm: boolean) { |
||||
if (confirm) { |
||||
if (cameraAimPopoverData.cameraType === null || cameraAimPopoverData.x === null || cameraAimPopoverData.y === null) { |
||||
message.error('Input error') |
||||
return |
||||
} |
||||
try { |
||||
await cameraAim(props.sn, { |
||||
payload_index: payloadSelectInfo.payloadIndex, |
||||
camera_type: cameraAimPopoverData.cameraType, |
||||
locked: cameraAimPopoverData.locked, |
||||
x: cameraAimPopoverData.x, |
||||
y: cameraAimPopoverData.y, |
||||
}) |
||||
} catch (error) { |
||||
} |
||||
} |
||||
cameraAimPopoverData.visible = false |
||||
} |
||||
|
||||
watch(() => errorInfo, (errorInfo) => { |
||||
if (errorInfo.value) { |
||||
message.error(errorInfo.value) |
||||
console.error(errorInfo.value) |
||||
errorInfo.value = '' |
||||
} |
||||
}, { |
||||
immediate: true, |
||||
deep: true |
||||
}) |
||||
</script> |
||||
|
||||
<style lang='scss' scoped> |
||||
.drone-control-wrapper{ |
||||
// border-bottom: 1px solid #515151; |
||||
|
||||
.drone-control-header{ |
||||
font-size: 14px; |
||||
font-weight: 600; |
||||
padding: 10px 10px 0px; |
||||
} |
||||
|
||||
.drone-control-box { |
||||
display: flex; |
||||
flex-wrap: 1; |
||||
.box { |
||||
width: 50%; |
||||
padding: 5px; |
||||
border: 0.5px solid rgba(255,255,255,0.3); |
||||
|
||||
.row { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
padding: 2px; |
||||
|
||||
+ .row{ |
||||
margin-bottom: 6px; |
||||
} |
||||
|
||||
&::v-deep{ |
||||
.ant-btn{ |
||||
font-size: 12px; |
||||
padding: 0px 4px; |
||||
margin-right: 5px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.drone-control{ |
||||
&::v-deep{ |
||||
|
||||
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector{ |
||||
padding: 0 2px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.drone-control-direction{ |
||||
margin-right: 10px; |
||||
|
||||
.ant-btn { |
||||
// padding: 0px 1px; |
||||
margin-right: 0; |
||||
} |
||||
|
||||
.word{ |
||||
width: 12px; |
||||
margin-left: 2px; |
||||
font-size: 12px; |
||||
color: #aaa; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
<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 || 'cancel'}} |
||||
</a-button> |
||||
<a-button size="sm" |
||||
:loading="loading" |
||||
type="primary" |
||||
class="confirm-btn" |
||||
@click="onConfirm"> |
||||
{{ okText || 'ok' }} |
||||
</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 = ['drone-control-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"> |
||||
.drone-control-popconfirm { |
||||
min-width: 300px; |
||||
|
||||
.uranus-popconfirm-btns{ |
||||
display: flex; |
||||
padding: 10px 0px; |
||||
justify-content: flex-end; |
||||
|
||||
.confirm-btn{ |
||||
margin-left: 10px; |
||||
} |
||||
} |
||||
|
||||
.form-content{ |
||||
display: flex; |
||||
flex-direction: column; |
||||
|
||||
> div { |
||||
display: flex; |
||||
margin-bottom: 5px; |
||||
|
||||
.form-label { |
||||
flex: 1 0 60px; |
||||
margin-right: 10px; |
||||
} |
||||
|
||||
> div { |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
|
||||
import { |
||||
ref, |
||||
watch, |
||||
computed, |
||||
onUnmounted, |
||||
} from 'vue' |
||||
import { useMyStore } from '/@/store' |
||||
import { postDrc } from '/@/api/drc' |
||||
import { |
||||
UranusMqtt, |
||||
} from '/@/mqtt' |
||||
|
||||
type StatusOptions = { |
||||
status: 'close'; |
||||
event?: CloseEvent; |
||||
} | { |
||||
status: 'open'; |
||||
retryCount: number; |
||||
} | { |
||||
status: 'pending'; |
||||
} |
||||
|
||||
export function useConnectMqtt () { |
||||
const store = useMyStore() |
||||
const dockOsdVisible = computed(() => { |
||||
return store.state.osdVisible && store.state.osdVisible.visible && store.state.osdVisible.is_dock |
||||
}) |
||||
const mqttState = ref<UranusMqtt | null>(null) |
||||
|
||||
// 监听已打开的设备小窗 窗口数量
|
||||
watch(() => dockOsdVisible.value, async (val) => { |
||||
// 1.打开小窗
|
||||
// 2.设备拥有飞行控制权
|
||||
// 3.请求建立mqtt连接的认证信息
|
||||
if (val) { |
||||
if (mqttState.value) return |
||||
const result = await postDrc({}) |
||||
if (result?.code === 0) { |
||||
const { address, client_id, username, password, expire_time } = result.data |
||||
// @TODO: 校验 expire_time
|
||||
mqttState.value = new UranusMqtt(address, { |
||||
clientId: client_id, |
||||
username, |
||||
password, |
||||
}) |
||||
mqttState.value?.initMqtt() |
||||
mqttState.value?.on('onStatus', (statusOptions: StatusOptions) => { |
||||
// @TODO: 异常case
|
||||
}) |
||||
|
||||
store.commit('SET_MQTT_STATE', mqttState.value) |
||||
store.commit('SET_CLIENT_ID', client_id) |
||||
} |
||||
// @TODO: 认证失败case
|
||||
return |
||||
} |
||||
// 关闭所有小窗后
|
||||
// 1.销毁mqtt连接重置mqtt状态
|
||||
if (mqttState?.value) { |
||||
mqttState.value?.destroyed() |
||||
mqttState.value = null |
||||
store.commit('SET_MQTT_STATE', null) |
||||
store.commit('SET_CLIENT_ID', '') |
||||
} |
||||
}, { immediate: true }) |
||||
|
||||
onUnmounted(() => { |
||||
mqttState.value?.destroyed() |
||||
}) |
||||
} |
@ -0,0 +1,56 @@
@@ -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 |
||||
} |
||||
} |
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue' |
||||
import EventBus from '/@/event-bus/' |
||||
import { |
||||
DRC_METHOD, |
||||
DRCHsiInfo, |
||||
DRCOsdInfo, |
||||
DRCDelayTimeInfo, |
||||
DrcResponseInfo, |
||||
} from '/@/types/drc' |
||||
|
||||
export function useDroneControlMqttEvent (sn: string) { |
||||
const drcInfo = ref('') |
||||
const hsiInfo = ref('') |
||||
const osdInfo = ref('') |
||||
const delayInfo = ref('') |
||||
const errorInfo = ref('') |
||||
|
||||
function handleHsiInfo (data: DRCHsiInfo) { |
||||
hsiInfo.value = `method: ${DRC_METHOD.HSI_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n ` |
||||
} |
||||
|
||||
function handleOsdInfo (data: DRCOsdInfo) { |
||||
osdInfo.value = `method: ${DRC_METHOD.OSD_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n ` |
||||
} |
||||
|
||||
function handleDelayTimeInfo (data: DRCDelayTimeInfo) { |
||||
delayInfo.value = `method: ${DRC_METHOD.DELAY_TIME_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n ` |
||||
} |
||||
|
||||
function handleDroneControlErrorInfo (data: DrcResponseInfo) { |
||||
if (!data.result) { |
||||
return |
||||
} |
||||
errorInfo.value = `Drc error code: ${data.result}, seq: ${data.output?.seq}` |
||||
} |
||||
|
||||
function handleDroneControlMqttEvent (payload: any) { |
||||
if (!payload || !payload.method) { |
||||
return |
||||
} |
||||
|
||||
switch (payload.method) { |
||||
case DRC_METHOD.HSI_INFO_PUSH: { |
||||
handleHsiInfo(payload.data) |
||||
break |
||||
} |
||||
case DRC_METHOD.OSD_INFO_PUSH: { |
||||
handleOsdInfo(payload.data) |
||||
break |
||||
} |
||||
case DRC_METHOD.DELAY_TIME_INFO_PUSH: { |
||||
handleDelayTimeInfo(payload.data) |
||||
break |
||||
} |
||||
case DRC_METHOD.DRONE_EMERGENCY_STOP: |
||||
case DRC_METHOD.DRONE_CONTROL: { |
||||
handleDroneControlErrorInfo(payload.data) |
||||
break |
||||
} |
||||
} |
||||
drcInfo.value = hsiInfo.value + osdInfo.value + delayInfo.value |
||||
} |
||||
|
||||
onMounted(() => { |
||||
EventBus.on('droneControlMqttInfo', handleDroneControlMqttEvent) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
EventBus.off('droneControlMqttInfo', handleDroneControlMqttEvent) |
||||
}) |
||||
|
||||
return { |
||||
drcInfo: drcInfo, |
||||
errorInfo: errorInfo |
||||
} |
||||
} |
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
import { message, notification } from 'ant-design-vue' |
||||
import { ref, onMounted, onBeforeUnmount } from 'vue' |
||||
import EventBus from '/@/event-bus/' |
||||
import { EBizCode } from '/@/types' |
||||
import { ControlSource } from '/@/types/device' |
||||
import { ControlSourceChangeType, ControlSourceChangeInfo, FlyToPointMessage, TakeoffToPointMessage, DrcModeExitNotifyMessage, DrcStatusNotifyMessage } from '/@/types/drone-control' |
||||
|
||||
export interface UseDroneControlWsEventParams { |
||||
} |
||||
|
||||
export function useDroneControlWsEvent (sn: string, payloadSn: string, funcs?: UseDroneControlWsEventParams) { |
||||
const droneControlSource = ref(ControlSource.A) |
||||
const payloadControlSource = ref(ControlSource.B) |
||||
function onControlSourceChange (data: ControlSourceChangeInfo) { |
||||
if (data.type === ControlSourceChangeType.Flight && data.sn === sn) { |
||||
droneControlSource.value = data.control_source |
||||
message.info(`Flight control is changed to ${droneControlSource.value}`) |
||||
return |
||||
} |
||||
if (data.type === ControlSourceChangeType.Payload && data.sn === payloadSn) { |
||||
payloadControlSource.value = data.control_source |
||||
message.info(`Payload control is changed to ${payloadControlSource.value}.`) |
||||
} |
||||
} |
||||
|
||||
function handleProgress (key: string, message: string, error: number) { |
||||
if (error !== 0) { |
||||
notification.error({ |
||||
key: key, |
||||
message: key + 'Error code:' + error, |
||||
description: message, |
||||
duration: null |
||||
}) |
||||
} else { |
||||
notification.info({ |
||||
key: key, |
||||
message: key, |
||||
description: message, |
||||
duration: 30 |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function handleDroneControlWsEvent (payload: any) { |
||||
if (!payload) { |
||||
return |
||||
} |
||||
|
||||
switch (payload.biz_code) { |
||||
case EBizCode.ControlSourceChange: { |
||||
onControlSourceChange(payload.data) |
||||
break |
||||
} |
||||
case EBizCode.FlyToPointProgress: { |
||||
const { sn: deviceSn, result, message: msg } = payload.data as FlyToPointMessage |
||||
if (deviceSn !== sn) return |
||||
handleProgress(EBizCode.FlyToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result) |
||||
break |
||||
} |
||||
case EBizCode.TakeoffToPointProgress: { |
||||
const { sn: deviceSn, result, message: msg } = payload.data as TakeoffToPointMessage |
||||
if (deviceSn !== sn) return |
||||
handleProgress(EBizCode.TakeoffToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result) |
||||
break |
||||
} |
||||
case EBizCode.JoystickInvalidNotify: { |
||||
const { sn: deviceSn, result, message: msg } = payload.data as DrcModeExitNotifyMessage |
||||
if (deviceSn !== sn) return |
||||
handleProgress(EBizCode.JoystickInvalidNotify, `device(sn: ${deviceSn}) ${msg}`, result) |
||||
break |
||||
} |
||||
case EBizCode.DrcStatusNotify: { |
||||
const { sn: deviceSn, result, message: msg } = payload.data as DrcStatusNotifyMessage |
||||
// handleProgress(EBizCode.DrcStatusNotify, `device(sn: ${deviceSn}) ${msg}`, result)
|
||||
|
||||
break |
||||
} |
||||
} |
||||
// eslint-disable-next-line no-unused-expressions
|
||||
// console.log('payload.biz_code', payload.data)
|
||||
} |
||||
|
||||
onMounted(() => { |
||||
EventBus.on('droneControlWs', handleDroneControlWsEvent) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
EventBus.off('droneControlWs', handleDroneControlWsEvent) |
||||
}) |
||||
|
||||
return { |
||||
droneControlSource: droneControlSource, |
||||
payloadControlSource: payloadControlSource |
||||
} |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
import { ref } from 'vue' |
||||
import { postFlyToPoint, PostFlyToPointBody, deleteFlyToPoint, postTakeoffToPoint, PostTakeoffToPointBody } from '/@/api/drone-control/drone' |
||||
import { message } from 'ant-design-vue' |
||||
|
||||
export function useDroneControl () { |
||||
const droneControlPanelVisible = ref(false) |
||||
|
||||
function setDroneControlPanelVisible (visible: boolean) { |
||||
droneControlPanelVisible.value = visible |
||||
} |
||||
|
||||
async function flyToPoint (sn: string, body: PostFlyToPointBody) { |
||||
const { code } = await postFlyToPoint(sn, body) |
||||
if (code === 0) { |
||||
message.success('Fly to') |
||||
} |
||||
} |
||||
|
||||
async function stopFlyToPoint (sn: string) { |
||||
const { code } = await deleteFlyToPoint(sn) |
||||
if (code === 0) { |
||||
message.success('Stop fly to') |
||||
} |
||||
} |
||||
|
||||
async function takeoffToPoint (sn: string, body: PostTakeoffToPointBody) { |
||||
const { code } = await postTakeoffToPoint(sn, body) |
||||
if (code === 0) { |
||||
message.success('Take off successfully') |
||||
} |
||||
} |
||||
|
||||
return { |
||||
droneControlPanelVisible, |
||||
setDroneControlPanelVisible, |
||||
flyToPoint, |
||||
stopFlyToPoint, |
||||
takeoffToPoint |
||||
} |
||||
} |
@ -0,0 +1,165 @@
@@ -0,0 +1,165 @@
|
||||
import { |
||||
ref, |
||||
onUnmounted, |
||||
watch, |
||||
Ref, |
||||
} from 'vue' |
||||
import { message } from 'ant-design-vue' |
||||
import { |
||||
DRC_METHOD, |
||||
DroneControlProtocol, |
||||
} from '/@/types/drc' |
||||
import { |
||||
useMqtt, |
||||
DeviceTopicInfo |
||||
} from './use-mqtt' |
||||
|
||||
let myInterval: any |
||||
|
||||
export enum KeyCode { |
||||
KEY_W = 'KeyW', |
||||
KEY_A = 'KeyA', |
||||
KEY_S = 'KeyS', |
||||
KEY_D = 'KeyD', |
||||
KEY_Q = 'KeyQ', |
||||
KEY_E = 'KeyE', |
||||
ARROW_UP = 'ArrowUp', |
||||
ARROW_DOWN = 'ArrowDown', |
||||
} |
||||
|
||||
export function useManualControl (deviceTopicInfo: DeviceTopicInfo, isCurrentFlightController: Ref<boolean>) { |
||||
const activeCodeKey = ref(null) as Ref<KeyCode | null> |
||||
const mqttHooks = useMqtt(deviceTopicInfo) |
||||
let seq = 0 |
||||
function handlePublish (params: DroneControlProtocol) { |
||||
const body = { |
||||
method: DRC_METHOD.DRONE_CONTROL, |
||||
data: params, |
||||
} |
||||
handleClearInterval() |
||||
myInterval = setInterval(() => { |
||||
body.data.seq = seq++ |
||||
seq++ |
||||
window.console.log('keyCode>>>>', activeCodeKey.value, body) |
||||
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 }) |
||||
}, 50) |
||||
} |
||||
|
||||
function handleKeyup (keyCode: KeyCode) { |
||||
if (!deviceTopicInfo.pubTopic) { |
||||
message.error('请确保已经建立DRC链路') |
||||
return |
||||
} |
||||
const SPEED = 5 // check
|
||||
const HEIGHT = 5 // check
|
||||
const W_SPEED = 20 // 机头角速度
|
||||
seq = 0 |
||||
switch (keyCode) { |
||||
case 'KeyA': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ y: -SPEED }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'KeyW': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ x: SPEED }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'KeyS': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ x: -SPEED }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'KeyD': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ y: SPEED }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'ArrowUp': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ h: HEIGHT }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'ArrowDown': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ h: -HEIGHT }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'KeyQ': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ w: -W_SPEED }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
case 'KeyE': |
||||
if (activeCodeKey.value === keyCode) return |
||||
handlePublish({ w: W_SPEED }) |
||||
activeCodeKey.value = keyCode |
||||
break |
||||
default: |
||||
break |
||||
} |
||||
} |
||||
|
||||
function handleClearInterval () { |
||||
clearInterval(myInterval) |
||||
myInterval = undefined |
||||
} |
||||
|
||||
function resetControlState () { |
||||
activeCodeKey.value = null |
||||
seq = 0 |
||||
handleClearInterval() |
||||
} |
||||
|
||||
function onKeyup () { |
||||
resetControlState() |
||||
} |
||||
|
||||
function onKeydown (e: KeyboardEvent) { |
||||
handleKeyup(e.code as KeyCode) |
||||
} |
||||
|
||||
function startKeyboardManualControl () { |
||||
window.addEventListener('keydown', onKeydown) |
||||
window.addEventListener('keyup', onKeyup) |
||||
} |
||||
|
||||
function closeKeyboardManualControl () { |
||||
resetControlState() |
||||
window.removeEventListener('keydown', onKeydown) |
||||
window.removeEventListener('keyup', onKeyup) |
||||
} |
||||
|
||||
watch(() => isCurrentFlightController.value, (val) => { |
||||
if (val && deviceTopicInfo.pubTopic) { |
||||
startKeyboardManualControl() |
||||
} else { |
||||
closeKeyboardManualControl() |
||||
} |
||||
}, { immediate: true }) |
||||
|
||||
onUnmounted(() => { |
||||
closeKeyboardManualControl() |
||||
}) |
||||
|
||||
function handleEmergencyStop () { |
||||
if (!deviceTopicInfo.pubTopic) { |
||||
message.error('请确保已经建立DRC链路') |
||||
return |
||||
} |
||||
const body = { |
||||
method: DRC_METHOD.DRONE_EMERGENCY_STOP, |
||||
data: {} |
||||
} |
||||
resetControlState() |
||||
window.console.log('handleEmergencyStop>>>>', deviceTopicInfo.pubTopic, body) |
||||
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 1 }) |
||||
} |
||||
|
||||
return { |
||||
activeCodeKey, |
||||
handleKeyup, |
||||
handleEmergencyStop, |
||||
resetControlState, |
||||
} |
||||
} |
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
import { |
||||
ref, |
||||
reactive, |
||||
computed, |
||||
watch, |
||||
onUnmounted, |
||||
} from 'vue' |
||||
import { |
||||
IClientPublishOptions, |
||||
IPublishPacket, |
||||
} from '/@/mqtt' |
||||
import { useMyStore } from '/@/store' |
||||
import { |
||||
DRC_METHOD, |
||||
} from '/@/types/drc' |
||||
import EventBus from '/@/event-bus' |
||||
|
||||
export interface DeviceTopicInfo{ |
||||
sn: string |
||||
pubTopic: string |
||||
subTopic: string |
||||
} |
||||
|
||||
type MessageMqtt = (topic: string, payload: Buffer, packet: IPublishPacket) => void | Promise<void> |
||||
|
||||
export function useMqtt (deviceTopicInfo: DeviceTopicInfo) { |
||||
let cacheSubscribeArr: { |
||||
topic: string; |
||||
callback?: MessageMqtt; |
||||
}[] = [] |
||||
|
||||
const store = useMyStore() |
||||
|
||||
const mqttState = computed(() => { |
||||
return store.state.mqttState |
||||
}) |
||||
|
||||
function publishMqtt (topic: string, body: object, ots?: IClientPublishOptions) { |
||||
// const buffer = Buffer.from(JSON.stringify(body))
|
||||
mqttState.value?.publishMqtt(topic, JSON.stringify(body), ots) |
||||
} |
||||
|
||||
function subscribeMqtt (topic: string, handleMessageMqtt?: MessageMqtt) { |
||||
mqttState.value?.subscribeMqtt(topic) |
||||
const handler = handleMessageMqtt || onMessageMqtt |
||||
mqttState.value?.on('onMessageMqtt', handler) |
||||
cacheSubscribeArr.push({ |
||||
topic, |
||||
callback: handler, |
||||
}) |
||||
} |
||||
|
||||
function onMessageMqtt (message: any) { |
||||
if (cacheSubscribeArr.findIndex(item => item.topic === message?.topic) !== -1) { |
||||
const payloadStr = new TextDecoder('utf-8').decode(message?.payload) |
||||
const payloadObj = JSON.parse(payloadStr) |
||||
switch (payloadObj?.method) { |
||||
case DRC_METHOD.HEART_BEAT: |
||||
break |
||||
case DRC_METHOD.DELAY_TIME_INFO_PUSH: |
||||
case DRC_METHOD.HSI_INFO_PUSH: |
||||
case DRC_METHOD.OSD_INFO_PUSH: |
||||
case DRC_METHOD.DRONE_CONTROL: |
||||
case DRC_METHOD.DRONE_EMERGENCY_STOP: |
||||
EventBus.emit('droneControlMqttInfo', payloadObj) |
||||
break |
||||
default: |
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
function unsubscribeDrc () { |
||||
// 销毁已订阅事件
|
||||
cacheSubscribeArr.forEach(item => { |
||||
mqttState.value?.off('onMessageMqtt', item.callback) |
||||
mqttState.value?.unsubscribeMqtt(item.topic) |
||||
}) |
||||
cacheSubscribeArr = [] |
||||
} |
||||
|
||||
// 心跳
|
||||
const heartBeatSeq = ref(0) |
||||
const state = reactive({ |
||||
heartState: new Map<string, { |
||||
pingInterval: any; |
||||
}>(), |
||||
}) |
||||
|
||||
// 监听云控控制权
|
||||
watch(() => deviceTopicInfo, (val, oldVal) => { |
||||
if (val.subTopic !== '') { |
||||
// 1.订阅topic
|
||||
subscribeMqtt(deviceTopicInfo.subTopic) |
||||
// 2.发心跳
|
||||
publishDrcPing(deviceTopicInfo.sn) |
||||
} else { |
||||
clearInterval(state.heartState.get(deviceTopicInfo.sn)?.pingInterval) |
||||
state.heartState.delete(deviceTopicInfo.sn) |
||||
heartBeatSeq.value = 0 |
||||
} |
||||
}, { immediate: true, deep: true }) |
||||
|
||||
function publishDrcPing (sn: string) { |
||||
const body = { |
||||
method: DRC_METHOD.HEART_BEAT, |
||||
data: { |
||||
ts: new Date().getTime(), |
||||
seq: heartBeatSeq.value, |
||||
}, |
||||
} |
||||
const pingInterval = setInterval(() => { |
||||
if (!mqttState.value) return |
||||
heartBeatSeq.value += 1 |
||||
body.data.ts = new Date().getTime() |
||||
body.data.seq = heartBeatSeq.value |
||||
publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 }) |
||||
}, 1000) |
||||
state.heartState.set(sn, { |
||||
pingInterval, |
||||
}) |
||||
} |
||||
|
||||
onUnmounted(() => { |
||||
unsubscribeDrc() |
||||
heartBeatSeq.value = 0 |
||||
}) |
||||
|
||||
return { |
||||
mqttState, |
||||
publishMqtt, |
||||
subscribeMqtt, |
||||
} |
||||
} |
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
import { message } from 'ant-design-vue' |
||||
import { |
||||
postPayloadAuth, |
||||
postPayloadCommands, |
||||
PayloadCommandsEnum, |
||||
PostCameraModeBody, |
||||
PostCameraFocalLengthBody, |
||||
PostGimbalResetBody, |
||||
PostCameraAimBody, |
||||
} from '/@/api/drone-control/payload' |
||||
import { ControlSource } from '/@/types/device' |
||||
|
||||
export function usePayloadControl () { |
||||
function checkPayloadAuth (controlSource?: ControlSource) { |
||||
if (controlSource !== ControlSource.A) { |
||||
message.error('Get Payload Control first') |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
async function authPayload (sn: string, payloadIndx: string) { |
||||
const { code } = await postPayloadAuth(sn, { |
||||
payload_index: payloadIndx |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Get Payload Control successfully') |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
async function resetGimbal (sn: string, data: PostGimbalResetBody) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.GimbalReset, |
||||
data: data |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Gimbal Reset successfully') |
||||
} |
||||
} |
||||
|
||||
async function switchCameraMode (sn: string, data: PostCameraModeBody) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.CameraModeSwitch, |
||||
data: data |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Camera Mode Switch successfully') |
||||
} |
||||
} |
||||
|
||||
async function takeCameraPhoto (sn: string, payloadIndx: string) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.CameraPhotoTake, |
||||
data: { |
||||
payload_index: payloadIndx |
||||
} |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Take Photo successfully') |
||||
} |
||||
} |
||||
|
||||
async function startCameraRecording (sn: string, payloadIndx: string) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.CameraRecordingStart, |
||||
data: { |
||||
payload_index: payloadIndx |
||||
} |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Start Recording successfully') |
||||
} |
||||
} |
||||
|
||||
async function stopCameraRecording (sn: string, payloadIndx: string) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.CameraRecordingStop, |
||||
data: { |
||||
payload_index: payloadIndx |
||||
} |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Stop Recording successfully') |
||||
} |
||||
} |
||||
|
||||
async function changeCameraFocalLength (sn: string, data: PostCameraFocalLengthBody) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.CameraFocalLengthSet, |
||||
data: data, |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Zoom successfully') |
||||
} |
||||
} |
||||
|
||||
async function cameraAim (sn: string, data: PostCameraAimBody) { |
||||
const { code } = await postPayloadCommands(sn, { |
||||
cmd: PayloadCommandsEnum.CameraAim, |
||||
data: data, |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Zoom Aim successfully') |
||||
} |
||||
} |
||||
|
||||
return { |
||||
checkPayloadAuth, |
||||
authPayload, |
||||
resetGimbal, |
||||
switchCameraMode, |
||||
takeCameraPhoto, |
||||
startCameraRecording, |
||||
stopCameraRecording, |
||||
changeCameraFocalLength, |
||||
cameraAim, |
||||
} |
||||
} |
@ -0,0 +1,450 @@
@@ -0,0 +1,450 @@
|
||||
<template> |
||||
<div class="create-plan-wrapper"> |
||||
<div class="header"> |
||||
Create Plan |
||||
</div> |
||||
<div class="content"> |
||||
<a-form ref="valueRef" layout="horizontal" :hideRequiredMark="true" :rules="rules" :model="planBody" labelAlign="left"> |
||||
<a-form-item label="Plan Name" name="name" :labelCol="{span: 23}"> |
||||
<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'}" |
||||
@click="selectRoute" |
||||
> |
||||
Select Route |
||||
</router-link> |
||||
</a-form-item> |
||||
<a-form-item v-if="planBody.file_id" style="margin-top: -15px;"> |
||||
<div class="wayline-panel" style="padding-top: 5px;"> |
||||
<div class="title"> |
||||
<a-tooltip :title="wayline.name"> |
||||
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div> |
||||
</a-tooltip> |
||||
<div class="ml10"><UserOutlined /></div> |
||||
<a-tooltip :title="wayline.user_name"> |
||||
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||
<span><RocketOutlined /></span> |
||||
<span class="ml5">{{ DEVICE_NAME[wayline.drone_model_key] }}</span> |
||||
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> |
||||
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> |
||||
{{ DEVICE_NAME[payload] }} |
||||
</span> |
||||
</div> |
||||
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span> |
||||
</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'}" |
||||
@click="selectDevice" |
||||
>Select Device</router-link> |
||||
</a-form-item> |
||||
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;"> |
||||
<div class="panel" style="padding-top: 5px;"> |
||||
<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> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||
<span><RocketOutlined /></span> |
||||
<span class="ml5">{{ dock.children?.nickname ?? 'No drone' }}</span> |
||||
</div> |
||||
</div> |
||||
</a-form-item> |
||||
<!-- 任务类型 --> |
||||
<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 v-for="type in TaskTypeOptions" :value="type.value" :key="type.value">{{ type.label }}</a-radio-button> |
||||
</a-radio-group> |
||||
</div> |
||||
</a-form-item> |
||||
<!-- execute date --> |
||||
<a-form-item label="Date" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition" name="select_execute_date" :labelCol="{span: 23}"> |
||||
<a-range-picker |
||||
v-model:value="planBody.select_execute_date" |
||||
:disabledDate="(current: Moment) => current < moment().subtract(1, 'days')" |
||||
format="YYYY-MM-DD" |
||||
:placeholder="['Start Time', 'End Time']" |
||||
style="width: 100%;" |
||||
/> |
||||
</a-form-item> |
||||
<!-- execute time --> |
||||
<a-form-item label="Time" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition" |
||||
name="select_execute_time" ref="select_execute_time" :labelCol="{span: 23}" :autoLink="false"> |
||||
<div class="mb10 flex-row flex-align-center flex-justify-around" v-for="n in planBody.select_time_number" :key="n"> |
||||
<a-time-picker |
||||
v-model:value="planBody.select_time[n - 1][0]" |
||||
format="HH:mm:ss" |
||||
show-time |
||||
placeholder="Start Time" |
||||
:style="planBody.task_type === TaskType.Condition ? 'width: 40%' : 'width: 82%'" |
||||
@change="() => $refs.select_execute_time.onFieldChange()" |
||||
/> |
||||
<template v-if="planBody.task_type === TaskType.Condition"> |
||||
<div><span style="color: white;">-</span></div> |
||||
<a-time-picker |
||||
v-model:value="planBody.select_time[n - 1][1]" |
||||
format="HH:mm:ss" |
||||
show-time |
||||
placeholder="End Time" |
||||
style="width: 40%;" |
||||
/> |
||||
</template> |
||||
<div class="ml5" style="font-size:18px"> |
||||
<PlusCircleOutlined class="mr5" style="color: #1890ff" @click="addTime"/> |
||||
<MinusCircleOutlined :style="planBody.select_time_number === 1 ? 'color: gray' : 'color: red;'" @click="removeTime"/> |
||||
</div> |
||||
</div> |
||||
</a-form-item> |
||||
<template v-if="planBody.task_type === TaskType.Condition"> |
||||
<!-- battery capacity --> |
||||
<a-form-item label="Start task when battery level reaches" :labelCol="{span: 23}" name="min_battery_capacity"> |
||||
<a-input-number class="width-100" v-model:value="planBody.min_battery_capacity" :min="50" :max="100" |
||||
:formatter="(value: number) => `${value}%`" :parser="(value: string) => value.replace('%', '')"> |
||||
</a-input-number> |
||||
</a-form-item> |
||||
<!-- storage capacity --> |
||||
<a-form-item label="Start task when storage level reaches (MB)" :labelCol="{span: 23}" name="storage_capacity"> |
||||
<a-input-number v-model:value="planBody.min_storage_capacity" class="width-100"> |
||||
</a-input-number> |
||||
</a-form-item> |
||||
</template> |
||||
<!-- RTH Altitude Relative to Dock --> |
||||
<a-form-item label="RTH Altitude Relative to Dock (m)" :labelCol="{span: 23}" name="rth_altitude"> |
||||
<a-input-number v-model:value="planBody.rth_altitude" :min="20" :max="1500" class="width-100" required> |
||||
</a-input-number> |
||||
</a-form-item> |
||||
<!-- Lost Action --> |
||||
<a-form-item label="Lost Action" :labelCol="{span: 23}" 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 class="width-100" style="margin-bottom: 40px;"> |
||||
<div class="footer"> |
||||
<a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel |
||||
</a-button> |
||||
<a-button type="primary" @click="onSubmit" :disabled="disabled">OK |
||||
</a-button> |
||||
</div> |
||||
</a-form-item> |
||||
</a-form> |
||||
</div> |
||||
</div> |
||||
<div v-if="drawerVisible" style="position: absolute; left: 335px; width: 280px; height: 100vh; float: right; top: 0; z-index: 1000; color: white; background: #282828;"> |
||||
<div> |
||||
<router-view :name="routeName"/> |
||||
</div> |
||||
<div style="position: absolute; top: 15px; right: 10px;"> |
||||
<a style="color: white;" @click="closePanel"><CloseOutlined /></a> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, UnwrapRef } from 'vue' |
||||
import { CloseOutlined, RocketOutlined, CameraFilled, UserOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue' |
||||
import { ELocalStorageKey, ERouterName } from '/@/types' |
||||
import { useMyStore } from '/@/store' |
||||
import { WaylineType, WaylineFile } from '/@/types/wayline' |
||||
import { Device, DEVICE_NAME } from '/@/types/device' |
||||
import { createPlan, CreatePlan } from '/@/api/wayline' |
||||
import { getRoot } from '/@/root' |
||||
import { TaskType, OutOfControlActionOptions, OutOfControlAction, TaskTypeOptions } from '/@/types/task' |
||||
import moment, { Moment } from 'moment' |
||||
import { RuleObject } from 'ant-design-vue/es/form/interface' |
||||
|
||||
const root = getRoot() |
||||
const store = useMyStore() |
||||
|
||||
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||
|
||||
const wayline = computed<WaylineFile>(() => { |
||||
return store.state.waylineInfo |
||||
}) |
||||
|
||||
const dock = computed<Device>(() => { |
||||
return store.state.dockInfo |
||||
}) |
||||
|
||||
const disabled = ref(false) |
||||
|
||||
const routeName = ref('') |
||||
const planBody = reactive({ |
||||
name: '', |
||||
file_id: computed(() => store.state?.waylineInfo.id), |
||||
dock_sn: computed(() => store.state?.dockInfo.device_sn), |
||||
task_type: TaskType.Immediate, |
||||
select_execute_date: [moment(), moment()] as Moment[], |
||||
select_time_number: 1, |
||||
select_time: [[]] as Moment[][], |
||||
rth_altitude: '', |
||||
out_of_control_action: OutOfControlAction.ReturnToHome, |
||||
min_battery_capacity: 90 as number, |
||||
min_storage_capacity: undefined as number | undefined, |
||||
}) |
||||
|
||||
const drawerVisible = ref(false) |
||||
const valueRef = ref() |
||||
const rules = { |
||||
name: [ |
||||
{ required: true, message: 'Please enter plan name.' }, |
||||
{ max: 20, message: 'Length should be 1 to 20' } |
||||
], |
||||
file_id: [{ required: true, message: 'Select Route' }], |
||||
dock_sn: [{ required: true, message: 'Select Device' }], |
||||
select_execute_time: [{ |
||||
validator: async (rule: RuleObject, value: Moment[]) => { |
||||
validEndTime() |
||||
validStartTime() |
||||
if (planBody.select_time.length < planBody.select_time_number) { |
||||
throw new Error('Select time') |
||||
} |
||||
validOverlapped() |
||||
} |
||||
}], |
||||
select_execute_date: [{ required: true, message: 'Select date' }], |
||||
rth_altitude: [ |
||||
{ |
||||
validator: async (rule: RuleObject, value: string) => { |
||||
if (!/^[0-9]{1,}$/.test(value)) { |
||||
throw new Error('RTH Altitude Require number') |
||||
} |
||||
}, |
||||
} |
||||
], |
||||
min_battery_capacity: [ |
||||
{ |
||||
validator: async (rule: RuleObject, value: any) => { |
||||
if (TaskType.Condition === planBody.task_type && !value) { |
||||
throw new Error('Please enter battery capacity') |
||||
} |
||||
}, |
||||
} |
||||
], |
||||
out_of_control_action: [{ required: true, message: 'Select Lost Action' }], |
||||
} |
||||
|
||||
function validStartTime (): Error | void { |
||||
for (let i = 0; i < planBody.select_time.length; i++) { |
||||
if (!planBody.select_time[i][0]) { |
||||
throw new Error('Select start time') |
||||
} |
||||
} |
||||
} |
||||
function validEndTime (): Error | void { |
||||
if (TaskType.Condition !== planBody.task_type) return |
||||
for (let i = 0; i < planBody.select_time.length; i++) { |
||||
if (!planBody.select_time[i][1]) { |
||||
throw new Error('Select end time') |
||||
} |
||||
if (planBody.select_time[i][0] && planBody.select_time[i][1].isSameOrBefore(planBody.select_time[i][0])) { |
||||
throw new Error('End time should be later than start time') |
||||
} |
||||
} |
||||
} |
||||
function validOverlapped (): Error | void { |
||||
if (TaskType.Condition !== planBody.task_type) return |
||||
const arr = planBody.select_time.slice() |
||||
arr.sort((a, b) => a[0].unix() - b[0].unix()) |
||||
arr.forEach((v, i, arr) => { |
||||
if (i > 0 && v[0] < arr[i - 1][1]) { |
||||
throw new Error('Overlapping time periods.') |
||||
} |
||||
}) |
||||
} |
||||
|
||||
function onSubmit () { |
||||
console.info(dock, '12131231') |
||||
valueRef.value.validate().then(() => { |
||||
disabled.value = true |
||||
const createPlanBody = { ...planBody } as unknown as CreatePlan |
||||
if (planBody.select_execute_date.length === 2) { |
||||
createPlanBody.task_days = [] |
||||
for (let i = planBody.select_execute_date[0]; i.isSameOrBefore(planBody.select_execute_date[1]); i.add(1, 'days')) { |
||||
createPlanBody.task_days.push(i.unix()) |
||||
} |
||||
} |
||||
createPlanBody.task_periods = [] |
||||
if (TaskType.Immediate !== planBody.task_type) { |
||||
for (let i = 0; i < planBody.select_time.length; i++) { |
||||
const result = [] |
||||
result.push(planBody.select_time[i][0].unix()) |
||||
if (TaskType.Condition === planBody.task_type) { |
||||
result.push(planBody.select_time[i][1].unix()) |
||||
} |
||||
createPlanBody.task_periods.push(result) |
||||
} |
||||
} |
||||
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] |
||||
} |
||||
createPlan(workspaceId, createPlanBody) |
||||
.then(res => { |
||||
disabled.value = false |
||||
}).finally(() => { |
||||
closePlan() |
||||
}) |
||||
}).catch((e: any) => { |
||||
console.log('validate err', e) |
||||
}) |
||||
} |
||||
|
||||
function closePlan () { |
||||
root.$router.push('/' + ERouterName.TASK) |
||||
} |
||||
|
||||
function closePanel () { |
||||
drawerVisible.value = false |
||||
routeName.value = '' |
||||
} |
||||
|
||||
function selectRoute () { |
||||
drawerVisible.value = true |
||||
routeName.value = 'WaylinePanel' |
||||
} |
||||
|
||||
function selectDevice () { |
||||
drawerVisible.value = true |
||||
routeName.value = 'DockPanel' |
||||
} |
||||
|
||||
function addTime () { |
||||
valueRef.value.validateFields(['select_execute_time']).then(() => { |
||||
planBody.select_time_number++ |
||||
planBody.select_time.push([]) |
||||
}) |
||||
} |
||||
function removeTime () { |
||||
if (planBody.select_time_number === 1) return |
||||
planBody.select_time_number-- |
||||
planBody.select_time.splice(planBody.select_time_number) |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
.create-plan-wrapper { |
||||
background-color: #232323; |
||||
color: fff; |
||||
padding-bottom: 0; |
||||
height: 100vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
width: 285px; |
||||
|
||||
.header { |
||||
height: 52px; |
||||
border-bottom: 1px solid #4f4f4f; |
||||
font-weight: 700; |
||||
font-size: 16px; |
||||
padding-left: 10px; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
::-webkit-scrollbar { |
||||
display: none; |
||||
} |
||||
|
||||
.content { |
||||
height: calc(100% - 54px); |
||||
overflow-y: auto; |
||||
|
||||
form { |
||||
margin: 10px; |
||||
} |
||||
|
||||
form label, input, .ant-input, .ant-calendar-range-picker-separator, |
||||
.ant-input:hover, .ant-time-picker .anticon, .ant-calendar-picker .anticon { |
||||
background-color: #232323; |
||||
color: #fff; |
||||
} |
||||
|
||||
.ant-input-suffix { |
||||
color: #fff; |
||||
} |
||||
|
||||
.plan-timer-form-item { |
||||
|
||||
.ant-radio-button-wrapper{ |
||||
background-color: #232323; |
||||
color: #fff; |
||||
width: 33%; |
||||
text-align: center; |
||||
&.ant-radio-button-wrapper-checked{ |
||||
background-color: #1890ff; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.footer { |
||||
display: flex; |
||||
padding:10px 0; |
||||
|
||||
button { |
||||
width: 45%; |
||||
color: #fff ; |
||||
border: 0; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.wayline-panel { |
||||
background: #3c3c3c; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
margin-top: 10px; |
||||
height: 90px; |
||||
width: 95%; |
||||
font-size: 13px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
.title { |
||||
display: flex; |
||||
color: white; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
height: 30px; |
||||
font-weight: bold; |
||||
margin: 0px 10px 0 10px; |
||||
} |
||||
} |
||||
|
||||
.panel { |
||||
background: #3c3c3c; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
margin-top: 10px; |
||||
height: 70px; |
||||
width: 95%; |
||||
font-size: 13px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
.title { |
||||
display: flex; |
||||
color: white; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
height: 30px; |
||||
font-weight: bold; |
||||
margin: 0px 10px 0 10px; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,365 @@
@@ -0,0 +1,365 @@
|
||||
<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 class="flex-row" style="white-space: pre-wrap"> |
||||
<div> |
||||
<div>{{ formatTaskTime(record.begin_time) }}</div> |
||||
<div>{{ formatTaskTime(record.end_time) }}</div> |
||||
</div> |
||||
<div class="ml10"> |
||||
<div>{{ formatTaskTime(record.execute_time) }}</div> |
||||
<div>{{ formatTaskTime(record.completed_time) }}</div> |
||||
</div> |
||||
</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 #taskType="{ record }"> |
||||
<div>{{ formatTaskType(record) }}</div> |
||||
</template> |
||||
<!-- 失控动作 --> |
||||
<template #lostAction="{ record }"> |
||||
<div>{{ formatLostAction(record) }}</div> |
||||
</template> |
||||
<!-- 媒体上传状态 --> |
||||
<template #media_upload="{ record }"> |
||||
<div> |
||||
<div class="flex-display flex-align-center"> |
||||
<span class="circle-icon" :style="{backgroundColor: formatMediaTaskStatus(record).color}"></span> |
||||
{{ formatMediaTaskStatus(record).text }} |
||||
</div> |
||||
<div class="pl15"> |
||||
{{ formatMediaTaskStatus(record).number }} |
||||
<a-tooltip v-if="formatMediaTaskStatus(record).status === MediaStatus.ToUpload" placement="bottom" arrow-point-at-center > |
||||
<template #title> |
||||
<div>Upload now</div> |
||||
</template> |
||||
<UploadOutlined class="ml5" :style="{color: commonColor.BLUE, fontSize: '16px' }" @click="onUploadMediaFileNow(record.job_id)"/> |
||||
</a-tooltip> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<!-- 操作 --> |
||||
<template #action="{ record }"> |
||||
<div 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> |
||||
<a-popconfirm |
||||
v-if="record.status === TaskStatus.Carrying" |
||||
title="Are you sure you want to suspend?" |
||||
ok-text="Yes" |
||||
cancel-text="No" |
||||
@confirm="onSuspendTask(record.job_id)" |
||||
> |
||||
<a-button type="primary" size="small">Suspend</a-button> |
||||
</a-popconfirm> |
||||
<a-popconfirm |
||||
v-if="record.status === TaskStatus.Paused" |
||||
title="Are you sure you want to resume?" |
||||
ok-text="Yes" |
||||
cancel-text="No" |
||||
@confirm="onResumeTask(record.job_id)" |
||||
> |
||||
<a-button type="primary" size="small">Resume</a-button> |
||||
</a-popconfirm> |
||||
</div> |
||||
</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, updateTaskStatus, UpdateTaskStatus, getWaylineJobs, Task, uploadMediaFileNow } from '/@/api/wayline' |
||||
import { useMyStore } from '/@/store' |
||||
import { ELocalStorageKey } from '/@/types/enums' |
||||
import { useFormatTask } from './use-format-task' |
||||
import { TaskStatus, TaskProgressInfo, TaskProgressStatus, TaskProgressWsStatusMap, MediaStatus, MediaStatusProgressInfo, TaskMediaHighestPriorityProgressInfo } from '/@/types/task' |
||||
import { useTaskWsEvent } from './use-task-ws-event' |
||||
import { getErrorMessage } from '/@/utils/error-code/index' |
||||
import { commonColor } from '/@/utils/color' |
||||
import { ExclamationCircleOutlined, UploadOutlined } 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: 200, |
||||
slots: { customRender: 'duration' }, |
||||
}, |
||||
{ |
||||
title: 'Status', |
||||
key: 'status', |
||||
width: 150, |
||||
slots: { customRender: 'status' } |
||||
}, |
||||
{ |
||||
title: 'Plan Name', |
||||
dataIndex: 'job_name', |
||||
width: 100, |
||||
}, |
||||
{ |
||||
title: 'Type', |
||||
dataIndex: 'taskType', |
||||
width: 100, |
||||
slots: { customRender: 'taskType' }, |
||||
}, |
||||
{ |
||||
title: 'Flight Route Name', |
||||
dataIndex: 'file_name', |
||||
width: 100, |
||||
}, |
||||
{ |
||||
title: 'Dock Name', |
||||
dataIndex: 'dock_name', |
||||
width: 100, |
||||
ellipsis: true |
||||
}, |
||||
{ |
||||
title: 'RTH Altitude Relative to Dock (m)', |
||||
dataIndex: 'rth_altitude', |
||||
width: 120, |
||||
}, |
||||
{ |
||||
title: 'Lost Action', |
||||
dataIndex: 'out_of_control_action', |
||||
width: 120, |
||||
slots: { customRender: 'lostAction' }, |
||||
}, |
||||
{ |
||||
title: 'Creator', |
||||
dataIndex: 'username', |
||||
width: 120, |
||||
}, |
||||
{ |
||||
title: 'Media File Upload', |
||||
key: 'media_upload', |
||||
width: 160, |
||||
slots: { customRender: 'media_upload' } |
||||
}, |
||||
{ |
||||
title: 'Action', |
||||
width: 120, |
||||
slots: { customRender: 'action' } |
||||
} |
||||
] |
||||
type Pagination = TableState['pagination'] |
||||
|
||||
const plansData = reactive({ |
||||
data: [] as Task[] |
||||
}) |
||||
|
||||
const { formatTaskType, formatTaskTime, formatLostAction, formatTaskStatus, formatMediaTaskStatus } = 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 onTaskMediaProgressWs (data: MediaStatusProgressInfo) { |
||||
const { media_count: mediaCount, uploaded_count: uploadedCount, job_id: jobId } = data |
||||
if (isNaN(mediaCount) || isNaN(uploadedCount) || !jobId) { |
||||
return |
||||
} |
||||
const taskItem = plansData.data.find(task => task.job_id === jobId) |
||||
if (!taskItem) return |
||||
if (mediaCount === uploadedCount) { |
||||
taskItem.uploading = false |
||||
} else { |
||||
taskItem.uploading = true |
||||
} |
||||
taskItem.media_count = mediaCount |
||||
taskItem.uploaded_count = uploadedCount |
||||
} |
||||
|
||||
function onoTaskMediaHighestPriorityWS (data: TaskMediaHighestPriorityProgressInfo) { |
||||
const { pre_job_id: preJobId, job_id: jobId } = data |
||||
const preTaskItem = plansData.data.find(task => task.job_id === preJobId) |
||||
const taskItem = plansData.data.find(task => task.job_id === jobId) |
||||
if (preTaskItem) { |
||||
preTaskItem.uploading = false |
||||
} |
||||
if (taskItem) { |
||||
taskItem.uploading = true |
||||
} |
||||
} |
||||
|
||||
function getCodeMessage (code: number) { |
||||
return getErrorMessage(code) + `(code: ${code})` |
||||
} |
||||
|
||||
useTaskWsEvent({ |
||||
onTaskProgressWs, |
||||
onTaskMediaProgressWs, |
||||
onoTaskMediaHighestPriorityWS, |
||||
}) |
||||
|
||||
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() |
||||
} |
||||
} |
||||
|
||||
// 挂起任务 |
||||
async function onSuspendTask (jobId: string) { |
||||
const { code } = await updateTaskStatus(workspaceId, { |
||||
job_id: jobId, |
||||
status: UpdateTaskStatus.Suspend |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Suspended successfully') |
||||
getPlans() |
||||
} |
||||
} |
||||
|
||||
// 解除挂起任务 |
||||
async function onResumeTask (jobId: string) { |
||||
const { code } = await updateTaskStatus(workspaceId, { |
||||
job_id: jobId, |
||||
status: UpdateTaskStatus.Resume |
||||
}) |
||||
if (code === 0) { |
||||
message.success('Resumed successfully') |
||||
getPlans() |
||||
} |
||||
} |
||||
|
||||
// 立即上传媒体 |
||||
async function onUploadMediaFileNow (jobId: string) { |
||||
const { code } = await uploadMediaFileNow(workspaceId, jobId) |
||||
if (code === 0) { |
||||
message.success('Upload Media File successfully') |
||||
getPlans() |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.plan-panel-wrapper { |
||||
width: 100%; |
||||
padding: 16px; |
||||
.plan-table { |
||||
background: #fff; |
||||
margin-top: 10px; |
||||
} |
||||
.action-area { |
||||
|
||||
&::v-deep { |
||||
.ant-btn { |
||||
margin-right: 10px; |
||||
margin-bottom: 10px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.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> |
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
import { DEFAULT_PLACEHOLDER } from '/@/utils/constants' |
||||
import { Task } from '/@/api/wayline' |
||||
import { TaskStatusColor, TaskStatusMap, TaskTypeMap, OutOfControlActionMap, MediaStatusMap, MediaStatusColorMap, MediaStatus } from '/@/types/task' |
||||
import { isNil } from 'lodash' |
||||
|
||||
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 |
||||
} |
||||
|
||||
function formatMediaTaskStatus (task: Task) { |
||||
const statusObj = { |
||||
text: '', |
||||
color: '', |
||||
number: '', |
||||
status: MediaStatus.Empty, |
||||
} |
||||
const { media_count, uploaded_count, uploading } = task |
||||
if (isNil(media_count) || isNaN(media_count)) { |
||||
return statusObj |
||||
} |
||||
const expectedFileCount = media_count || 0 |
||||
const uploadedFileCount = uploaded_count || 0 |
||||
if (media_count === 0) { |
||||
statusObj.text = MediaStatusMap[MediaStatus.Empty] |
||||
statusObj.color = MediaStatusColorMap[MediaStatus.Empty] |
||||
} else if (media_count === uploaded_count) { |
||||
statusObj.text = MediaStatusMap[MediaStatus.Success] |
||||
statusObj.color = MediaStatusColorMap[MediaStatus.Success] |
||||
statusObj.number = `(${uploadedFileCount}/${expectedFileCount})` |
||||
statusObj.status = MediaStatus.Success |
||||
} else { |
||||
if (uploading) { |
||||
statusObj.text = MediaStatusMap[MediaStatus.Uploading] |
||||
statusObj.color = MediaStatusColorMap[MediaStatus.Uploading] |
||||
statusObj.status = MediaStatus.Uploading |
||||
} else { |
||||
statusObj.text = MediaStatusMap[MediaStatus.ToUpload] |
||||
statusObj.color = MediaStatusColorMap[MediaStatus.ToUpload] |
||||
statusObj.status = MediaStatus.ToUpload |
||||
} |
||||
statusObj.number = `(${uploadedFileCount}/${expectedFileCount})` |
||||
} |
||||
return statusObj |
||||
} |
||||
|
||||
return { |
||||
formatTaskType, |
||||
formatTaskTime, |
||||
formatLostAction, |
||||
formatTaskStatus, |
||||
formatMediaTaskStatus, |
||||
} |
||||
} |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
import EventBus from '/@/event-bus/' |
||||
import { onMounted, onBeforeUnmount } from 'vue' |
||||
import { TaskProgressInfo, MediaStatusProgressInfo, TaskMediaHighestPriorityProgressInfo } from '/@/types/task' |
||||
import { EBizCode } from '/@/types' |
||||
|
||||
export interface UseTaskWsEventParams { |
||||
onTaskProgressWs: (data: TaskProgressInfo) => void, |
||||
onTaskMediaProgressWs: (data: MediaStatusProgressInfo) => void |
||||
onoTaskMediaHighestPriorityWS: (data: TaskMediaHighestPriorityProgressInfo) => void |
||||
} |
||||
|
||||
export function useTaskWsEvent (funcs: UseTaskWsEventParams): void { |
||||
function handleTaskWsEvent (payload: any) { |
||||
if (!payload) { |
||||
return |
||||
} |
||||
|
||||
switch (payload.biz_code) { |
||||
case EBizCode.FlightTaskProgress: { |
||||
funcs?.onTaskProgressWs(payload.data) |
||||
break |
||||
} |
||||
case EBizCode.FlightTaskMediaProgress: { |
||||
funcs?.onTaskMediaProgressWs(payload.data) |
||||
break |
||||
} |
||||
case EBizCode.FlightTaskMediaHighestPriority: { |
||||
funcs?.onoTaskMediaHighestPriorityWS(payload.data) |
||||
break |
||||
} |
||||
} |
||||
// eslint-disable-next-line no-unused-expressions
|
||||
// console.log('payload', payload.data)
|
||||
} |
||||
|
||||
onMounted(() => { |
||||
EventBus.on('flightTaskWs', handleTaskWsEvent) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
EventBus.off('flightTaskWs', handleTaskWsEvent) |
||||
}) |
||||
} |
@ -1,123 +0,0 @@
@@ -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> |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
<template> |
||||
<Divider class="divider" /> |
||||
</template> |
||||
<script lang="ts" setup> |
||||
import { Divider } from 'ant-design-vue' |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.divider { |
||||
margin: 10px 0; |
||||
height: 1px; |
||||
background-color: #4f4f4f; |
||||
} |
||||
</style> |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
<template> |
||||
<div style="height: 40px; line-height: 50px; font-weight: 450;"> |
||||
<a-row> |
||||
<a-col :span="1"></a-col> |
||||
<a-col :span="(23 - (extSpan || 0))">{{ title }}</a-col> |
||||
<a-col :span="extSpan"><slot /></a-col> |
||||
</a-row> |
||||
</div> |
||||
<DividerLine /> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { defineProps } from 'vue' |
||||
import DividerLine from '/@/components/workspace/DividerLine.vue' |
||||
|
||||
const props = defineProps < { |
||||
extSpan?: number, |
||||
title: string, |
||||
} >() |
||||
|
||||
</script> |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
import { nextTick, App } from 'vue' |
||||
|
||||
export default function useDragWindowDirective (app: App): void { |
||||
app.directive('drag-window', async (el) => { |
||||
await nextTick() |
||||
|
||||
const modal = el |
||||
const header = el.getElementsByClassName('drag-title')[0] |
||||
let left = 0 |
||||
let top = 0 |
||||
header.style.cursor = 'move' |
||||
top = top || modal.offsetTop |
||||
|
||||
header.onpointerdown = (e: { clientX: number; clientY: number; pointerId: number }) => { |
||||
const startX = e.clientX |
||||
const startY = e.clientY |
||||
header.left = header.offsetLeft |
||||
header.top = header.offsetTop |
||||
header.setPointerCapture(e.pointerId) |
||||
|
||||
el.onpointermove = (event: { clientX: number; clientY: number }) => { |
||||
const endX = event.clientX |
||||
const endY = event.clientY |
||||
modal.left = header.left + (endX - startX) + left |
||||
modal.top = header.top + (endY - startY) + top |
||||
modal.style.left = modal.left + 'px' |
||||
modal.style.top = modal.top + 'px' |
||||
} |
||||
el.onpointerup = () => { |
||||
left = modal.left || 0 |
||||
top = modal.top || 0 |
||||
el.onpointermove = null |
||||
el.onpointerup = null |
||||
header.releasePointerCapture(e.pointerId) |
||||
} |
||||
} |
||||
}) |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
import { App } from 'vue' |
||||
import useDragWindowDirective from './drag-window' |
||||
|
||||
export function useDirectives (app: App): void { |
||||
useDragWindowDirective(app) |
||||
} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
import { GeojsonCoordinate } from '../utils/genjson' |
||||
import { getRoot } from '/@/root' |
||||
|
||||
export function useMapTool () { |
||||
const root = getRoot() |
||||
const map = root.$map |
||||
const AMap = root.$aMap |
||||
|
||||
function panTo (coordinate: GeojsonCoordinate) { |
||||
map.panTo(coordinate, 100) |
||||
map.setZoom(18, false, 100) |
||||
} |
||||
return { |
||||
panTo, |
||||
} |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
|
||||
import { |
||||
IClientOptions, |
||||
} from 'mqtt' |
||||
|
||||
export const OPTIONS: IClientOptions = { |
||||
clean: true, // true: 清除会话, false: 保留会话
|
||||
connectTimeout: 10000, // mqtt 超时时间
|
||||
resubscribe: true, // 断开重连后,再次订阅原订阅
|
||||
reconnectPeriod: 10000, // 重连间隔时间: 5s
|
||||
keepalive: 1, // 心跳间隔时间:1s
|
||||
} |
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
import EventEmitter from 'eventemitter3' |
||||
import { |
||||
OPTIONS, |
||||
} from './config' |
||||
import { |
||||
connect, |
||||
MqttClient, |
||||
IClientPublishOptions, |
||||
IPublishPacket, |
||||
Packet, |
||||
ISubscriptionGrant, |
||||
IClientOptions, |
||||
} from 'mqtt/dist/mqtt.min' |
||||
|
||||
export class UranusMqtt extends EventEmitter { |
||||
_url: string |
||||
_options?: IClientOptions |
||||
_client: MqttClient | null |
||||
_hasInit: boolean |
||||
|
||||
constructor (url?: string, options?: IClientOptions) { |
||||
super() |
||||
this._url = url || '' |
||||
this._options = options |
||||
this._client = null |
||||
this._hasInit = false |
||||
} |
||||
|
||||
initMqtt = () => { |
||||
// 仅初始化一次
|
||||
if (this._hasInit) return |
||||
// 建立连接
|
||||
this._client = connect(this._url, { |
||||
...OPTIONS, |
||||
...this._options, |
||||
}) |
||||
this._hasInit = true |
||||
if (this._client) { |
||||
this._client.on('reconnect', this._onReconnect) |
||||
|
||||
// 消息监听
|
||||
this._client.on('message', this._onMessage) |
||||
|
||||
// 连接关闭
|
||||
this._client.on('close', this._onClose) |
||||
|
||||
// 连接异常
|
||||
this._client.on('error', this._onError) |
||||
} |
||||
} |
||||
|
||||
// 发布
|
||||
publishMqtt = (topic: string, body: string | Buffer, opts?: IClientPublishOptions) => { |
||||
if (!this._client?.connected) { |
||||
this.initMqtt() |
||||
} |
||||
this._client?.publish(topic, body, opts || {}, (error?: Error, packet?: Packet) => { |
||||
if (error) { |
||||
window.console.error('mqtt publish error,', error, packet) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 订阅
|
||||
subscribeMqtt = (topic: string) => { |
||||
if (!this._client?.connected) { |
||||
this.initMqtt() |
||||
} |
||||
window.console.log('subscribeMqtt>>>>>', topic) |
||||
this._client?.subscribe(topic, (error: Error, granted: ISubscriptionGrant[]) => { |
||||
window.console.log('mqtt subscribe,', error, granted) |
||||
}) |
||||
} |
||||
|
||||
// 取消订阅
|
||||
unsubscribeMqtt = (topic: string) => { |
||||
window.console.log('mqtt unsubscribeMqtt,', topic) |
||||
this._client?.unsubscribe(topic) |
||||
} |
||||
|
||||
// 关闭 mqtt 客户端
|
||||
destroyed = () => { |
||||
window.console.log('mqtt destroyed') |
||||
this._client?.end() |
||||
} |
||||
|
||||
_onReconnect = () => { |
||||
if (this._client) { window.console.error('mqtt reconnect,') } |
||||
} |
||||
|
||||
_onMessage = (topic: string, payload: Buffer, packet: IPublishPacket) => { |
||||
this.emit('onMessageMqtt', { topic, payload, packet }) |
||||
} |
||||
|
||||
_onClose = () => { |
||||
// 连接异常关闭会自动重连
|
||||
window.console.error('mqtt close,') |
||||
this.emit('onStatus', { |
||||
status: 'close', |
||||
}) |
||||
} |
||||
|
||||
_onError = (error: Error) => { |
||||
// 连接错误会自动重连
|
||||
window.console.error('mqtt error,', error) |
||||
this.emit('onStatus', { |
||||
status: 'error', |
||||
data: error, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
export { |
||||
IClientOptions, |
||||
IPublishPacket, |
||||
IClientPublishOptions, |
||||
} |
@ -0,0 +1,325 @@
@@ -0,0 +1,325 @@
|
||||
|
||||
<template> |
||||
<div class="ml20 mt20 mr20 flex-row flex-align-center flex-justify-between"> |
||||
<div class="flex-row"> |
||||
<a-button type="primary" @click="sVisible = true"> |
||||
Click to Upload |
||||
</a-button> |
||||
<a-modal :visible="sVisible" |
||||
title="Import Firmware File" |
||||
:closable="false" |
||||
@cancel="onCancel" |
||||
@ok="uploadFile" |
||||
centered> |
||||
<a-form :rules="rules" ref="formRef" :model="uploadParam" :label-col="{ span: 6 }"> |
||||
<a-form-item name="status" label="Avaliable" required> |
||||
<a-switch v-model:checked="uploadParam.status" /> |
||||
</a-form-item> |
||||
<a-form-item name="device_name" label="Device Name" required> |
||||
<a-select |
||||
style="width: 220px" |
||||
mode="multiple" |
||||
placeholder="can choose multiple" |
||||
v-model:value="uploadParam.device_name"> |
||||
<a-select-option |
||||
v-for="k in DeviceNameEnum" |
||||
:key="k" |
||||
:value="k" |
||||
> |
||||
{{ k }} |
||||
</a-select-option> |
||||
</a-select> |
||||
</a-form-item> |
||||
<a-form-item name="release_note" label="Release Note" required> |
||||
<a-textarea v-model:value="uploadParam.release_note" showCount :maxlength="300" /> |
||||
</a-form-item> |
||||
<a-form-item label="File" required> |
||||
<a-upload |
||||
:multiple="false" |
||||
:before-upload="beforeUpload" |
||||
:show-upload-list="true" |
||||
:file-list="fileList" |
||||
:remove="removeFile" |
||||
> |
||||
<a-button type="primary"> |
||||
<UploadOutlined /> |
||||
Import Firmware File |
||||
</a-button> |
||||
</a-upload> |
||||
</a-form-item> |
||||
</a-form> |
||||
</a-modal> |
||||
</div> |
||||
<div class="flex-row"> |
||||
<div class="ml5"> |
||||
<a-select |
||||
style="width: 150px" |
||||
v-model:value="param.firmware_status" |
||||
@select="getAllFirmwares(pageParam)"> |
||||
<a-select-option |
||||
v-for="(key, value) in FirmwareStatusEnum" |
||||
:key="key" |
||||
:value="value" |
||||
> |
||||
{{ key }} |
||||
</a-select-option> |
||||
</a-select> |
||||
</div> |
||||
<div class="ml5"> |
||||
<a-select |
||||
style="width: 150px" |
||||
v-model:value="param.device_name" |
||||
@select="getAllFirmwares(pageParam)"> |
||||
<a-select-option |
||||
v-for="item in deviceNameList" |
||||
:key="item.label" |
||||
:value="item.value" |
||||
> |
||||
{{ item.label }} |
||||
</a-select-option> |
||||
</a-select> |
||||
</div> |
||||
<div class="ml5"> |
||||
<a-input-search |
||||
:enter-button="true" |
||||
v-model:value="param.product_version" |
||||
placeholder="input search verison" |
||||
style="width: 250px" |
||||
@search="getAllFirmwares(pageParam)"/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="table flex-display flex-column"> |
||||
<a-table :columns="columns" :data-source="data.firmware" :pagination="paginationProp" @change="refreshData" row-key="firmware_id" |
||||
:rowClassName="(record, index) => ((index % 2) === 0 ? 'table-striped' : null)" :scroll="{ x: '100%', y: 600 }"> |
||||
<template #device_name="{ record }"> |
||||
<div v-for="text in record.device_name" :key="text"> |
||||
{{ text }} |
||||
</div> |
||||
</template> |
||||
<template #file_size="{ record }"> |
||||
<div>{{ bytesToSize(record.file_size) }}</div> |
||||
</template> |
||||
<template #firmware_status="{ record }"> |
||||
<DeviceFirmwareStatus :firmware="record" /> |
||||
</template> |
||||
<template v-for="col in ['file_name', 'release_note']" #[col]="{ text }" :key="col"> |
||||
<a-tooltip :title="text"> |
||||
<span>{{ text }}</span> |
||||
</a-tooltip> |
||||
</template> |
||||
|
||||
</a-table> |
||||
</div> |
||||
</template> |
||||
<script lang="ts" setup> |
||||
import { message, notification, PaginationProps } from 'ant-design-vue' |
||||
import { TableState } from 'ant-design-vue/lib/table/interface' |
||||
import { onMounted, reactive, Ref, ref, UnwrapRef } from 'vue' |
||||
import { IPage } from '/@/api/http/type' |
||||
import { getFirmwares, importFirmareFile } from '/@/api/manage' |
||||
import DeviceFirmwareStatus from '/@/components/devices/DeviceFirmwareStatus.vue' |
||||
import { ELocalStorageKey } from '/@/types' |
||||
import { UploadOutlined } from '@ant-design/icons-vue' |
||||
import { Firmware, FirmwareQueryParam, FirmwareStatusEnum, DeviceNameEnum, FirmwareUploadParam } from '/@/types/device-firmware' |
||||
import { commonColor } from '/@/utils/color' |
||||
import { bytesToSize } from '/@/utils/bytes' |
||||
import moment from 'moment' |
||||
|
||||
interface FirmwareData { |
||||
firmware: Firmware[] |
||||
} |
||||
const columns = [ |
||||
{ title: 'Model', dataIndex: 'device_name', width: 120, ellipsis: true, className: 'titleStyle', slots: { customRender: 'device_name' } }, |
||||
{ title: 'File Name', dataIndex: 'file_name', width: 220, ellipsis: true, className: 'titleStyle', slots: { customRender: 'file_name' } }, |
||||
{ title: 'Firmware Version', dataIndex: 'product_version', width: 180, className: 'titleStyle' }, |
||||
{ title: 'File Size', dataIndex: 'file_size', width: 150, className: 'titleStyle', slots: { customRender: 'file_size' } }, |
||||
{ title: 'Creator', dataIndex: 'username', width: 100, className: 'titleStyle' }, |
||||
{ title: 'Release Date', dataIndex: 'released_time', width: 160, sorter: (a: Firmware, b: Firmware) => a.released_time.localeCompare(b.released_time), className: 'titleStyle' }, |
||||
{ title: 'Release Note', dataIndex: 'release_note', width: 300, ellipsis: true, className: 'titleStyle', slots: { customRender: 'release_note' } }, |
||||
{ title: 'Status', dataIndex: 'firmware_status', width: 100, className: 'titleStyle', slots: { customRender: 'firmware_status' } }, |
||||
] |
||||
|
||||
const data = reactive<FirmwareData>({ |
||||
firmware: [] |
||||
}) |
||||
|
||||
const paginationProp = reactive({ |
||||
pageSizeOptions: ['20', '50', '100'], |
||||
showQuickJumper: true, |
||||
showSizeChanger: true, |
||||
pageSize: 50, |
||||
current: 1, |
||||
total: 0 |
||||
}) |
||||
|
||||
const deviceNameList = ref<any[]>([{ label: 'All', value: '' }]) |
||||
|
||||
type Pagination = TableState['pagination'] |
||||
|
||||
const pageParam: IPage = { |
||||
page: 1, |
||||
total: 0, |
||||
page_size: 50 |
||||
} |
||||
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||
|
||||
const param = reactive<FirmwareQueryParam>({ |
||||
product_version: '', |
||||
device_name: '', |
||||
firmware_status: FirmwareStatusEnum.NONE |
||||
}) |
||||
|
||||
onMounted(() => { |
||||
getAllFirmwares(pageParam) |
||||
for (const key in DeviceNameEnum) { |
||||
const value = DeviceNameEnum[key] |
||||
deviceNameList.value.push({ label: value, value: value }) |
||||
} |
||||
}) |
||||
|
||||
function refreshData (page: Pagination) { |
||||
pageParam.page = page?.current! |
||||
pageParam.page_size = page?.pageSize! |
||||
getAllFirmwares(pageParam) |
||||
} |
||||
|
||||
function getAllFirmwares (page: IPage) { |
||||
getFirmwares(workspaceId, page, param).then(res => { |
||||
const firmwareList: Firmware[] = res.data.list |
||||
data.firmware = firmwareList |
||||
paginationProp.total = res.data.pagination.total |
||||
paginationProp.current = res.data.pagination.page |
||||
}) |
||||
} |
||||
|
||||
const sVisible = ref(false) |
||||
const uploadParam = reactive<FirmwareUploadParam>({ |
||||
device_name: [], |
||||
release_note: '', |
||||
status: true |
||||
}) |
||||
|
||||
const rules = { |
||||
status: [{ required: true }], |
||||
release_note: [{ required: true, message: 'Please input release note.' }], |
||||
device_name: [{ required: true, message: 'Please select which models this firmware belongs to.' }] |
||||
} |
||||
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) { |
||||
if (!file.name || !file.name?.endsWith('.zip')) { |
||||
message.error('Format error. Please select zip file.') |
||||
return false |
||||
} |
||||
fileList.value = [file] |
||||
return false |
||||
} |
||||
|
||||
const formRef = ref() |
||||
function removeFile (file: FileItem) { |
||||
fileList.value = [] |
||||
} |
||||
function onCancel () { |
||||
formRef.value.resetFields() |
||||
fileList.value = [] |
||||
sVisible.value = false |
||||
} |
||||
|
||||
const uploadFile = async () => { |
||||
if (fileList.value.length === 0) { |
||||
message.error('Please select at least one file.') |
||||
} |
||||
let uploading: string |
||||
formRef.value.validate().then(async () => { |
||||
const file: FileItem = fileList.value[0] |
||||
const fileData = new FormData() |
||||
fileData.append('file', file as any, file.name) |
||||
Object.keys(uploadParam).forEach((key) => { |
||||
const val = uploadParam[key as keyof FirmwareUploadParam] |
||||
if (val instanceof Array) { |
||||
val.forEach((value) => { |
||||
fileData.append(key, value) |
||||
}) |
||||
} else { |
||||
fileData.append(key, val.toString()) |
||||
} |
||||
}) |
||||
const timestamp = new Date().getTime() |
||||
uploading = (file.name ?? 'uploding') + timestamp |
||||
notification.open({ |
||||
key: uploading, |
||||
message: `Uploading ${moment().format()}`, |
||||
description: `[${file.name}] is uploading... `, |
||||
duration: null |
||||
}) |
||||
importFirmareFile(workspaceId, fileData).then((res) => { |
||||
if (res.code === 0) { |
||||
notification.success({ |
||||
message: `Uploaded ${moment().format()}`, |
||||
description: `[${file.name}] file uploaded successfully. Duration: ${moment.duration(new Date().getTime() - timestamp).asSeconds()}`, |
||||
duration: null |
||||
}) |
||||
getAllFirmwares(pageParam) |
||||
} else { |
||||
notification.error({ |
||||
message: `Failed to upload [${file.name}]. Check and try again.`, |
||||
description: `Error message: ${res.message} ${moment().format()}`, |
||||
style: { color: commonColor.FAIL }, |
||||
duration: null, |
||||
}) |
||||
} |
||||
}).finally(() => { |
||||
notification.close(uploading) |
||||
}) |
||||
fileList.value = [] |
||||
formRef.value.resetFields() |
||||
sVisible.value = false |
||||
}) |
||||
} |
||||
|
||||
</script> |
||||
<style> |
||||
|
||||
.table { |
||||
background-color: white; |
||||
margin: 20px; |
||||
padding: 20px; |
||||
height: 88vh; |
||||
} |
||||
.table-striped { |
||||
background-color: #f7f9fa; |
||||
} |
||||
.ant-table { |
||||
border-top: 1px solid rgb(0,0,0,0.06); |
||||
border-bottom: 1px solid rgb(0,0,0,0.06); |
||||
} |
||||
.ant-table-tbody tr td { |
||||
border: 0; |
||||
} |
||||
.ant-table td { |
||||
white-space: nowrap; |
||||
} |
||||
.ant-table-thead tr th { |
||||
background: white !important; |
||||
border: 0; |
||||
} |
||||
th.ant-table-selection-column { |
||||
background-color: white !important; |
||||
} |
||||
.ant-table-header { |
||||
background-color: white !important; |
||||
} |
||||
</style> |
@ -1,251 +0,0 @@
@@ -1,251 +0,0 @@
|
||||
<template> |
||||
<div class="plan"> |
||||
<div class="header"> |
||||
Create Plan |
||||
</div> |
||||
<div class="content"> |
||||
<a-form ref="valueRef" layout="horizontal" :hideRequiredMark="true" :rules="rules" :model="planBody"> |
||||
<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'}" |
||||
@click="selectRoute" |
||||
> |
||||
Select Route |
||||
</router-link> |
||||
</a-form-item> |
||||
<a-form-item v-if="planBody.file_id" style="margin-top: -15px;"> |
||||
<div class="wayline-panel" style="padding-top: 5px;"> |
||||
<div class="title"> |
||||
<a-tooltip :title="wayline.name"> |
||||
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div> |
||||
</a-tooltip> |
||||
<div class="ml10"><UserOutlined /></div> |
||||
<a-tooltip :title="wayline.user_name"> |
||||
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||
<span><RocketOutlined /></span> |
||||
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span> |
||||
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> |
||||
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> |
||||
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }} |
||||
</span> |
||||
</div> |
||||
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span> |
||||
</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'}" |
||||
@click="selectDevice" |
||||
>Select Device</router-link> |
||||
</a-form-item> |
||||
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;"> |
||||
<div 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> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||
<span><RocketOutlined /></span> |
||||
<span class="ml5">{{ dock.children?.nickname }}</span> |
||||
</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> |
||||
<a-form-item style="position: absolute; bottom: 0px; margin-bottom: 0; margin-left: -10px; width: 280px;"> |
||||
<div class="footer"> |
||||
<a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel |
||||
</a-button> |
||||
<a-button type="primary" @click="onSubmit" :disabled="disabled">OK |
||||
</a-button> |
||||
</div> |
||||
</a-form-item> |
||||
</a-form> |
||||
</div> |
||||
</div> |
||||
<div v-if="drawerVisible" style="position: absolute; left: 330px; width: 280px; height: 100vh; float: right; top: 0; z-index: 1000; color: white; background: #282828;"> |
||||
<div> |
||||
<router-view :name="routeName"/> |
||||
</div> |
||||
<div style="position: absolute; top: 15px; right: 10px;"> |
||||
<a style="color: white;" @click="closePanel"><CloseOutlined /></a> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<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 { ELocalStorageKey, ERouterName } from '/@/types' |
||||
import { useMyStore } from '/@/store' |
||||
import { WaylineFile } from '/@/types/wayline' |
||||
import { Device, EDeviceType } from '/@/types/device' |
||||
import { createPlan, CreatePlan } from '/@/api/wayline' |
||||
import { getRoot } from '/@/root' |
||||
|
||||
const root = getRoot() |
||||
const store = useMyStore() |
||||
|
||||
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||
|
||||
const wayline = computed<WaylineFile>(() => { |
||||
return store.state.waylineInfo |
||||
}) |
||||
|
||||
const dock = computed<Device>(() => { |
||||
return store.state.dockInfo |
||||
}) |
||||
|
||||
const disabled = ref(false) |
||||
|
||||
const routeName = ref('') |
||||
const planBody: UnwrapRef<CreatePlan> = reactive({ |
||||
name: '', |
||||
file_id: computed(() => store.state.waylineInfo.id), |
||||
dock_sn: computed(() => store.state.dockInfo.device_sn), |
||||
immediate: false, |
||||
type: 'wayline' |
||||
}) |
||||
const drawerVisible = ref(false) |
||||
const valueRef = ref() |
||||
const rules = { |
||||
name: [ |
||||
{ required: true, message: 'Please enter plan name.' }, |
||||
{ 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' }] |
||||
} |
||||
|
||||
function onSubmit () { |
||||
valueRef.value.validate().then(() => { |
||||
disabled.value = true |
||||
createPlan(workspaceId, planBody) |
||||
.then(res => { |
||||
message.success('Saved Successfully') |
||||
setTimeout(() => { |
||||
disabled.value = false |
||||
}, 1500) |
||||
}).finally(() => { |
||||
closePlan() |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
function closePlan () { |
||||
root.$router.push('/' + ERouterName.TASK) |
||||
} |
||||
|
||||
function closePanel () { |
||||
drawerVisible.value = false |
||||
routeName.value = '' |
||||
} |
||||
|
||||
function selectRoute () { |
||||
drawerVisible.value = true |
||||
routeName.value = 'WaylinePanel' |
||||
} |
||||
|
||||
function selectDevice () { |
||||
drawerVisible.value = true |
||||
routeName.value = 'DockPanel' |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
|
||||
.plan { |
||||
background-color: #232323; |
||||
color: white; |
||||
padding-bottom: 0; |
||||
height: 100vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
.header { |
||||
height: 53px; |
||||
border-bottom: 1px solid #4f4f4f; |
||||
font-weight: 700; |
||||
font-size: 16px; |
||||
padding-left: 10px; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.content { |
||||
height: 100%; |
||||
form { |
||||
margin: 10px; |
||||
} |
||||
form label, input { |
||||
color: white; |
||||
} |
||||
} |
||||
.footer { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border-top: 1px solid #4f4f4f; |
||||
min-height: 65px; |
||||
margin-bottom: 0; |
||||
padding-bottom: 0; |
||||
button { |
||||
width: 45%; |
||||
color: white; |
||||
border: 0; |
||||
} |
||||
} |
||||
} |
||||
.wayline-panel { |
||||
background: #3c3c3c; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
margin-top: 10px; |
||||
height: 90px; |
||||
width: 95%; |
||||
font-size: 13px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
.title { |
||||
display: flex; |
||||
color: white; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
height: 30px; |
||||
font-weight: bold; |
||||
margin: 0px 10px 0 10px; |
||||
} |
||||
} |
||||
.panel { |
||||
background: #3c3c3c; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
margin-top: 10px; |
||||
height: 70px; |
||||
width: 95%; |
||||
font-size: 13px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
.title { |
||||
display: flex; |
||||
color: white; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
height: 30px; |
||||
font-weight: bold; |
||||
margin: 0px 10px 0 10px; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
<template> |
||||
<div class="project-flight-area-wrapper height-100"> |
||||
<a-spin :spinning="loading" :delay="300" tip="loading" size="large" class="height-100"> |
||||
<Title title="Custom Flight Area" /> |
||||
<FlightAreaPanel :data="flightAreaList" @location-area="clickArea" @delete-area="deleteAreaById"/> |
||||
<DividerLine /> |
||||
<FlightAreaSyncPanel /> |
||||
</a-spin> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { onMounted, ref } from 'vue' |
||||
import Title from '/@/components/workspace/Title.vue' |
||||
import DividerLine from '/@/components/workspace/DividerLine.vue' |
||||
import FlightAreaPanel from '/@/components/flight-area/FlightAreaPanel.vue' |
||||
import FlightAreaSyncPanel from '/@/components/flight-area/FlightAreaSyncPanel.vue' |
||||
import { GetFlightArea, deleteFlightArea, getFlightAreaList } from '/@/api/flight-area' |
||||
import { useGMapCover } from '/@/hooks/use-g-map-cover' |
||||
import { useMapTool } from '/@/hooks/use-map-tool' |
||||
import { EFlightAreaType, EGeometryType, FlightAreaUpdate } from '/@/types/flight-area' |
||||
import { useFlightArea } from '/@/components/flight-area/use-flight-area' |
||||
import { useFlightAreaUpdateEvent } from '/@/components/flight-area/use-flight-area-update' |
||||
|
||||
const loading = ref(false) |
||||
const flightAreaList = ref<GetFlightArea[]>([]) |
||||
let useGMapCoverHook = useGMapCover() |
||||
let useMapToolHook = useMapTool() |
||||
|
||||
onMounted(() => { |
||||
getDataList() |
||||
}) |
||||
const { getGcj02 } = useFlightArea() |
||||
|
||||
const initMapFlightArea = () => { |
||||
useMapToolHook = useMapTool() |
||||
useGMapCoverHook = useGMapCover() |
||||
flightAreaList.value.forEach(area => { |
||||
updateMapFlightArea(area) |
||||
}) |
||||
} |
||||
|
||||
const updateMapFlightArea = (area: GetFlightArea) => { |
||||
switch (area.content.geometry.type) { |
||||
case EGeometryType.CIRCLE: |
||||
useGMapCoverHook.updateFlightAreaCircle(area.area_id, area.name, area.content.geometry.radius, getGcj02(area.content.geometry.coordinates), area.status, area.type) |
||||
break |
||||
case 'Polygon': |
||||
useGMapCoverHook.updateFlightAreaPolygon(area.area_id, area.name, getGcj02(area.content.geometry.coordinates[0]), area.status, area.type) |
||||
break |
||||
} |
||||
} |
||||
|
||||
const getDataList = () => { |
||||
loading.value = true |
||||
getFlightAreaList().then(res => { |
||||
flightAreaList.value = res.data |
||||
setTimeout(initMapFlightArea, 2000) |
||||
}).finally(() => { |
||||
loading.value = false |
||||
}) |
||||
} |
||||
|
||||
const deleteAreaById = (areaId: string) => { |
||||
deleteFlightArea(areaId) |
||||
} |
||||
|
||||
const deleteArea = (area: FlightAreaUpdate) => { |
||||
flightAreaList.value = flightAreaList.value.filter(data => data.area_id !== area.area_id) |
||||
useGMapCoverHook.removeCoverFromMap(area.area_id) |
||||
} |
||||
|
||||
const updateArea = (area: FlightAreaUpdate) => { |
||||
flightAreaList.value = flightAreaList.value.map(data => data.area_id === area.area_id ? area : data) |
||||
updateMapFlightArea(area as GetFlightArea) |
||||
} |
||||
|
||||
const addArea = (area: FlightAreaUpdate) => { |
||||
flightAreaList.value.push(area as GetFlightArea) |
||||
updateMapFlightArea(area as GetFlightArea) |
||||
} |
||||
|
||||
useFlightAreaUpdateEvent(addArea, deleteArea, updateArea) |
||||
|
||||
const clickArea = (area: GetFlightArea) => { |
||||
console.info(area) |
||||
let coordinate |
||||
switch (area.content.geometry.type) { |
||||
case EGeometryType.CIRCLE: |
||||
coordinate = getGcj02(area.content.geometry.coordinates) |
||||
break |
||||
case 'Polygon': |
||||
coordinate = useGMapCoverHook.calcPolygonPosition(getGcj02(area.content.geometry.coordinates[0])) |
||||
break |
||||
} |
||||
useMapToolHook.panTo(coordinate) |
||||
} |
||||
</script> |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
declare module 'mqtt/dist/mqtt.min' { |
||||
import MQTT from 'mqtt' |
||||
export = MQTT |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
export interface Firmware { |
||||
firmware_id: string |
||||
file_name: string |
||||
product_version: string |
||||
file_size: number |
||||
device_name: string[] |
||||
username: string |
||||
release_note: string |
||||
released_time: string |
||||
firmware_status: boolean |
||||
} |
||||
|
||||
export enum FirmwareStatusEnum { |
||||
NONE = 'All', |
||||
FALSE = 'Disabled', |
||||
TRUE = 'Available' |
||||
} |
||||
|
||||
export interface FirmwareQueryParam { |
||||
product_version: string |
||||
device_name: string |
||||
firmware_status: FirmwareStatusEnum |
||||
} |
||||
|
||||
export interface FirmwareUploadParam { |
||||
device_name: string[] |
||||
release_note: string |
||||
status: boolean |
||||
} |
||||
|
||||
export enum DeviceNameEnum { |
||||
DJI_DOCK = 'DJI Dock', |
||||
DJI_DOCK2 = 'DJI Dock2', |
||||
MATRICE_30 = 'Matrice 30', |
||||
MATRICE_30T = 'Matrice 30T', |
||||
M3D = 'M3D', |
||||
M3TD = 'M3TD', |
||||
} |
@ -0,0 +1,148 @@
@@ -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 |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
export enum DRC_METHOD { |
||||
HEART_BEAT = 'heart_beat', |
||||
DRONE_CONTROL = 'drone_control', // 飞行控制-虚拟摇杆
|
||||
DRONE_EMERGENCY_STOP = 'drone_emergency_stop', // 急停
|
||||
OSD_INFO_PUSH = 'osd_info_push', // 高频osd信息上报
|
||||
HSI_INFO_PUSH = 'hsi_info_push', // 避障信息上报
|
||||
DELAY_TIME_INFO_PUSH = 'delay_info_push', // 图传链路延时信息上报
|
||||
} |
||||
|
||||
// 手动控制
|
||||
export interface DroneControlProtocol { |
||||
x?: number; // 水平方向速度,正值为A指令 负值为D指令 单位:m/s
|
||||
y?: number; // 前进后退方向速度,正值为W指令 负值为S指令 单位:m/s
|
||||
h?: number;// 上下高度值,正值为上升指令 负值为下降指令 单位:m
|
||||
w?: number; // 机头角速度,正值为顺时针,负值为逆时针 单位:degree/s (web端暂无此设计)
|
||||
seq?: number; // 从0计时
|
||||
} |
||||
|
||||
// 低延时osd
|
||||
export interface DRCOsdInfo { |
||||
attitude_head: number;// 飞机姿态head角,单位:度
|
||||
latitude: number;// 飞机经纬度
|
||||
longitude: number; |
||||
altitude: number; |
||||
speed_x: number; |
||||
speed_y: number; |
||||
speed_z: number; |
||||
gimbal_pitch: number;// 云台pitch角
|
||||
gimbal_roll: number;// 云台roll角
|
||||
gimbal_yaw: number;// 云台yaw角
|
||||
} |
||||
|
||||
// 态势感知-HSI
|
||||
export interface DRCHsiInfo { |
||||
up_distance: number;// 上方的障碍物距离,单位:mm
|
||||
down_distance: number;// 下方的障碍物距离,单位:mm
|
||||
around_distances: number[]; // 水平方向观察点,分布在[0,360)区间,表示障碍物与飞机距离,单位为mm。 0对应机头方向正前方,顺时针分布,例如0度为机头正前方,90度为飞机正右方
|
||||
up_enable: boolean; // 上视避障开关状态,true:已开启 false:已关闭
|
||||
up_work: boolean; // 上视避障工作状态,true:正常工作 false:异常或离线
|
||||
down_enable: boolean; // 下视避障开关状态,true:已开启 false:已关闭
|
||||
down_work: boolean; // 下视避障工作状态,true:正常工作 false:异常或离线
|
||||
left_enable: boolean; // 左视避障开关状态,true:已开启 false:已关闭
|
||||
left_work: boolean; // 左视避障工作状态,true:正常工作 false:异常或离线
|
||||
right_enable: boolean; // 右视避障开关状态,true:已开启 false:已关闭
|
||||
right_work: boolean; // 右视避障工作状态,true:正常工作 false:异常或离线
|
||||
front_enable: boolean; // 前视避障开关状态,true:已开启 false:已关闭
|
||||
front_work: boolean; // 前视避障工作状态,true:正常工作 false:异常或离线
|
||||
back_enable: boolean; // 后视避障开关状态,true:已开启 false:已关闭
|
||||
back_work: boolean; // 后视避障工作状态,true:正常工作 false:异常或离线
|
||||
vertical_enable: boolean; // 垂直方向综合开关状态,当本协议中上、下视开关状态均为true时,输出true:已开启,否则输出false:已关闭
|
||||
vertical_work: boolean; // 垂直方向避障工作状态,当本协议中上、下视工作均为true时,输出true:正常工作,否则输出false:异常或离线
|
||||
horizontal_enable: boolean; // 水平方向综合开关状态,当本协议中前、后、左、右、开关状态均为true时,输出true:已开启,否则输出false:已关闭
|
||||
horizontal_work: boolean; // 水平方向避障工作综合状态,当本协议中前、后、左、右视工作均为true时,输出true:正常工作,否则输出false:异常或离线
|
||||
} |
||||
|
||||
export interface LiveViewDelayItem { |
||||
video_id: string; |
||||
liveview_delay_time: number; |
||||
} |
||||
|
||||
// 链路时延信息
|
||||
export interface DRCDelayTimeInfo { |
||||
sdr_cmd_delay: number; // sdr链路命令延时,单位:ms
|
||||
liveview_delay_list: LiveViewDelayItem[]; |
||||
} |
||||
|
||||
export interface DrcResponseInfo { |
||||
result: number; |
||||
output: { |
||||
seq: number |
||||
} |
||||
} |
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
import { ControlSource } from './device' |
||||
import { ECommanderModeLostAction, ERthMode, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone' |
||||
|
||||
export enum ControlSourceChangeType { |
||||
Flight = 1, |
||||
Payload = 2, |
||||
} |
||||
|
||||
// 控制权变化消息
|
||||
export interface ControlSourceChangeInfo { |
||||
sn: string, |
||||
type: ControlSourceChangeType, |
||||
control_source: ControlSource |
||||
} |
||||
|
||||
// 飞向目标点结果
|
||||
export interface FlyToPointMessage { |
||||
sn: string, |
||||
result: number, |
||||
message: string, |
||||
} |
||||
|
||||
// 一键起飞结果
|
||||
export interface TakeoffToPointMessage { |
||||
sn: string, |
||||
result: number, |
||||
message: string, |
||||
} |
||||
|
||||
// 设备端退出drc模式
|
||||
export interface DrcModeExitNotifyMessage { |
||||
sn: string, |
||||
result: number, |
||||
message: string, |
||||
} |
||||
|
||||
// 飞行控制模式状态
|
||||
export interface DrcStatusNotifyMessage { |
||||
sn: string, |
||||
result: number, |
||||
message: string, |
||||
} |
||||
|
||||
export const WaylineLostControlActionInCommandFlightOptions = [ |
||||
{ label: 'Continue', value: WaylineLostControlActionInCommandFlight.CONTINUE }, |
||||
{ label: 'Execute Lost Action', value: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION } |
||||
] |
||||
|
||||
export const LostControlActionInCommandFLightOptions = [ |
||||
{ label: 'Return Home', value: LostControlActionInCommandFLight.RETURN_HOME }, |
||||
{ label: 'Hover', value: LostControlActionInCommandFLight.HOVER }, |
||||
{ label: 'Landing', value: LostControlActionInCommandFLight.Land } |
||||
] |
||||
|
||||
export const RthModeInCommandFlightOptions = [ |
||||
{ label: 'Smart Height', value: ERthMode.SMART }, |
||||
{ label: 'Setting Height', value: ERthMode.SETTING } |
||||
] |
||||
|
||||
export const CommanderModeLostActionInCommandFlightOptions = [ |
||||
{ label: 'Continue', value: ECommanderModeLostAction.CONTINUE }, |
||||
{ label: 'Execute Lost Action', value: ECommanderModeLostAction.EXEC_LOST_ACTION } |
||||
] |
||||
|
||||
export const CommanderFlightModeInCommandFlightOptions = [ |
||||
{ label: 'Smart Height', value: ERthMode.SMART }, |
||||
{ label: 'Setting Height', value: ERthMode.SETTING } |
||||
] |
||||
|
||||
// 云台重置模式
|
||||
export enum GimbalResetMode { |
||||
Recenter = 0, |
||||
Down = 1, |
||||
RecenterGimbalPan = 2, |
||||
PitchDown = 3, |
||||
} |
||||
|
||||
export const GimbalResetModeOptions = [ |
||||
{ label: 'Gimbal Recenter', value: GimbalResetMode.Recenter }, |
||||
{ label: 'Gimbal down', value: GimbalResetMode.Down }, |
||||
{ label: 'Recenter Gimbal Pan', value: GimbalResetMode.RecenterGimbalPan }, |
||||
{ label: 'Gimbal Pitch Down', value: GimbalResetMode.PitchDown } |
||||
] |
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
import { GeojsonCoordinate, GeojsonPolygon } from '../utils/genjson' |
||||
|
||||
export enum EFlightAreaType { |
||||
NFZ = 'nfz', |
||||
DFENCE = 'dfence', |
||||
} |
||||
|
||||
export enum EGeometryType { |
||||
CIRCLE = 'Circle', |
||||
POLYGON = 'Polygon', |
||||
} |
||||
|
||||
export enum EFlightAreaUpdate { |
||||
ADD = 'add', |
||||
UPDATE = 'update', |
||||
DELETE = 'delete', |
||||
} |
||||
|
||||
export enum ESyncStatus { |
||||
WAIT_SYNC = 'wait_sync', |
||||
SWITCH_FAIL = 'switch_fail', |
||||
SYNCHRONIZING = 'synchronizing', |
||||
SYNCHRONIZED = 'synchronized', |
||||
FAIL = 'fail', |
||||
} |
||||
|
||||
export interface GeojsonCircle { |
||||
type: 'Feature' |
||||
properties: { |
||||
color: string |
||||
clampToGround?: boolean |
||||
} |
||||
geometry: { |
||||
type: EGeometryType.CIRCLE |
||||
coordinates: GeojsonCoordinate |
||||
radius: number |
||||
} |
||||
} |
||||
|
||||
export interface DroneLocation { |
||||
area_distance: number, |
||||
area_id: string, |
||||
is_in_area: boolean, |
||||
} |
||||
|
||||
export interface FlightAreasDroneLocation { |
||||
drone_locations: DroneLocation[] |
||||
} |
||||
|
||||
export type FlightAreaContent = GeojsonCircle | GeojsonPolygon |
||||
|
||||
export interface FlightAreaUpdate { |
||||
operation: EFlightAreaUpdate, |
||||
area_id: string, |
||||
name: string, |
||||
type: EFlightAreaType, |
||||
content: FlightAreaContent, |
||||
status: boolean, |
||||
username: string, |
||||
create_time: number, |
||||
update_time: number, |
||||
} |
||||
|
||||
export interface FlightAreaSyncProgress { |
||||
sn: string, |
||||
result: number, |
||||
status: ESyncStatus, |
||||
message: string, |
||||
} |
||||
|
||||
export const FlightAreaTypeTitleMap = { |
||||
[EFlightAreaType.NFZ]: { |
||||
[EGeometryType.CIRCLE]: 'Circular GEO Zone', |
||||
[EGeometryType.POLYGON]: 'Polygonal GEO Zone', |
||||
}, |
||||
[EFlightAreaType.DFENCE]: { |
||||
[EGeometryType.CIRCLE]: 'Circular Task Area', |
||||
[EGeometryType.POLYGON]: 'Polygonal Task Area', |
||||
}, |
||||
} |
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
import { commonColor } from '/@/utils/color' |
||||
|
||||
// 任务类型
|
||||
export enum TaskType { |
||||
Immediate = 0, // 立即执行
|
||||
Timed = 1, // 单次定时任务
|
||||
Condition = 2, |
||||
} |
||||
|
||||
export const TaskTypeMap = { |
||||
[TaskType.Immediate]: 'Immediate', |
||||
[TaskType.Timed]: 'Timed', |
||||
[TaskType.Condition]: 'Continuous', |
||||
} |
||||
|
||||
export const TaskTypeOptions = [ |
||||
{ value: TaskType.Immediate, label: TaskTypeMap[TaskType.Immediate] }, |
||||
{ value: TaskType.Timed, label: TaskTypeMap[TaskType.Timed] }, |
||||
{ value: TaskType.Condition, label: TaskTypeMap[TaskType.Condition] }, |
||||
] |
||||
|
||||
// 失控动作
|
||||
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, // 失败
|
||||
Paused = 6, // 暂停
|
||||
} |
||||
|
||||
export const TaskStatusMap = { |
||||
[TaskStatus.Wait]: 'To be performed', |
||||
[TaskStatus.Carrying]: 'In progress', |
||||
[TaskStatus.Success]: 'Task completed', |
||||
[TaskStatus.CanCel]: 'Task canceled', |
||||
[TaskStatus.Fail]: 'Task failed', |
||||
[TaskStatus.Paused]: 'Paused', |
||||
|
||||
} |
||||
|
||||
export const TaskStatusColor = { |
||||
[TaskStatus.Wait]: commonColor.BLUE, |
||||
[TaskStatus.Carrying]: commonColor.BLUE, |
||||
[TaskStatus.Success]: commonColor.NORMAL, |
||||
[TaskStatus.CanCel]: commonColor.FAIL, |
||||
[TaskStatus.Fail]: commonColor.FAIL, |
||||
[TaskStatus.Paused]: commonColor.BLUE, |
||||
} |
||||
|
||||
// 任务执行 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.Paused, |
||||
} |
||||
|
||||
// 根据媒体文件上传进度信息,前端自己判断出的状态
|
||||
export enum MediaStatus { // 媒体上传进度
|
||||
ToUpload = 1, // 待上传
|
||||
Uploading = 2, // 上传中
|
||||
Empty = 3, // 无媒体文件
|
||||
Success = 4, // 上传成功
|
||||
} |
||||
|
||||
export const MediaStatusMap = { |
||||
[MediaStatus.ToUpload]: 'Waiting to upload', |
||||
[MediaStatus.Uploading]: 'Uploading…', |
||||
[MediaStatus.Success]: 'Uploaded', |
||||
[MediaStatus.Empty]: 'No media files', |
||||
} |
||||
|
||||
export const MediaStatusColorMap = { |
||||
[MediaStatus.ToUpload]: commonColor.BLUE, |
||||
[MediaStatus.Uploading]: commonColor.BLUE, |
||||
[MediaStatus.Success]: commonColor.NORMAL, |
||||
[MediaStatus.Empty]: commonColor.WARN, |
||||
} |
||||
|
||||
// 媒体上传进度消息
|
||||
export interface MediaStatusProgressInfo { |
||||
job_id: string, |
||||
media_count: number |
||||
uploaded_count: number, |
||||
} |
||||
|
||||
// 媒体上传优先级消息
|
||||
export interface TaskMediaHighestPriorityProgressInfo { |
||||
pre_job_id: string, |
||||
job_id: string, |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue