This commit is contained in:
hehuan 2026-04-20 14:03:40 +08:00
commit e716efc587
191 changed files with 30620 additions and 0 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
/src/vendors/**

22
.eslintrc.js Normal file
View File

@ -0,0 +1,22 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
node: true
},
extends: ['standard', 'plugin:vue/vue3-essential'],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser'
},
plugins: ['vue', '@typescript-eslint'],
rules: {
'comma-dangle': 'off',
'import/no-absolute-path': 'off',
'no-unused-vars': 'off',
camelcase: 'off',
'no-redeclare': 'off',
'vue/no-unused-components': 'off'
}
}

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
node_modules/
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
/coverage
/backup
node_modules

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://registry.npmmirror.com/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 DJI-SDK
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# pilot-web

BIN
dist.zip Normal file

Binary file not shown.

2
env/.env vendored Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_ENVIRONMENT=DEV
VITE_APP_APIGATEWAY_BACKEND_HOST=''

2
env/.env.production vendored Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_ENVIRONMENT=production
VITE_APP_APIGATEWAY_BACKEND_HOST=''

2
env/.env.stag vendored Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_ENVIRONMENT=STAG
VITE_APP_APIGATEWAY_BACKEND_HOST=''

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>demo-web</title>
</head>
<body>
<div id="demo-app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

9857
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

117
package.json Normal file
View File

@ -0,0 +1,117 @@
{
"name": "demo-web",
"version": "0.0.1",
"scripts": {
"serve": "vite",
"build:test": "vite build --mode stag",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint --fix"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/icons-vue": "^6.0.1",
"@vitejs/plugin-legacy": "^1.6.2",
"agora-rtc-sdk-ng": "^4.12.1",
"ant-design-vue": "^2.2.8",
"axios": "^0.21.4",
"eventemitter3": "^5.0.0",
"mitt": "^3.0.0",
"mqtt": "^4.3.7",
"query-string": "^7.0.1",
"reconnecting-websocket": "^4.4.0",
"vconsole": "^3.8.1",
"vite-plugin-components": "^0.13.3",
"vite-plugin-importer": "^0.2.5",
"vite-plugin-optimize-persist": "^0.1.2",
"vite-plugin-package-config": "^0.1.1",
"vue": "^3.2.26",
"vue-cookies": "^1.7.4",
"vue-i18n": "^9.1.6",
"vue-router": "4",
"vuex": "^4.0.2"
},
"devDependencies": {
"@types/node": "^16.3.2",
"@types/urlencode": "^1.1.2",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.0.5",
"eslint": "^7.30.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.13.0",
"rollup-plugin-external-globals": "^0.6.1",
"sass": "^1.35.1",
"typescript": "^4.5.4",
"vite": "^2.4.0",
"vite-plugin-eslint": "^1.3.0",
"vite-plugin-style-import": "^1.0.1",
"vite-plugin-svg-icons": "^1.0.5",
"vite-plugin-vconsole": "^1.1.0",
"vue-tsc": "^0.0.24"
},
"license": "ISC",
"vite": {
"optimizeDeps": {
"include": [
"@amap/amap-jsapi-loader",
"@ant-design/icons-vue",
"@vue/reactivity",
"agora-rtc-sdk-ng",
"ant-design-vue",
"ant-design-vue/es",
"ant-design-vue/es/avatar/style/css",
"ant-design-vue/es/breadcrumb/style/css",
"ant-design-vue/es/button/style/css",
"ant-design-vue/es/checkbox/style/css",
"ant-design-vue/es/col/style/css",
"ant-design-vue/es/collapse/style/css",
"ant-design-vue/es/date-picker/style/css",
"ant-design-vue/es/divider/style/css",
"ant-design-vue/es/drawer/style/css",
"ant-design-vue/es/dropdown/style/css",
"ant-design-vue/es/empty/style/css",
"ant-design-vue/es/form/style/css",
"ant-design-vue/es/image/style/css",
"ant-design-vue/es/input-number/style/css",
"ant-design-vue/es/input/style/css",
"ant-design-vue/es/layout/style/css",
"ant-design-vue/es/menu/style/css",
"ant-design-vue/es/message/style/css",
"ant-design-vue/es/modal/style/css",
"ant-design-vue/es/pagination/style/css",
"ant-design-vue/es/popconfirm/style/css",
"ant-design-vue/es/popover/style/css",
"ant-design-vue/es/progress/style/css",
"ant-design-vue/es/radio/style/css",
"ant-design-vue/es/row/style/css",
"ant-design-vue/es/select/style/css",
"ant-design-vue/es/space/style/css",
"ant-design-vue/es/spin/style/css",
"ant-design-vue/es/switch/style/css",
"ant-design-vue/es/table/style/css",
"ant-design-vue/es/tag/style/css",
"ant-design-vue/es/time-picker/style/css",
"ant-design-vue/es/tooltip/style/css",
"ant-design-vue/es/tree/style/css",
"ant-design-vue/es/upload/style/css",
"axios",
"eventemitter3",
"lodash",
"mitt",
"moment",
"mqtt",
"mqtt/dist/mqtt.min",
"reconnecting-websocket",
"vconsole",
"vue",
"vue-router",
"vuex"
]
}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

42
src/App.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<div class="demo-app">
<router-view />
<!-- <div class="map-wrapper">
<GMap/>
</div> -->
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
import { useMyStore } from './store'
import GMap from '/@/components/GMap.vue'
export default defineComponent({
name: 'App',
components: { GMap },
setup () {
const store = useMyStore()
return {}
}
})
</script>
<style lang="scss" scoped>
.demo-app {
width: 100%;
height: 100%;
.map-wrapper {
height: 100%;
width: 100%;
}
}
</style>
<style lang="scss">
#demo-app {
width: 100%;
height: 100%
}
</style>

13
src/antd.ts Normal file
View File

@ -0,0 +1,13 @@
// import Icon from '@ant-design/icons-vue'
import * as antDesign from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import { App } from 'vue'
import svgIcon from '/@/components/svgIcon.vue'
export const antComponents = {
install (app: App): void {
app.use(antDesign)
// app.component('Icon', Icon)
app.component('svg-icon', svgIcon)
}
}

View File

@ -0,0 +1,23 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { DeviceCmd, DeviceCmdItemAction } from '/@/types/device-cmd'
const CMD_API_PREFIX = '/control/api/v1'
export interface SendCmdParams {
dock_sn: string, // 机场cn
device_cmd: DeviceCmd // 指令
}
export interface PostSendCmdBody {
action: DeviceCmdItemAction
}
/**
*
* @param params
* @returns
*/
// /control/api/v1/devices/{dock_sn}/jobs/{service_identifier}
export async function postSendCmd (params: SendCmdParams, body?: PostSendCmdBody): Promise<IWorkspaceResponse<{}>> {
const resp = await request.post(`${CMD_API_PREFIX}/devices/${params.dock_sn}/jobs/${params.device_cmd}`, body)
return resp.data
}

172
src/api/device-log/index.ts Normal file
View File

@ -0,0 +1,172 @@
import request, { IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
import { DeviceValue, DOMAIN } from '/@/types/device'
import { DeviceLogUploadStatusEnum } from '/@/types/device-log'
import { ELocalStorageKey } from '/@/types'
import { CURRENT_CONFIG } from '/@/api/http/config'
const MNG_API_PREFIX = '/manage/api/v1'
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
export interface GetDeviceUploadLogListParams {
device_sn: string,
page: number,
page_size: number,
begin_time?: number, // 开始时间
end_time?: number, // 结束时间
status?: DeviceLogUploadStatusEnum, // 日志上传状态
logs_information?: string // 搜索内容
}
export interface BriefDeviceInfo {
sn: string,
device_model: DeviceValue,
device_callsign: string
}
export interface DeviceLogProgressInfo{
device_sn: string,
device_model_domain: DOMAIN,
progress: number, // 进度
result: number, // 上传结果
upload_rate: number, // 上传速率
status: DeviceLogUploadStatusEnum // 上传状态
}
export interface DeviceLogItem {
boot_index: number, // 日志id
start_time: number, // 日志开始时间
end_time: number, // 日志结束时间
size: number // 日志大小
}
export interface DeviceLogFileInfo {
device_sn: string,
module: DOMAIN,
result: number,
object_key: string,
file_id: string,
list: DeviceLogItem[]
}
export interface DeviceLogFileListInfo {
files: DeviceLogFileInfo[]
}
export interface GetDeviceUploadLogListRsp {
logs_id: string, // 记录id
happen_time: string, // 发生时间
user_name: string, // 用户
logs_information: string, // 异常描述
create_time: string, // 上传时间
status:DeviceLogUploadStatusEnum, // 日志上传状态
device_topo:{ // 设备topo
hosts: BriefDeviceInfo[],
parents: BriefDeviceInfo[]
},
logs_progress: DeviceLogProgressInfo[], // 日志上传进度
device_logs: DeviceLogFileListInfo // 设备日志
}
/**
*
* @param params
* @returns
*/
export async function getDeviceUploadLogList (params: GetDeviceUploadLogListParams): Promise<IListWorkspaceResponse<GetDeviceUploadLogListRsp>> {
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${params.device_sn}/logs-uploaded`, {
params: params
})
return resp.data
}
export interface GetDeviceLogListParams{
device_sn: string,
domain: DOMAIN[]
}
/**
*
* @param params
* @returns
*/
export async function getDeviceLogList (params: GetDeviceLogListParams): Promise<IWorkspaceResponse<DeviceLogFileListInfo>> {
const domain = params.domain ? params.domain : []
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${params.device_sn}/logs`, {
params: {
domain_list: domain.join(',')
}
})
return resp.data
}
export interface UploadDeviceLogBody {
device_sn: string
happen_time: string // 发生时间
logs_information: string // 异常描述
files:{
list: DeviceLogItem[],
device_sn: string,
module: DOMAIN
}[]
}
/**
*
* @param body
* @returns
*/
export async function postDeviceUpgrade (body: UploadDeviceLogBody): Promise<IWorkspaceResponse<{}>> {
const resp = await request.post(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs`, body)
return resp.data
}
export type DeviceLogUploadAction = 'cancel'
export interface CancelDeviceLogUploadBody {
device_sn: string
status: DeviceLogUploadAction
module_list: DOMAIN[]
}
// 取消上传
export async function cancelDeviceLogUpload (body: CancelDeviceLogUploadBody): Promise<IWorkspaceResponse<{}>> {
const url = `${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs`
const result = await request.delete(url, {
data: body
})
return result.data
}
export interface DeleteDeviceLogUploadBody {
device_sn: string
logs_id: string
}
// 取消上传
export async function deleteDeviceLogUpload (body: DeleteDeviceLogUploadBody): Promise<IWorkspaceResponse<{}>> {
const url = `${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs/${body.logs_id}`
const result = await request.delete(url, {
data: body
})
return result.data
}
export interface GetUploadDeviceLogUrlParams{
logs_id: string,
file_id: string,
}
// export interface GetUploadDeviceLogRsp{
// url: string
// }
/**
* url
* @param params
* @returns
*/
export async function getUploadDeviceLogUrl (params: GetUploadDeviceLogUrlParams): Promise<IWorkspaceResponse<string>> {
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/logs/${params.logs_id}/url/${params.file_id}`)
return resp.data
}

View File

@ -0,0 +1,24 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { ELocalStorageKey } from '/@/types'
import { NightLightsStateEnum, DistanceLimitStatus, ObstacleAvoidance } from '/@/types/device-setting'
const MNG_API_PREFIX = '/manage/api/v1'
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
export interface PutDevicePropsBody {
night_lights_state?: NightLightsStateEnum;// 夜航灯开关
height_limit?: number;// 限高设置
distance_limit_status?: DistanceLimitStatus;// 限远开关
obstacle_avoidance?: ObstacleAvoidance;// 飞行器避障开关设置
}
/**
*
* @param params
* @returns
*/
// /manage/api/v1/devices/{{workspace_id}}/devices/{{device_sn}}/property
export async function putDeviceProps (deviceSn: string, body: PutDevicePropsBody): Promise<IWorkspaceResponse<{}>> {
const resp = await request.put(`${MNG_API_PREFIX}/devices/${workspaceId}/devices/${deviceSn}/property`, body)
return resp.data
}

View File

@ -0,0 +1,47 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { DeviceFirmwareTypeEnum } from '/@/types/device'
const MNG_API_PREFIX = '/manage/api/v1'
export interface GetDeviceUpgradeInfoParams {
device_name: string
}
export interface GetDeviceUpgradeInfoRsp {
device_name: string
product_version: string
release_note: string
released_time: string
}
/**
*
* @param params
* @returns
*/
export async function getDeviceUpgradeInfo (params: GetDeviceUpgradeInfoParams): Promise<IWorkspaceResponse<GetDeviceUpgradeInfoRsp[]>> {
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/firmware-release-notes/latest`, {
params: params
})
return resp.data
}
export interface UpgradeDeviceInfo {
device_name: string,
sn: string,
product_version: string,
firmware_upgrade_type: DeviceFirmwareTypeEnum // 1-普通升级2-一致性升级
}
export type DeviceUpgradeBody = UpgradeDeviceInfo[]
/**
*
* @param workspace_id
* @param body
* @returns
*/
export async function postDeviceUpgrade (workspace_id: string, body: DeviceUpgradeBody): Promise<IWorkspaceResponse<{}>> {
const resp = await request.post(`${MNG_API_PREFIX}/devices/${workspace_id}/devices/ota`, body)
return resp.data
}

58
src/api/drc.ts Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

42
src/api/http.ts Normal file
View File

@ -0,0 +1,42 @@
/**
*
* 1. axios
* 2.
*
* API
* 1. axios 实例: singleAxiosInstance
* 2. axios createAxiosInstance
* 3.允许外界进行定制: bindCommonRequestInterceptorsbindCommonResponseInterceptors
*/
import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
// 统一的 request 拦截器
export function bindCommonRequestInterceptors (instance: AxiosInstance): void {
instance.interceptors.request.use(config => {
return config
})
}
// Unified response interceptor
export function bindCommonResponseInterceptors (instance: AxiosInstance): void {
instance.interceptors.response.use(config => {
return config
}, err => {
return Promise.reject(err)
})
}
export function createAxiosInstance (config?: AxiosRequestConfig, commonInterceptorConf: { request?: boolean, response?: boolean } = {}): AxiosInstance {
const instance = Axios.create(config)
// Binding a unified interceptor, binding by default
commonInterceptorConf.request !== false && bindCommonRequestInterceptors(instance)
commonInterceptorConf.response !== false && bindCommonResponseInterceptors(instance)
return instance
}
const singleAxios = createAxiosInstance({}, { request: true, response: false })
export default singleAxios

48
src/api/http/config.ts Normal file
View File

@ -0,0 +1,48 @@
export const CURRENT_CONFIG = {
// license
appId: '159035', // You need to go to the development website to apply.
appKey: '5132f8002819503ee42bd86208a12f6', // You need to go to the development website to apply.
appLicense: 'SjRIEnTeouiw47DcEra+OLNcbqvpGn4Uep8zQi3DJGFsI/us1nEp5hhL/JJ7PfPHmMopXN+Q6daOgXIKqCKBaCX2ntvKfmgxjTAqwS2gu3AxCkBlqTgZZsFNtTRdo98tbMbo+EsOQu/EGWjlrk6yCzfqB2iSRWQiQsbwswdcrlo=', // You need to go to the development website to apply.
// appId: '159812', // You need to go to the development website to apply.
// appKey: 'db9f0fd8956539e2985ba9156027cfa', // You need to go to the development website to apply.
// appLicense: 'GL9Y/UGd0cYI5vCkQWTqIEQsgFWvkIYKqCBPjKYhe7JMObEVsF2ZRUt+emBppV5llg0AAWRJknXQJDSgL0H/qaECdGYZELAUzaDRtbJZHOM2NDlmZUMiJCI95a9KvgS8Bs/u0XYU6cKxCKqZr0uVyF7Dbgk4WgHb6SOD4Dv7Qd8=', // You need to go to the development website to apply.
// http
baseURL: '/', // 修改为根路径
// baseURL: 'http://192.168.0.90:12311/', // 修改为根路径
// baseURL: 'http://8.137.54.85:11501/', // 修改为根路径
// baseURL: 'http://220.167.122.36:12311/', // 修改为根路径
// baseURL: 'http://139.155.129.59:6789/', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
// baseURL: 'http://47.108.71.9:12311/', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
// websocketURL: 'ws://139.155.129.59:6789/api/v1/ws', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
// websocketURL: '/api/v1/ws', // 保持现有配置
websocketURL: '', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
// websocketURL: 'ws://192.168.31.197:8083/mqtt', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
// livestreaming
// RTMP Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
// rtmpURL: 'rtmp://47.108.71.9:1935/live/', // Example: 'rtmp://192.168.1.1/live/'
rtmpURL: '', // Example: 'rtmp://192.168.1.1/live/'
// GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
gbServerIp: 'Please enter the server ip.',
gbServerPort: 'Please enter the server port.',
gbServerId: 'Please enter the server id.',
gbAgentId: 'Please enter the agent id',
gbPassword: 'Please enter the agent password',
gbAgentPort: 'Please enter the local port.',
gbAgentChannel: 'Please enter the channel.',
// RTSP
rtspUserName: 'Please enter the username.',
rtspPassword: 'Please enter the password.',
rtspPort: '8554',
// Agora
agoraAPPID: 'Please enter the agora app id.',
agoraToken: 'Please enter the agora temporary token.',
agoraChannel: 'Please enter the agora channel.',
// map
// You can apply on the AMap website.
amapKey: 'Please enter the amap key.',
}

84
src/api/http/request.ts Normal file
View File

@ -0,0 +1,84 @@
import axios from 'axios'
import { uuidv4 } from '/@/utils/uuid'
import { CURRENT_CONFIG } from './config'
import { message } from 'ant-design-vue'
import router from '/@/router'
import { ELocalStorageKey, ERouterName, EUserType } from '/@/types/enums'
export * from './type'
const REQUEST_ID = 'X-Request-Id'
function getAuthToken () {
return localStorage.getItem(ELocalStorageKey.Token)
}
const instance = axios.create({
// withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
// timeout: 12000,
})
instance.interceptors.request.use(
config => {
config.headers[ELocalStorageKey.Token] = getAuthToken()
// config.headers[REQUEST_ID] = uuidv4()
config.baseURL = CURRENT_CONFIG.baseURL
return config
},
error => {
return Promise.reject(error)
},
)
instance.interceptors.response.use(
response => {
console.info('URL: ' + response.config.baseURL + response.config.url, '\nData: ', response.data, '\nResponse:', response)
/* if (response.data.code && response.data.code !== 1) {
console.error(response.data.message)
} */
return response
},
err => {
const requestId = err?.config?.headers && err?.config?.headers[REQUEST_ID]
if (requestId) {
console.info(REQUEST_ID, '', requestId)
}
console.info('url: ', err?.config?.url, `${err?.config?.method}\n>>>> err: `, err)
let description = '-'
if (err.response?.data && err.response.data.message) {
description = err.response.data.message
}
if (err.response?.data && err.response.data.result) {
description = err.response.data.result.message
}
// @See: https://github.com/axios/axios/issues/383
if (!err.response || !err.response.status) {
console.error('The network is abnormal, please check the backend service and try again')
return
}
if (err.response?.status !== 200) {
console.error(`ERROR_CODE: ${err.response?.status}`)
}
// if (err.response?.status === 403) {
// // window.location.href = '/'
// }
if (err.response?.status === 401) {
console.error(err.response)
const flag: number = Number(localStorage.getItem(ELocalStorageKey.Flag))
switch (flag) {
case EUserType.Web:
router.push(ERouterName.PROJECT)
break
case EUserType.Pilot:
router.push(ERouterName.PILOT)
break
}
}
return Promise.reject(err)
},
)
export default instance

38
src/api/http/type.ts Normal file
View File

@ -0,0 +1,38 @@
export interface IResult {
code: number;
message: string;
}
export interface IPage {
page: number;
total: number;
page_size: number;
}
export interface IListWorkspaceResponse<T> {
code: number;
message: string;
data: {
list: T[];
pagination: IPage;
};
}
// Workspace
export interface IWorkspaceResponse<T> {
code: number;
data: T;
message: string;
}
export type IStatus = 'WAITING' | 'DOING' | 'SUCCESS' | 'FAILED';
export interface CommonListResponse<T> extends IResult {
data: {
list: T[];
pagination: IPage;
};
}
export interface CommonResponse<T> extends IResult {
data: T
}

53
src/api/layer.ts Normal file
View File

@ -0,0 +1,53 @@
import { ELocalStorageKey } from '../types/enums'
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { mapLayers } from '/@/constants/mock-layers'
import { elementGroupsReq, PostElementsBody, PutElementsBody } from '/@/types/mapLayer'
const PREFIX = '/map/api/v1'
const workspace_id = localStorage.getItem(ELocalStorageKey.WorkspaceId)
type UnknownResponse = Promise<IWorkspaceResponse<unknown>>
// get elements group
// export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
// const url = `${PREFIX}/workspaces/${workspace_id}/element_groups`
// const result = await request.get(url, {
// params: {
// group_id: reqParams.groupId,
// is_distributed: reqParams.isDistributed
// },
// })
// return result.data
// }
export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
return mapLayers
}
// Get elements groups request
export const getElementGroupsReq = async (body: elementGroupsReq): Promise<IWorkspaceResponse<any>> => {
const url = `${PREFIX}/workspaces/` + workspace_id + '/element-groups'
const result = await request.get(url, body)
return result.data
}
// add element
export const postElementsReq = async (pid: string, body: PostElementsBody): Promise<IWorkspaceResponse<{ id: string }>> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/element-groups/${pid}/elements`
const result = await request.post(url, body)
return result.data
}
// Update map element request
export const updateElementsReq = async (id: string, body: PutElementsBody): Promise<IWorkspaceResponse<{ id: string }>> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/elements/${id}`
const result = await request.put(url, body)
return result.data
}
// Delete map element
export const deleteElementReq = async (id: string, body: {}): Promise<any> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/elements/${id}`
const result = await request.delete(url, body)
return result.data
}
// Delete layer elements
export const deleteLayerEleReq = async (id: string, body: {}): Promise<any> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/element-groups/${id}/elements`
const result = await request.delete(url, body)
return result.data
}

196
src/api/manage.ts Normal file
View File

@ -0,0 +1,196 @@
import { Firmware, FirmwareQueryParam, FirmwareUploadParam } from '/@/types/device-firmware'
import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
import { Device } from '/@/types/device'
const HTTP_PREFIX = '/manage/api/v1'
const HTTP_V1 = '/api/v1'
const HTTP_V2 = '/api/v2'
// login
export interface LoginBody {
username: string,
password: string,
flag: number,
}
export interface BindBody {
device_sn: string,
user_id: string,
workspace_id: string,
domain?: string
}
export interface HmsQueryBody {
sns: string[],
children_sn: string,
device_sn: string,
language: string,
level: number | string,
begin_time: number,
end_time: number,
message: string,
domain: number,
}
export const login = async function (body: LoginBody): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/login`
const result = await request.post(url, body)
return result.data
}
// Refresh Token
export const refreshToken = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/token/refresh`
const result = await request.post(url, body)
return result.data
}
// Get Platform Info
export const getPlatformInfo = async function (): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/current`
const result = await request.get(url)
return result.data
}
// Get User Info
export const getUserInfo = async function (): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/users/current`
const result = await request.get(url)
return result.data
}
// Get Device Topo
export const getDeviceTopo = async function (workspace_id: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices`
const result = await request.get(url)
return result.data
}
// Get Livestream Capacity
export const getLiveCapacity = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/capacity`
const result = await request.get(url, body)
return result.data
}
// Start Livestream
export const startLivestream = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/streams/start`
const result = await request.post(url, body)
return result.data
}
// Stop Livestream
export const stopLivestream = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/streams/stop`
const result = await request.post(url, body)
return result.data
}
// Update Quality
export const setLivestreamQuality = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/streams/update`
const result = await request.post(url, body)
return result.data
}
export const getAllUsersInfo = async function (wid: string, body: IPage): Promise<CommonListResponse<any>> {
const url = `${HTTP_PREFIX}/users/${wid}/users?&page=${body.page}&page_size=${body.page_size}`
const result = await request.get(url)
return result.data
}
export const updateUserInfo = async function (wid: string, user_id: string, body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/users/${wid}/users/${user_id}`
const result = await request.put(url, body)
return result.data
}
export const bindDevice = async function (body: BindBody): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${body.device_sn}/binding`
const result = await request.post(url, body)
return result.data
}
export const unbindDevice = async function (device_sn: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${device_sn}/unbinding`
const result = await request.delete(url)
return result.data
}
export const getDeviceBySn = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
const result = await request.get(url)
return result.data
}
export const getWayLineList = async function (workspace_id: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_V1}/wayline/api/v1/workspaces/${workspace_id}/waylines`
const result = await request.get(url)
return result.data
}
/**
*
* @param workspace_id
* @param body
* @param domain
* @returns
*/
export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: number): Promise<IListWorkspaceResponse<Device>> {
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}&domain=${domain}`
const result = await request.get(url)
return result.data
}
export const updateDevice = async function (body: {}, workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
const result = await request.put(url, body)
return result.data
}
export const getUnreadDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
const result = await request.get(url)
return result.data
}
export const updateDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
const result = await request.put(url)
return result.data
}
export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: string, pagination: IPage): Promise<IListWorkspaceResponse<any>> {
let url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms?page=${pagination.page}&page_size=${pagination.page_size}` +
`&level=${body.level ?? ''}&begin_time=${body.begin_time ?? ''}&end_time=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}`
body.sns.forEach((sn: string) => {
if (sn !== '') {
url = url.concat(`&device_sn=${sn}`)
}
})
const result = await request.get(url)
return result.data
}
export const changeLivestreamLens = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/streams/switch`
const result = await request.post(url, body)
return result.data
}
export const getFirmwares = async function (workspace_id: string, page: IPage, body: FirmwareQueryParam): Promise<IListWorkspaceResponse<Firmware>> {
const url = `${HTTP_PREFIX}/workspaces/${workspace_id}/firmwares?page=${page.page}&page_size=${page.page_size}` +
`&device_name=${body.device_name}&product_version=${body.product_version}&status=${body.firmware_status ?? ''}`
const result = await request.get(url)
return result.data
}
export const importFirmareFile = async function (workspaceId: string, param: FormData): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/firmwares/file/upload`
const result = await request.post(url, param)
return result.data
}
export const changeFirmareStatus = async function (workspaceId: string, firmwareId: string, param: {status: boolean}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/firmwares/${firmwareId}`
const result = await request.put(url, param)
return result.data
}

26
src/api/media.ts Normal file
View File

@ -0,0 +1,26 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/media/api/v1'
// Get Media Files
export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}`
const result = await request.get(url)
return result.data
}
// Download Media File
export const downloadMediaFile = async function (workspaceId: string, fileId: string): Promise<any> {
const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fileId}/url`
const result = await request.get(url, { responseType: 'blob' })
if (result.data.type === 'application/json') {
const reader = new FileReader()
reader.onload = function (e) {
const text = reader.result as string
const result = JSON.parse(text)
console.error(result.message)
}
reader.readAsText(result.data, 'utf-8')
} else {
return result.data
}
}

283
src/api/pilot-bridge.ts Normal file
View File

@ -0,0 +1,283 @@
import { message } from 'ant-design-vue'
import { EComponentName, EPhotoType, ERouterName } from '../types'
import { CURRENT_CONFIG } from './http/config'
import { EVideoPublishType, LiveStreamStatus } from '../types/live-stream'
import { getRoot } from '/@/root'
const root = getRoot()
export const components = new Map()
declare let window:any
interface JsResponse{
code:number,
message:string,
data:any
}
export interface ThingParam {
host: string,
username: string,
password: string,
connectCallback: string
}
export interface LiveshareParam {
videoPublishType: string, // video-on-demand、video-by-manual、video-demand-aux-manual
statusCallback: string
}
export interface MapParam {
userName: string,
elementPreName: string
}
export interface WsParam {
host: string,
token: string,
connectCallback: string
}
export interface ApiParam {
host: string,
token: string
}
export interface MediaParam {
autoUploadPhoto: boolean, // 是否自动上传图片, 非必需
autoUploadPhotoType: number, // 自动上传的照片类型0原图 1缩略图 非必需
autoUploadVideo: boolean // 是否自动上传视频, 非必需
}
function returnBool (response: string): boolean {
const res: JsResponse = JSON.parse(response)
const isError = errorHint(res)
if (JSON.stringify(res.data) !== '{}') {
return isError && res.data
}
return isError
}
function returnString (response: string): string {
const res: JsResponse = JSON.parse(response)
return errorHint(res) ? res.data : ''
}
function returnNumber (response: string): number {
const res: JsResponse = JSON.parse(response)
return errorHint(res) ? res.data : -1
}
function errorHint (response: JsResponse): boolean {
if (response.code !== 0) {
console.error(response.message)
console.error(response.message)
return false
}
return true
}
export default {
init (): Map<EComponentName, any> {
const thingParam: ThingParam = {
host: '',
connectCallback: '',
username: '',
password: ''
}
components.set(EComponentName.Thing, thingParam)
const liveshareParam: LiveshareParam = {
videoPublishType: EVideoPublishType.VideoDemandAuxManual,
statusCallback: 'liveStatusCallback'
}
components.set(EComponentName.Liveshare, liveshareParam)
const mapParam: MapParam = {
userName: '',
elementPreName: 'PILOT'
}
components.set(EComponentName.Map, mapParam)
const wsParam: WsParam = {
host: CURRENT_CONFIG.websocketURL,
token: '',
connectCallback: 'wsConnectCallback'
}
components.set(EComponentName.Ws, wsParam)
const apiParam: ApiParam = {
host: '',
token: ''
}
components.set(EComponentName.Api, apiParam)
components.set(EComponentName.Tsa, {})
const mediaParam: MediaParam = {
autoUploadPhoto: true,
autoUploadPhotoType: EPhotoType.Preview,
autoUploadVideo: true
}
components.set(EComponentName.Media, mediaParam)
components.set(EComponentName.Mission, {})
return components
},
getComponentParam (key:EComponentName): any {
return components.get(key)
},
setComponentParam (key:EComponentName, value:any) {
components.set(key, value)
},
loadComponent (name:string, param:any):string {
return returnString(window.djiBridge.platformLoadComponent(name, JSON.stringify(param)))
},
unloadComponent (name:string) :string {
return returnString(window.djiBridge.platformUnloadComponent(name))
},
isComponentLoaded (module:string): boolean {
return returnBool(window.djiBridge.platformIsComponentLoaded(module))
},
setWorkspaceId (uuid:string):string {
return returnString(window.djiBridge.platformSetWorkspaceId(uuid))
},
setPlatformMessage (platformName:string, title:string, desc:string): boolean {
return returnBool(window.djiBridge.platformSetInformation(platformName, title, desc))
},
getRemoteControllerSN () :string {
return returnString(window.djiBridge.platformGetRemoteControllerSN())
},
getAircraftSN ():string {
return returnString(window.djiBridge.platformGetAircraftSN())
},
stopwebview ():string {
return returnString(window.djiBridge.platformStopSelf())
},
setLogEncryptKey (key:string):string {
return window.djiBridge.platformSetLogEncryptKey(key)
},
clearLogEncryptKey ():string {
return window.djiBridge.platformClearLogEncryptKey()
},
getLogPath ():string {
return returnString(window.djiBridge.platformGetLogPath())
},
platformVerifyLicense (appId:string, appKey:string, appLicense:string): boolean {
return returnBool(window.djiBridge.platformVerifyLicense(appId, appKey, appLicense))
},
isPlatformVerifySuccess (): boolean {
return returnBool(window.djiBridge.platformIsVerified())
},
isAppInstalled (pkgName: string): boolean {
return returnBool(window.djiBridge.platformIsAppInstalled(pkgName))
},
getVersion (): string {
return window.djiBridge.platformGetVersion()
},
// thing
thingGetConnectState (): boolean {
return returnBool(window.djiBridge.thingGetConnectState())
},
thingGetConfigs (): ThingParam {
const thingParam = JSON.parse(window.djiBridge.thingGetConfigs())
return thingParam.code === 0 ? JSON.parse(thingParam.data) : {}
},
// api
getToken () : string {
return returnString(window.djiBridge.apiGetToken())
},
setToken (token:string):string {
return returnString(window.djiBridge.apiSetToken(token))
},
getHost (): string {
return returnString(window.djiBridge.apiGetHost())
},
// liveshare
/**
*
* @param type
* video-on-demand: 服务器点播thing模块
* video-by-manual
* video-demand-aux-manual: 混合模式
*/
setVideoPublishType (type:string): boolean {
return returnBool(window.djiBridge.liveshareSetVideoPublishType(type))
},
/**
*
* @returns
* type: liveshare type 0unknown, 1:agora, 2:rtmp, 3:rtsp, 4:gb28181
*/
getLiveshareConfig (): string {
return returnString(window.djiBridge.liveshareGetConfig())
},
setLiveshareConfig (type:number, params:string):string {
return window.djiBridge.liveshareSetConfig(type, params)
},
setLiveshareStatusCallback (callbackFunc:string) :string {
return window.djiBridge.liveshareSetStatusCallback(callbackFunc)
},
getLiveshareStatus (): LiveStreamStatus {
return JSON.parse(JSON.parse(window.djiBridge.liveshareGetStatus()).data)
},
startLiveshare (): boolean {
return returnBool(window.djiBridge.liveshareStartLive())
},
stopLiveshare (): boolean {
return returnBool(window.djiBridge.liveshareStopLive())
},
// WebSocket
wsGetConnectState (): boolean {
return returnBool(window.djiBridge.wsGetConnectState())
},
wsConnect (host: string, token: string, callback: string): string {
return window.djiBridge.wsConnect(host, token, callback)
},
wsDisconnect (): string {
return window.djiBridge.wsConnect()
},
wsSend (message: string): string {
return window.djiBridge.wsSend(message)
},
// media
setAutoUploadPhoto (auto:boolean):string {
return window.djiBridge.mediaSetAutoUploadPhoto(auto)
},
getAutoUploadPhoto (): boolean {
return returnBool(window.djiBridge.mediaGetAutoUploadPhoto())
},
setUploadPhotoType (type:number):string {
return window.djiBridge.mediaSetUploadPhotoType(type)
},
getUploadPhotoType (): number {
return returnNumber(window.djiBridge.mediaGetUploadPhotoType())
},
setAutoUploadVideo (auto:boolean):string {
return window.djiBridge.mediaSetAutoUploadVideo(auto)
},
getAutoUploadVideo (): boolean {
return returnBool(window.djiBridge.mediaGetAutoUploadVideo())
},
setDownloadOwner (rcIndex:number):string {
return window.djiBridge.mediaSetDownloadOwner(rcIndex)
},
getDownloadOwner (): number {
return returnNumber(window.djiBridge.mediaGetDownloadOwner())
},
onBackClickReg () {
window.djiBridge.onBackClick = () => {
if (root.$router.currentRoute.value.path === '/' + ERouterName.PILOT_HOME) {
return false
} else {
history.go(-1)
return true
}
}
},
onStopPlatform () {
window.djiBridge.onStopPlatform = () => {
localStorage.clear()
}
}
}

139
src/api/wayline.ts Normal file
View File

@ -0,0 +1,139 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
import { TaskType, TaskStatus, OutOfControlAction } from '/@/types/task'
import { WaylineType } from '/@/types/wayline'
const HTTP_PREFIX = '/wayline/api/v1'
// Get Wayline Files
export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?order_by=${body.order_by}&page=${body.page}&page_size=${body.page_size}`
const result = await request.get(url)
return result.data
}
// Download Wayline File
export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url`
const result = await request.get(url, { responseType: 'blob' })
if (result.data.type === 'application/json') {
const reader = new FileReader()
reader.onload = function (e) {
const text = reader.result as string
const result = JSON.parse(text)
console.error(result.message)
}
reader.readAsText(result.data, 'utf-8')
} else {
return result.data
}
}
// Delete Wayline File
export const deleteWaylineFile = async function (workspaceId: string, waylineId: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}`
const result = await request.delete(url)
return result.data
}
export interface CreatePlan {
name: string,
file_id: string,
dock_sn: string,
task_type: TaskType, // 任务类型
wayline_type: WaylineType, // 航线类型
task_days: number[] // 执行任务的日期(秒)
task_periods: number[][] // 执行任务的时间点(秒)
rth_altitude: number // 相对机场返航高度 20 - 500
out_of_control_action: OutOfControlAction // 失控动作
min_battery_capacity?: number, // The minimum battery capacity of aircraft.
min_storage_capacity?: number, // The minimum storage capacity of dock and aircraft.
}
// Create Wayline Job
export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks`
const result = await request.post(url, plan)
return result.data
}
export interface Task {
job_id: string,
job_name: string,
task_type: TaskType, // 任务类型
file_id: string, // 航线文件id
file_name: string, // 航线名称
wayline_type: WaylineType, // 航线类型
dock_sn: string,
dock_name: string,
workspace_id: string,
username: string,
begin_time: string,
end_time: string,
execute_time: string,
completed_time: string,
status: TaskStatus, // 任务状态
progress: number, // 执行进度
code: number, // 错误码
rth_altitude: number // 相对机场返航高度 20 - 500
out_of_control_action: OutOfControlAction // 失控动作
media_count: number // 媒体数量
uploading:boolean // 是否正在上传媒体
uploaded_count: number // 已上传媒体数量
}
// Get Wayline Jobs
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IListWorkspaceResponse<Task>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}`
const result = await request.get(url)
return result.data
}
export interface DeleteTaskParams {
job_id: string
}
// 删除机场任务
export async function deleteTask (workspaceId: string, params: DeleteTaskParams): Promise<IWorkspaceResponse<{}>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs`
const result = await request.delete(url, {
params: params
})
return result.data
}
export enum UpdateTaskStatus {
Suspend = 0, // 暂停
Resume = 1, // 恢复
}
export interface UpdateTaskStatusBody {
job_id: string
status: UpdateTaskStatus
}
// 更新机场任务状态
export async function updateTaskStatus (workspaceId: string, body: UpdateTaskStatusBody): Promise<IWorkspaceResponse<{}>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${body.job_id}`
const result = await request.put(url, {
status: body.status
})
return result.data
}
// Upload Wayline file
export const importKmzFile = async function (workspaceId: string, file: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/file/upload`
const result = await request.post(url, file, {
headers: {
'Content-Type': 'multipart/form-data',
}
})
return result.data
}
// 媒体立即上传
export const uploadMediaFileNow = async function (workspaceId: string, jobId: string): Promise<IWorkspaceResponse<{}>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${jobId}/media-highest`
const result = await request.post(url)
return result.data
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>ic/panel/checkbox_active</title>
<desc>Created with Sketch.</desc>
<g id="ic/panel/checkbox_active" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-3" fill="#FFFFFF">
<polygon id="Shape-Copy" points="11.2103387 4.23529412 6.47425817 8.97137463 4.50089127 6.99800776 2.82352941 8.65563591 4.79689632 10.6290028 6.37558979 12.2076964 6.47425817 12.286631 12.8877005 5.8929223"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="layer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 652 652" style="enable-background:new 0 0 652 652;" xml:space="preserve">
<style type="text/css">
.st0{fill:#241F1F;}
</style>
<g>
<path class="st0" d="M464.9,404.7l44.3-183.9h-95l-40.5,164.9c-5.9,32.2-40.4,47.2-64.9,47.6h-67.3l-22.9,66.3h141.5
C395,499.6,446.5,481.8,464.9,404.7"/>
<path class="st0" d="M265.5,339.9L310.1,153h97.8l-50.8,212.6c-9.8,41.1-40.3,50.9-68.5,50.9H63.2c-24.8,0-45.6-10.6-34.4-58
l20.3-84.8c10.3-43,42.3-52.9,65.4-52.9h157.3l-12.7,53h-80.3c-11.8,0-18.3,2.6-21.6,16.3l-13,54.1c-4.6,19.4,2.2,20.8,16.4,20.8
h73.6C247.8,365.2,259.6,364.3,265.5,339.9"/>
<polygon class="st0" points="530.7,220.9 484.6,416.5 579.7,416.5 625.7,220.9 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
src/assets/icons/dock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/assets/icons/drone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs><path d="m14.042 0 14.043 8.055-5.128 2.941 5.128 2.941-5.127 2.94 5.127 2.942-14.043 8.055L0 19.82l5.126-2.942L0 13.937l5.127-2.941L0 8.056 14.042 0zM7.638 18.318l-2.614 1.5 9.018 5.175 9.017-5.174-2.614-1.5-6.403 3.674-6.404-3.675zm0-5.882-2.615 1.5 9.02 5.175 9.017-5.173-2.615-1.501-6.403 3.674-6.404-3.675zm6.404-9.554L5.024 8.056l9.018 5.174 9.017-5.174-9.017-5.174z" id="layer_a"></path></defs><g transform="translate(2 2)" fill-rule="evenodd"><mask id="layer_b"><use xlink:href="#layer_a"></use></mask><use fill-rule="nonzero" xlink:href="#layer_a"></use><g mask="url(#layer_b)"><path d="M-19-20h68v68h-68z"></path></g></g>
</svg>

After

Width:  |  Height:  |  Size: 830 B

BIN
src/assets/icons/m30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs><path d="M29.5 4v21.5h-4v2H3V8h3V4h23.5zM10.553 18.859 5.5 22.399V25H23v-6.088l-7.011 5.108-5.436-5.161zM27 6.5H8.499L8.5 8h17v15H27V6.5zm-4 4H5.5v8.847l5.293-3.707 5.406 5.133L23 15.819V10.5zm-6.362 1.956a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" id="media_a"></path></defs><g fill-rule="evenodd"><mask id="media_b"><use xlink:href="#media_a"></use></mask><use fill-rule="nonzero" xlink:href="#media_a"></use><g mask="url(#media_b)"><path d="M-17-18h68v68h-68z"></path></g></g>
</svg>

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#19BE6B" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#19BE6B" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#19BE6B">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#212121" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#212121" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#212121">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#2D8CF0" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#2D8CF0" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#2D8CF0">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#b620e0" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#b620e0" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#b620e0">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#e23c39" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#e23c39" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#e23c39">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#FFBB00" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#FFBB00" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#FFBB00">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/icons/rc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

14
src/assets/icons/tsa.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="m19.87 10.131-1.01 2.518-.86-.012v-.006l-7.045.002-.068.004a1.75 1.75 0 0 0-1.614 1.737l.006.144.656 8.388h4.804l-1.005 2.5h-6.11l-.168-2.137H.544l-.53-6.896a4.25 4.25 0 0 1 4.03-4.598l.206-.005h2.48a4.25 4.25 0 0 1 .873.09 4.238 4.238 0 0 1 3.089-1.716l.166-.01.166-.003h8.846zm1.106 1.7 5.476 13.566-5.454-1.781-5.497 1.78 5.475-13.565zm.008 4.028.011 6.18H21l2.854.931-2.87-7.111zm-14.21-1.588L4.25 14.27a1.75 1.75 0 0 0-1.75 1.75l.007.153.352 4.597 4.401-.001-.473-6.056a4.3 4.3 0 0 1-.012-.442zM6.193 3.748a3.25 3.25 0 1 1 0 6.5 3.25 3.25 0 0 1 0-6.5zM14.3 0a4.25 4.25 0 1 1 0 8.5 4.25 4.25 0 0 1 0-8.5zM6.192 5.748a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5zM14.3 2a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5z" id="team_a">
</path>
</defs>
<g transform="translate(3 3)" fill-rule="evenodd">
<mask id="team_b">
<use xlink:href="#team_a"></use>
</mask>
<use fill-rule="nonzero" xlink:href="#team_a"></use>
<g mask="url(#team_b)"><path d="M-20-21h68v68h-68z"></path>
</g></g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1014
src/components/GMap.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
<template>
<span>
<a-tree
draggable
:defaultExpandAll="true"
class="device-map-layers"
@drop="onDrop"
v-bind="$attrs"
>
<a-tree-node
:title="layer.name"
:id="layer.id"
v-for="layer in getTreeData"
:key="layer.id"
>
<!-- <template #title>
{{layer.name}}
</template> -->
<template v-if="layer.elements">
<a-tree-node
v-for="resource in layer.elements"
:id="getLayerTreeKey('resource', resource.id)"
:key="getLayerTreeKey('resource', resource.id)"
>
<template #title>
{{ resource.name }}
</template>
</a-tree-node>
</template>
</a-tree-node>
</a-tree>
</span>
</template>
<script lang="ts" setup>
import { computed, defineProps, PropType, reactive } from 'vue'
import { useMyStore } from '/@/store'
import { DropEvent, mapLayer } from '/@/types/mapLayer'
import { getLayerTreeKey } from '/@/utils/layer-tree'
const store = useMyStore()
const props = defineProps({
layerData: Array as PropType<mapLayer[]>
})
const state = reactive({
checkedKeys: [] as string[],
expandedKeys: [] as string[]
})
const getTreeData = computed(() => {
// console.log('props.treeData', JSON.parse(JSON.stringify(props.layerData)))
return JSON.parse(JSON.stringify(props.layerData))
})
const shareId = computed(() => {
return store.state.layerBaseInfo.share
})
const defaultId = computed(() => {
return store.state.layerBaseInfo.default
})
async function onDrop ({ node, dragNode, dropPosition, dropToGap }: DropEvent) {
let _treeData = props.layerData || []
let dragKey = dragNode.eventKey
dragKey = dragKey.replaceAll('resource__', '')
const dropPos = node.pos.split('-')
let dropKey =
node.eventKey.includes(shareId.value) ||
node.eventKey.includes(defaultId.value)
? node.eventKey
: node.$parent.eventKey
if (!dragKey || !dropKey) return
dropKey = dropKey.replaceAll('resource__', '')
const loop = (data: mapLayer[], key: string, callback: Function) => {
data.forEach((item, index, arr) => {
if (item.id === key) {
return callback(item, index, arr)
}
if (item.elements) {
return loop(item.elements, key, callback)
}
})
}
const data = [..._treeData] as mapLayer[]
// Find dragObject
let dragObj = {} as mapLayer
loop(data, dragKey, (item: mapLayer, index: number, arr: mapLayer[]) => {
arr.splice(index, 1)
dragObj = item
})
if (!dropToGap) {
// Drop on the content
loop(data, dropKey, (item: mapLayer) => {
item.elements = item.elements || []
// where to insert
item.elements.push(dragObj)
})
}
_treeData = data
// console.log('_treeData', _treeData)
}
</script>
<style lang="scss">
$antPrefix: 'ant';
.device-map-layers.#{$antPrefix}-tree {
color: #fff;
.#{$antPrefix}-tree-checkbox:not(.#{$antPrefix}-tree-checkbox-checked)
.#{$antPrefix}-tree-checkbox-inner {
background-color: unset;
}
.anticon {
font-size: 16px;
}
// li 16px
> li {
padding-left: 16px;
padding-right: 16px;
}
li {
display: flex;
flex-wrap: wrap;
align-items: center;
padding-top: 0;
padding-bottom: 0;
&:first-child {
padding-top: 4px;
}
&.#{$antPrefix}-tree-treenode-disabled
> .#{$antPrefix}-tree-node-content-wrapper {
height: 20px;
span {
color: #fff;
}
}
> ul {
width: 100%;
}
.#{$antPrefix}-tree-switcher {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.#{$antPrefix}-tree-checkbox {
z-index: 1;
}
.#{$antPrefix}-tree-checkbox:hover::after,
.#{$antPrefix}-tree-checkbox-wrapper:hover
.#{$antPrefix}-tree-checkbox::after {
visibility: collapse;
}
.#{$antPrefix}-tree-title {
display: block;
}
.#{$antPrefix}-tree-node-content-wrapper {
color: #fff;
width: calc(100% - 46px);
flex: 1;
box-sizing: content-box;
height: 20px;
min-width: 0; //
padding-right: 0;
&:not([draggable='true']) {
border-top: 2px transparent solid;
border-bottom: 2px transparent solid;
}
&:hover {
background-color: transparent;
}
> span {
&::before {
// position: absolute;
// right: 0;
// left: 0;
height: 28px;
transition: all 0.3s;
content: '';
}
// positionrelative
> *:not(.progress-wrapper) {
position: relative;
z-index: 1;
}
}
&.#{$antPrefix}-tree-node-selected {
background-color: transparent;
color: #2d8cf0;
> span {
&::before {
background-color: #4f4f4f;
}
}
}
}
}
span.#{$antPrefix}-tree-switcher.#{$antPrefix}-tree-switcher_open
.#{$antPrefix}-tree-switcher-icon {
transform: rotate(0deg) !important;
}
span.#{$antPrefix}-tree-switcher.#{$antPrefix}-tree-switcher_close
.#{$antPrefix}-tree-switcher-icon {
transform: rotate(0deg) !important;
}
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<div class="header">Media Files</div>
<a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
<div class="media-panel-wrapper">
<a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint"
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
<template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col">
<a-tooltip :title="text">
<a v-if="col === 'name'">{{ text }}</a>
<span v-else>{{ text }}</span>
</a-tooltip>
</template>
<template #original="{ text }">
{{ text }}
</template>
<template #action="{ record }">
<a-tooltip title="download">
<a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a>
</a-tooltip>
</template>
</a-table>
</div>
</a-spin>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { TableState } from 'ant-design-vue/lib/table/interface'
import { onMounted, reactive } from 'vue'
import { IPage } from '../api/http/type'
import { ELocalStorageKey } from '../types/enums'
import { downloadFile } from '../utils/common'
import { downloadMediaFile, getMediaFiles } from '/@/api/media'
import { DownloadOutlined } from '@ant-design/icons-vue'
import { message, Pagination } from 'ant-design-vue'
import { load } from '@amap/amap-jsapi-loader'
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const loading = ref(false)
const columns = [
{
title: 'File Name',
dataIndex: 'file_name',
ellipsis: true,
slots: { customRender: 'name' }
},
{
title: 'File Path',
dataIndex: 'file_path',
ellipsis: true,
slots: { customRender: 'path' }
},
// {
// title: 'FileSize',
// dataIndex: 'size',
// },
{
title: 'Drone',
dataIndex: 'drone'
},
{
title: 'Payload Type',
dataIndex: 'payload'
},
{
title: 'Original',
dataIndex: 'is_original',
slots: { customRender: 'original' }
},
{
title: 'Created',
dataIndex: 'create_time'
},
{
title: 'Action',
slots: { customRender: 'action' }
}
]
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
})
type Pagination = TableState['pagination']
interface MediaFile {
fingerprint: string,
drone: string,
payload: string,
is_original: string,
file_name: string,
file_path: string,
create_time: string,
file_id: string,
}
const mediaData = reactive({
data: [] as MediaFile[]
})
onMounted(() => {
getFiles()
})
function getFiles () {
getMediaFiles(workspaceId, body).then(res => {
mediaData.data = res.data.list
paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page
console.info(mediaData.data[0])
})
}
function refreshData (page: Pagination) {
body.page = page?.current!
body.page_size = page?.pageSize!
getFiles()
}
function downloadMedia (media: MediaFile) {
loading.value = true
downloadMediaFile(workspaceId, media.file_id).then(res => {
if (!res) {
return
}
const data = new Blob([res])
downloadFile(data, media.file_name)
}).finally(() => {
loading.value = false
})
}
</script>
<style lang="scss" scoped>
.media-panel-wrapper {
width: 100%;
padding: 16px;
.media-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>

View File

@ -0,0 +1,124 @@
<template>
<div class="demo-project-sidebar-wrapper flex-justify-between">
<div>
<router-link
v-for="item in options"
:key="item.key"
:to="item.path"
:class="{
'menu-item': true,
selected: selectedRoute(item),
}"
>
<a-tooltip :title="item.label" placement="right">
<Icon class="fz20" style="width: 50px;" :icon="item.icon"/>
</a-tooltip>
</router-link>
</div>
<div class="mb20 flex-display flex-column flex-align-center flex-justify-between">
<a-tooltip title="Back to home" placement="right">
<a @click="goHome"> <Icon icon="ImportOutlined" style="font-size: 22px; color: white"/></a>
</a-tooltip>
</div>
</div>
</template>
<script lang="ts">
import { createVNode, defineComponent } from 'vue'
import { getRoot } from '/@/root'
import * as icons from '@ant-design/icons-vue'
import { ERouterName } from '/@/types'
interface IOptions {
key: number
label: string
path:
| string
| {
path: string
query?: any
}
icon: string
}
const Icon = (props: {icon: string}) => {
return createVNode((icons as any)[props.icon])
}
export default defineComponent({
components: {
Icon,
},
name: 'Sidebar',
setup () {
const root = getRoot()
const options = [
{ key: 0, label: 'Tsa', path: '/' + ERouterName.TSA, icon: 'TeamOutlined' },
{ key: 1, label: 'Livestream', path: '/' + ERouterName.LIVESTREAM, icon: 'VideoCameraOutlined' },
{ key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
{ key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' },
{ key: 4, label: 'Flight Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
{ key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' },
{ key: 6, label: 'Flight Area', path: '/' + ERouterName.FLIGHT_AREA, icon: 'GroupOutlined' },
]
function selectedRoute (item: IOptions) {
const path = typeof item.path === 'string' ? item.path : item.path.path
return root.$route.path?.indexOf(path) === 0
}
function goHome () {
root.$router.push('/' + ERouterName.MEMBERS)
}
return {
options,
selectedRoute,
goHome,
}
}
})
</script>
<style scoped lang="scss">
.demo-project-sidebar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 50px;
border-right: 1px solid #4f4f4f;
color: $text-white-basic;
// flex: 1;
overflow: hidden;
.menu-item {
width: 100%;
padding: 16px 0px;
display: flex;
flex-direction: column;
align-items: center;
color: $text-white-basic;
cursor: pointer;
&.selected {
background-color: #101010;
color: $primary;
}
&.disabled {
pointer-events: none;
opacity: 0.45;
}
}
.filling {
flex: 1;
}
.setting-icon {
font-size: 24px;
margin-bottom: 24px;
color: $text-white-basic;
}
}
.ant-tooltip-open {
border: 0;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="width-100 flex-row flex-justify-between flex-align-center" style="height: 60px;">
<div class="height-100">
<a-avatar :size="40" shape="square" :src="cloudapi" />
<span class="ml10 fontBold">{{ workspaceName }}</span>
</div>
<a-space class="fz16 height-100" size="large">
<router-link
v-for="item in options"
:key="item.key"
:to="item.path"
:class="{
'menu-item': true,
}">
<span @click="selectedRoute(item.path)" :style="selected === item.path ? 'color: #2d8cf0;' : 'color: white'">{{ item.label }}</span>
</router-link>
</a-space>
<div class="height-100 fz16 flex-row flex-justify-between flex-align-center">
<a-dropdown>
<div class="height-100">
<span class="fz20 mt20" style="border: 2px solid white; border-radius: 50%; display: inline-flex;"><UserOutlined /></span>
<span class="ml10 mr10" style="float: right;">{{ username }}</span>
</div>
<template #overlay>
<a-menu theme="dark" class="flex-column flex-justify-between flex-align-center">
<a-menu-item>
<span class="mr10" style="font-size: 16px;"><ExportOutlined /></span>
<span @click="logout">Log Out</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { defineComponent, onMounted, ref } from 'vue'
import { getRoot } from '/@/root'
import { getPlatformInfo } from '/@/api/manage'
import { ELocalStorageKey, ERouterName } from '/@/types'
import { UserOutlined, ExportOutlined } from '@ant-design/icons-vue'
import cloudapi from '/@/assets/icons/cloudapi.png'
const root = getRoot()
interface IOptions {
key: number
label: string
path:
| string
| {
path: string
query?: any
}
icon: string
}
const username = ref(localStorage.getItem(ELocalStorageKey.Username))
const workspaceName = ref('')
const options = [
{ key: 0, label: ERouterName.WORKSPACE.charAt(0).toUpperCase() + ERouterName.WORKSPACE.substr(1), path: '/' + ERouterName.WORKSPACE },
{ key: 1, label: ERouterName.MEMBERS.charAt(0).toUpperCase() + ERouterName.MEMBERS.substr(1), path: '/' + ERouterName.MEMBERS },
{ key: 2, label: ERouterName.DEVICES.charAt(0).toUpperCase() + ERouterName.DEVICES.substr(1), path: '/' + ERouterName.DEVICES },
{ key: 3, label: ERouterName.FIRMWARES.charAt(0).toUpperCase() + ERouterName.FIRMWARES.substr(1), path: '/' + ERouterName.FIRMWARES },
]
const selected = ref<string>(root.$route.path)
onMounted(() => {
/* getPlatformInfo().then(res => {
workspaceName.value = res.data.workspace_name
}) */
})
function selectedRoute (path: string) {
selected.value = path
}
const logout = () => {
localStorage.clear()
root.$router.push(ERouterName.PROJECT)
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.fontBold {
font-weight: 500;
font-size: 18px;
}
</style>

View File

@ -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 === 1) {
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>

View File

@ -0,0 +1,274 @@
<template>
<a-drawer
title="Hms Info"
placement="right"
v-model:visible="sVisible"
@update:visible="onVisibleChange"
:destroyOnClose="true"
:width="800">
<div class="flex-row flex-align-center">
<div style="width: 240px;">
<a-range-picker
v-model:value="time"
format="YYYY-MM-DD"
:placeholder="['Start Time', 'End Time']"
@change="onTimeChange"/>
</div>
<div class="ml5">
<a-select
style="width: 150px"
v-model:value="param.level"
@select="onLevelSelect">
<a-select-option
v-for="item in levels"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="ml5">
<a-select
v-model:value="param.domain"
:disabled="!param.children_sn || !param.device_sn"
style="width: 150px"
@select="onDeviceTypeSelect">
<a-select-option
v-for="item in deviceTypes"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="ml5">
<a-input-search
v-model:value="param.message"
placeholder="input search message"
style="width: 200px"
@search="getHms"/>
</div>
</div>
<div>
<a-table :columns="hmsColumns" :scroll="{ x: '100%', y: 600 }" :data-source="hmsData.data" :pagination="hmsPaginationProp" @change="refreshHmsData" row-key="hms_id"
:rowClassName="rowClassName" :loading="loading">
<template #time="{ record }">
<div>{{ record.create_time }}</div>
<div :style="record.update_time ? '' : record.level === EHmsLevel.CAUTION ? 'color: orange;' :
record.level === EHmsLevel.WARN ? 'color: red;' : 'color: #28d445;'">{{ record.update_time ?? 'It is happening...' }}</div>
</template>
<template #level="{ text }">
<div class="flex-row flex-align-center">
<div :class="text === EHmsLevel.CAUTION ? 'caution' : text === EHmsLevel.WARN ? 'warn' : 'notice'" style="width: 10px; height: 10px; border-radius: 50%;"></div>
<div style="margin-left: 3px;">{{ EHmsLevel[text] }}</div>
</div>
</template>
<template v-for="col in ['code', 'message']" #[col]="{ text }" :key="col">
<a-tooltip :title="text">
<div >{{ text }}</div>
</a-tooltip>
</template>
<template #domain="{text}">
<a-tooltip :title="EDeviceTypeName[text]">
<div >{{ EDeviceTypeName[text] }}</div>
</a-tooltip>
</template>
</a-table>
</div>
</a-drawer>
</template>
<!-- 暂时只抽取该组件 -->
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits, watch } from 'vue'
import { getDeviceHms, HmsQueryBody } from '/@/api/manage'
import moment, { Moment } from 'moment'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { Device, DeviceHms } from '/@/types/device'
import { IPage } from '/@/api/http/type'
import { EDeviceTypeName, EHmsLevel, ELocalStorageKey } from '/@/types'
const props = defineProps<{
visible: boolean,
device: null | Device,
}>()
const emit = defineEmits(['update:visible', 'ok', 'cancel'])
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
//
const sVisible = ref(false)
watch(props, () => {
sVisible.value = props.visible
// hms
if (props.visible) {
showHms()
}
})
function onVisibleChange (sVisible: boolean) {
setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
sVisible.value = v
emit('update:visible', v, e)
}
const loading = ref(false)
const hmsColumns: ColumnProps[] = [
{ title: 'Alarm Begin | End Time', dataIndex: 'create_time', width: '25%', className: 'titleStyle', slots: { customRender: 'time' } },
{ title: 'Level', dataIndex: 'level', width: '120px', className: 'titleStyle', slots: { customRender: 'level' } },
{ title: 'Device', dataIndex: 'domain', width: '12%', className: 'titleStyle', slots: { customRender: 'domain' } },
{ title: 'Error Code', dataIndex: 'key', width: '20%', className: 'titleStyle', ellipsis: true, slots: { customRender: 'code' } },
{ title: 'Hms Message', dataIndex: 'message_en', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } },
{ title: 'Hms Message', dataIndex: 'message_zh', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } },
]
interface DeviceHmsData {
data: DeviceHms[]
}
const hmsData = reactive<DeviceHmsData>({
data: []
})
type Pagination = TableState['pagination']
const hmsPaginationProp = reactive({
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
})
//
function getPaginationBody () {
return {
page: hmsPaginationProp.current,
page_size: hmsPaginationProp.pageSize
} as IPage
}
function showHms () {
const dock = props.device
if (!dock) return
if (dock.domain === EDeviceTypeName.Dock) {
getDeviceHmsBySn(dock.device_sn, dock.children?.[0].device_sn ?? '')
}
if (dock.domain === EDeviceTypeName.Aircraft) {
param.domain = EDeviceTypeName.Aircraft
getDeviceHmsBySn('', dock.device_sn)
}
}
function refreshHmsData (page: Pagination) {
hmsPaginationProp.current = page?.current!
hmsPaginationProp.pageSize = page?.pageSize!
getHms()
}
const param = reactive<HmsQueryBody>({
sns: [],
device_sn: '',
children_sn: '',
language: 'en',
begin_time: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0),
end_time: new Date().setHours(23, 59, 59, 999),
domain: -1,
level: '',
message: ''
})
const levels = [
{
label: 'All',
value: ''
}, {
label: EHmsLevel[0],
value: EHmsLevel.NOTICE
}, {
label: EHmsLevel[1],
value: EHmsLevel.CAUTION
}, {
label: EHmsLevel[2],
value: EHmsLevel.WARN
}
]
const deviceTypes = [
{
label: 'All',
value: -1
}, {
label: EDeviceTypeName[EDeviceTypeName.Aircraft],
value: EDeviceTypeName.Aircraft
}, {
label: EDeviceTypeName[EDeviceTypeName.Dock],
value: EDeviceTypeName.Dock
}
]
const rowClassName = (record: any, index: number) => {
const className = []
if ((index & 1) === 0) {
className.push('table-striped')
}
if (record.domain !== EDeviceTypeName.Dock) {
className.push('child-row')
}
return className.toString().replaceAll(',', ' ')
}
const time = ref([moment(param.begin_time), moment(param.end_time)])
function getHms () {
loading.value = true
getDeviceHms(param, workspaceId, getPaginationBody())
.then(res => {
hmsPaginationProp.total = res.data.pagination.total
hmsPaginationProp.current = res.data.pagination.page
hmsData.data = res.data.list
hmsData.data.forEach(hms => {
hms.domain = hms.sn === param.children_sn ? EDeviceTypeName.Aircraft : EDeviceTypeName.Dock
})
loading.value = false
}).catch(_err => {
loading.value = false
})
}
function getDeviceHmsBySn (sn: string, childSn: string) {
param.device_sn = sn
param.children_sn = childSn
param.sns = [param.device_sn, param.children_sn]
getHms()
}
function onTimeChange (newTime: [Moment, Moment]) {
param.begin_time = newTime[0].valueOf()
param.end_time = newTime[1].valueOf()
getHms()
}
function onDeviceTypeSelect (val: number) {
param.sns = [param.device_sn, param.children_sn]
if (val === EDeviceTypeName.Dock) {
param.sns = [param.device_sn, '']
}
if (val === EDeviceTypeName.Aircraft) {
param.sns = ['', param.children_sn]
}
getHms()
}
function onLevelSelect (val: number) {
param.level = val
getHms()
}
</script>

View File

@ -0,0 +1,150 @@
<template>
<a-modal
title="日志上传详情"
v-model:visible="sVisible"
width="900px"
:footer="null"
@update:visible="onVisibleChange">
<div class="device-log-detail-wrap">
<div class="device-log-list">
<div class="log-list-item">
<a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id || !airportTableLogState.logList?.object_key" size="small" @click="onDownloadLog(airportTableLogState.logList.file_id)">
下载机场日志
</a-button>
<a-table :columns="airportLogColumns"
:scroll="{ x: '100%', y: 600 }"
:data-source="airportTableLogState.logList?.list"
rowKey="boot_index"
:pagination = "false"
>
<template #log_time="{record}">
<div>{{getLogTime(record)}}</div>
</template>
<template #size="{record}">
<div>{{getLogSize(record.size)}}</div>
</template>
</a-table>
</div>
<div class="log-list-item">
<a-button type="primary" class="download-btn" :disabled="!droneTableLogState.logList?.file_id || !droneTableLogState.logList?.object_key" size="small" @click="onDownloadLog(droneTableLogState.logList.file_id)">
下载飞行器日志
</a-button>
<a-table :columns="droneLogColumns"
:scroll="{ x: '100%', y: 600 }"
:data-source="droneTableLogState.logList?.list"
rowKey="boot_index"
:pagination = "false"
>
<template #log_time="{record}">
<div>{{getLogTime(record)}}</div>
</template>
<template #size="{record}">
<div>{{getLogSize(record.size)}}</div>
</template>
</a-table>
</div>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits } from 'vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { IPage } from '/@/api/http/type'
import { DOMAIN } from '/@/types/device'
import { DeviceLogFileInfo, GetDeviceUploadLogListRsp, getUploadDeviceLogUrl } from '/@/api/device-log'
import { useDeviceLogUploadDetail } from './use-device-log-upload-detail'
import { download } from '/@/utils/download'
const props = defineProps<{
visible: boolean,
deviceLog: null | GetDeviceUploadLogListRsp,
}>()
const emit = defineEmits(['update:visible'])
const sVisible = ref(false)
watchEffect(() => {
sVisible.value = props.visible
if (props.visible) {
classifyDeviceLog()
}
})
function onVisibleChange (sVisible: boolean) {
setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
sVisible.value = v
emit('update:visible', v, e)
}
//
const airportLogColumns: ColumnProps[] = [
{ title: '机场日志', dataIndex: 'time', width: '70%', slots: { customRender: 'log_time' } },
{ title: '文件大小', dataIndex: 'size', width: '30%', slots: { customRender: 'size' } },
]
const droneLogColumns: ColumnProps[] = [
{ title: '飞行器日志', dataIndex: 'time', width: '70%', slots: { customRender: 'log_time' } },
{ title: '文件大小', dataIndex: 'size', width: '30%', slots: { customRender: 'size' } },
]
const airportTableLogState = reactive({
logList: {} as DeviceLogFileInfo,
})
const droneTableLogState = reactive({
logList: {} as DeviceLogFileInfo,
})
function classifyDeviceLog () {
if (!props.deviceLog) return
const { device_logs } = props.deviceLog
const { files } = device_logs || {}
if (files && files.length > 0) {
files.forEach(file => {
if (file.module === DOMAIN.DOCK) {
airportTableLogState.logList = file
} else if (file.module === DOMAIN.DRONE) {
droneTableLogState.logList = file
}
})
}
}
const { getLogTime, getLogSize } = useDeviceLogUploadDetail()
async function onDownloadLog (fileId: string) {
const { data } = await getUploadDeviceLogUrl({
file_id: fileId,
logs_id: props.deviceLog?.logs_id || ''
})
if (data) {
download(data)
// download('https:/github.com/dji-sdk/Mobile-SDK-Android-V5/archive/refs/heads/dev-sdk-main.zip')
}
}
</script>
<style lang="scss" scoped>
.device-log-detail-wrap{
.device-log-list{
display: flex;
justify-content: space-between;
padding: 8px 0;
.log-list-item{
width: 420px;
.download-btn{
margin-bottom: 10px;
}
}
}
}
</style>
>

View File

@ -0,0 +1,210 @@
<template>
<a-modal
title="设备日志上传"
v-model:visible="sVisible"
width="900px"
:footer="null"
@update:visible="onVisibleChange">
<div class="device-log-upload-wrap">
<div class="page-action-row">
<a-button type="primary" :disabled="deviceLogUploadBtnDisabled" @click="uploadDeviceLog">上传日志</a-button>
</div>
<div class="device-log-list">
<div class="log-list-item">
<a-table :columns="airportLogColumns"
:scroll="{ x: '100%', y: 600 }"
:data-source="airportTableLogState.logList?.list"
:loading="airportTableLogState.tableLoading"
:row-selection="airportTableLogState.rowSelection"
rowKey="boot_index"
:pagination = "false">
<template #log_time="{record}">
<div>{{getLogTime(record)}}</div>
</template>
<template #size="{record}">
<div>{{getLogSize(record.size)}}</div>
</template>
</a-table>
</div>
<div class="log-list-item">
<a-table :columns="droneLogColumns"
:scroll="{ x: '100%', y: 600 }"
:data-source="droneTableLogState.logList?.list"
:loading="droneTableLogState.tableLoading"
:row-selection="droneTableLogState.rowSelection"
rowKey="boot_index"
:pagination = "false">
<template #log_time="{record}">
<div>{{getLogTime(record)}}</div>
</template>
<template #size="{record}">
<div>{{getLogSize(record.size)}}</div>
</template>
</a-table>
</div>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { watchEffect, reactive, ref, computed, defineProps, defineEmits } from 'vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { IPage } from '/@/api/http/type'
import { Device, DOMAIN } from '/@/types/device'
import { getDeviceLogList, postDeviceUpgrade, DeviceLogFileInfo, UploadDeviceLogBody, DeviceLogItem } from '/@/api/device-log'
import { message } from 'ant-design-vue'
import { useDeviceLogUploadDetail } from './use-device-log-upload-detail'
const props = defineProps<{
visible: boolean,
device: null | Device,
}>()
const emit = defineEmits(['update:visible', 'upload-log-ok'])
const sVisible = ref(false)
watchEffect(() => {
sVisible.value = props.visible
//
if (props.visible) {
getDeviceLogInfo()
}
})
function onVisibleChange (sVisible: boolean) {
setVisible(sVisible)
if (!sVisible) {
resetTableLogState()
}
}
function setVisible (v: boolean, e?: Event) {
sVisible.value = v
emit('update:visible', v, e)
}
//
const airportLogColumns: ColumnProps[] = [
{ title: '机场日志', dataIndex: 'time', width: 100, slots: { customRender: 'log_time' } },
{ title: '文件大小', dataIndex: 'size', width: 25, slots: { customRender: 'size' } },
]
const droneLogColumns: ColumnProps[] = [
{ title: '飞行器日志', dataIndex: 'time', width: 100, slots: { customRender: 'log_time' } },
{ title: '文件大小', dataIndex: 'size', width: 25, slots: { customRender: 'size' } },
]
const airportTableLogState = reactive({
logList: {} as DeviceLogFileInfo,
tableLoading: false,
selectRow: [],
rowSelection: {
columnWidth: 15,
selectedRowKeys: [] as number[],
onChange: (selectedRowKeys:number[], selectedRows: []) => {
airportTableLogState.rowSelection.selectedRowKeys = selectedRowKeys
airportTableLogState.selectRow = selectedRows
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
},
}
})
function resetTableLogState () {
airportTableLogState.logList = {} as DeviceLogFileInfo
airportTableLogState.selectRow = []
airportTableLogState.tableLoading = false
}
const droneTableLogState = reactive({
logList: {} as DeviceLogFileInfo,
tableLoading: false,
selectRow: [],
rowSelection: {
columnWidth: 15,
selectedRowKeys: [] as number[],
onChange: (selectedRowKeys: number[], selectedRows: []) => {
droneTableLogState.rowSelection.selectedRowKeys = selectedRowKeys
droneTableLogState.selectRow = selectedRows
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
},
}
})
const deviceLogUploadBtnDisabled = computed(() => {
return (airportTableLogState.rowSelection.selectedRowKeys && airportTableLogState.rowSelection.selectedRowKeys.length <= 0) &&
(droneTableLogState.rowSelection.selectedRowKeys && droneTableLogState.rowSelection.selectedRowKeys.length <= 0)
})
//
async function getDeviceLogInfo () {
airportTableLogState.tableLoading = true
droneTableLogState.tableLoading = true
try {
const { code, data } = await getDeviceLogList({
device_sn: props.device?.device_sn || '',
domain: [DOMAIN.DOCK, DOMAIN.DRONE]
})
if (code === 0) {
const { files } = data
if (files && files.length > 0) {
files.forEach(file => {
if (file.module === DOMAIN.DOCK) {
airportTableLogState.logList = file
} else if (file.module === DOMAIN.DRONE) {
droneTableLogState.logList = file
}
})
}
}
} catch (err) {
}
airportTableLogState.tableLoading = false
droneTableLogState.tableLoading = false
}
//
async function uploadDeviceLog () {
const body = {
device_sn: props.device?.device_sn || '',
files: [] as any
} as UploadDeviceLogBody
if (airportTableLogState.selectRow && airportTableLogState.selectRow.length > 0) {
body.files.push({
list: airportTableLogState.selectRow,
device_sn: airportTableLogState.logList.device_sn,
module: airportTableLogState.logList.module
})
}
if (droneTableLogState.selectRow && droneTableLogState.selectRow.length > 0) {
body.files.push({
list: droneTableLogState.selectRow,
device_sn: droneTableLogState.logList.device_sn,
module: droneTableLogState.logList.module
})
}
const { code } = await postDeviceUpgrade(body)
if (code === 0) {
message.success('日志上传任务执行成功')
emit('upload-log-ok')
setVisible(false)
}
}
const { getLogTime, getLogSize } = useDeviceLogUploadDetail()
</script>
<style lang="scss" scoped>
.device-log-upload-wrap{
.device-log-list{
display: flex;
justify-content: space-between;
padding: 8px 0;
.log-list-item{
width: 420px;
}
}
}
</style>

View File

@ -0,0 +1,326 @@
<template>
<a-drawer
title="设备日志上传记录"
placement="right"
v-model:visible="sVisible"
@update:visible="onVisibleChange"
:width="800">
<!-- 设备日志上传记录 -->
<div class="device-log-upload-record-wrap">
<div class="page-action-row">
<a-button type="primary" @click="onUploadDeviceLog">上传日志</a-button>
</div>
<div class="device-log-upload-list">
<a-table :columns="deviceLogUploadListColumns"
:scroll="{ x: '100%', y: 600 }"
:data-source="deviceUploadLogState.uploadLogList"
:loading="deviceUploadLogState.loading"
:pagination="deviceUploadLogState.paginationProp"
@change="onDeviceUploadLogTableChange"
rowKey="logs_id">
<!-- 设备类型 -->
<template #device_type="{ record }">
<div>
<div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).parents[0].device_model.device_model_key]}}</div>
<div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).hosts[0].device_model.device_model_key]}}</div>
</div>
</template>
<!-- 设备sn -->
<template #device_sn="{ record }">
<div>
<div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ getDeviceInfo(record).parents[0].sn }}</div>
<div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ getDeviceInfo(record).hosts[0].sn }}</div>
</div>
</template>
<!-- 上传状态 -->
<template #status="{ record }">
<div>
<div>
<span class="circle-icon" :style="{backgroundColor: getDeviceLogUploadStatus(record).color}"></span>
{{ getDeviceLogUploadStatus(record).text }}
</div>
<div v-if="record.status === DeviceLogUploadStatusEnum.Uploading">
<a-progress :percent="getLogProgress(record)" />
</div>
</div>
</template>
<!-- 操作 -->
<template #action="{ record }">
<div class="row-action">
<a-tooltip title="查看详情">
<FileTextOutlined @click="showDeviceLogDetail(record)"/>
</a-tooltip>
<span v-if="record.status === DeviceLogUploadStatusEnum.Uploading">
<a-tooltip title="取消">
<StopOutlined @click="onCancelUploadDeviceLog(record)"/>
</a-tooltip>
</span>
<span v-else>
<a-tooltip title="删除">
<DeleteOutlined @click="onDeleteUploadDeviceLog(record)"/>
</a-tooltip>
</span>
</div>
</template>
</a-table>
</div>
</div>
</a-drawer>
<!-- 设备日志上传弹框 -->
<DeviceLogUploadModal
v-model:visible="deviceLogUploadModalVisible"
:device="props.device"
@upload-log-ok="onUploadLogOk"
></DeviceLogUploadModal>
<!-- 设备日志上传详情弹框 -->
<DeviceLogDetailModal
v-model:visible="deviceLogDetailModalVisible"
:deviceLog="currentDeviceLog"
></DeviceLogDetailModal>
</template>
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits } from 'vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { IPage } from '/@/api/http/type'
import { Device, DOMAIN, DEVICE_NAME } from '/@/types/device'
import DeviceLogUploadModal from './DeviceLogUploadModal.vue'
import DeviceLogDetailModal from './DeviceLogDetailModal.vue'
import { getDeviceUploadLogList, GetDeviceUploadLogListRsp, cancelDeviceLogUpload, deleteDeviceLogUpload } from '/@/api/device-log'
import { StopOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons-vue'
import { DeviceLogUploadStatusEnum, DeviceLogUploadStatusMap, DeviceLogUploadStatusColor, DeviceLogUploadInfo, DeviceLogUploadWsStatusMap, DeviceLogProgressInfo } from '/@/types/device-log'
import { useDeviceLogUploadProgressEvent } from './use-device-log-upload-progress-event'
import { Modal } from 'ant-design-vue'
const props = defineProps<{
visible: boolean,
device: null | Device,
}>()
const emit = defineEmits(['update:visible'])
const sVisible = ref(false)
watchEffect(() => {
sVisible.value = props.visible
//
if (props.visible) {
getDeviceUploadLogInfo()
}
})
function onVisibleChange (sVisible: boolean) {
setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
sVisible.value = v
emit('update:visible', v, e)
}
//
const deviceLogUploadListColumns: ColumnProps[] = [
{ title: '上传时间', dataIndex: 'create_time', width: 100 },
{ title: '设备型号', dataIndex: 'device_type', width: 80, slots: { customRender: 'device_type' } },
{ title: '设备SN', dataIndex: 'device_sn', width: 120, slots: { customRender: 'device_sn' } },
{ title: '上传状态', dataIndex: 'status', width: 120, slots: { customRender: 'status' } },
{ title: '操作', dataIndex: 'actions', width: 80, slots: { customRender: 'action' } },
]
const deviceUploadLogState = reactive({
uploadLogList: [] as GetDeviceUploadLogListRsp[],
loading: false,
paginationProp: {
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
}
})
//
async function getDeviceUploadLogInfo () {
deviceUploadLogState.loading = true
try {
const { code, data } = await getDeviceUploadLogList({
device_sn: props.device?.device_sn || '',
page: deviceUploadLogState.paginationProp.current,
page_size: deviceUploadLogState.paginationProp.pageSize
})
if (code === 0) {
deviceUploadLogState.uploadLogList = data.list
deviceUploadLogState.paginationProp.total = data.pagination.total
deviceUploadLogState.paginationProp.current = data.pagination.page
deviceUploadLogState.paginationProp.pageSize = data.pagination.page_size
}
deviceUploadLogState.loading = false
} catch (error) {
deviceUploadLogState.loading = false
}
}
type Pagination = TableState['pagination']
//
function getDeviceInfo (deviceLogItem: GetDeviceUploadLogListRsp) {
const { device_topo: deviceTopo } = deviceLogItem
return deviceTopo
}
//
function getDeviceLogUploadStatus (deviceLogItem: GetDeviceUploadLogListRsp) {
const statusObj = {
color: '',
text: ''
}
const { status } = deviceLogItem
statusObj.color = DeviceLogUploadStatusColor[status]
statusObj.text = DeviceLogUploadStatusMap[status]
return statusObj
}
//
function getLogProgress (deviceLogItem: GetDeviceUploadLogListRsp) {
let percent = 0
const { logs_progress } = deviceLogItem
if (logs_progress && logs_progress.length > 0) {
logs_progress.forEach(log => {
percent += (log.progress || 0)
})
percent = percent / logs_progress.length
}
return Math.floor(percent)
}
//
function onDeviceLogUploadWs (data: DeviceLogUploadInfo) {
const { sn, output } = data
if (output) {
const { files, status, logs_id: logId } = output || {}
const deviceLogItem = deviceUploadLogState.uploadLogList.find(log => log.logs_id === logId)
if (!deviceLogItem) return
if (status) {
deviceLogItem.status = DeviceLogUploadWsStatusMap[status]
}
if (files && files.length > 0) {
const logsProgress = [] as DeviceLogProgressInfo[]
files.forEach(file => {
logsProgress.push({
...file,
status: DeviceLogUploadWsStatusMap[file.status]
})
})
deviceLogItem.logs_progress = logsProgress
}
}
}
useDeviceLogUploadProgressEvent(onDeviceLogUploadWs)
//
async function onDeviceUploadLogTableChange (page: Pagination) {
deviceUploadLogState.paginationProp.current = page?.current || 1
deviceUploadLogState.paginationProp.pageSize = page?.pageSize || 20
await getDeviceUploadLogInfo()
}
//
const deviceLogDetailModalVisible = ref(false)
const currentDeviceLog = ref({} as GetDeviceUploadLogListRsp)
function showDeviceLogDetail (deviceLogItem: GetDeviceUploadLogListRsp) {
if (!deviceLogItem) return
currentDeviceLog.value = deviceLogItem
deviceLogDetailModalVisible.value = true
}
//
async function onCancelUploadDeviceLog (deviceLogItem: GetDeviceUploadLogListRsp) {
Modal.confirm({
title: '取消日志上传',
content: '您确认取消设备日志上传吗?',
okType: 'danger',
onOk () {
cancelDeviceLogUploadOk()
},
})
}
async function cancelDeviceLogUploadOk () {
const { code } = await cancelDeviceLogUpload({
device_sn: props.device?.device_sn || '',
module_list: [DOMAIN.DOCK, DOMAIN.DRONE],
status: 'cancel'
})
if (code === 0) {
await getDeviceUploadLogInfo()
}
}
//
function onDeleteUploadDeviceLog (deviceLogItem: GetDeviceUploadLogListRsp) {
Modal.confirm({
title: '删除上传日志',
content: '您确认删除该条已上传设备日志吗?',
okType: 'danger',
onOk () {
deleteUploadDeviceLogOk(deviceLogItem)
},
})
}
async function deleteUploadDeviceLogOk (deviceLogItem: GetDeviceUploadLogListRsp) {
const { code } = await deleteDeviceLogUpload({
device_sn: props.device?.device_sn || '',
logs_id: deviceLogItem.logs_id
})
if (code === 0) {
await getDeviceUploadLogInfo()
}
}
//
const deviceLogUploadModalVisible = ref(false)
function onUploadDeviceLog () {
deviceLogUploadModalVisible.value = true
}
function onUploadLogOk () {
//
getDeviceUploadLogInfo()
}
</script>
<style lang="scss" scoped>
.device-log-upload-record-wrap{
.page-action-row{
display: flex;
justify-content: space-between;
width: 100%;
}
.device-log-upload-list{
padding: 20px 0 10px;
}
.circle-icon {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 3px;
border-radius: 50%;
vertical-align: middle;
flex-shrink: 0;
}
.row-action{
color: #2d8cf0;
& > span{
margin-right: 10px;
}
}
}
</style>

View File

@ -0,0 +1,23 @@
import { DeviceLogItem } from '/@/api/device-log'
import { bytesToSize } from '/@/utils/bytes'
import { formatUnixTime } from '/@/utils/time'
import {
DATE_FORMAT_MINUTE
} from '/@/utils/constants'
export function useDeviceLogUploadDetail () {
function getLogTime (deviceLog: DeviceLogItem): string {
const startTime = formatUnixTime(deviceLog.start_time, DATE_FORMAT_MINUTE)
const endTime = formatUnixTime(deviceLog.end_time, DATE_FORMAT_MINUTE)
return `${startTime}${endTime}`
}
function getLogSize (size: number) {
return bytesToSize(size)
}
return {
getLogTime,
getLogSize
}
}

View File

@ -0,0 +1,19 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { DeviceLogUploadInfo } from '/@/types/device-log'
export function useDeviceLogUploadProgressEvent (onDeviceLogUploadWs: (data: DeviceLogUploadInfo) => void): void {
function handleDeviceLogUploadProgress (payload: any) {
onDeviceLogUploadWs(payload.data)
// eslint-disable-next-line no-unused-expressions
// console.log('payload', payload.data)
}
onMounted(() => {
EventBus.on('deviceLogUploadProgress', handleDeviceLogUploadProgress)
})
onBeforeUnmount(() => {
EventBus.off('deviceLogUploadProgress', handleDeviceLogUploadProgress)
})
}

View File

@ -0,0 +1,64 @@
<template>
<div class="firmware_upgrade_wrap">
<!-- 版本 -->
<span class="version"> {{ device.firmware_version }}</span>
<!-- tag -->
<span v-if="getTagStatus(device)"
class="status-tag pointer">
<a-tag class="pointer"
:color="getFirmwareTag(device.firmware_status).color"
@click="deviceUpgrade(device)">
{{ getFirmwareTag(device.firmware_status).text }}
</a-tag>
</span>
<!-- 进度 -->
<span v-if="device.firmware_status === DeviceFirmwareStatusEnum.DuringUpgrade">
{{ `${device.firmware_progress}`}}
</span>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
import { Device, DeviceFirmwareStatusEnum, DeviceFirmwareStatus, DeviceFirmwareStatusColor } from '/@/types/device'
const props = defineProps<{
device: Device,
}>()
const emit = defineEmits(['device-upgrade'])
const needUpgrade = computed(() => {
return props.device.firmware_status === DeviceFirmwareStatusEnum.ConsistencyUpgrade ||
props.device.firmware_status === DeviceFirmwareStatusEnum.ToUpgraded
})
function getTagStatus (record: Device) {
return record.firmware_status && record.firmware_status !== DeviceFirmwareStatusEnum.None
}
function getFirmwareTag (status: DeviceFirmwareStatusEnum) {
return {
text: DeviceFirmwareStatus[status] || '',
color: DeviceFirmwareStatusColor[status] || ''
}
}
function deviceUpgrade (record: Device) {
if (!needUpgrade.value) return
emit('device-upgrade', record)
}
</script>
<style lang="scss" scoped>
.firmware_upgrade_wrap{
.status-tag{
margin-left: 10px;
}
.pointer {
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<a-modal :visible="sVisible"
:title="title"
:closable="false"
centered
@update:visible="onVisibleChange"
@cancel="onCancel"
@ok="onConfirm">
<div>
升级固件版本: {{ deviceUpgradeInfo?.product_version }}
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, Ref, watchEffect } from 'vue'
import { Device, DeviceFirmwareStatusEnum, DeviceFirmwareStatus, DeviceFirmwareTypeEnum } from '/@/types/device'
import { getDeviceUpgradeInfo, GetDeviceUpgradeInfoRsp, DeviceUpgradeBody } from '/@/api/device-upgrade'
const props = defineProps<{
visible: boolean,
title: string,
device: null | Device,
}>()
const emit = defineEmits(['update:visible', 'ok', 'cancel'])
const deviceUpgradeInfo:Ref<GetDeviceUpgradeInfoRsp> = ref({} as GetDeviceUpgradeInfoRsp)
const sVisible = ref(false)
watchEffect(() => {
sVisible.value = props.visible
//
if (props.visible) {
initDeviceUpgradeInfo()
}
})
function onVisibleChange (sVisible: boolean) {
setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
sVisible.value = v
emit('update:visible', v, e)
}
//
async function initDeviceUpgradeInfo () {
if (!props.device?.device_name) {
return
}
const { code, data } = await getDeviceUpgradeInfo({ device_name: props.device?.device_name })
if (code === 0) {
deviceUpgradeInfo.value = data && data[0]
}
}
//
function checkConfirm () {
if (!deviceUpgradeInfo.value.product_version) {
return false
}
if (!props.device) {
return false
}
if (props.device.firmware_status !== DeviceFirmwareStatusEnum.ToUpgraded && props.device.firmware_status !== DeviceFirmwareStatusEnum.ConsistencyUpgrade) {
return false
}
return true
}
function onConfirm (e: Event) {
if (!checkConfirm()) {
return
}
setVisible(false, e)
emit('ok', [{
device_name: props.device?.device_name,
sn: props.device?.device_sn,
product_version: deviceUpgradeInfo.value.product_version,
firmware_upgrade_type: props.device?.firmware_status === DeviceFirmwareStatusEnum.ToUpgraded ? DeviceFirmwareTypeEnum.ToUpgraded : DeviceFirmwareTypeEnum.ConsistencyUpgrade // 1-2-
}] as DeviceUpgradeBody, e)
}
function onCancel (e: Event) {
setVisible(false, e)
emit('cancel', e)
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,19 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { DeviceCmdExecuteInfo, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
export function useDeviceUpgradeEvent (onDeviceUpgradeWs: (payload: DeviceCmdExecuteInfo) => void): void {
function handleDeviceUpgrade (payload: any) {
onDeviceUpgradeWs(payload.data)
// eslint-disable-next-line no-unused-expressions
// console.log('payload', payload.data)
}
onMounted(() => {
EventBus.on('deviceUpgrade', handleDeviceUpgrade)
})
onBeforeUnmount(() => {
EventBus.off('deviceUpgrade', handleDeviceUpgrade)
})
}

View File

@ -0,0 +1,42 @@
import { Ref, ref } from 'vue'
import { Device } from '/@/types/device'
import { postDeviceUpgrade, DeviceUpgradeBody } from '/@/api/device-upgrade'
export function useDeviceFirmwareUpgrade (workspaceId: string) {
const deviceFirmwareUpgradeModalVisible = ref(false)
const selectedDevice: Ref<null | Device> = ref(null)
function setDeviceFirmwareUpgradeModalVisible (visible: boolean) {
deviceFirmwareUpgradeModalVisible.value = visible
}
function setSelectedDevice (device: null | Device) {
selectedDevice.value = device
}
// 点击设备升级
function onDeviceUpgrade (record: Device) {
if (!record) {
return
}
setSelectedDevice(record)
setDeviceFirmwareUpgradeModalVisible(true)
}
// 确认设备升级
async function onUpgradeDeviceOk (deviceUpgradeBody: DeviceUpgradeBody) {
const { code } = await postDeviceUpgrade(workspaceId, deviceUpgradeBody)
if (code === 0) {
// setDeviceFirmwareUpgradeModalVisible(false)
}
}
return {
deviceFirmwareUpgradeModalVisible,
setDeviceFirmwareUpgradeModalVisible,
selectedDevice,
setSelectedDevice,
onDeviceUpgrade,
onUpgradeDeviceOk,
}
}

View File

@ -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>

View File

@ -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 !== 1) {
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 === 1) {
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>

View File

@ -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>

View File

@ -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 === 1) {
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>

View File

@ -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>

View File

@ -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 === 1) {
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>

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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) {
console.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) {
console.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:
console.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 !== 1) {
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,
}
}

View File

@ -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>

View File

@ -0,0 +1,106 @@
<template>
<a-popover :visible="state.sVisible"
trigger="click"
v-bind="$attrs"
:overlay-class-name="overlayClassName"
placement="bottom"
@visibleChange=";"
v-on="$attrs">
<template #content>
<div class="title-content">
</div>
<slot name="formContent" />
<div class="uranus-popconfirm-btns">
<a-button size="sm"
@click="onCancel">
{{ cancelText || '取消'}}
</a-button>
<a-button size="sm"
:loading="loading"
type="primary"
class="confirm-btn"
@click="onConfirm">
{{ okText || '确定' }}
</a-button>
</div>
</template>
<template v-if="$slots.default">
<slot></slot>
</template>
</a-popover>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, reactive, watch, computed } from 'vue'
const props = defineProps<{
visible?: boolean,
loading?: Boolean,
disabled?: Boolean,
title?: String,
okText?: String,
cancelText?: String,
width?: Number,
}>()
const emit = defineEmits(['cancel', 'confirm'])
const state = reactive({
sVisible: false,
loading: false,
})
watch(() => props.visible, (val) => {
state.sVisible = val || false
})
const loading = computed(() => {
return props.loading
})
const okLabel = computed(() => {
return props.loading ? '' : '确定'
})
const overlayClassName = computed(() => {
const classList = ['device-setting-popconfirm']
return classList.join(' ')
})
function onConfirm (e: Event) {
if (props.disabled) {
return
}
emit('confirm', e)
}
function onCancel (e: Event) {
state.sVisible = false
emit('cancel', e)
}
</script>
<style lang="scss">
.device-setting-popconfirm {
min-width: 300px;
.uranus-popconfirm-btns{
display: flex;
padding: 10px 0px;
justify-content: flex-end;
.confirm-btn{
margin-left: 10px;
}
}
.form-content{
display: inline-flex;
align-items: center;
.form-label{
padding-right: 10px;
}
}
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div class="dock-control-panel">
<!-- title -->
<div class="dock-control-panel-header fz16 pl5 pr5 flex-align-center flex-row flex-justify-between">
<span>Device Control<span class="fz12 pl15">{{ props.sn}}</span></span>
<span @click="closeControlPanel">
<CloseOutlined />
</span>
</div>
<!-- setting -->
<DeviceSettingBox :sn="props.sn" :deviceInfo="props.deviceInfo"></DeviceSettingBox>
<!-- cmd -->
<div class="control-cmd-wrapper">
<div class="control-cmd-header">
Device Remote Debug
<a-switch class="debug-btn" checked-children="" un-checked-children="" v-model:checked="debugStatus" @change="onDeviceStatusChange"/>
</div>
<div class="control-cmd-box">
<div v-for="(cmdItem, index) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
<div class="control-cmd-item-left">
<div class="item-label">{{ cmdItem.label }}</div>
<div class="item-status">{{ cmdItem.status }}</div>
</div>
<div class="control-cmd-item-right">
<a-button :disabled="!debugStatus || cmdItem.disabled" :loading="cmdItem.loading" size="small" type="primary" @click="sendControlCmd(cmdItem, index)">
{{ cmdItem.operateText }}
</a-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, ref, watch } from 'vue'
import {
CloseOutlined
} from '@ant-design/icons-vue'
import { useDockControl } from './use-dock-control'
import { DeviceInfoType, EDockModeCode } from '/@/types/device'
import { cmdList as baseCmdList, DeviceCmdItem } from '/@/types/device-cmd'
import { useMyStore } from '/@/store'
import { updateDeviceCmdInfoByOsd, updateDeviceCmdInfoByExecuteInfo } from '/@/utils/device-cmd'
import DeviceSettingBox from './DeviceSettingBox.vue'
const props = defineProps<{
sn: string,
deviceInfo: DeviceInfoType,
}>()
const store = useMyStore()
const initCmdList = baseCmdList.map(cmdItem => Object.assign({}, cmdItem))
const cmdList = ref(initCmdList)
//
watch(() => store.state.devicesCmdExecuteInfo, (devicesCmdExecuteInfo) => {
if (props.sn && devicesCmdExecuteInfo[props.sn]) {
updateDeviceCmdInfoByExecuteInfo(cmdList.value, devicesCmdExecuteInfo[props.sn])
}
}, {
immediate: true,
deep: true,
})
// osd
watch(() => props.deviceInfo, (value) => {
updateDeviceCmdInfoByOsd(cmdList.value, value)
// console.log('deviceInfo', value)
}, {
immediate: true,
deep: true
})
// dock
const debugStatus = ref(props.deviceInfo.dock?.basic_osd?.mode_code === EDockModeCode.Remote_Debugging)
const emit = defineEmits(['close-control-panel'])
function closeControlPanel () {
emit('close-control-panel', props.sn, debugStatus.value)
}
async function onDeviceStatusChange (status: boolean) {
let result = false
if (status) {
result = await dockDebugOnOff(props.sn, true)
} else {
result = await dockDebugOnOff(props.sn, false)
}
if (!result) {
if (status) {
debugStatus.value = false
} else {
debugStatus.value = true
}
}
}
const {
sendDockControlCmd,
dockDebugOnOff
} = useDockControl()
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
const success = await sendDockControlCmd({
sn: props.sn,
cmd: cmdItem.cmdKey,
action: cmdItem.action
}, true)
if (success) {
// updateDeviceSingleCmdInfo(cmdList.value[index])
}
}
</script>
<style lang='scss' scoped>
.dock-control-panel{
position: absolute;
left: calc(100% + 10px);
top: 0px;
width: 480px;
padding: 0 !important;
background: #000;
color: #fff;
border-radius: 2px;
.dock-control-panel-header{
border-bottom: 1px solid #515151;
}
.control-cmd-wrapper{
.control-cmd-header{
font-size: 14px;
font-weight: 600;
padding: 10px 10px 0px;
.debug-btn{
margin-left: 10px;
border:1px solid #585858;
}
}
.control-cmd-box{
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 4px 10px;
.control-cmd-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-cmd-item-left{
display: flex;
flex-direction: column;
.item-label{
font-weight: 700;
}
}
}
}
}
}
</style>

View File

@ -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>

View File

@ -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 {
console.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) {
console.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) {
console.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) {
console.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) {
console.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) {
console.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) {
console.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>

View File

@ -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>

View File

@ -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()
})
}

View File

@ -0,0 +1,56 @@
import { message } from 'ant-design-vue'
import { putDeviceProps, PutDevicePropsBody } from '/@/api/device-setting'
import { DeviceSettingKeyEnum, DeviceSettingFormModel, ObstacleAvoidanceStatusEnum, NightLightsStateEnum, DistanceLimitStatusEnum } from '/@/types/device-setting'
export function useDeviceSetting () {
// 生成参数
function genDevicePropsBySettingKey (key: DeviceSettingKeyEnum, fromModel: DeviceSettingFormModel) {
const body = {} as PutDevicePropsBody
if (key === DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET) {
body.night_lights_state = fromModel.nightLightsState ? NightLightsStateEnum.OPEN : NightLightsStateEnum.CLOSE
} else if (key === DeviceSettingKeyEnum.HEIGHT_LIMIT_SET) {
body.height_limit = fromModel.heightLimit
} else if (key === DeviceSettingKeyEnum.DISTANCE_LIMIT_SET) {
body.distance_limit_status = {}
if (fromModel.distanceLimitStatus.state) {
body.distance_limit_status.state = DistanceLimitStatusEnum.SET
body.distance_limit_status.distance_limit = fromModel.distanceLimitStatus.distanceLimit
} else {
body.distance_limit_status.state = DistanceLimitStatusEnum.UNSET
}
} else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON) {
body.obstacle_avoidance = {
horizon: fromModel.obstacleAvoidanceHorizon ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
}
} else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE) {
body.obstacle_avoidance = {
upside: fromModel.obstacleAvoidanceUpside ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
}
} else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE) {
body.obstacle_avoidance = {
downside: fromModel.obstacleAvoidanceDownside ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
}
}
return body
}
// 设置设备属性
async function setDeviceProps (sn: string, body: PutDevicePropsBody) {
try {
const { code, message: msg } = await putDeviceProps(sn, body)
if (code === 0) {
// message.success('指令发送成功')
return true
}
throw (msg)
} catch (e) {
console.error('设备属性设置失败')
return false
}
}
return {
genDevicePropsBySettingKey,
setDeviceProps
}
}

View File

@ -0,0 +1,62 @@
import { message } from 'ant-design-vue'
import { ref } from 'vue'
import { postSendCmd } from '/@/api/device-cmd'
import { DeviceCmd, DeviceCmdItemAction } from '/@/types/device-cmd'
export function useDockControl () {
const dockControlPanelVisible = ref(false)
function setDockControlPanelVisible (visible: boolean) {
dockControlPanelVisible.value = visible
}
// 远程调试开关
async function dockDebugOnOff (sn: string, on: boolean) {
const result = await sendDockControlCmd({
sn: sn,
cmd: on ? DeviceCmd.DebugModeOpen : DeviceCmd.DebugModeClose
}, false)
return result
}
// 发送指令
async function sendDockControlCmd (params: {
sn: string,
cmd: DeviceCmd
action?: DeviceCmdItemAction
}, tip = true) {
try {
let body = undefined as any
if (params.action !== undefined) {
body = {
action: params.action
}
}
const { code, message: msg } = await postSendCmd({ dock_sn: params.sn, device_cmd: params.cmd }, body)
if (code === 0) {
tip && message.success('指令发送成功')
return true
}
throw (msg)
} catch (e) {
tip && console.error('指令发送失败')
return false
}
}
// 控制面板关闭
async function onCloseControlPanel (sn: string, debugging: boolean) {
if (debugging) {
await dockDebugOnOff(sn, false)
}
setDockControlPanelVisible(false)
}
return {
dockControlPanelVisible,
setDockControlPanelVisible,
sendDockControlCmd,
dockDebugOnOff,
onCloseControlPanel,
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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) {
console.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) {
console.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,
}
}

View File

@ -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,
}
}

View File

@ -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) {
console.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,
}
}

View File

@ -0,0 +1,398 @@
<template>
<div class="flex-column flex-justify-start flex-align-center">
<div id="player" style="width: 720px; height: 420px; border: 1px solid"></div>
<p class="fz24">Live streaming source selection</p>
<div class="flex-row flex-justify-center flex-align-center mt10">
<template v-if="livePara.liveState && dronePara.isDockLive">
<span class="mr10">Lens:</span>
<a-radio-group v-model:value="dronePara.lensSelected" button-style="solid">
<a-radio-button v-for="lens in dronePara.lensList" :key="lens" :value="lens">{{lens}}</a-radio-button>
</a-radio-group>
</template>
<template v-else>
<a-select
style="width:150px"
placeholder="Select Drone"
v-model:value="dronePara.droneSelected"
>
<a-select-option
v-for="item in dronePara.droneList"
:key="item.value"
:value="item.value"
@click="onDroneSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Camera"
v-model:value="dronePara.cameraSelected"
>
<a-select-option
v-for="item in dronePara.cameraList"
:key="item.value"
:value="item.value"
@click="onCameraSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
<!-- <a-select
class="ml10"
style="width:150px"
placeholder="Select Lens"
@select="onVideoSelect"
>
<a-select-option
v-for="item in dronePara.videoList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select> -->
</template>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Clarity"
@select="onClaritySelect"
>
<a-select-option
v-for="item in clarityList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
</div>
<p class="fz16 mt10">
Note: Obtain The Following Parameters From https://console.agora.io
</p>
<div class="flex-row flex-justify-center flex-align-center">
<span class="mr10">AppId:</span>
<a-input v-model:value="agoraPara.appid" placeholder="APP ID"></a-input>
<span class="ml10">Token:</span>
<a-input
class="ml10"
v-model:value="agoraPara.token"
placeholder="Token"
></a-input>
<span class="ml10">Channel:</span>
<a-input
class="ml10"
v-model:value="agoraPara.channel"
placeholder="Channel"
></a-input>
</div>
<div class="mt20 flex-row flex-justify-center flex-align-center">
<a-button v-if="livePara.liveState && dronePara.isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
<a-button v-else type="primary" large @click="onStart">Play</a-button>
<a-button class="ml20" type="primary" large @click="onStop"
>Stop</a-button
>
<a-button class="ml20" type="primary" large @click="onUpdateQuality"
>Update Clarity</a-button
>
<a-button v-if="!livePara.liveState || !dronePara.isDockLive" class="ml20" type="primary" large @click="onRefresh"
>Refresh Live Capacity</a-button
>
</div>
</div>
</template>
<script lang="ts" setup>
import AgoraRTC, { IAgoraRTCClient, IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
import { message } from 'ant-design-vue'
import { onMounted, reactive } from 'vue'
import { uuidv4 } from '../utils/uuid'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
const root = getRoot()
const clarityList = [
{
value: 0,
label: 'Adaptive'
},
{
value: 1,
label: 'Smooth'
},
{
value: 2,
label: 'Standard'
},
{
value: 3,
label: 'HD'
},
{
value: 4,
label: 'Super Clear'
}
]
interface SelectOption {
value: any,
label: string,
more?: any
}
let agoraClient = {} as IAgoraRTCClient
const agoraPara = reactive({
appid: config.agoraAPPID,
token: config.agoraToken,
channel: config.agoraChannel,
uid: 123456,
stream: {}
})
const dronePara = reactive({
livestreamSource: [],
droneList: [] as SelectOption[],
cameraList: [] as SelectOption[],
videoList: [] as SelectOption[],
droneSelected: undefined as string | undefined,
cameraSelected: undefined as string | undefined,
videoSelected: undefined as string | undefined,
claritySelected: 0,
lensList: [] as string[],
lensSelected: undefined as string | undefined,
isDockLive: false
})
const livePara = reactive({
url: '',
webrtc: {} as any,
videoId: '',
liveState: false
})
const nonSwitchable = 'normal'
const onRefresh = async () => {
dronePara.droneList = []
dronePara.cameraList = []
dronePara.videoList = []
dronePara.droneSelected = undefined
dronePara.cameraSelected = undefined
dronePara.videoSelected = undefined
await getLiveCapacity({})
.then(res => {
if (res.code === 1) {
if (res.data === null) {
console.warn('warning: get live capacity is null!!!')
return
}
dronePara.livestreamSource = res.data
dronePara.droneList = []
console.log('live_capacity:', dronePara.livestreamSource)
if (dronePara.livestreamSource) {
dronePara.livestreamSource.forEach((ele: any) => {
dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
})
}
}
})
.catch(error => {
console.error(error)
console.error(error)
})
}
onMounted(() => {
onRefresh()
agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'vp8' })
agoraClient.setClientRole('audience', { level: 2 })
if (agoraClient.connectionState === 'DISCONNECTED') {
agoraClient.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
}
// Subscribe when a remote user publishes a stream
agoraClient.on('user-joined', async (user: IAgoraRTCRemoteUser) => {
message.info('user[' + user.uid + '] join')
})
agoraClient.on('user-published', async (user: IAgoraRTCRemoteUser, mediaType: 'audio' | 'video') => {
await agoraClient.subscribe(user, mediaType)
if (mediaType === 'video') {
console.log('subscribe success')
// Get `RemoteVideoTrack` in the `user` object.
const remoteVideoTrack = user.videoTrack!
// Dynamically create a container in the form of a DIV element for playing the remote video track.
remoteVideoTrack.play(document.getElementById('player') as HTMLElement)
}
})
agoraClient.on('user-unpublished', async (user: any) => {
console.log('unpublish live:', user)
message.info('unpublish live')
})
agoraClient.on('exception', async (e: any) => {
console.log(e)
console.error(e.msg)
})
})
const handleError = (err: any) => {
console.error(err)
}
const handleJoinChannel = (uid: any) => {
agoraPara.uid = uid
}
const onStart = async () => {
const that = this
console.log(
'drone parameter',
dronePara.droneSelected,
dronePara.cameraSelected,
dronePara.videoSelected,
dronePara.claritySelected
)
const timestamp = new Date().getTime().toString()
const liveTimestamp = timestamp
if (
dronePara.droneSelected == null ||
dronePara.cameraSelected == null ||
dronePara.claritySelected == null
) {
message.warn('waring: not select live para!!!')
return
}
agoraClient.setClientRole('audience', { level: 2 })
if (agoraClient.connectionState === 'DISCONNECTED') {
await agoraClient.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
}
livePara.videoId =
dronePara.droneSelected +
'/' +
dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
console.log(agoraPara)
livePara.url =
'channel=' +
agoraPara.channel +
'&sn=' +
dronePara.droneSelected +
'&token=' +
encodeURIComponent(agoraPara.token) +
'&uid=' +
agoraPara.uid
startLivestream({
url: livePara.url,
video_id: livePara.videoId,
url_type: 0,
video_quality: dronePara.claritySelected
})
.then(res => {
if (res.code !== 1) {
return
}
livePara.liveState = true
})
.catch(err => {
console.error(err)
})
}
const onStop = async () => {
if (
dronePara.droneSelected == null ||
dronePara.cameraSelected == null ||
dronePara.claritySelected == null
) {
message.warn('waring: not select live para!!!')
return
}
livePara.videoId =
dronePara.droneSelected +
'/' +
dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
stopLivestream({
video_id: livePara.videoId
}).then(res => {
if (res.code === 1) {
message.success(res.message)
livePara.liveState = false
dronePara.lensSelected = ''
console.log('stop play livestream')
}
})
}
const onDroneSelect = (val: SelectOption) => {
dronePara.cameraList = []
dronePara.videoList = []
dronePara.lensList = []
dronePara.cameraSelected = undefined
dronePara.videoSelected = undefined
dronePara.lensSelected = undefined
dronePara.droneSelected = val.value
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
dronePara.cameraList.push({ label: ele.name, value: ele.index, more: ele.videos_list })
})
}
const onCameraSelect = (val: SelectOption) => {
dronePara.cameraSelected = val.value
dronePara.videoSelected = undefined
dronePara.lensSelected = undefined
dronePara.videoList = []
dronePara.lensList = []
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
dronePara.videoList.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
})
if (dronePara.videoList.length === 0) {
return
}
const firstVideo: SelectOption = dronePara.videoList[0]
dronePara.videoSelected = firstVideo.value
dronePara.lensList = firstVideo.more
dronePara.lensSelected = firstVideo.label
dronePara.isDockLive = dronePara.lensList?.length > 0
}
const onVideoSelect = (val: SelectOption) => {
dronePara.videoSelected = val.value
dronePara.lensList = val.more
dronePara.lensSelected = val.label
}
const onClaritySelect = (val: any) => {
dronePara.claritySelected = val
}
const onUpdateQuality = () => {
if (!livePara.liveState) {
message.info('Please turn on the livestream first.')
return
}
setLivestreamQuality({
video_id: livePara.videoId,
video_quality: dronePara.claritySelected
}).then(res => {
if (res.code === 1) {
message.success('Set the clarity to ' + clarityList[dronePara.claritySelected].label)
}
})
}
const onSwitch = () => {
if (dronePara.lensSelected === undefined || dronePara.lensSelected === nonSwitchable) {
message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
return
}
changeLivestreamLens({
video_id: livePara.videoId,
video_type: dronePara.lensSelected
}).then(res => {
if (res.code === 1) {
message.success('Switching live camera successfully.')
}
})
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
</style>

View File

@ -0,0 +1,441 @@
<template>
<div class="flex-column flex-justify-start flex-align-center">
<video
:style="{ width: '720px', height: '480px' }"
id="video-webrtc"
ref="videowebrtc"
controls
autoplay
class="mt20"
></video>
<p class="fz24">Live streaming source selection</p>
<div class="flex-row flex-justify-center flex-align-center mt10">
<template v-if="liveState && isDockLive">
<span class="mr10">Lens:</span>
<a-radio-group v-model:value="lensSelected" button-style="solid">
<a-radio-button v-for="lens in lensList" :key="lens" :value="lens">{{lens}}</a-radio-button>
</a-radio-group>
</template>
<template v-else>
<a-select
style="width: 150px"
placeholder="Select Live Type"
@select="onLiveTypeSelect"
v-model:value="livetypeSelected"
>
<a-select-option
v-for="item in liveTypeList"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Drone"
v-model:value="droneSelected"
>
<a-select-option
v-for="item in droneList"
:key="item.value"
:value="item.value"
@click="onDroneSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Camera"
v-model:value="cameraSelected"
>
<a-select-option
v-for="item in cameraList"
:key="item.value"
:value="item.value"
@click="onCameraSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
<!-- <a-select
class="ml10"
style="width:150px"
placeholder="Select Lens"
v-model:value="videoSelected"
>
<a-select-option
v-for="item in videoList"
:key="item.value"
:value="item.value"
@click="onVideoSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select> -->
</template>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Clarity"
@select="onClaritySelect"
v-model:value="claritySelected"
>
<a-select-option
v-for="item in clarityList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
</div>
<div class="mt20">
<p class="fz10" v-if="livetypeSelected == 2">
Please use VLC media player to play the RTSP livestream !!!
</p>
<p class="fz10" v-if="livetypeSelected == 2">
RTSP Parameter:{{ rtspData }}
</p>
</div>
<div class="mt10 flex-row flex-justify-center flex-align-center">
<a-button v-if="liveState && isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
<a-button v-else type="primary" large @click="onStart">Play</a-button>
<a-button class="ml20" type="primary" large @click="onStop"
>Stop</a-button
>
<a-button class="ml20" type="primary" large @click="onUpdateQuality"
>Update Clarity</a-button
>
<a-button v-if="!liveState || !isDockLive" class="ml20" type="primary" large @click="onRefresh"
>Refresh Live Capacity</a-button
>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, reactive, ref } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
import jswebrtc from '/@/vendors/jswebrtc.min.js'
import srs from '/@/vendors/srs.sdk.js'
const root = getRoot()
interface SelectOption {
value: any,
label: string,
more?: any
}
const liveTypeList: SelectOption[] = [
{
value: 1,
label: 'RTMP'
},
{
value: 2,
label: 'RTSP'
},
{
value: 3,
label: 'GB28181'
},
{
value: 4,
label: 'WEBRTC'
}
]
const clarityList: SelectOption[] = [
{
value: 0,
label: 'Adaptive'
},
{
value: 1,
label: 'Smooth'
},
{
value: 2,
label: 'Standard'
},
{
value: 3,
label: 'HD'
},
{
value: 4,
label: 'Super Clear'
}
]
const videowebrtc = ref(null)
const livestreamSource = ref()
const droneList = ref()
const cameraList = ref()
const videoList = ref()
const droneSelected = ref()
const cameraSelected = ref()
const videoSelected = ref()
const claritySelected = ref()
const videoId = ref()
const liveState = ref<boolean>(false)
const livetypeSelected = ref()
const rtspData = ref()
const lensList = ref<string[]>([])
const lensSelected = ref<String>()
const isDockLive = ref(false)
const nonSwitchable = 'normal'
let webrtc: any = null
const onRefresh = async () => {
droneList.value = []
cameraList.value = []
videoList.value = []
droneSelected.value = null
cameraSelected.value = null
videoSelected.value = null
await getLiveCapacity({})
.then(res => {
console.log(res)
if (res.code === 1) {
if (res.data === null) {
console.warn('warning: get live capacity is null!!!')
return
}
const resData: Array<[]> = res.data
console.log('live_capacity:', resData)
livestreamSource.value = resData
const temp: Array<SelectOption> = []
if (livestreamSource.value) {
livestreamSource.value.forEach((ele: any) => {
temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
})
droneList.value = temp
}
}
})
.catch(error => {
console.error(error)
console.error(error)
})
}
onMounted(() => {
onRefresh()
})
const onStart = async () => {
console.log(
'Param:',
livetypeSelected.value,
droneSelected.value,
cameraSelected.value,
videoSelected.value,
claritySelected.value
)
const timestamp = new Date().getTime().toString()
if (
livetypeSelected.value == null ||
droneSelected.value == null ||
cameraSelected.value == null ||
claritySelected.value == null
) {
message.warn('waring: not select live para!!!')
return
}
videoId.value =
droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
let liveURL = ''
switch (livetypeSelected.value) {
case 1: {
// RTMP
liveURL = config.rtmpURL + timestamp
break
}
case 2: {
// RTSP
liveURL = `userName=${config.rtspUserName}&password=${config.rtspPassword}&port=${config.rtspPort}`
break
}
case 3: {
liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}`
break
}
case 4: {
break
}
default:
console.warn('warning: live type is not correct!!!')
break
}
await startLivestream({
url: liveURL,
video_id: videoId.value,
url_type: livetypeSelected.value,
video_quality: claritySelected.value
})
.then(res => {
if (res.code !== 1) {
return
}
if (livetypeSelected.value === 3) {
const url = res.data.url
const videoElement = videowebrtc.value
// gb28181,it will fail if not wait.
message.loading({
content: 'Loding...',
duration: 4,
onClose () {
const player = new jswebrtc.Player(url, {
video: videoElement,
autoplay: true,
onPlay: (obj: any) => {
console.log('start play livestream')
}
})
}
})
} else if (livetypeSelected.value === 2) {
console.log(res)
rtspData.value = 'url:' + res.data.url
} else if (livetypeSelected.value === 1) {
const url = res.data.url
const videoElement = videowebrtc.value
console.log('start live:', url)
console.log(videoElement)
const player = new jswebrtc.Player(url, {
video: videoElement,
autoplay: true,
onPlay: (obj: any) => {
console.log('start play livestream')
}
})
} else if (livetypeSelected.value === 4) {
const videoElement = videowebrtc.value as unknown as HTMLMediaElement
videoElement.muted = true
playWebrtc(videoElement, res.data.url)
}
liveState.value = true
})
.catch(err => {
console.error(err)
})
}
const onStop = () => {
videoId.value =
droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
stopLivestream({
video_id: videoId.value
}).then(res => {
if (res.code === 1) {
message.success(res.message)
liveState.value = false
lensSelected.value = undefined
console.log('stop play livestream')
}
})
}
const onUpdateQuality = () => {
if (!liveState.value) {
message.info('Please turn on the livestream first.')
return
}
setLivestreamQuality({
video_id: videoId.value,
video_quality: claritySelected.value
}).then(res => {
if (res.code === 1) {
message.success('Set the clarity to ' + clarityList[claritySelected.value].label)
}
})
}
const onLiveTypeSelect = (val: any) => {
livetypeSelected.value = val
}
const onDroneSelect = (val: SelectOption) => {
droneSelected.value = val.value
const temp: Array<SelectOption> = []
cameraList.value = []
cameraSelected.value = undefined
videoSelected.value = undefined
videoList.value = []
lensList.value = []
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
temp.push({ label: ele.name, value: ele.index, more: ele.videos_list })
})
cameraList.value = temp
}
const onCameraSelect = (val: SelectOption) => {
cameraSelected.value = val.value
const result: Array<SelectOption> = []
videoSelected.value = undefined
videoList.value = []
lensList.value = []
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
result.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
})
videoList.value = result
if (videoList.value.length === 0) {
return
}
const firstVideo: SelectOption = videoList.value[0]
videoSelected.value = firstVideo.value
lensList.value = firstVideo.more
lensSelected.value = firstVideo.label
isDockLive.value = lensList.value?.length > 0
}
const onVideoSelect = (val: SelectOption) => {
videoSelected.value = val.value
lensList.value = val.more
lensSelected.value = val.label
}
const onClaritySelect = (val: any) => {
claritySelected.value = val
}
const onSwitch = () => {
if (lensSelected.value === undefined || lensSelected.value === nonSwitchable) {
message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
return
}
changeLivestreamLens({
video_id: videoId.value,
video_type: lensSelected.value
}).then(res => {
if (res.code === 1) {
message.success('Switching live camera successfully.')
}
})
}
const playWebrtc = (videoElement: HTMLMediaElement, url: string) => {
if (webrtc) {
webrtc.close()
}
webrtc = new srs.SrsRtcWhipWhepAsync()
videoElement.srcObject = webrtc.stream
webrtc.play(url).then(function (session: any) {
console.info(session)
}).catch(function (reason: any) {
webrtc.close()
console.error(reason)
})
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
</style>

View File

@ -0,0 +1,42 @@
<template>
<svg :class="svgClass" :aria-hidden="true" :style="{color: color, width:computedWidth, height:computedWidth}">
<use :xlink:href="iconName" :fill="color"/>
</svg>
</template>
<script setup>
import { defineProps, computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
color: {
type: String,
default: ''
},
size: {
type: Number,
},
})
const iconName = computed(() => `#icon-${props.name}`)
const svgClass = computed(() => {
console.log(props.name, 'props.name')
if (props.name) {
return `svg-icon icon-${props.name}`
}
return 'svg-icon'
})
const computedWidth = computed(() => {
const result = props.width || props.size
return result ? result + 'px' : '1em'
})
</script>
<style lang='scss'>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
vertical-align: middle;
}
</style>

View File

@ -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>

View File

@ -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 !== 1) {
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>

View File

@ -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,
}
}

View File

@ -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)
})
}

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More