Compare commits

...

16 Commits

Author SHA1 Message Date
zhangxu 9d8e1c6eae xxx 6 days ago
jessie.huo@dji.com b26ef22ab6 feat(v1.10.0): v1.10.0 版本增加 10 months ago
jessie.huo@dji.com ae62a2c5ef 1. 增加webrtc 功能示例;2. 增加自定义飞行区功能示例 11 months ago
sean.zhou 42854e4db7 Merge branch 'v1.8.0' 1 year ago
sean.zhou 3ef74070df What's new? 1 year ago
sean.zhou 91f0f86730 Merge branch 'v1.7.0' 1 year ago
sean.zhou 07ff9ab4cc What's new? 1 year ago
sean.zhou 763e39bd71 Merge branch 'v1.5.0' 2 years ago
sean.zhou 91fbc1342f What's new? 2 years ago
sean.zhou a6d5406418 fix the debugging parameters 2 years ago
sean.zhou f3e92fa131 initial v1.4.0 2 years ago
sean.zhou b09ed762ed Fix the issue that the data is not displayed when the map is not loaded. 2 years ago
sean.zhou 9650dd9f1d Fixed some issues 2 years ago
sean.zhou 20e5f4cd98 update device-firmware.ts 2 years ago
sean.zhou 83d4c07a67 initial v1.3.1 2 years ago
sean.zhou 31ad252341 initial v1.3.0 2 years ago
  1. 2
      .npmrc
  2. 5
      README.md
  3. 1525
      package-lock.json
  4. 9
      package.json
  5. 5185
      pnpm-lock.yaml
  6. 10
      src/App.vue
  7. 9
      src/api/device-cmd/index.ts
  8. 24
      src/api/device-setting/index.ts
  9. 58
      src/api/drc.ts
  10. 74
      src/api/drone-control/drone.ts
  11. 93
      src/api/drone-control/payload.ts
  12. 81
      src/api/flight-area/index.ts
  13. 21
      src/api/http/config.ts
  14. 34
      src/api/manage.ts
  15. 3
      src/api/media.ts
  16. 105
      src/api/wayline.ts
  17. 391
      src/components/GMap.vue
  18. 182
      src/components/TaskPanel.vue
  19. 3
      src/components/common/sidebar.vue
  20. 3
      src/components/common/topbar.vue
  21. 61
      src/components/devices/DeviceFirmwareStatus.vue
  22. 26
      src/components/devices/device-hms/DeviceHmsDrawer.vue
  23. 4
      src/components/devices/device-log/DeviceLogDetailModal.vue
  24. 4
      src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue
  25. 2
      src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue
  26. 59
      src/components/flight-area/FlightAreaActionIcon.vue
  27. 197
      src/components/flight-area/FlightAreaDevicePanel.vue
  28. 33
      src/components/flight-area/FlightAreaIcon.vue
  29. 89
      src/components/flight-area/FlightAreaItem.vue
  30. 43
      src/components/flight-area/FlightAreaPanel.vue
  31. 66
      src/components/flight-area/FlightAreaSyncPanel.vue
  32. 18
      src/components/flight-area/use-flight-area-drone-location-event.ts
  33. 17
      src/components/flight-area/use-flight-area-sync-progress-event.ts
  34. 30
      src/components/flight-area/use-flight-area-update.ts
  35. 155
      src/components/flight-area/use-flight-area.ts
  36. 242
      src/components/g-map/DeviceSettingBox.vue
  37. 106
      src/components/g-map/DeviceSettingPopover.vue
  38. 111
      src/components/g-map/DockControlPanel.vue
  39. 34
      src/components/g-map/DroneControlInfoPanel.vue
  40. 837
      src/components/g-map/DroneControlPanel.vue
  41. 115
      src/components/g-map/DroneControlPopover.vue
  42. 71
      src/components/g-map/use-connect-mqtt.ts
  43. 56
      src/components/g-map/use-device-setting.ts
  44. 36
      src/components/g-map/use-dock-control.ts
  45. 76
      src/components/g-map/use-drone-control-mqtt-event.ts
  46. 95
      src/components/g-map/use-drone-control-ws-event.ts
  47. 40
      src/components/g-map/use-drone-control.ts
  48. 165
      src/components/g-map/use-manual-control.ts
  49. 134
      src/components/g-map/use-mqtt.ts
  50. 120
      src/components/g-map/use-payload-control.ts
  51. 198
      src/components/livestream-agora.vue
  52. 213
      src/components/livestream-others.vue
  53. 450
      src/components/task/CreatePlan.vue
  54. 365
      src/components/task/TaskPanel.vue
  55. 73
      src/components/task/use-format-task.ts
  56. 43
      src/components/task/use-task-ws-event.ts
  57. 123
      src/components/wayline-panel.vue
  58. 15
      src/components/workspace/DividerLine.vue
  59. 21
      src/components/workspace/Title.vue
  60. 6
      src/constants/map.ts
  61. 38
      src/directives/drag-window.ts
  62. 6
      src/directives/index.ts
  63. 10
      src/event-bus/index.ts
  64. 209
      src/hooks/use-g-map-cover.ts
  65. 37
      src/hooks/use-g-map-tsa.ts
  66. 4
      src/hooks/use-g-map.ts
  67. 16
      src/hooks/use-map-tool.ts
  68. 58
      src/hooks/use-mouse-tool.ts
  69. 4
      src/main.ts
  70. 12
      src/mqtt/config.ts
  71. 117
      src/mqtt/index.ts
  72. 48
      src/pages/page-pilot/pilot-home.vue
  73. 28
      src/pages/page-pilot/pilot-liveshare.vue
  74. 325
      src/pages/page-web/projects/Firmwares.vue
  75. 251
      src/pages/page-web/projects/create-plan.vue
  76. 19
      src/pages/page-web/projects/devices.vue
  77. 88
      src/pages/page-web/projects/dock.vue
  78. 98
      src/pages/page-web/projects/flight-area.vue
  79. 50
      src/pages/page-web/projects/layer.vue
  80. 19
      src/pages/page-web/projects/livestream.vue
  81. 28
      src/pages/page-web/projects/task.vue
  82. 97
      src/pages/page-web/projects/tsa.vue
  83. 115
      src/pages/page-web/projects/wayline.vue
  84. 83
      src/pages/page-web/projects/workspace.vue
  85. 22
      src/router/index.ts
  86. 4
      src/shims-mqtt.d.ts
  87. 73
      src/store/index.ts
  88. 42
      src/types/airport-tsa.ts
  89. 112
      src/types/device-cmd.ts
  90. 38
      src/types/device-firmware.ts
  91. 148
      src/types/device-setting.ts
  92. 274
      src/types/device.ts
  93. 72
      src/types/drc.ts
  94. 83
      src/types/drone-control.ts
  95. 28
      src/types/enums.ts
  96. 80
      src/types/flight-area.ts
  97. 71
      src/types/live-stream.ts
  98. 3
      src/types/map-enum.ts
  99. 144
      src/types/task.ts
  100. 29
      src/types/wayline.ts
  101. Some files were not shown because too many files have changed in this diff Show More

2
.npmrc

@ -1 +1 @@ @@ -1 +1 @@
registry=https://registry.npm.taobao.org/
registry=https://registry.npmmirror.com/

5
README.md

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
## What is the DJI Cloud API?
The launch of the Cloud API mainly solves the problem of developers reinventing the wheel. For developers who do not need in-depth customization of APP, they can directly use DJI Pilot2 to communicate with the third cloud platform, and developers can focus on the development and implementation of cloud service interfaces.
The launch of the Cloud API mainly solves the problem of developers reinventing the wheel. For developers who do not need in-depth customization of APP, they can directly use DJI Pilot2 to communicate with the third cloud platform, and developers can focus on the development and implementation of cloud service interfaces.
## Docker
@ -14,9 +14,8 @@ For more documentation, please visit the [DJI Developer Documentation](https://d @@ -14,9 +14,8 @@ For more documentation, please visit the [DJI Developer Documentation](https://d
## Latest Release
Cloud API 1.1.0 was released on 22 July 2022. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
Cloud API 1.10.0 was released on 7 April 2024. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
## License
Cloud API is MIT-licensed. Please refer to the LICENSE file for more information.

1525
package-lock.json generated

File diff suppressed because it is too large Load Diff

9
package.json

@ -15,7 +15,9 @@ @@ -15,7 +15,9 @@
"agora-rtc-sdk-ng": "^4.12.1",
"ant-design-vue": "^2.2.8",
"axios": "^0.21.1",
"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",
@ -75,6 +77,7 @@ @@ -75,6 +77,7 @@
"ant-design-vue/es/empty/style/css",
"ant-design-vue/es/form/style/css",
"ant-design-vue/es/image/style/css",
"ant-design-vue/es/input-number/style/css",
"ant-design-vue/es/input/style/css",
"ant-design-vue/es/layout/style/css",
"ant-design-vue/es/menu/style/css",
@ -92,11 +95,17 @@ @@ -92,11 +95,17 @@
"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",

5185
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

10
src/App.vue

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<template>
<div id="demo-app" class="demo-app">
<div class="demo-app">
<router-view />
<!-- <div class="map-wrapper">
<GMap/>
@ -26,9 +26,17 @@ export default defineComponent({ @@ -26,9 +26,17 @@ export default defineComponent({
.demo-app {
width: 100%;
height: 100%;
.map-wrapper {
height: 100%;
width: 100%;
}
}
</style>
<style lang="scss">
#demo-app {
width: 100%;
height: 100%
}
</style>

9
src/api/device-cmd/index.ts

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

24
src/api/device-setting/index.ts

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

58
src/api/drc.ts

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { ELocalStorageKey } from '/@/types'
// DRC 链路
const DRC_API_PREFIX = '/control/api/v1'
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
export interface PostDrcBody {
client_id?: string // token过期时,用于续期则必填
expire_sec?: number // 过期时间,单位秒,默认3600
}
export interface DrcParams {
address: string
username: string
password: string
client_id: string
expire_time: number // 过期时间
enable_tls: boolean // 是否开启tls
}
// 获取 mqtt 连接认证
export async function postDrc (body: PostDrcBody): Promise<IWorkspaceResponse<DrcParams>> {
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/connect`, body)
return resp.data
}
export interface DrcEnterBody {
client_id: string
dock_sn: string
expire_sec?: number // 过期时间,单位秒,默认3600
device_info?: {
osd_frequency?: number
hsi_frequency?: number
}
}
export interface DrcEnterResp {
sub: string[] // 需要订阅接收的topic
pub: string[] // 推送的topic地址
}
// 进入飞行控制 (建立drc连接&获取云控控制权)
export async function postDrcEnter (body: DrcEnterBody): Promise<IWorkspaceResponse<DrcEnterResp>> {
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body)
return resp.data
}
export interface DrcExitBody {
client_id: string
dock_sn: string
}
// 退出飞行控制 (退出drc连接&退出云控控制权)
export async function postDrcExit (body: DrcExitBody): Promise<IWorkspaceResponse<null>> {
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/exit`, body)
return resp.data
}

74
src/api/drone-control/drone.ts

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
// import { ELocalStorageKey } from '/@/types'
const API_PREFIX = '/control/api/v1'
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
// 获取飞行控制权
export async function postFlightAuth (sn: string): Promise<IWorkspaceResponse<null>> {
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/flight`)
return resp.data
}
export enum WaylineLostControlActionInCommandFlight {
CONTINUE = 0,
EXEC_LOST_ACTION = 1
}
export enum LostControlActionInCommandFLight {
HOVER = 0, // 悬停
Land = 1, // 着陆
RETURN_HOME = 2, // 返航
}
export enum ERthMode {
SMART = 0,
SETTING = 1
}
export enum ECommanderModeLostAction {
CONTINUE = 0,
EXEC_LOST_ACTION = 1
}
export enum ECommanderFlightMode {
SMART = 0,
SETTING = 1
}
export interface PointBody {
latitude: number;
longitude: number;
height: number;
}
export interface PostFlyToPointBody {
max_speed: number,
points: PointBody[]
}
// 飞向目标点
export async function postFlyToPoint (sn: string, body: PostFlyToPointBody): Promise<IWorkspaceResponse<null>> {
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`, body)
return resp.data
}
// 停止飞向目标点
export async function deleteFlyToPoint (sn: string): Promise<IWorkspaceResponse<null>> {
const resp = await request.delete(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`)
return resp.data
}
export interface PostTakeoffToPointBody{
target_height: number;
target_latitude: number;
target_longitude: number;
security_takeoff_height: number; // 安全起飞高
max_speed: number; // flyto过程中能达到的最大速度, 单位m/s 跟飞机档位有关
rc_lost_action: LostControlActionInCommandFLight; // 失控行为
rth_altitude: number; // 返航高度
exit_wayline_when_rc_lost: WaylineLostControlActionInCommandFlight;
rth_mode: ERthMode;
commander_mode_lost_action: ECommanderModeLostAction;
commander_flight_mode: ECommanderFlightMode;
commander_flight_height: number;
}
// 一键起飞
export async function postTakeoffToPoint (sn: string, body: PostTakeoffToPointBody): Promise<IWorkspaceResponse<null>> {
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/takeoff-to-point`, body)
return resp.data
}

93
src/api/drone-control/payload.ts

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { CameraType, CameraMode } from '/@/types/live-stream'
import { GimbalResetMode } from '/@/types/drone-control'
// import { ELocalStorageKey } from '/@/types'
const API_PREFIX = '/control/api/v1'
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
export interface PostPayloadAuthBody {
payload_index: string
}
// 获取负载控制权
export async function postPayloadAuth (sn: string, body: PostPayloadAuthBody): Promise<IWorkspaceResponse<null>> {
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/payload`, body)
return resp.data
}
// TODO: 画面拖动控制
export enum PayloadCommandsEnum {
CameraModeSwitch = 'camera_mode_switch',
CameraPhotoTake = 'camera_photo_take',
CameraRecordingStart = 'camera_recording_start',
CameraRecordingStop = 'camera_recording_stop',
CameraFocalLengthSet = 'camera_focal_length_set',
GimbalReset = 'gimbal_reset',
CameraAim = 'camera_aim'
}
export interface PostCameraModeBody {
payload_index: string
camera_mode: CameraMode
}
export interface PostCameraPhotoBody {
payload_index: string
}
export interface PostCameraRecordingBody {
payload_index: string
}
export interface DeleteCameraRecordingParams {
payload_index: string
}
export interface PostCameraFocalLengthBody {
payload_index: string,
camera_type: CameraType,
zoom_factor: number
}
export interface PostGimbalResetBody{
payload_index: string,
reset_mode: GimbalResetMode,
}
export interface PostCameraAimBody{
payload_index: string,
camera_type: CameraType,
locked: boolean,
x: number,
y: number,
}
export type PostPayloadCommandsBody = {
cmd: PayloadCommandsEnum.CameraModeSwitch,
data: PostCameraModeBody
} | {
cmd: PayloadCommandsEnum.CameraPhotoTake,
data: PostCameraPhotoBody
} | {
cmd: PayloadCommandsEnum.CameraRecordingStart,
data: PostCameraRecordingBody
} | {
cmd: PayloadCommandsEnum.CameraRecordingStop,
data: DeleteCameraRecordingParams
} | {
cmd: PayloadCommandsEnum.CameraFocalLengthSet,
data: PostCameraFocalLengthBody
} | {
cmd: PayloadCommandsEnum.GimbalReset,
data: PostGimbalResetBody
} | {
cmd: PayloadCommandsEnum.CameraAim,
data: PostCameraAimBody
}
// 发送负载名称
export async function postPayloadCommands (sn: string, body: PostPayloadCommandsBody): Promise<IWorkspaceResponse<null>> {
const resp = await request.post(`${API_PREFIX}/devices/${sn}/payload/commands`, body)
return resp.data
}

81
src/api/flight-area/index.ts

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
import request from '../http/request'
import { IWorkspaceResponse } from '../http/type'
import { EFlightAreaType, ESyncStatus, FlightAreaContent } from './../../types/flight-area'
import { ELocalStorageKey } from '/@/types/enums'
import { GeojsonCoordinate } from '/@/utils/genjson'
export interface GetFlightArea {
area_id: string,
name: string,
type: EFlightAreaType,
content: FlightAreaContent,
status: boolean,
username: string,
create_time: number,
update_time: number,
}
export interface PostFlightAreaBody {
id: string,
name: string,
type: EFlightAreaType,
content: {
properties: {
color: string,
clampToGround: boolean,
},
geometry: {
type: string,
coordinates: GeojsonCoordinate | GeojsonCoordinate[][],
radius?: number,
}
}
}
export interface FlightAreaStatus {
sync_code: number,
sync_status: ESyncStatus,
sync_msg: string,
}
export interface GetDeviceStatus {
device_sn: string,
nickname?: string,
device_name?: string,
online?: boolean,
flight_area_status: FlightAreaStatus,
}
const MAP_API_PREFIX = '/map/api/v1'
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
export async function getFlightAreaList (): Promise<IWorkspaceResponse<GetFlightArea[]>> {
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-areas`)
return resp.data
}
export async function changeFlightAreaStatus (area_id: string, status: boolean): Promise<IWorkspaceResponse<any>> {
const resp = await request.put(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`, { status })
return resp.data
}
export async function saveFlightArea (body: PostFlightAreaBody): Promise<IWorkspaceResponse<any>> {
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area`, body)
return resp.data
}
export async function deleteFlightArea (area_id: string): Promise<IWorkspaceResponse<any>> {
const resp = await request.delete(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`)
return resp.data
}
export async function syncFlightArea (device_sn: string[]): Promise<IWorkspaceResponse<any>> {
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/sync`, { device_sn })
return resp.data
}
export async function getDeviceStatus (): Promise<IWorkspaceResponse<GetDeviceStatus[]>> {
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/device-status`)
return resp.data
}

21
src/api/http/config.ts

@ -1,17 +1,17 @@ @@ -1,17 +1,17 @@
export const CURRENT_CONFIG = {
// license
appId: 'Please enter the app id.', // You need to go to the development website to apply.
appKey: 'Please enter the app key.', // You need to go to the development website to apply.
appLicense: 'Please enter the app license.', // You need to go to the development website to apply.
appId: '156484', // You need to go to the development website to apply.
appKey: 'fdfca95ec7e04aa41c743b837f5b6c5', // You need to go to the development website to apply.
appLicense:
'rpyFPcmu47YOtazIxOYXZDJEmmboLVt3urGN2HbP7yocd2oNxvI2Ty3luE+KL0iM5rCPb0R+o8xR3IebOhSzDdK0DU21iujkuRCt70FdMTZdm8uRXC6NMJXfmej6BUFZtAhBhT+87jEnPGjhmfd51rMgOmovSsw/IndV6as3dbQ=', // You need to go to the development website to apply.
// http
baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
websocketURL: 'Please enter the WebSocket access address.', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
baseURL: 'http://47.113.196.86:30001', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
websocketURL: 'http://47.113.196.86:30001/api/v1/ws', // 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: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
rtmpURL: 'Please enter the rtmp access address.', // 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.',
@ -29,8 +29,7 @@ export const CURRENT_CONFIG = { @@ -29,8 +29,7 @@ export const CURRENT_CONFIG = {
agoraToken: 'Please enter the agora temporary token.',
agoraChannel: 'Please enter the agora channel.',
// map
// map
// You can apply on the AMap website.
amapKey: 'Please enter the amap key.',
amapKey: '8b0131c34d408f6663aae7a779e62cd1',
}

34
src/api/manage.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { Firmware, FirmwareQueryParam, FirmwareUploadParam } from '/@/types/device-firmware'
import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
import { Device } from '/@/types/device'
@ -24,7 +25,7 @@ export interface HmsQueryBody { @@ -24,7 +25,7 @@ export interface HmsQueryBody {
begin_time: number,
end_time: number,
message: string,
domain: string,
domain: number,
}
export const login = async function (body: LoginBody): Promise<IWorkspaceResponse<any>> {
@ -125,7 +126,7 @@ export const getDeviceBySn = async function (workspace_id: string, device_sn: st @@ -125,7 +126,7 @@ export const getDeviceBySn = async function (workspace_id: string, device_sn: st
* @param domain
* @returns
*/
export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: string): Promise<IListWorkspaceResponse<Device>> {
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
@ -150,7 +151,7 @@ export const updateDeviceHms = async function (workspace_id: string, device_sn: @@ -150,7 +151,7 @@ export const updateDeviceHms = async function (workspace_id: string, device_sn:
}
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}` +
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 !== '') {
@ -159,4 +160,29 @@ export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: st @@ -159,4 +160,29 @@ export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: st
})
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
}

3
src/api/media.ts

@ -15,12 +15,11 @@ export const downloadMediaFile = async function (workspaceId: string, fileId: st @@ -15,12 +15,11 @@ export const downloadMediaFile = async function (workspaceId: string, fileId: st
if (result.data.type === 'application/json') {
const reader = new FileReader()
reader.onload = function (e) {
let text = reader.result as string
const text = reader.result as string
const result = JSON.parse(text)
message.error(result.message)
}
reader.readAsText(result.data, 'utf-8')
return
} else {
return result.data
}

105
src/api/wayline.ts

@ -1,14 +1,9 @@ @@ -1,14 +1,9 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1'
import request, { IPage, IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
import { TaskType, TaskStatus, OutOfControlAction } from '/@/types/task'
import { WaylineType } from '/@/types/wayline'
export interface CreatePlan {
name: string,
file_id: string,
dock_sn: string,
immediate: boolean,
type: string,
}
const HTTP_PREFIX = '/wayline/api/v1'
// Get Wayline Files
export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> {
@ -24,12 +19,11 @@ export const downloadWaylineFile = async function (workspaceId: string, waylineI @@ -24,12 +19,11 @@ export const downloadWaylineFile = async function (workspaceId: string, waylineI
if (result.data.type === 'application/json') {
const reader = new FileReader()
reader.onload = function (e) {
let text = reader.result as string
const text = reader.result as string
const result = JSON.parse(text)
message.error(result.message)
}
reader.readAsText(result.data, 'utf-8')
return
} else {
return result.data
}
@ -42,6 +36,20 @@ export const deleteWaylineFile = async function (workspaceId: string, waylineId: @@ -42,6 +36,20 @@ export const deleteWaylineFile = async function (workspaceId: string, waylineId:
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`
@ -49,16 +57,83 @@ export const createPlan = async function (workspaceId: string, plan: CreatePlan) @@ -49,16 +57,83 @@ export const createPlan = async function (workspaceId: string, plan: CreatePlan)
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<IWorkspaceResponse<any>> {
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IListWorkspaceResponse<Task>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}`
const result = await request.get(url)
return result.data
}
// Execute Wayline Job
export const executeWaylineJobs = async function (workspaceId: string, plan_id: string): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${plan_id}`
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
}

391
src/components/GMap.vue

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
<template>
<div class="g-map-wrapper">
<!-- 地图区域 -->
<div id="g-container" :style="{ width: '100%', height: '100%' }" />
<!-- 绘制面板 -->
<div
class="g-action-panle"
class="g-action-panel"
:style="{ right: drawVisible ? '316px' : '16px' }"
>
<div :class="state.currentType === 'pin' ? 'g-action-item selection' : 'g-action-item'" @click="draw('pin', true)">
@ -11,20 +13,22 @@ @@ -11,20 +13,22 @@
<div :class="state.currentType === 'polyline' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polyline', true)">
<a><LineOutlined :rotate="135" class="fz20"/></a>
</div>
<div :class="state.currentType === 'polygon' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polygon', true)">
<div :class="state.currentType === 'polygon' && !state.isFlightArea ? 'g-action-item selection' : 'g-action-item'" @click="draw('polygon', true)">
<a><BorderOutlined class="fz18" /></a>
</div>
<FlightAreaActionIcon class="g-action-item mt10" :class="{'selection': mouseMode && state.isFlightArea}" @select-action="selectFlightAreaAction" @click="selectFlightAreaAction"/>
<div v-if="mouseMode" class="g-action-item" @click="draw('off', false)">
<a style="color: red;"><CloseOutlined /></a>
</div>
</div>
<div v-if="osdVisible.visible && !osdVisible.is_dock" class="osd-panel fz12">
<div class="pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 18%;">
<!-- 飞机OSD -->
<div v-if="osdVisible.visible && !osdVisible.is_dock" v-drag-window class="osd-panel fz12">
<div class="drag-title pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 18%;">
<span>{{ osdVisible.callsign }}</span>
<span><a class="fz16" style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
</div>
<div style="height: 82%;">
<div class="flex-column flex-align-center flex-justify-center" style="float: left; width: 60px; height: 100%; background: #2d2d2d;">
<div class="flex-column flex-align-center flex-justify-center" style="margin-top: -5px; padding-top: 25px; float: left; width: 60px; background: #2d2d2d;">
<a-tooltip :title="osdVisible.model">
<div style="width: 90%;" class="flex-column flex-align-center flex-justify-center">
<span><a-image :src="M30" :preview="false"/></span>
@ -46,7 +50,7 @@ @@ -46,7 +50,7 @@
<a-col span="6">
<a-tooltip title="RC Battery Level">
<span><ThunderboltOutlined class="fz14"/></span>
<span class="ml10">{{ deviceInfo.gateway && deviceInfo.gateway.capacity_percent !== str ? deviceInfo.gateway.capacity_percent + ' %' : deviceInfo.gateway.capacity_percent }}</span>
<span class="ml10">{{ deviceInfo.gateway && deviceInfo.gateway.capacity_percent !== str ? deviceInfo.gateway?.capacity_percent + ' %' : deviceInfo.gateway?.capacity_percent }}</span>
</a-tooltip>
</a-col>
@ -138,11 +142,11 @@ @@ -138,11 +142,11 @@
</div>
</div>
<!-- 机场OSD -->
<div v-if="osdVisible.visible && osdVisible.is_dock" class="osd-panel fz12">
<div class="fz16 pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 10%;">
<div v-if="osdVisible.visible && osdVisible.is_dock" v-drag-window class="osd-panel fz12">
<div class="drag-title fz16 pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 10%;">
<span>{{ osdVisible.gateway_callsign }}</span>
<span><a style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
</div>
<span><a style="color: white; position: absolute; top: 5px; right: 5px;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
<!-- 机场 -->
<div class ="flex-display" style="border-bottom: 1px solid #515151;">
<div class="flex-column flex-align-stretch flex-justify-center" style="width: 60px; background: #2d2d2d;">
@ -155,57 +159,62 @@ @@ -155,57 +159,62 @@
</div>
<div class="osd flex-1" style="flex: 1">
<a-row>
<a-col span="16" :style="deviceInfo.dock.mode_code === EDockModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">
{{ EDockModeCode[deviceInfo.dock.mode_code] }}</a-col>
<a-col span="16" :style="deviceInfo.dock.basic_osd?.mode_code === EDockModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">
{{ EDockModeCode[deviceInfo.dock.basic_osd?.mode_code] }}</a-col>
</a-row>
<a-row>
<a-col span="12">
<a-tooltip title="Accumulated Running Time">
<span><HistoryOutlined /></span>
<span class="ml10">
<span v-if="deviceInfo.dock.acc_time >= 2592000"> {{ Math.floor(deviceInfo.dock.acc_time / 2592000) }}m </span>
<span v-if="(deviceInfo.dock.acc_time % 2592000) >= 86400"> {{ Math.floor((deviceInfo.dock.acc_time % 2592000) / 86400) }}d </span>
<span v-if="(deviceInfo.dock.acc_time % 2592000 % 86400) >= 3600"> {{ Math.floor((deviceInfo.dock.acc_time % 2592000 % 86400) / 3600) }}h </span>
<span v-if="(deviceInfo.dock.acc_time % 2592000 % 86400 % 3600) >= 60"> {{ Math.floor((deviceInfo.dock.acc_time % 2592000 % 86400 % 3600) / 60) }}min </span>
<span>{{ Math.floor(deviceInfo.dock.acc_time % 2592000 % 86400 % 3600 % 60) }} s</span>
<span v-if="deviceInfo.dock.work_osd?.acc_time >= 2592000"> {{ Math.floor(deviceInfo.dock.work_osd?.acc_time / 2592000) }}m </span>
<span v-if="(deviceInfo.dock.work_osd?.acc_time % 2592000) >= 86400"> {{ Math.floor((deviceInfo.dock.work_osd?.acc_time % 2592000) / 86400) }}d </span>
<span v-if="(deviceInfo.dock.work_osd?.acc_time % 2592000 % 86400) >= 3600"> {{ Math.floor((deviceInfo.dock.work_osd?.acc_time % 2592000 % 86400) / 3600) }}h </span>
<span v-if="(deviceInfo.dock.work_osd?.acc_time % 2592000 % 86400 % 3600) >= 60"> {{ Math.floor((deviceInfo.dock.work_osd?.acc_time % 2592000 % 86400 % 3600) / 60) }}min </span>
<span>{{ Math.floor(deviceInfo.dock.work_osd?.acc_time % 2592000 % 86400 % 3600 % 60) }} s</span>
</span>
</a-tooltip>
</a-col>
<a-col span="12">
<a-tooltip title="Last login">
<a-tooltip title="Activation time">
<span><FieldTimeOutlined /></span>
<span class="ml10">{{ new Date(deviceInfo.dock.first_power_on).toLocaleString() }}
<span class="ml10">{{ new Date((deviceInfo.dock.work_osd?.activation_time ?? 0) * 1000).toLocaleString() }}
</span>
</a-tooltip>
</a-col>
</a-row>
<a-row>
<a-col span="12">
<a-col span="6">
<a-tooltip title="Network State">
<span :style="deviceInfo.dock.network_state.quality === 2 ? 'color: #00ee8b' :
deviceInfo.dock.network_state.quality === 1 ? 'color: yellow' : 'color: red'">
<span v-if="deviceInfo.dock.network_state.type === 1"><SignalFilled /></span>
<span :style="qualityStyle">
<span v-if="deviceInfo.dock.basic_osd?.network_state?.type === NetworkStateTypeEnum.FOUR_G"><SignalFilled /></span>
<span v-else><GlobalOutlined /></span>
</span>
<span class="ml10" >{{ deviceInfo.dock.network_state.rate }} KB/S</span>
<span class="ml10" >{{ deviceInfo.dock.basic_osd?.network_state?.rate }} kb/s</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="The total number of times the dock has performed missions.">
<span><CarryOutOutlined /></span>
<span class="ml10" >{{ deviceInfo.dock.work_osd?.job_number }} </span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Media File Remain Upload">
<span><CloudUploadOutlined class="fz14"/></span>
<span class="ml10">{{ deviceInfo.dock.media_file_detail?.remain_upload }}</span>
<span class="ml10">{{ deviceInfo.dock.link_osd?.media_file_detail?.remain_upload }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip>
<template #title>
<p>total: {{ deviceInfo.dock.storage.total }}</p>
<p>used: {{ deviceInfo.dock.storage.used }}</p>
<p>total: {{ deviceInfo.dock.basic_osd?.storage?.total }}</p>
<p>used: {{ deviceInfo.dock.basic_osd?.storage?.used }}</p>
</template>
<span><FolderOpenOutlined /></span>
<span class="ml10" v-if="deviceInfo.dock.storage.total > 0">
<a-progress type="circle" :width="20" :percent="deviceInfo.dock.storage.used * 100/ deviceInfo.dock.storage.total"
:strokeWidth="20" :showInfo="false" :strokeColor="deviceInfo.dock.storage.used * 100 / deviceInfo.dock.storage.total > 80 ? 'red' : '#00ee8b' "/>
<span class="ml10" v-if="deviceInfo.dock.basic_osd?.storage?.total > 0">
<a-progress type="circle" :width="20" :percent="deviceInfo.dock.basic_osd?.storage?.used * 100/ deviceInfo.dock.basic_osd?.storage?.total"
:strokeWidth="20" :showInfo="false" :strokeColor="deviceInfo.dock.basic_osd?.storage?.used * 100 / deviceInfo.dock.basic_osd?.storage?.total > 80 ? 'red' : '#00ee8b' "/>
</span>
</a-tooltip>
</a-col>
@ -214,62 +223,63 @@ @@ -214,62 +223,63 @@
<a-col span="6">
<a-tooltip title="Wind Speed">
<span>W.S</span>
<span class="ml10">{{ deviceInfo.dock.wind_speed === str ? str : (deviceInfo.dock.wind_speed / 10).toFixed(2) + ' m/s'}}</span>
<span class="ml10">{{ (deviceInfo.dock.basic_osd?.wind_speed ?? str) + ' m/s'}}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Rainfall">
<span>🌧</span>
<span class="ml10">{{ deviceInfo.dock.rainfall === str ? str : deviceInfo.dock.rainfall + ' mm/h' }}</span>
<span class="ml10">{{ RainfallEnum[deviceInfo.dock.basic_osd?.rainfall] }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Environment Temperature">
<span>°C</span>
<span class="ml10">{{ deviceInfo.dock.environment_temperature }}</span>
<span class="ml10">{{ deviceInfo.dock.basic_osd?.environment_temperature }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Environment Humidity">
<span>💦</span>
<span class="ml10">{{ deviceInfo.dock.environment_humidity === str ? str : deviceInfo.dock.environment_humidity }}</span>
</a-tooltip>
</a-col>
</a-row>
<a-row>
<a-col span="6">
<a-tooltip title="Dock Temperature">
<span>°C</span>
<span class="ml10">{{ deviceInfo.dock.temperature }}</span>
<span class="ml10">{{ deviceInfo.dock.basic_osd?.temperature }}</span>
</a-tooltip>
</a-col>
</a-row>
<a-row>
<a-col span="6">
<a-tooltip title="Dock Humidity">
<span>💦</span>
<span class="ml10">{{ deviceInfo.dock.humidity === str ? str : deviceInfo.dock.humidity }}</span>
<span class="ml10">{{ deviceInfo.dock.basic_osd?.humidity }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Working Voltage">
<span style="border: 1px solid; border-radius: 50%; width: 18px; height: 18px; line-height: 16px; text-align: center; float: left;">V</span>
<span class="ml10">{{ deviceInfo.dock.working_voltage === str ? str : deviceInfo.dock.working_voltage + ' mV' }}</span>
<span class="ml10">{{ (deviceInfo.dock.work_osd?.working_voltage ?? str) + ' mV' }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Working Current">
<span style="border: 1px solid; border-radius: 50%; width: 18px; height: 18px; line-height: 15px; text-align: center; float: left;" >A</span>
<span class="ml10">{{ deviceInfo.dock.working_current === str ? str : deviceInfo.dock.working_current + ' mA' }}</span>
<span class="ml10">{{ (deviceInfo.dock.work_osd?.working_current ?? str) + ' mA' }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Drone in dock">
<span><RocketOutlined /></span>
<span class="ml10">{{ deviceInfo.dock.basic_osd?.drone_in_dock }}</span>
</a-tooltip>
</a-col>
</a-row>
<a-row class="p5">
<a-col span="24">
<a-button type="primary" :disabled="controlPanelVisible" size="small" @click="dockDebugOnOff(osdVisible.gateway_sn, true)">
远程调试
<a-button type="primary" :disabled="dockControlPanelVisible" size="small" @click="setDockControlPanelVisible(true)">
Actions
</a-button>
</a-col>
</a-row>
<DockControlPanel v-if="controlPanelVisible" :sn="osdVisible.gateway_sn" :deviceInfo="deviceInfo" @close-control-panel="dockDebugOnOff">
<!-- 机场控制面板 -->
<DockControlPanel v-if="dockControlPanelVisible" :sn="osdVisible.gateway_sn" :deviceInfo="deviceInfo" @close-control-panel="onCloseControlPanel">
</DockControlPanel>
</div>
</div>
@ -292,13 +302,13 @@ @@ -292,13 +302,13 @@
<a-col span="6">
<a-tooltip title="Upward Quality">
<span><SignalFilled /><ArrowUpOutlined style="font-size: 9px; vertical-align: top;" /></span>
<span class="ml10">{{ deviceInfo.dock.sdr?.up_quality }}</span>
<span class="ml10">{{ deviceInfo.dock.link_osd?.sdr?.up_quality }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip title="Downward Quality">
<span><SignalFilled /><ArrowDownOutlined style="font-size: 9px; vertical-align: top;" /></span>
<span class="ml10">{{ deviceInfo.dock.sdr?.down_quality }}</span>
<span class="ml10">{{ deviceInfo.dock.link_osd?.sdr?.down_quality }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
@ -307,6 +317,19 @@ @@ -307,6 +317,19 @@
<span class="ml10">{{ deviceInfo.device && deviceInfo.device.battery.capacity_percent !== str ? deviceInfo.device?.battery.capacity_percent + ' %' : str }}</span>
</a-tooltip>
</a-col>
<a-col span="6">
<a-tooltip>
<template #title>
<p>total: {{ deviceInfo.device?.storage?.total }}</p>
<p>used: {{ deviceInfo.device?.storage?.used }}</p>
</template>
<span><FolderOpenOutlined /></span>
<span class="ml10" v-if="deviceInfo.device?.storage?.total > 0">
<a-progress type="circle" :width="20" :percent="deviceInfo.device?.storage?.used * 100/ deviceInfo.device?.storage?.total"
:strokeWidth="20" :showInfo="false" :strokeColor="deviceInfo.device?.storage?.used * 100 / deviceInfo.device?.storage?.total > 80 ? 'red' : '#00ee8b' "/>
</span>
</a-tooltip>
</a-col>
</a-row>
<a-row>
<a-tooltip title="RTK Fixed">
@ -387,7 +410,19 @@ @@ -387,7 +410,19 @@
{{ 10 > (deviceInfo.device.battery.remain_flight_time % 60) ? '0' : ''}}{{deviceInfo.device.battery.remain_flight_time % 60 }}
</div>
</div>
<!-- 飞行指令 -->
<DroneControlPanel :sn="osdVisible.gateway_sn" :deviceInfo="deviceInfo" :payloads="osdVisible.payloads"></DroneControlPanel>
</div>
<!-- liveview -->
<div class="liveview" v-if="livestreamOthersVisible" v-drag-window >
<div style="height: 40px; width: 100%" class="drag-title"></div>
<a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="closeLivestreamOthers"><CloseOutlined /></a>
<LivestreamOthers />
</div>
<div class="liveview" v-if="livestreamAgoraVisible" v-drag-window >
<div style="height: 40px; width: 100%" class="drag-title"></div>
<a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="closeLivestreamAgora"><CloseOutlined /></a>
<LivestreamAgora />
</div>
</div>
</template>
@ -404,7 +439,7 @@ import { MapDoodleType, MapElementEnum } from '/@/constants/map' @@ -404,7 +439,7 @@ import { MapDoodleType, MapElementEnum } from '/@/constants/map'
import { useGMapManage } from '/@/hooks/use-g-map'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { useMouseTool } from '/@/hooks/use-mouse-tool'
import { getApp } from '/@/root'
import { getApp, getRoot } from '/@/root'
import { useMyStore } from '/@/store'
import { GeojsonCoordinate } from '/@/types/map'
import { MapDoodleEnum } from '/@/types/map-enum'
@ -412,17 +447,28 @@ import { PostElementsBody } from '/@/types/mapLayer' @@ -412,17 +447,28 @@ import { PostElementsBody } from '/@/types/mapLayer'
import { uuidv4 } from '/@/utils/uuid'
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform'
import { deviceTsaUpdate } from '/@/hooks/use-g-map-tsa'
import { DeviceOsd, DeviceStatus, DockOsd, EGear, EModeCode, GatewayOsd, EDockModeCode } from '/@/types/device'
import {
DeviceOsd, DeviceStatus, DockOsd, EGear, EModeCode, GatewayOsd, EDockModeCode,
NetworkStateQualityEnum, NetworkStateTypeEnum, RainfallEnum, DroneInDockEnum
} from '/@/types/device'
import pin from '/@/assets/icons/pin-2d8cf0.svg'
import M30 from '/@/assets/icons/m30.png'
import {
BorderOutlined, LineOutlined, CloseOutlined, ControlOutlined, TrademarkOutlined, ArrowDownOutlined,
ThunderboltOutlined, SignalFilled, GlobalOutlined, HistoryOutlined, CloudUploadOutlined,
FieldTimeOutlined, CloudOutlined, CloudFilled, FolderOpenOutlined, RobotFilled, ArrowUpOutlined
ThunderboltOutlined, SignalFilled, GlobalOutlined, HistoryOutlined, CloudUploadOutlined, RocketOutlined,
FieldTimeOutlined, CloudOutlined, CloudFilled, FolderOpenOutlined, RobotFilled, ArrowUpOutlined, CarryOutOutlined
} from '@ant-design/icons-vue'
import { EDeviceTypeName } from '../types'
import DockControlPanel from './g-map/DockControlPanel.vue'
import { useDockControl } from './g-map/useDockControl'
import { useDockControl } from './g-map/use-dock-control'
import DroneControlPanel from './g-map/DroneControlPanel.vue'
import { useConnectMqtt } from './g-map/use-connect-mqtt'
import LivestreamOthers from './livestream-others.vue'
import LivestreamAgora from './livestream-agora.vue'
import FlightAreaActionIcon from './flight-area/FlightAreaActionIcon.vue'
import { EFlightAreaType } from '../types/flight-area'
import { useFlightArea } from './flight-area/use-flight-area'
import { useFlightAreaDroneLocationEvent } from './flight-area/use-flight-area-drone-location-event'
export default defineComponent({
components: {
@ -443,20 +489,28 @@ export default defineComponent({ @@ -443,20 +489,28 @@ export default defineComponent({
RobotFilled,
ArrowUpOutlined,
ArrowDownOutlined,
DockControlPanel
DockControlPanel,
DroneControlPanel,
CarryOutOutlined,
RocketOutlined,
LivestreamOthers,
LivestreamAgora,
FlightAreaActionIcon,
},
name: 'GMap',
props: {},
setup () {
const useMouseToolHook = useMouseTool()
const useGMapManageHook = useGMapManage()
const deviceTsaUpdateHook = ref()
const deviceTsaUpdateHook = deviceTsaUpdate()
const root = getRoot()
const mouseMode = ref(false)
const store = useMyStore()
const state = reactive({
currentType: '',
coverIndex: 0
coverIndex: 0,
isFlightArea: false,
})
const str: string = '--'
const deviceInfo = reactive({
@ -465,52 +519,6 @@ export default defineComponent({ @@ -465,52 +519,6 @@ export default defineComponent({
transmission_signal_quality: str,
} as GatewayOsd,
dock: {
media_file_detail: {
remain_upload: 0
},
sdr: {
up_quality: str,
down_quality: str,
frequency_band: -1,
},
network_state: {
type: 0,
quality: 0,
rate: 0,
},
drone_in_dock: 0,
drone_charge_state: {
state: 0,
capacity_percent: str,
},
rainfall: str,
wind_speed: str,
environment_temperature: str,
environment_humidity: str,
temperature: str,
humidity: str,
job_number: 0,
acc_time: 0,
first_power_on: 0,
positionState: {
gps_number: str,
is_fixed: 0,
rtk_number: str,
is_calibration: 0,
quality: 0,
},
storage: {
total: 0,
used: 0,
},
electric_supply_voltage: 0,
working_voltage: str,
working_current: str,
backup_battery_voltage: 0,
mode_code: -1,
cover_state: -1,
supplement_light_state: -1,
putter_state: -1,
} as DockOsd,
device: {
@ -547,19 +555,33 @@ export default defineComponent({ @@ -547,19 +555,33 @@ export default defineComponent({
const drawVisible = computed(() => {
return store.state.drawVisible
})
const livestreamOthersVisible = computed(() => {
return store.state.livestreamOthersVisible
})
const livestreamAgoraVisible = computed(() => {
return store.state.livestreamAgoraVisible
})
const osdVisible = computed(() => {
return store.state.osdVisible
})
const qualityStyle = computed(() => {
if (deviceInfo.dock.basic_osd?.network_state?.type === NetworkStateTypeEnum.ETHERNET ||
(deviceInfo.dock.basic_osd?.network_state?.quality || 0) > NetworkStateQualityEnum.FAIR) {
return 'color: #00ee8b'
}
if ((deviceInfo.dock.basic_osd?.network_state?.quality || 0) === NetworkStateQualityEnum.FAIR) {
return 'color: yellow'
}
return 'color: red'
})
watch(() => store.state.deviceStatusEvent,
data => {
deviceTsaUpdateHook.value = deviceTsaUpdate()
if (Object.keys(data.deviceOnline).length !== 0) {
deviceTsaUpdateHook.value.initMarker(data.deviceOnline.domain, data.deviceOnline.device_callsign, data.deviceOnline.sn)
deviceTsaUpdateHook.initMarker(data.deviceOnline.domain, data.deviceOnline.device_callsign, data.deviceOnline.sn)
store.state.deviceStatusEvent.deviceOnline = {} as DeviceStatus
}
if (Object.keys(data.deviceOffline).length !== 0) {
deviceTsaUpdateHook.value.removeMarker(data.deviceOffline.sn)
deviceTsaUpdateHook.removeMarker(data.deviceOffline.sn)
if ((data.deviceOffline.sn === osdVisible.value.sn) || (osdVisible.value.is_dock && data.deviceOffline.sn === osdVisible.value.gateway_sn)) {
osdVisible.value.visible = false
store.commit('SET_OSD_VISIBLE_INFO', osdVisible)
@ -573,25 +595,26 @@ export default defineComponent({ @@ -573,25 +595,26 @@ export default defineComponent({
)
watch(() => store.state.deviceState, data => {
if (!deviceTsaUpdateHook.value) {
deviceTsaUpdateHook.value = deviceTsaUpdate()
}
if (data.currentType === EDeviceTypeName.Gateway && data.gatewayInfo[data.currentSn]) {
deviceTsaUpdateHook.value.moveTo(data.currentSn, data.gatewayInfo[data.currentSn].longitude, data.gatewayInfo[data.currentSn].latitude)
const coordinate = wgs84togcj02(data.gatewayInfo[data.currentSn].longitude, data.gatewayInfo[data.currentSn].latitude)
deviceTsaUpdateHook.moveTo(data.currentSn, coordinate[0], coordinate[1])
if (osdVisible.value.visible && osdVisible.value.gateway_sn !== '') {
deviceInfo.gateway = data.gatewayInfo[osdVisible.value.gateway_sn]
}
}
if (data.currentType === EDeviceTypeName.Aircraft && data.deviceInfo[data.currentSn]) {
deviceTsaUpdateHook.value.moveTo(data.currentSn, data.deviceInfo[data.currentSn].longitude, data.deviceInfo[data.currentSn].latitude)
const coordinate = wgs84togcj02(data.deviceInfo[data.currentSn].longitude, data.deviceInfo[data.currentSn].latitude)
deviceTsaUpdateHook.moveTo(data.currentSn, coordinate[0], coordinate[1])
if (osdVisible.value.visible && osdVisible.value.sn !== '') {
deviceInfo.device = data.deviceInfo[osdVisible.value.sn]
}
}
if (data.currentType === EDeviceTypeName.Dock && data.dockInfo[data.currentSn]) {
const coordinate = wgs84togcj02(data.dockInfo[data.currentSn].basic_osd?.longitude, data.dockInfo[data.currentSn].basic_osd?.latitude)
deviceTsaUpdateHook.initMarker(EDeviceTypeName.Dock, EDeviceTypeName[EDeviceTypeName.Dock], data.currentSn, coordinate[0], coordinate[1])
if (osdVisible.value.visible && osdVisible.value.is_dock && osdVisible.value.gateway_sn !== '') {
deviceInfo.dock = data.dockInfo[osdVisible.value.gateway_sn]
deviceInfo.device = data.deviceInfo[deviceInfo.dock.sub_device?.device_sn]
deviceInfo.device = data.deviceInfo[deviceInfo.dock.basic_osd.sub_device?.device_sn ?? osdVisible.value.sn]
}
}
}, {
@ -623,15 +646,27 @@ export default defineComponent({ @@ -623,15 +646,27 @@ export default defineComponent({
})
updateCoordinates('wgs84-gcj02', ele)
useGMapCoverHook.init2DPin(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
{
id: ele.id,
name: ele.name
}
)
const data = { id: ele.id, name: ele.name }
if (MapElementEnum.PIN === ele.resource?.type) {
useGMapCoverHook.init2DPin(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
data
)
} else if (MapElementEnum.LINE === ele.resource?.type) {
useGMapCoverHook.initPolyline(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
data)
} else if (MapElementEnum.POLY === ele.resource?.type) {
useGMapCoverHook.initPolygon(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
data)
}
}
store.state.wsEvent.mapElementCreat = {}
@ -652,25 +687,40 @@ export default defineComponent({ @@ -652,25 +687,40 @@ export default defineComponent({
}
)
function draw (type: MapDoodleType, bool: boolean) {
function draw (type: MapDoodleType, bool: boolean, flightAreaType?: EFlightAreaType) {
state.currentType = type
useMouseToolHook.mouseTool(type, getDrawCallback)
mouseMode.value = bool
state.isFlightArea = !!flightAreaType
useMouseToolHook.mouseTool(type, getDrawCallback, flightAreaType)
}
// dock
// dock
const {
controlPanelVisible,
setControlPanelVisible,
sendDockControlCmd,
dockDebugOnOff,
dockControlPanelVisible,
setDockControlPanelVisible,
onCloseControlPanel,
} = useDockControl()
// drc
useConnectMqtt()
onMounted(() => {
const app = getApp()
useGMapManageHook.globalPropertiesConfig(app)
})
function getDrawCallback ({ obj }) {
const { getDrawFlightAreaCallback, onFlightAreaDroneLocationWs } = useFlightArea()
useFlightAreaDroneLocationEvent(onFlightAreaDroneLocationWs)
function selectFlightAreaAction ({ type, isCircle }: { type: EFlightAreaType, isCircle: boolean }) {
draw(isCircle ? MapDoodleEnum.CIRCLE : MapDoodleEnum.POLYGON, true, type)
}
function getDrawCallback ({ obj }: { obj : any }) {
if (state.isFlightArea) {
getDrawFlightAreaCallback(obj)
return
}
switch (state.currentType) {
case MapDoodleEnum.PIN:
postPinPositionResource(obj)
@ -693,8 +743,7 @@ export default defineComponent({ @@ -693,8 +743,7 @@ export default defineComponent({
(req.resource.content.geometry.coordinates as GeojsonCoordinate).push((coordinates as GeojsonCoordinate)[2])
const result = await postElementsReq(shareId.value, req)
obj.setExtData({ id: req.id, name: req.name })
store.state.coverList.push(obj)
// console.log(store.state.coverList)
store.state.coverMap[req.id] = [obj]
}
async function postPolylineResource (obj) {
const req = getPolylineResource(obj)
@ -702,8 +751,7 @@ export default defineComponent({ @@ -702,8 +751,7 @@ export default defineComponent({
updateCoordinates('gcj02-wgs84', req)
const result = await postElementsReq(shareId.value, req)
obj.setExtData({ id: req.id, name: req.name })
store.state.coverList.push(obj)
// console.log(store.state.coverList)
store.state.coverMap[req.id] = [obj]
}
async function postPolygonResource (obj) {
const req = getPoygonResource(obj)
@ -711,8 +759,7 @@ export default defineComponent({ @@ -711,8 +759,7 @@ export default defineComponent({
updateCoordinates('gcj02-wgs84', req)
const result = await postElementsReq(shareId.value, req)
obj.setExtData({ id: req.id, name: req.name })
store.state.coverList.push(obj)
// console.log(store.state.coverList)
store.state.coverMap[req.id] = [obj]
}
function getPinPositionResource (obj) {
@ -762,6 +809,12 @@ export default defineComponent({ @@ -762,6 +809,12 @@ export default defineComponent({
console.log('layers', layers)
store.commit('SET_LAYER_INFO', layers)
}
function closeLivestreamOthers () {
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', false)
}
function closeLivestreamAgora () {
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', false)
}
function updateCoordinates (transformType: string, element: any) {
const geoType = element.resource?.content.geometry.type
const type = element.resource?.type as number
@ -782,39 +835,38 @@ export default defineComponent({ @@ -782,39 +835,38 @@ export default defineComponent({
) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult
}
} else if (MapElementEnum.LINE === type && geoType === 'LineString') {
} else if (MapElementEnum.LINE === type) {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
}
element.resource.content.geometry.coordinates = coordinates
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') {
} else if (MapElementEnum.POLY === type) {
const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
@ -828,6 +880,8 @@ export default defineComponent({ @@ -828,6 +880,8 @@ export default defineComponent({
draw,
mouseMode,
drawVisible,
livestreamOthersVisible,
livestreamAgoraVisible,
osdVisible,
pin,
state,
@ -837,8 +891,17 @@ export default defineComponent({ @@ -837,8 +891,17 @@ export default defineComponent({
EModeCode,
str,
EDockModeCode,
controlPanelVisible,
dockDebugOnOff,
dockControlPanelVisible,
setDockControlPanelVisible,
onCloseControlPanel,
NetworkStateTypeEnum,
NetworkStateQualityEnum,
RainfallEnum,
DroneInDockEnum,
closeLivestreamOthers,
closeLivestreamAgora,
qualityStyle,
selectFlightAreaAction,
}
}
})
@ -850,7 +913,7 @@ export default defineComponent({ @@ -850,7 +913,7 @@ export default defineComponent({
height: 100%;
width: 100%;
.g-action-panle {
.g-action-panel {
position: absolute;
top: 16px;
right: 16px;
@ -883,16 +946,17 @@ export default defineComponent({ @@ -883,16 +946,17 @@ export default defineComponent({
}
.osd-panel {
position: absolute;
left: 350px;
margin-left: 10px;
left: 0;
top: 10px;
width: 480px;
background: black;
color: white;
background: #000;
color: #fff;
border-radius: 2px;
opacity: 0.7;
opacity: 0.8;
}
.osd > div {
padding-top: 5px;
.osd > div:not(.dock-control-panel) {
margin-top: 5px;
padding-left: 5px;
}
@ -934,4 +998,17 @@ export default defineComponent({ @@ -934,4 +998,17 @@ export default defineComponent({
min-height: 2px;
border-radius: 2px;
}
.liveview {
position: absolute;
color: #fff;
z-index: 1;
left: 0;
margin-left: 10px;
top: 10px;
text-align: center;
width: 800px;
height: 720px;
background: #232323;
}
</style>

182
src/components/TaskPanel.vue

@ -1,182 +0,0 @@ @@ -1,182 +0,0 @@
<template>
<div class="header">Task Plan Library</div>
<div class="plan-panel-wrapper">
<!-- <router-link :to=" '/' + ERouterName.CREATE_PLAN">
<a-button type="primary">Create Plan</a-button>
</router-link> -->
<a-table class="plan-table" :columns="columns" :data-source="plansData.data" row-key="job_id"
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
<template #status="{ record }">
<span v-if="taskProgressMap[record.bid]">
<a-progress type="line" :percent="taskProgressMap[record.bid]?.progress?.percent"
:status="taskProgressMap[record.bid]?.status.indexOf(ETaskStatus.FAILED) != -1 ? 'exception' : taskProgressMap[record.bid]?.status.indexOf(ETaskStatus.OK) != -1 ? 'success' : 'normal'">
<template #format="percent">
<a-tooltip :title="taskProgressMap[record.bid]?.status">
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;">
{{ percent }}% {{ taskProgressMap[record.bid]?.status }}
</div>
</a-tooltip>
</template>
</a-progress>
</span>
</template>
<template #action="{ record }">
<span class="action-area">
<a-popconfirm
title="Are you sure execute this task?"
ok-text="Yes"
cancel-text="No"
@confirm="executePlan(record.job_id)"
>
<a-button type="primary" size="small">Execute</a-button>
</a-popconfirm>
</span>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { TableState } from 'ant-design-vue/lib/table/interface'
import { computed, onMounted, watch } from 'vue'
import { IPage } from '../api/http/type'
import { executeWaylineJobs, getWaylineJobs } from '../api/wayline'
import { getRoot } from '../root'
import { useMyStore } from '../store'
import { ELocalStorageKey, ERouterName } from '../types/enums'
import router from '/@/router'
import { ETaskStatus } from '/@/types/wayline'
const store = useMyStore()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const root = getRoot()
const body: IPage = {
page: 1,
total: 0,
page_size: 50
}
const paginationProp = reactive({
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
})
const columns = [
{
title: 'Plan Name',
dataIndex: 'job_name'
},
{
title: 'Flight Route Name',
dataIndex: 'file_name',
ellipsis: true
},
{
title: 'Dock Name',
dataIndex: 'dock_name',
ellipsis: true
},
{
title: 'Creator',
dataIndex: 'username',
},
{
title: 'Updated',
dataIndex: 'update_time'
},
{
title: 'Status',
key: 'status',
width: 200,
slots: { customRender: 'status' }
},
{
title: 'Action',
slots: { customRender: 'action' }
}
]
type Pagination = TableState['pagination']
interface TaskPlan {
bid: string,
job_id: string,
job_name: string,
file_name: string,
dock_name: string,
username: string,
create_time: string,
}
const plansData = reactive({
data: [] as TaskPlan[]
})
function createPlan () {
root.$router.push('/' + ERouterName.CREATE_PLAN)
}
const taskProgressMap = computed(() => store.state.taskProgressInfo)
onMounted(() => {
getPlans()
})
function getPlans () {
getWaylineJobs(workspaceId, body).then(res => {
if (res.code !== 0) {
return
}
plansData.data = res.data.list
paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page
})
}
function refreshData (page: Pagination) {
body.page = page?.current!
body.page_size = page?.pageSize!
getPlans()
}
function executePlan (jobId: string) {
executeWaylineJobs(workspaceId, jobId).then(res => {
if (res.code === 0) {
message.success('Executed Successfully')
getPlans()
}
})
}
</script>
<style lang="scss" scoped>
.plan-panel-wrapper {
width: 100%;
padding: 16px;
.plan-table {
background: #fff;
margin-top: 10px;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
.header {
width: 100%;
height: 60px;
background: #fff;
padding: 16px;
font-size: 20px;
font-weight: bold;
text-align: start;
color: #000;
}
</style>

3
src/components/common/sidebar.vue

@ -58,7 +58,8 @@ export default defineComponent({ @@ -58,7 +58,8 @@ export default defineComponent({
{ 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: 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) {

3
src/components/common/topbar.vue

@ -63,7 +63,8 @@ const workspaceName = ref('') @@ -63,7 +63,8 @@ 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: 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)

61
src/components/devices/DeviceFirmwareStatus.vue

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
<template>
<div>
<span class="status-tag pointer">
<a-popconfirm
:title="getTitle()"
ok-text="Yes"
cancel-text="No"
placement="left"
@confirm="onFirmwareStatusClick(firmware)"
>
<a-tag :color="firmware.firmware_status ? commonColor.NORMAL : commonColor.FAIL"
:class="firmware.firmware_status ? 'border-corner ' : 'status-disable border-corner'">
{{ getText(firmware.firmware_status) }}
</a-tag>
</a-popconfirm>
</span>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
import { changeFirmareStatus } from '/@/api/manage'
import { ELocalStorageKey } from '/@/types'
import { Firmware, FirmwareStatusEnum } from '/@/types/device-firmware'
import { commonColor } from '/@/utils/color'
const props = defineProps<{
firmware: Firmware
}>()
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
function getTitle () {
return `Are you sure to set this firmware to ${getText(!props.firmware.firmware_status)}?`
}
function getText (status: boolean) {
return status ? FirmwareStatusEnum.TRUE : FirmwareStatusEnum.FALSE
}
function onFirmwareStatusClick (record: Firmware) {
changeFirmareStatus(workspaceId, record.firmware_id, { status: !record.firmware_status }).then((res) => {
if (res.code === 0) {
record.firmware_status = !record.firmware_status
}
})
}
</script>
<style lang="scss" scoped>
.status-disable{
opacity: 0.4;
}
.border-corner {
border-radius: 3px;
}
.pointer {
cursor: pointer;
}
</style>

26
src/components/devices/device-hms/DeviceHmsDrawer.vue

@ -67,7 +67,12 @@ @@ -67,7 +67,12 @@
</template>
<template v-for="col in ['code', 'message']" #[col]="{ text }" :key="col">
<a-tooltip :title="text">
<span>{{ text }}</span>
<div >{{ text }}</div>
</a-tooltip>
</template>
<template #domain="{text}">
<a-tooltip :title="EDeviceTypeName[text]">
<div >{{ EDeviceTypeName[text] }}</div>
</a-tooltip>
</template>
</a-table>
@ -77,7 +82,7 @@ @@ -77,7 +82,7 @@
<!-- 暂时只抽取该组件 -->
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits } from 'vue'
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'
@ -95,7 +100,7 @@ const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) | @@ -95,7 +100,7 @@ const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) |
//
const sVisible = ref(false)
watchEffect(() => {
watch(props, () => {
sVisible.value = props.visible
// hms
if (props.visible) {
@ -117,9 +122,10 @@ const loading = ref(false) @@ -117,9 +122,10 @@ 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' },
{ title: 'Error Code', dataIndex: 'key', width: '20%', className: 'titleStyle', slots: { customRender: 'code' } },
{ 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 {
@ -174,7 +180,7 @@ const param = reactive<HmsQueryBody>({ @@ -174,7 +180,7 @@ const param = reactive<HmsQueryBody>({
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: '',
domain: -1,
level: '',
message: ''
})
@ -198,12 +204,12 @@ const levels = [ @@ -198,12 +204,12 @@ const levels = [
const deviceTypes = [
{
label: 'All',
value: ''
value: -1
}, {
label: EDeviceTypeName.Aircraft,
label: EDeviceTypeName[EDeviceTypeName.Aircraft],
value: EDeviceTypeName.Aircraft
}, {
label: EDeviceTypeName.Dock,
label: EDeviceTypeName[EDeviceTypeName.Dock],
value: EDeviceTypeName.Dock
}
]
@ -250,7 +256,7 @@ function onTimeChange (newTime: [Moment, Moment]) { @@ -250,7 +256,7 @@ function onTimeChange (newTime: [Moment, Moment]) {
getHms()
}
function onDeviceTypeSelect (val: string) {
function onDeviceTypeSelect (val: number) {
param.sns = [param.device_sn, param.children_sn]
if (val === EDeviceTypeName.Dock) {
param.sns = [param.device_sn, '']

4
src/components/devices/device-log/DeviceLogDetailModal.vue

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
<div class="device-log-detail-wrap">
<div class="device-log-list">
<div class="log-list-item">
<a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id" size="small" @click="onDownloadLog(airportTableLogState.logList.file_id)">
<a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id || !airportTableLogState.logList?.object_key" size="small" @click="onDownloadLog(airportTableLogState.logList.file_id)">
下载机场日志
</a-button>
<a-table :columns="airportLogColumns"
@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
</a-table>
</div>
<div class="log-list-item">
<a-button type="primary" class="download-btn" :disabled="!droneTableLogState.logList?.file_id" size="small" @click="onDownloadLog(droneTableLogState.logList.file_id)">
<a-button type="primary" class="download-btn" :disabled="!droneTableLogState.logList?.file_id || !droneTableLogState.logList?.object_key" size="small" @click="onDownloadLog(droneTableLogState.logList.file_id)">
下载飞行器日志
</a-button>
<a-table :columns="droneLogColumns"

4
src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue

@ -21,8 +21,8 @@ @@ -21,8 +21,8 @@
<!-- 设备类型 -->
<template #device_type="{ record }">
<div>
<div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).parents[0].device_model.key]}}</div>
<div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).hosts[0].device_model.key]}}</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 -->

2
src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue

@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
</span>
<!-- 进度 -->
<span v-if="device.firmware_status === DeviceFirmwareStatusEnum.DuringUpgrade">
{{ `${device.firmware_progress}%`}}
{{ `${device.firmware_progress}`}}
</span>
</div>
</template>

59
src/components/flight-area/FlightAreaActionIcon.vue

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
<template>
<div @click="selectCurrent">
<a-dropdown class="height-100 width-100 icon-panel">
<FlightAreaIcon :type="actionMap[selectedKey].type" :is-circle="actionMap[selectedKey].isCircle" :hide-title="true"/>
<template #overlay>
<a-menu @click="selectAction" mode="vertical-right" :selectedKeys="[selectedKey]">
<a-menu-item v-for="(v, k) in actionMap" :key="k">
<FlightAreaIcon :type="v.type" :is-circle="v.isCircle"/>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
<script lang="ts" setup>
import { ref, defineEmits } from 'vue'
import { EFlightAreaType } from '../../types/flight-area'
import FlightAreaIcon from './FlightAreaIcon.vue'
const emit = defineEmits(['select-action', 'click'])
const actionMap: Record<string, { type: EFlightAreaType, isCircle: boolean}> = {
1: {
type: EFlightAreaType.DFENCE,
isCircle: true,
},
2: {
type: EFlightAreaType.DFENCE,
isCircle: false,
},
3: {
type: EFlightAreaType.NFZ,
isCircle: true,
},
4: {
type: EFlightAreaType.NFZ,
isCircle: false,
},
}
const selectedKey = ref<string>('1')
const selectAction = (item: any) => {
selectedKey.value = item.key
emit('select-action', actionMap[item.key])
}
const selectCurrent = () => {
emit('click', actionMap[selectedKey.value])
}
</script>
<style lang="scss">
.icon-panel {
align-items: center;
justify-content: center;
cursor: pointer;
}
</style>

197
src/components/flight-area/FlightAreaDevicePanel.vue

@ -0,0 +1,197 @@ @@ -0,0 +1,197 @@
<template>
<div class="flight-area-device-panel">
<Title title="Choose Synchronous Devices">
<div style="position: absolute; right: 10px;">
<a style="color: white;" @click="closePanel"><CloseOutlined /></a>
</div>
</Title>
<div class="scrollbar">
<div id="data" v-if="data.length !== 0">
<div v-for="dock in data" :key="dock.device_sn">
<div class="pt5 panel flex-row" @click="selectDock(dock)" :style="{opacity: selectedDocksMap[dock.device_sn] ? 1 : 0.5 }">
<div style="width: 88%">
<div class="title">
<RobotFilled class="fz20"/>
<a-tooltip :title="dock.nickname">
<div class="pr10 ml5" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
</a-tooltip>
</div>
<div class="ml10 mr10 pr5 pl5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
<div>
Custom Flight Area
</div>
<div>
<div v-if="!dock.status">
<a-tooltip title="Dock offline">
<ApiOutlined />
</a-tooltip>
</div>
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZED">
<a-tooltip title="Data synced">
<CheckCircleTwoTone twoToneColor="#28d445"/>
</a-tooltip>
</div>
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING
|| deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC">
<a-tooltip title="To be synced">
<SyncOutlined spin />
</a-tooltip>
</div>
<div v-else>
<a-tooltip :title="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_msg || 'No synchronization'">
<ExclamationCircleTwoTone twoToneColor="#e70102" />
</a-tooltip>
</div>
</div>
</div>
</div>
<div class="box" v-if="selectedDocksMap[dock.device_sn]">
<CheckOutlined />
</div>
</div>
</div>
<DividerLine style="position: absolute; bottom: 68px;" />
<div class="flex-row flex-justify-between footer">
<a-button class="mr10" @click="closePanel">Cancel
</a-button>
<a-button type="primary" :disabled="confirmDisabled" @click="syncDeviceFlightArea">Sync
</a-button>
</div>
</div>
<div v-else>
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { CloseOutlined, RobotFilled, CheckOutlined, ApiOutlined, CheckCircleTwoTone, SyncOutlined, ExclamationCircleTwoTone } from '@ant-design/icons-vue'
import Title from '/@/components/workspace/Title.vue'
import { defineEmits, onMounted, ref, defineProps, computed } from 'vue'
import { getBindingDevices } from '/@/api/manage'
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
import { IPage } from '/@/api/http/type'
import { Device } from '/@/types/device'
import DividerLine from '../workspace/DividerLine.vue'
import { message } from 'ant-design-vue'
import { GetDeviceStatus, syncFlightArea } from '/@/api/flight-area'
import { ESyncStatus } from '/@/types/flight-area'
const props = defineProps<{
data: GetDeviceStatus[]
}>()
const emit = defineEmits(['closePanel'])
const closePanel = () => {
emit('closePanel', false)
}
const confirmDisabled = ref(false)
const deviceStatusMap = computed(() => props.data.reduce((obj: Record<string, GetDeviceStatus>, val: GetDeviceStatus) => {
obj[val.device_sn] = val
return obj
}, {} as Record<string, GetDeviceStatus>))
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
const body: IPage = {
page: 1,
total: 0,
page_size: 10,
}
const data = ref<Device[]>([])
const selectedDocksMap = ref<Record<string, boolean>>({})
const getDocks = async () => {
await getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => {
if (res.code !== 0) {
return
}
data.value.push(...res.data.list)
body.page = res.data.pagination.page
body.page_size = res.data.pagination.page_size
body.total = res.data.pagination.total
})
}
const selectDock = (dock: Device) => {
if (!dock.status) {
message.info(`Dock(${dock.nickname}) is offline.`)
return
}
if (deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING ||
deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC) {
message.info('The dock is synchronizing.')
return
}
selectedDocksMap.value[dock.device_sn] = !selectedDocksMap.value[dock.device_sn]
}
onMounted(() => {
getDocks()
const key = setInterval(() => {
if (body.total === 0 || Math.ceil(body.total / body.page_size) <= body.page) {
clearInterval(key)
return
}
body.page++
getDocks()
}, 1000)
})
const syncDeviceFlightArea = () => {
const keys = Object.keys(selectedDocksMap.value)
if (keys.length === 0) {
message.warn('Please select the docks that need to be synchronized.')
return
}
confirmDisabled.value = true
Object.keys(selectedDocksMap.value).forEach(k => {
const device = deviceStatusMap.value[k]
if (device) {
device.flight_area_status = { sync_code: 0, sync_status: ESyncStatus.WAIT_SYNC, sync_msg: '' }
}
})
syncFlightArea(keys).then(res => {
if (res.code === 0) {
message.success('The devices are synchronizing...')
selectedDocksMap.value = {}
}
}).finally(() => setTimeout(() => {
confirmDisabled.value = false
}, 3000))
}
</script>
<style lang="scss" scoped>
.flight-area-device-panel {
position: absolute;
left: 285px;
width: 280px;
height: 100vh;
float: right;
top: 0;
z-index: 1000;
color: white;
background: #282828;
.footer {
position: absolute;
width: 100%;
bottom: 10px;
padding: 10px;
button {
width: 45%;
border: 0;
}
}
.scrollbar {
overflow-y: auto;
height: calc(100vh - 150px);
}
.box {
font-size: 22px;
line-height: 60px;
}
}
</style>

33
src/components/flight-area/FlightAreaIcon.vue

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
<template>
<div class="flex-row flex-align-center">
<div class="shape" :class="type" :style="isCircle ? 'border-radius: 50%;' : ''"></div>
<div class="ml5" v-if="!hideTitle">{{ FlightAreaTypeTitleMap[type][isCircle ? EGeometryType.CIRCLE : EGeometryType.POLYGON] }}</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
import { EFlightAreaType, EGeometryType, FlightAreaTypeTitleMap } from '../../types/flight-area'
const props = defineProps<{
type: EFlightAreaType,
isCircle: boolean,
hideTitle?: boolean
}>()
</script>
<style lang="scss">
.nfz {
border-color: red;
}
.dfence {
border-color: $tag-green;
}
.shape {
width: 16px;
height: 16px;
border-width: 3px;
border-style: solid;
}
</style>

89
src/components/flight-area/FlightAreaItem.vue

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
<template>
<div class="panel" style="padding-top: 5px;" :class="{disable: !flightArea.status}">
<div class="title">
<a-tooltip :title="flightArea.name">
<div class="pr10" style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ flightArea.name }}</div>
</a-tooltip>
</div>
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
<span class="mr10">Update at {{ formatDateTime(flightArea.update_time).toLocaleString() }}</span>
</div>
<div class="flex-row flex-justify-between flex-align-center ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<FlightAreaIcon :type="flightArea.type" :isCircle="EGeometryType.CIRCLE === flightArea.content.geometry.type"/>
<div class="mr10 operate">
<a-popconfirm v-if="flightArea.status" title="Is it determined to disable the current area?" okText="Disable" @confirm="changeAreaStatus(false)">
<stop-outlined />
</a-popconfirm>
<a-popconfirm v-else @confirm="changeAreaStatus(true)" title="Is it determined to enable the current area?" okText="Enable" >
<check-circle-outlined />
</a-popconfirm>
<EnvironmentFilled class="ml10" @click="clickLocation"/>
<a-popconfirm title="Is it determined to delete the current area?" okText="Delete" okType="danger" @confirm="deleteArea">
<delete-outlined class="ml10" />
</a-popconfirm>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, reactive, defineEmits, computed } from 'vue'
import { GetFlightArea, changeFlightAreaStatus } from '../../api/flight-area'
import FlightAreaIcon from './FlightAreaIcon.vue'
import { formatDateTime } from '../../utils/time'
import { EGeometryType } from '../../types/flight-area'
import { StopOutlined, CheckCircleOutlined, DeleteOutlined, EnvironmentFilled } from '@ant-design/icons-vue'
const props = defineProps<{
data: GetFlightArea
}>()
const emit = defineEmits(['delete', 'update', 'location'])
const flightArea = computed(() => props.data)
const changeAreaStatus = (status: boolean) => {
changeFlightAreaStatus(props.data.area_id, status).then(res => {
if (res.code === 0) {
flightArea.value.status = status
emit('update', flightArea)
}
})
}
const deleteArea = () => {
emit('delete', flightArea.value.area_id)
}
const clickLocation = () => {
emit('location', flightArea.value.area_id)
}
</script>
<style lang="scss" scoped>
.panel {
background: #3c3c3c;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
height: 90px;
width: 95%;
font-size: 13px;
border-radius: 2px;
cursor: pointer;
.title {
display: flex;
flex-direction: row;
align-items: center;
height: 30px;
font-weight: bold;
margin: 0px 10px 0 10px;
}
.operate > *{
font-size: 16px;
}
}
.disable {
opacity: 50%;
}
</style>

43
src/components/flight-area/FlightAreaPanel.vue

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
<template>
<div class="flight-area-panel">
<div v-if="data.length === 0">
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
</div>
<div v-else v-for="area in flightAreaList" :key="area.area_id">
<FlightAreaItem :data="area" @delete="deleteArea" @update="updateArea" @location="clickLocation(area)"/>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, computed } from 'vue'
import FlightAreaItem from './FlightAreaItem.vue'
import { GetFlightArea } from '/@/api/flight-area'
const emit = defineEmits(['deleteArea', 'updateArea', 'locationArea'])
const props = defineProps<{
data: GetFlightArea[]
}>()
const flightAreaList = computed(() => props.data)
const deleteArea = (areaId: string) => {
emit('deleteArea', areaId)
}
const updateArea = (area: GetFlightArea) => {
emit('updateArea', area)
}
const clickLocation = (area: GetFlightArea) => {
emit('locationArea', area)
}
</script>
<style lang="scss" scoped>
.flight-area-panel {
overflow-y: auto;
height: calc(100vh - 150px);
}
</style>

66
src/components/flight-area/FlightAreaSyncPanel.vue

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
<template>
<div class="flight-area-sync-panel p10 flex-row flex-align-center" >
<RobotFilled class="fz30" twoToneColor="red" fill="#00ff00"/>
<div class="ml20 mr10 flex-column" @click="switchPanel">
<div class="fz18">Sync Across Devices</div>
<div v-if="syncDevicesCount > 0"><a-spin /> Syncing to {{ syncDevicesCount }} devices</div>
</div>
<RightOutlined class="fz18" @click="switchPanel"/>
<FlightAreaDevicePanel v-if="visible" @close-panel="closePanel" :data="syncDevices"/>
</div>
</template>
<script lang="ts" setup>
import { RobotFilled, RightOutlined } from '@ant-design/icons-vue'
import FlightAreaDevicePanel from '/@/components/flight-area/FlightAreaDevicePanel.vue'
import { computed, onMounted, ref, watch } from 'vue'
import { GetDeviceStatus, getDeviceStatus } from '/@/api/flight-area'
import { ESyncStatus, FlightAreaSyncProgress } from '/@/types/flight-area'
import { useFlightAreaSyncProgressEvent } from './use-flight-area-sync-progress-event'
const visible = ref(false)
const syncDevices = ref<GetDeviceStatus[]>([])
const syncDevicesCount = computed(() => syncDevices.value.filter(device =>
device.flight_area_status.sync_status === ESyncStatus.SYNCHRONIZING || device.flight_area_status.sync_status === ESyncStatus.WAIT_SYNC).length)
const getAllDeviceStatus = () => {
getDeviceStatus().then(res => {
if (res.code === 0) {
syncDevices.value = res.data
}
})
}
onMounted(() => {
getAllDeviceStatus()
})
const switchPanel = () => {
visible.value = !visible.value
}
const closePanel = (val: boolean) => {
visible.value = val
}
const handleSyncProgress = (data: FlightAreaSyncProgress) => {
let has = false
const status = { sync_code: data.result, sync_status: data.status, sync_msg: data.message }
syncDevices.value.forEach(device => {
if (data.sn === device.device_sn) {
device.flight_area_status = status
has = true
}
})
if (!has) {
syncDevices.value.push({ device_sn: data.sn, flight_area_status: status })
}
}
useFlightAreaSyncProgressEvent(handleSyncProgress)
</script>
<style lang="scss" scoped>
.flight-area-sync-panel {
height: 70px;
cursor: pointer;
}
</style>

18
src/components/flight-area/use-flight-area-drone-location-event.ts

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
import { FlightAreasDroneLocation } from '/@/types/flight-area'
import { CommonHostWs } from '/@/websocket'
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
export function useFlightAreaDroneLocationEvent (onFlightAreaDroneLocationWs: (data: CommonHostWs<FlightAreasDroneLocation>) => void): void {
function handleDroneLocationEvent (data: any) {
onFlightAreaDroneLocationWs(data.data)
}
onMounted(() => {
EventBus.on('flightAreasDroneLocationWs', handleDroneLocationEvent)
})
onBeforeUnmount(() => {
EventBus.off('flightAreasDroneLocationWs', handleDroneLocationEvent)
})
}

17
src/components/flight-area/use-flight-area-sync-progress-event.ts

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { FlightAreaSyncProgress } from '/@/types/flight-area'
export function useFlightAreaSyncProgressEvent (onFlightAreaSyncProgressWs: (data: FlightAreaSyncProgress) => void): void {
function handleSyncProgressEvent (data: FlightAreaSyncProgress) {
onFlightAreaSyncProgressWs(data)
}
onMounted(() => {
EventBus.on('flightAreasSyncProgressWs', handleSyncProgressEvent)
})
onBeforeUnmount(() => {
EventBus.off('flightAreasSyncProgressWs', handleSyncProgressEvent)
})
}

30
src/components/flight-area/use-flight-area-update.ts

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
import { EFlightAreaUpdate, FlightAreaUpdate, FlightAreasDroneLocation } from '/@/types/flight-area'
import { CommonHostWs } from '/@/websocket'
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
function doNothing (data: FlightAreaUpdate) {
}
export function useFlightAreaUpdateEvent (addFunc = doNothing, deleteFunc = doNothing, updateFunc = doNothing): void {
function handleDroneLocationEvent (data: FlightAreaUpdate) {
switch (data.operation) {
case EFlightAreaUpdate.ADD:
addFunc(data)
break
case EFlightAreaUpdate.UPDATE:
updateFunc(data)
break
case EFlightAreaUpdate.DELETE:
deleteFunc(data)
break
}
}
onMounted(() => {
EventBus.on('flightAreasUpdateWs', handleDroneLocationEvent)
})
onBeforeUnmount(() => {
EventBus.off('flightAreasUpdateWs', handleDroneLocationEvent)
})
}

155
src/components/flight-area/use-flight-area.ts

@ -0,0 +1,155 @@ @@ -0,0 +1,155 @@
import { message, notification } from 'ant-design-vue'
import { MapDoodleEnum } from '/@/types/map-enum'
import { getRoot } from '/@/root'
import { PostFlightAreaBody, saveFlightArea } from '/@/api/flight-area'
import { generateCircleContent, generatePolyContent } from '/@/utils/map-layer-utils'
import { GeojsonCoordinate } from '/@/utils/genjson'
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform.js'
import { uuidv4 } from '/@/utils/uuid'
import { CommonHostWs } from '/@/websocket'
import { FlightAreasDroneLocation } from '/@/types/flight-area'
import rootStore from '/@/store'
import { h } from 'vue'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import moment from 'moment'
import { DATE_FORMAT } from '/@/utils/constants'
export function useFlightArea () {
const root = getRoot()
const store = rootStore
const coverMap = store.state.coverMap
let useGMapCoverHook = useGMapCover()
const MIN_RADIUS = 10
function checkCircle (obj: any): boolean {
if (obj.getRadius() < MIN_RADIUS) {
message.error(`The radius must be greater than ${MIN_RADIUS}m.`)
root.$map.remove(obj)
return false
}
return true
}
function checkPolygon (obj: any): boolean {
const path: any[][] = obj.getPath()
if (path.length < 3) {
message.error('The path of the polygon cannot be crossed.')
root.$map.remove(obj)
return false
}
// root.$aMap.GeometryUtil.doesLineLineIntersect()
return true
}
function setExtData (obj: any) {
let ext = obj.getExtData()
const id = uuidv4()
const name = `${ext.type}-${moment().format(DATE_FORMAT)}`
ext = Object.assign({}, ext, { id, name })
obj.setExtData(ext)
return ext
}
function createFlightArea (obj: any) {
const ext = obj.getExtData()
const data = {
id: ext.id,
type: ext.type,
name: ext.name,
}
let coordinates: GeojsonCoordinate | GeojsonCoordinate[][]
let content
switch (ext.mapType) {
case 'circle':
content = generateCircleContent(obj.getCenter(), obj.getRadius())
coordinates = getWgs84(content.geometry.coordinates as GeojsonCoordinate)
break
case 'polygon':
content = generatePolyContent(obj.getPath()).content
coordinates = [getWgs84(content.geometry.coordinates[0] as GeojsonCoordinate[])]
break
default:
message.error(`Invalid type: ${obj.mapType}`)
root.$map.remove(obj)
return
}
content.geometry.coordinates = coordinates
saveFlightArea(Object.assign({}, data, { content }) as PostFlightAreaBody).then(res => {
if (res.code !== 0) {
useGMapCoverHook.removeCoverFromMap(ext.id)
}
}).finally(() => root.$map.remove(obj))
}
function getDrawFlightAreaCallback (obj: any) {
useGMapCoverHook = useGMapCover()
const ext = setExtData(obj)
switch (ext.mapType) {
case MapDoodleEnum.CIRCLE:
if (!checkCircle(obj)) {
return
}
break
case MapDoodleEnum.POLYGON:
if (!checkPolygon(obj)) {
return
}
break
default:
break
}
createFlightArea(obj)
}
const getWgs84 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => {
if (coordinate[0] instanceof Array) {
return (coordinate as GeojsonCoordinate[]).map(c => gcj02towgs84(c[0], c[1])) as T
}
return gcj02towgs84(coordinate[0], coordinate[1])
}
const getGcj02 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => {
if (coordinate[0] instanceof Array) {
return (coordinate as GeojsonCoordinate[]).map(c => wgs84togcj02(c[0], c[1])) as T
}
return wgs84togcj02(coordinate[0], coordinate[1])
}
const onFlightAreaDroneLocationWs = (data: CommonHostWs<FlightAreasDroneLocation>) => {
const nearArea = data.host.drone_locations.filter(val => !val.is_in_area)
const inArea = data.host.drone_locations.filter(val => val.is_in_area)
notification.warning({
key: `flight-area-${data.sn}`,
message: `Drone(${data.sn}) flight area information`,
description: h('div',
[
h('div', [
h('span', { class: 'fz18' }, 'In the flight area: '),
h('ul', [
...inArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`))
])
]),
h('div', [
h('span', { class: 'fz18' }, 'Near the flight area: '),
h('ul', [
...nearArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`))
])
])
]),
duration: null,
style: {
width: '420px',
marginTop: '-8px',
marginLeft: '-28px',
}
})
}
return {
getDrawFlightAreaCallback,
getGcj02,
getWgs84,
onFlightAreaDroneLocationWs,
}
}

242
src/components/g-map/DeviceSettingBox.vue

@ -0,0 +1,242 @@ @@ -0,0 +1,242 @@
<template>
<div class="device-setting-wrapper">
<div class="device-setting-header">Device Property Set</div>
<div class="device-setting-box">
<!-- 飞行器夜航灯 -->
<div class="control-setting-item">
<div class="control-setting-item-left">
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].label }}</div>
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value }}</div>
</div>
<div class="control-setting-item-right">
<DeviceSettingPopover
:visible="deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].popConfirm.visible"
:loading="deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].popConfirm.loading"
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)"
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)"
>
<template #formContent>
<div class="form-content">
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].label }}:</span>
<a-switch checked-children="" un-checked-children="" v-model:checked="deviceSettingFormModel.nightLightsState" />
</div>
</template>
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)">Edit</a>
</DeviceSettingPopover>
</div>
</div>
<!-- 限高 -->
<div class="control-setting-item">
<div class="control-setting-item-left">
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].label }}</div>
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].value }}</div>
</div>
<div class="control-setting-item-right">
<DeviceSettingPopover
:visible="deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].popConfirm.visible"
:loading="deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].popConfirm.loading"
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)"
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)"
>
<template #formContent>
<div class="form-content">
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].label }}:</span>
<a-input-number v-model:value="deviceSettingFormModel.heightLimit" :min="20" :max="1500" />
m
</div>
</template>
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)">Edit</a>
</DeviceSettingPopover>
</div>
</div>
<!-- 限远 -->
<div class="control-setting-item">
<div class="control-setting-item-left">
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].label }}</div>
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value }}</div>
</div>
<div class="control-setting-item-right">
<DeviceSettingPopover
:visible="deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].popConfirm.visible"
:loading="deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].popConfirm.loading"
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)"
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)"
>
<template #formContent>
<div class="form-content">
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].label }}:</span>
<a-switch style="margin-right: 10px;" checked-children="" un-checked-children="" v-model:checked="deviceSettingFormModel.distanceLimitStatus.state" />
<a-input-number v-model:value="deviceSettingFormModel.distanceLimitStatus.distanceLimit" :min="15" :max="8000" />
m
</div>
</template>
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)">Edit</a>
</DeviceSettingPopover>
</div>
</div>
<!-- 水平避障 -->
<div class="control-setting-item">
<div class="control-setting-item-left">
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].label }}</div>
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value }}</div>
</div>
<div class="control-setting-item-right">
<DeviceSettingPopover
:visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].popConfirm.visible"
:loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].popConfirm.loading"
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)"
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)"
>
<template #formContent>
<div class="form-content">
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].label }}:</span>
<a-switch checked-children="" un-checked-children="" v-model:checked="deviceSettingFormModel.obstacleAvoidanceHorizon" />
</div>
</template>
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)">Edit</a>
</DeviceSettingPopover>
</div>
</div>
<!-- 上视避障 -->
<div class="control-setting-item">
<div class="control-setting-item-left">
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].label }}</div>
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value }}</div>
</div>
<div class="control-setting-item-right">
<DeviceSettingPopover
:visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].popConfirm.visible"
:loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].popConfirm.loading"
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)"
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)"
>
<template #formContent>
<div class="form-content">
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].label }}:</span>
<a-switch checked-children="" un-checked-children="" v-model:checked="deviceSettingFormModel.obstacleAvoidanceUpside" />
</div>
</template>
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)">Edit</a>
</DeviceSettingPopover>
</div>
</div>
<!-- 下视避障 -->
<div class="control-setting-item">
<div class="control-setting-item-left">
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].label }}</div>
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value }}</div>
</div>
<div class="control-setting-item-right">
<DeviceSettingPopover
:visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].popConfirm.visible"
:loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].popConfirm.loading"
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)"
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)"
>
<template #formContent>
<div class="form-content">
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].label }}:</span>
<a-switch checked-children="" un-checked-children="" v-model:checked="deviceSettingFormModel.obstacleAvoidanceDownside" />
</div>
</template>
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)">Edit</a>
</DeviceSettingPopover>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue'
import { DeviceInfoType } from '/@/types/device'
import { useMyStore } from '/@/store'
import { cloneDeep } from 'lodash'
import { initDeviceSetting, initDeviceSettingFormModel, DeviceSettingKeyEnum } from '/@/types/device-setting'
import { updateDeviceSettingInfoByOsd, updateDeviceSettingFormModelByOsd } from '/@/utils/device-setting'
import { useDeviceSetting } from './use-device-setting'
import DeviceSettingPopover from './DeviceSettingPopover.vue'
const props = defineProps<{
sn: string,
deviceInfo: DeviceInfoType,
}>()
const store = useMyStore()
const deviceSetting = ref(cloneDeep(initDeviceSetting))
const deviceSettingFormModelFromOsd = ref(cloneDeep(initDeviceSettingFormModel))
const deviceSettingFormModel = ref(cloneDeep(initDeviceSettingFormModel)) // 使formModel
// osd
watch(() => props.deviceInfo, (value) => {
updateDeviceSettingInfoByOsd(deviceSetting.value, value)
updateDeviceSettingFormModelByOsd(deviceSettingFormModelFromOsd.value, value)
// console.log('deviceInfo', value)
}, {
immediate: true,
deep: true
})
function onShowPopConfirm (settingKey: DeviceSettingKeyEnum) {
deviceSetting.value[settingKey].popConfirm.visible = true
deviceSettingFormModel.value = cloneDeep(deviceSettingFormModelFromOsd.value)
}
function onCancel (settingKey: DeviceSettingKeyEnum) {
deviceSetting.value[settingKey].popConfirm.visible = false
}
async function onConfirm (settingKey: DeviceSettingKeyEnum) {
deviceSetting.value[settingKey].popConfirm.loading = true
const body = genDevicePropsBySettingKey(settingKey, deviceSettingFormModel.value)
await setDeviceProps(props.sn, body)
deviceSetting.value[settingKey].popConfirm.loading = false
deviceSetting.value[settingKey].popConfirm.visible = false
}
//
const {
genDevicePropsBySettingKey,
setDeviceProps,
} = useDeviceSetting()
</script>
<style lang='scss' scoped>
.device-setting-wrapper{
border-bottom: 1px solid #515151;
.device-setting-header{
font-size: 14px;
font-weight: 600;
padding: 10px 10px 0px;
}
.device-setting-box{
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 4px 10px;
.control-setting-item{
width: 220px;
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #666;
margin: 4px 0;
padding: 0 8px;
.control-setting-item-left{
display: flex;
flex-direction: column;
.item-label{
font-weight: 700;
}
}
}
}
}
</style>

106
src/components/g-map/DeviceSettingPopover.vue

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

111
src/components/g-map/DockControlPanel.vue

@ -2,25 +2,33 @@ @@ -2,25 +2,33 @@
<div class="dock-control-panel">
<!-- title -->
<div class="dock-control-panel-header fz16 pl5 pr5 flex-align-center flex-row flex-justify-between">
<span>远程调试 {{ props.sn}}</span>
<span>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 v-for="(cmdItem, index) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
<div class="control-cmd-item-left">
<div class="item-label">{{ cmdItem.label }}</div>
<div class="item-status">{{ cmdItem.status }}</div>
</div>
<div class="control-cmd-item-right">
<a-button :loading="cmdItem.loading" size="small" type="primary" @click="sendControlCmd(cmdItem, index)">
{{ cmdItem.operateText }}
</a-button>
</div>
</div>
</div>
<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>
@ -30,11 +38,12 @@ import { defineProps, defineEmits, ref, watch } from 'vue' @@ -30,11 +38,12 @@ import { defineProps, defineEmits, ref, watch } from 'vue'
import {
CloseOutlined
} from '@ant-design/icons-vue'
import { useDockControl } from './useDockControl'
import { DeviceInfoType } from '/@/types/device'
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,
@ -64,21 +73,40 @@ watch(() => props.deviceInfo, (value) => { @@ -64,21 +73,40 @@ watch(() => props.deviceInfo, (value) => {
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, false)
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
}
}
}
// dock
const {
sendDockControlCmd,
dockDebugOnOff
} = useDockControl()
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
const success = await sendDockControlCmd({
sn: props.sn,
cmd: cmdItem.cmdKey
cmd: cmdItem.cmdKey,
action: cmdItem.action
}, true)
if (success) {
// updateDeviceSingleCmdInfo(cmdList.value[index])
@ -103,26 +131,39 @@ async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) { @@ -103,26 +131,39 @@ async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
}
.control-cmd-wrapper{
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 4px 10px;
.control-cmd-item{
width: 220px;
height: 58px;
.control-cmd-header{
font-size: 14px;
font-weight: 600;
padding: 10px 10px 0px;
.debug-btn{
margin-left: 10px;
border:1px solid #585858;
}
}
.control-cmd-box{
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: space-between;
border: 1px solid #666;
margin: 4px 0;
padding: 0 8px;
.control-cmd-item-left{
padding: 4px 10px;
.control-cmd-item{
width: 220px;
height: 58px;
display: flex;
flex-direction: column;
.item-label{
font-weight: 700;
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;
}
}
}
}

34
src/components/g-map/DroneControlInfoPanel.vue

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
<template>
<div class="drone-control-info-wrap">
<a-textarea v-model:value="info" placeholder="drc info" :rows="5" disabled/>
</div>
</template>
<script lang="ts" setup>
import { ref, defineProps, watch } from 'vue'
const props = defineProps<{
message?: string,
}>()
const info = ref('')
watch(() => props.message, message => {
info.value = message || ''
}, {
immediate: true
})
// const emit = defineEmits(['cancel', 'confirm'])
</script>
<style lang="scss" scoped>
.drone-control-info-wrap {
&::v-deep{
textarea.ant-input {
background-color: #000;
color: #fff;
white-space: pre-wrap;
}
}
}
</style>

837
src/components/g-map/DroneControlPanel.vue

@ -0,0 +1,837 @@ @@ -0,0 +1,837 @@
<template>
<div class="drone-control-wrapper">
<div class="drone-control-header">Drone Flight Control</div>
<div class="drone-control-box">
<div class="box">
<div class="row">
<div class="drone-control"><Button :ghost="!flightController" size="small" @click="onClickFightControl">{{ flightController ? 'Exit Remote Control' : 'Enter Remote Control'}}</Button></div>
</div>
<div class="row">
<div class="drone-control-direction">
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_Q)" @onmouseup="onMouseUp">
<template #icon><UndoOutlined /></template><span class="word">Q</span>
</Button>
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_W)" @onmouseup="onMouseUp">
<template #icon><UpOutlined/></template><span class="word">W</span>
</Button>
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_E)" @onmouseup="onMouseUp">
<template #icon><RedoOutlined /></template><span class="word">E</span>
</Button>
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_UP)" @onmouseup="onMouseUp">
<template #icon><ArrowUpOutlined /></template>
</Button>
<br />
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_A)" @onmouseup="onMouseUp">
<template #icon><LeftOutlined/></template><span class="word">A</span>
</Button>
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_S)" @onmouseup="onMouseUp">
<template #icon><DownOutlined/></template><span class="word">S</span>
</Button>
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_D)" @onmouseup="onMouseUp">
<template #icon><RightOutlined/></template><span class="word">D</span>
</Button>
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_DOWN)" @onmouseup="onMouseUp">
<template #icon><ArrowDownOutlined /></template>
</Button>
</div>
<Button type="primary" size="small" danger ghost @click="handleEmergencyStop" >
<template #icon><PauseCircleOutlined/></template><span>Break</span>
</Button>
</div>
<div class="row">
<DroneControlPopover
:visible="flyToPointPopoverData.visible"
:loading="flyToPointPopoverData.loading"
@confirm="($event) => onFlyToConfirm(true)"
@cancel="($event) =>onFlyToConfirm(false)"
>
<template #formContent>
<div class="form-content">
<div>
<span class="form-label">latitude:</span>
<a-input-number v-model:value="flyToPointPopoverData.latitude"/>
</div>
<div>
<span class="form-label">longitude:</span>
<a-input-number v-model:value="flyToPointPopoverData.longitude"/>
</div>
<div>
<span class="form-label">height(m):</span>
<a-input-number v-model:value="flyToPointPopoverData.height"/>
</div>
</div>
</template>
<Button size="small" ghost @click="onShowFlyToPopover" >
<span>Fly to</span>
</Button>
</DroneControlPopover>
<Button size="small" ghost @click="onStopFlyToPoint" >
<span>Stop Fly to</span>
</Button>
<DroneControlPopover
:visible="takeoffToPointPopoverData.visible"
:loading="takeoffToPointPopoverData.loading"
@confirm="($event) => onTakeoffToPointConfirm(true)"
@cancel="($event) =>onTakeoffToPointConfirm(false)"
>
<template #formContent>
<div class="form-content">
<div>
<span class="form-label">latitude:</span>
<a-input-number v-model:value="takeoffToPointPopoverData.latitude"/>
</div>
<div>
<span class="form-label">longitude:</span>
<a-input-number v-model:value="takeoffToPointPopoverData.longitude"/>
</div>
<div>
<span class="form-label">height(m):</span>
<a-input-number v-model:value="takeoffToPointPopoverData.height"/>
</div>
<div>
<span class="form-label">Safe Takeoff Altitude(m):</span>
<a-input-number v-model:value="takeoffToPointPopoverData.securityTakeoffHeight"/>
</div>
<div>
<span class="form-label">Return-to-Home Altitude(m):</span>
<a-input-number v-model:value="takeoffToPointPopoverData.rthAltitude"/>
</div>
<div>
<span class="form-label">Lost Action:</span>
<a-select
v-model:value="takeoffToPointPopoverData.rcLostAction"
style="width: 120px"
:options="LostControlActionInCommandFLightOptions"
></a-select>
</div>
<div>
<span class="form-label">Wayline Lost Action:</span>
<a-select
v-model:value="takeoffToPointPopoverData.exitWaylineWhenRcLost"
style="width: 120px"
:options="WaylineLostControlActionInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Return-to-Home Mode:</span>
<a-select
v-model:value="takeoffToPointPopoverData.rthMode"
style="width: 120px"
:options="RthModeInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Commander Mode Lost Action:</span>
<a-select
v-model:value="takeoffToPointPopoverData.commanderModeLostAction"
style="width: 120px"
:options="CommanderModeLostActionInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Commander Flight Mode:</span>
<a-select
v-model:value="takeoffToPointPopoverData.commanderFlightMode"
style="width: 120px"
:options="CommanderFlightModeInCommandFlightOptions"
></a-select>
</div>
<div>
<span class="form-label">Commander Flight Height(m):</span>
<a-input-number v-model:value="takeoffToPointPopoverData.commanderFlightHeight"/>
</div>
</div>
</template>
<Button size="small" ghost @click="onShowTakeoffToPointPopover" >
<span>Take off</span>
</Button>
<div v-for="(cmdItem) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
<Button :loading="cmdItem.loading" size="small" ghost @click="sendControlCmd(cmdItem, 0)">
{{ cmdItem.operateText }}
</Button>
</div>
<div>
<Button size="small" ghost @click="openLivestreamAgora" >
<span>Agora Live</span>
</Button>
<Button size="small" ghost @click="openLivestreamOthers" >
<span>RTMP/GB28181 Live</span>
</Button>
</div>
</DroneControlPopover>
</div>
</div>
<div class="box">
<div class="row">
<Select v-model:value="payloadSelectInfo.value" style="width: 110px; marginRight: 5px" :options="payloadSelectInfo.options" @change="handlePayloadChange"/>
<div class="drone-control">
<Button type="primary" size="small" @click="onAuthPayload">Payload Control</Button>
</div>
</div>
<div class="row">
<DroneControlPopover
:visible="gimbalResetPopoverData.visible"
:loading="gimbalResetPopoverData.loading"
@confirm="($event) => onGimbalResetConfirm(true)"
@cancel="($event) =>onGimbalResetConfirm(false)"
>
<template #formContent>
<div class="form-content">
<div>
<span class="form-label">reset mode:</span>
<a-select
v-model:value="gimbalResetPopoverData.resetMode"
style="width: 180px"
:options="GimbalResetModeOptions"
></a-select>
</div>
</div>
</template>
<Button size="small" ghost @click="onShowGimbalResetPopover">
<span>Gimbal Reset</span>
</Button>
</DroneControlPopover>
<Button size="small" ghost @click="onSwitchCameraMode">
<span>Camera Mode Switch</span>
</Button>
</div>
<div class="row">
<Button size="small" ghost @click="onStartCameraRecording">
<span>Start Recording</span>
</Button>
<Button size="small" ghost @click="onStopCameraRecording">
<span>Stop Recording</span>
</Button>
</div>
<div class="row">
<Button size="small" ghost @click="onTakeCameraPhoto">
<span>Take Photo</span>
</Button>
<DroneControlPopover
:visible="zoomFactorPopoverData.visible"
:loading="zoomFactorPopoverData.loading"
@confirm="($event) => onZoomFactorConfirm(true)"
@cancel="($event) =>onZoomFactorConfirm(false)"
>
<template #formContent>
<div class="form-content">
<div>
<span class="form-label">camera type:</span>
<a-select
v-model:value="zoomFactorPopoverData.cameraType"
style="width: 120px"
:options="ZoomCameraTypeOptions"
></a-select>
</div>
<div>
<span class="form-label">zoom factor:</span>
<a-input-number v-model:value="zoomFactorPopoverData.zoomFactor" :min="2" :max="200" />
</div>
</div>
</template>
<Button size="small" ghost @click="($event) => onShowZoomFactorPopover()">
<span class="word" @click=";">Zoom</span>
</Button>
</DroneControlPopover>
<DroneControlPopover
:visible="cameraAimPopoverData.visible"
:loading="cameraAimPopoverData.loading"
@confirm="($event) => onCameraAimConfirm(true)"
@cancel="($event) =>onCameraAimConfirm(false)"
>
<template #formContent>
<div class="form-content">
<div>
<span class="form-label">camera type:</span>
<a-select
v-model:value="cameraAimPopoverData.cameraType"
style="width: 120px"
:options="CameraTypeOptions"
></a-select>
</div>
<div>
<span class="form-label">locked:</span>
<a-switch v-model:checked="cameraAimPopoverData.locked"/>
</div>
<div>
<span class="form-label">x:</span>
<a-input-number v-model:value="cameraAimPopoverData.x" :min="0" :max="1"/>
</div>
<div>
<span class="form-label">y:</span>
<a-input-number v-model:value="cameraAimPopoverData.y" :min="0" :max="1"/>
</div>
</div>
</template>
<Button size="small" ghost @click="($event) => onShowCameraAimPopover()">
<span class="word" @click=";">AIM</span>
</Button>
</DroneControlPopover>
</div>
</div>
</div>
<!-- 信息提示 -->
<DroneControlInfoPanel :message="drcInfo"></DroneControlInfoPanel>
</div>
</template>
<script setup lang="ts">
import { defineProps, reactive, ref, watch, computed, onMounted, watchEffect } from 'vue'
import { Select, message, Button } from 'ant-design-vue'
import { PayloadInfo, DeviceInfoType, ControlSource, DeviceOsdCamera, DrcStateEnum } from '/@/types/device'
import { useMyStore } from '/@/store'
import { postDrcEnter, postDrcExit } from '/@/api/drc'
import { useMqtt, DeviceTopicInfo } from './use-mqtt'
import { DownOutlined, UpOutlined, LeftOutlined, RightOutlined, PauseCircleOutlined, UndoOutlined, RedoOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue'
import { useManualControl, KeyCode } from './use-manual-control'
import { usePayloadControl } from './use-payload-control'
import { CameraMode, CameraType, CameraTypeOptions, ZoomCameraTypeOptions, CameraListItem } from '/@/types/live-stream'
import { useDroneControlWsEvent } from './use-drone-control-ws-event'
import { useDroneControlMqttEvent } from './use-drone-control-mqtt-event'
import {
postFlightAuth, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight, ERthMode,
ECommanderModeLostAction, ECommanderFlightMode
} from '/@/api/drone-control/drone'
import { useDroneControl } from './use-drone-control'
import {
GimbalResetMode, GimbalResetModeOptions, LostControlActionInCommandFLightOptions, WaylineLostControlActionInCommandFlightOptions,
RthModeInCommandFlightOptions, CommanderModeLostActionInCommandFlightOptions, CommanderFlightModeInCommandFlightOptions
} from '/@/types/drone-control'
import DroneControlPopover from './DroneControlPopover.vue'
import DroneControlInfoPanel from './DroneControlInfoPanel.vue'
import { noDebugCmdList as baseCmdList, DeviceCmdItem, DeviceCmd } from '/@/types/device-cmd'
import { useDockControl } from './use-dock-control'
const props = defineProps<{
sn: string,
deviceInfo: DeviceInfoType,
payloads: null | PayloadInfo[]
}>()
const store = useMyStore()
const clientId = computed(() => {
return store.state.clientId
})
const initCmdList = baseCmdList.map(cmdItem => Object.assign({}, cmdItem))
const cmdList = ref(initCmdList)
const {
sendDockControlCmd
} = useDockControl()
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
cmdItem.loading = true
const result = await sendDockControlCmd({
sn: props.sn,
cmd: cmdItem.cmdKey,
action: cmdItem.action
}, false)
if (result) {
message.success('Return home successful')
if (flightController.value) {
exitFlightCOntrol()
}
} else {
message.error('Failed to return home')
}
cmdItem.loading = false
}
const { flyToPoint, stopFlyToPoint, takeoffToPoint } = useDroneControl()
const MAX_SPEED = 14
const flyToPointPopoverData = reactive({
visible: false,
loading: false,
latitude: null as null | number,
longitude: null as null | number,
height: null as null | number,
maxSpeed: MAX_SPEED,
})
function onShowFlyToPopover () {
flyToPointPopoverData.visible = !flyToPointPopoverData.visible
flyToPointPopoverData.loading = false
flyToPointPopoverData.latitude = null
flyToPointPopoverData.longitude = null
flyToPointPopoverData.height = null
}
async function onFlyToConfirm (confirm: boolean) {
if (confirm) {
if (!flyToPointPopoverData.height || !flyToPointPopoverData.latitude || !flyToPointPopoverData.longitude) {
message.error('Input error')
return
}
try {
await flyToPoint(props.sn, {
max_speed: flyToPointPopoverData.maxSpeed,
points: [
{
latitude: flyToPointPopoverData.latitude,
longitude: flyToPointPopoverData.longitude,
height: flyToPointPopoverData.height
}
]
})
} catch (error) {
}
}
flyToPointPopoverData.visible = false
}
async function onStopFlyToPoint () {
await stopFlyToPoint(props.sn)
}
const takeoffToPointPopoverData = reactive({
visible: false,
loading: false,
latitude: null as null | number,
longitude: null as null | number,
height: null as null | number,
securityTakeoffHeight: null as null | number,
maxSpeed: MAX_SPEED,
rthAltitude: null as null | number,
rcLostAction: LostControlActionInCommandFLight.RETURN_HOME,
exitWaylineWhenRcLost: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION,
rthMode: ERthMode.SETTING,
commanderModeLostAction: ECommanderModeLostAction.CONTINUE,
commanderFlightMode: ECommanderFlightMode.SETTING,
commanderFlightHeight: null as null | number,
})
function onShowTakeoffToPointPopover () {
takeoffToPointPopoverData.visible = !takeoffToPointPopoverData.visible
takeoffToPointPopoverData.loading = false
takeoffToPointPopoverData.latitude = null
takeoffToPointPopoverData.longitude = null
takeoffToPointPopoverData.securityTakeoffHeight = null
takeoffToPointPopoverData.rthAltitude = null
takeoffToPointPopoverData.rcLostAction = LostControlActionInCommandFLight.RETURN_HOME
takeoffToPointPopoverData.exitWaylineWhenRcLost = WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION
takeoffToPointPopoverData.rthMode = ERthMode.SETTING
takeoffToPointPopoverData.commanderModeLostAction = ECommanderModeLostAction.CONTINUE
takeoffToPointPopoverData.commanderFlightMode = ECommanderFlightMode.SETTING
takeoffToPointPopoverData.commanderFlightHeight = null
}
async function onTakeoffToPointConfirm (confirm: boolean) {
if (confirm) {
if (!takeoffToPointPopoverData.height ||
!takeoffToPointPopoverData.latitude ||
!takeoffToPointPopoverData.longitude ||
!takeoffToPointPopoverData.securityTakeoffHeight ||
!takeoffToPointPopoverData.rthAltitude ||
!takeoffToPointPopoverData.commanderFlightHeight) {
message.error('Input error')
return
}
try {
await takeoffToPoint(props.sn, {
target_latitude: takeoffToPointPopoverData.latitude,
target_longitude: takeoffToPointPopoverData.longitude,
target_height: takeoffToPointPopoverData.height,
security_takeoff_height: takeoffToPointPopoverData.securityTakeoffHeight,
rth_altitude: takeoffToPointPopoverData.rthAltitude,
max_speed: takeoffToPointPopoverData.maxSpeed,
rc_lost_action: takeoffToPointPopoverData.rcLostAction,
exit_wayline_when_rc_lost: takeoffToPointPopoverData.exitWaylineWhenRcLost,
rth_mode: takeoffToPointPopoverData.rthMode,
commander_mode_lost_action: takeoffToPointPopoverData.commanderModeLostAction,
commander_flight_mode: takeoffToPointPopoverData.commanderFlightMode,
commander_flight_height: takeoffToPointPopoverData.commanderFlightHeight,
})
} catch (error) {
}
}
takeoffToPointPopoverData.visible = false
}
const deviceTopicInfo: DeviceTopicInfo = reactive({
sn: props.sn,
pubTopic: '',
subTopic: ''
})
useMqtt(deviceTopicInfo)
//
// const drcState = computed(() => {
// return store.state.deviceState?.dockInfo[props.sn]?.link_osd?.drc_state === DrcStateEnum.CONNECTED
// })
const flightController = ref(false)
async function onClickFightControl () {
if (flightController.value) {
exitFlightCOntrol()
return
}
enterFlightControl()
}
//
async function enterFlightControl () {
try {
const { code, data } = await postDrcEnter({
client_id: clientId.value,
dock_sn: props.sn,
})
if (code === 0) {
flightController.value = true
if (data.sub && data.sub.length > 0) {
deviceTopicInfo.subTopic = data.sub[0]
}
if (data.pub && data.pub.length > 0) {
deviceTopicInfo.pubTopic = data.pub[0]
}
//
if (droneControlSource.value !== ControlSource.A) {
await postFlightAuth(props.sn)
}
message.success('Get flight control successfully')
}
} catch (error: any) {
}
}
// 退
async function exitFlightCOntrol () {
try {
const { code } = await postDrcExit({
client_id: clientId.value,
dock_sn: props.sn,
})
if (code === 0) {
flightController.value = false
deviceTopicInfo.subTopic = ''
deviceTopicInfo.pubTopic = ''
message.success('Exit flight control')
}
} catch (error: any) {
}
}
// drc mqtt message
const { drcInfo, errorInfo } = useDroneControlMqttEvent(props.sn)
const {
handleKeyup,
handleEmergencyStop,
resetControlState,
} = useManualControl(deviceTopicInfo, flightController)
function onMouseDown (type: KeyCode) {
handleKeyup(type)
}
function onMouseUp () {
resetControlState()
}
//
const payloadSelectInfo = {
value: null as any,
controlSource: undefined as undefined | ControlSource,
options: [] as any,
payloadIndex: '' as string,
camera: undefined as undefined | DeviceOsdCamera // osd
}
const handlePayloadChange = (value: string) => {
const payload = props.payloads?.find(item => item.payload_sn === value)
if (payload) {
payloadSelectInfo.payloadIndex = payload.payload_index || ''
payloadSelectInfo.controlSource = payload.control_source
payloadSelectInfo.camera = undefined
}
}
// function getCurrentCamera (cameraList: CameraListItem[], cameraIndex?: string):CameraListItem | null {
// let camera = null
// cameraList.forEach(item => {
// if (item.camera_index === cameraIndex) {
// camera = item
// }
// })
// return camera
// }
// const currentCamera = computed(() => {
// return getCurrentCamera(props.deviceInfo.dock.basic_osd.live_capacity?.device_list[0]?.camera_list as CameraListItem[], camera_index)
// })
//
watch(() => props.payloads, (payloads) => {
if (payloads && payloads.length > 0) {
payloadSelectInfo.value = payloads[0].payload_sn
payloadSelectInfo.controlSource = payloads[0].control_source || ControlSource.B
payloadSelectInfo.payloadIndex = payloads[0].payload_index || ''
payloadSelectInfo.options = payloads.map(item => ({ label: item.payload_name, value: item.payload_sn }))
payloadSelectInfo.camera = undefined
} else {
payloadSelectInfo.value = null
payloadSelectInfo.controlSource = undefined
payloadSelectInfo.options = []
payloadSelectInfo.payloadIndex = ''
payloadSelectInfo.camera = undefined
}
}, {
immediate: true,
deep: true
})
watch(() => props.deviceInfo.device, (droneOsd) => {
if (droneOsd && droneOsd.cameras) {
payloadSelectInfo.camera = droneOsd.cameras.find(item => item.payload_index === payloadSelectInfo.payloadIndex)
} else {
payloadSelectInfo.camera = undefined
}
}, {
immediate: true,
deep: true
})
// ws
const { droneControlSource, payloadControlSource } = useDroneControlWsEvent(props.sn, payloadSelectInfo.value)
watch(() => payloadControlSource, (controlSource) => {
payloadSelectInfo.controlSource = controlSource.value
}, {
immediate: true,
deep: true
})
const {
checkPayloadAuth,
authPayload,
resetGimbal,
switchCameraMode,
takeCameraPhoto,
startCameraRecording,
stopCameraRecording,
changeCameraFocalLength,
cameraAim,
} = usePayloadControl()
async function onAuthPayload () {
const result = await authPayload(props.sn, payloadSelectInfo.payloadIndex)
if (result) {
payloadControlSource.value = ControlSource.A
}
}
const gimbalResetPopoverData = reactive({
visible: false,
loading: false,
resetMode: null as null | GimbalResetMode,
})
function onShowGimbalResetPopover () {
gimbalResetPopoverData.visible = !gimbalResetPopoverData.visible
gimbalResetPopoverData.loading = false
gimbalResetPopoverData.resetMode = null
}
async function onGimbalResetConfirm (confirm: boolean) {
if (confirm) {
if (gimbalResetPopoverData.resetMode === null) {
message.error('Please select reset mode')
return
}
gimbalResetPopoverData.loading = true
try {
await resetGimbal(props.sn, {
payload_index: payloadSelectInfo.payloadIndex,
reset_mode: gimbalResetPopoverData.resetMode
})
} catch (err) {
}
}
gimbalResetPopoverData.visible = false
}
async function onSwitchCameraMode () {
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
return
}
const currentCameraMode = payloadSelectInfo.camera?.camera_mode
await switchCameraMode(props.sn, {
payload_index: payloadSelectInfo.payloadIndex,
camera_mode: currentCameraMode === CameraMode.Photo ? CameraMode.Video : CameraMode.Photo
})
}
async function onTakeCameraPhoto () {
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
return
}
await takeCameraPhoto(props.sn, payloadSelectInfo.payloadIndex)
}
async function onStartCameraRecording () {
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
return
}
await startCameraRecording(props.sn, payloadSelectInfo.payloadIndex)
}
async function onStopCameraRecording () {
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
return
}
await stopCameraRecording(props.sn, payloadSelectInfo.payloadIndex)
}
const zoomFactorPopoverData = reactive({
visible: false,
loading: false,
cameraType: null as null | CameraType,
zoomFactor: null as null | number,
})
function onShowZoomFactorPopover () {
zoomFactorPopoverData.visible = !zoomFactorPopoverData.visible
zoomFactorPopoverData.loading = false
zoomFactorPopoverData.cameraType = null
zoomFactorPopoverData.zoomFactor = null
}
async function onZoomFactorConfirm (confirm: boolean) {
if (confirm) {
if (!zoomFactorPopoverData.zoomFactor || zoomFactorPopoverData.cameraType === null) {
message.error('Please input Zoom Factor')
return
}
zoomFactorPopoverData.loading = true
try {
await changeCameraFocalLength(props.sn, {
payload_index: payloadSelectInfo.payloadIndex,
camera_type: zoomFactorPopoverData.cameraType,
zoom_factor: zoomFactorPopoverData.zoomFactor
})
} catch (err) {
}
}
zoomFactorPopoverData.visible = false
}
const cameraAimPopoverData = reactive({
visible: false,
loading: false,
cameraType: null as null | CameraType,
locked: false,
x: null as null | number,
y: null as null | number,
})
function onShowCameraAimPopover () {
cameraAimPopoverData.visible = !cameraAimPopoverData.visible
cameraAimPopoverData.loading = false
cameraAimPopoverData.cameraType = null
cameraAimPopoverData.locked = false
cameraAimPopoverData.x = null
cameraAimPopoverData.y = null
}
function openLivestreamOthers () {
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', true)
}
function openLivestreamAgora () {
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', true)
}
async function onCameraAimConfirm (confirm: boolean) {
if (confirm) {
if (cameraAimPopoverData.cameraType === null || cameraAimPopoverData.x === null || cameraAimPopoverData.y === null) {
message.error('Input error')
return
}
try {
await cameraAim(props.sn, {
payload_index: payloadSelectInfo.payloadIndex,
camera_type: cameraAimPopoverData.cameraType,
locked: cameraAimPopoverData.locked,
x: cameraAimPopoverData.x,
y: cameraAimPopoverData.y,
})
} catch (error) {
}
}
cameraAimPopoverData.visible = false
}
watch(() => errorInfo, (errorInfo) => {
if (errorInfo.value) {
message.error(errorInfo.value)
console.error(errorInfo.value)
errorInfo.value = ''
}
}, {
immediate: true,
deep: true
})
</script>
<style lang='scss' scoped>
.drone-control-wrapper{
// border-bottom: 1px solid #515151;
.drone-control-header{
font-size: 14px;
font-weight: 600;
padding: 10px 10px 0px;
}
.drone-control-box {
display: flex;
flex-wrap: 1;
.box {
width: 50%;
padding: 5px;
border: 0.5px solid rgba(255,255,255,0.3);
.row {
display: flex;
flex-wrap: wrap;
padding: 2px;
+ .row{
margin-bottom: 6px;
}
&::v-deep{
.ant-btn{
font-size: 12px;
padding: 0px 4px;
margin-right: 5px;
}
}
}
.drone-control{
&::v-deep{
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector{
padding: 0 2px;
}
}
}
.drone-control-direction{
margin-right: 10px;
.ant-btn {
// padding: 0px 1px;
margin-right: 0;
}
.word{
width: 12px;
margin-left: 2px;
font-size: 12px;
color: #aaa;
}
}
}
}
}
</style>

115
src/components/g-map/DroneControlPopover.vue

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

71
src/components/g-map/use-connect-mqtt.ts

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
import {
ref,
watch,
computed,
onUnmounted,
} from 'vue'
import { useMyStore } from '/@/store'
import { postDrc } from '/@/api/drc'
import {
UranusMqtt,
} from '/@/mqtt'
type StatusOptions = {
status: 'close';
event?: CloseEvent;
} | {
status: 'open';
retryCount: number;
} | {
status: 'pending';
}
export function useConnectMqtt () {
const store = useMyStore()
const dockOsdVisible = computed(() => {
return store.state.osdVisible && store.state.osdVisible.visible && store.state.osdVisible.is_dock
})
const mqttState = ref<UranusMqtt | null>(null)
// 监听已打开的设备小窗 窗口数量
watch(() => dockOsdVisible.value, async (val) => {
// 1.打开小窗
// 2.设备拥有飞行控制权
// 3.请求建立mqtt连接的认证信息
if (val) {
if (mqttState.value) return
const result = await postDrc({})
if (result?.code === 0) {
const { address, client_id, username, password, expire_time } = result.data
// @TODO: 校验 expire_time
mqttState.value = new UranusMqtt(address, {
clientId: client_id,
username,
password,
})
mqttState.value?.initMqtt()
mqttState.value?.on('onStatus', (statusOptions: StatusOptions) => {
// @TODO: 异常case
})
store.commit('SET_MQTT_STATE', mqttState.value)
store.commit('SET_CLIENT_ID', client_id)
}
// @TODO: 认证失败case
return
}
// 关闭所有小窗后
// 1.销毁mqtt连接重置mqtt状态
if (mqttState?.value) {
mqttState.value?.destroyed()
mqttState.value = null
store.commit('SET_MQTT_STATE', null)
store.commit('SET_CLIENT_ID', '')
}
}, { immediate: true })
onUnmounted(() => {
mqttState.value?.destroyed()
})
}

56
src/components/g-map/use-device-setting.ts

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

36
src/components/g-map/useDockControl.ts → src/components/g-map/use-dock-control.ts

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

76
src/components/g-map/use-drone-control-mqtt-event.ts

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'
import EventBus from '/@/event-bus/'
import {
DRC_METHOD,
DRCHsiInfo,
DRCOsdInfo,
DRCDelayTimeInfo,
DrcResponseInfo,
} from '/@/types/drc'
export function useDroneControlMqttEvent (sn: string) {
const drcInfo = ref('')
const hsiInfo = ref('')
const osdInfo = ref('')
const delayInfo = ref('')
const errorInfo = ref('')
function handleHsiInfo (data: DRCHsiInfo) {
hsiInfo.value = `method: ${DRC_METHOD.HSI_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n `
}
function handleOsdInfo (data: DRCOsdInfo) {
osdInfo.value = `method: ${DRC_METHOD.OSD_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n `
}
function handleDelayTimeInfo (data: DRCDelayTimeInfo) {
delayInfo.value = `method: ${DRC_METHOD.DELAY_TIME_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n `
}
function handleDroneControlErrorInfo (data: DrcResponseInfo) {
if (!data.result) {
return
}
errorInfo.value = `Drc error code: ${data.result}, seq: ${data.output?.seq}`
}
function handleDroneControlMqttEvent (payload: any) {
if (!payload || !payload.method) {
return
}
switch (payload.method) {
case DRC_METHOD.HSI_INFO_PUSH: {
handleHsiInfo(payload.data)
break
}
case DRC_METHOD.OSD_INFO_PUSH: {
handleOsdInfo(payload.data)
break
}
case DRC_METHOD.DELAY_TIME_INFO_PUSH: {
handleDelayTimeInfo(payload.data)
break
}
case DRC_METHOD.DRONE_EMERGENCY_STOP:
case DRC_METHOD.DRONE_CONTROL: {
handleDroneControlErrorInfo(payload.data)
break
}
}
drcInfo.value = hsiInfo.value + osdInfo.value + delayInfo.value
}
onMounted(() => {
EventBus.on('droneControlMqttInfo', handleDroneControlMqttEvent)
})
onBeforeUnmount(() => {
EventBus.off('droneControlMqttInfo', handleDroneControlMqttEvent)
})
return {
drcInfo: drcInfo,
errorInfo: errorInfo
}
}

95
src/components/g-map/use-drone-control-ws-event.ts

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
import { message, notification } from 'ant-design-vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import EventBus from '/@/event-bus/'
import { EBizCode } from '/@/types'
import { ControlSource } from '/@/types/device'
import { ControlSourceChangeType, ControlSourceChangeInfo, FlyToPointMessage, TakeoffToPointMessage, DrcModeExitNotifyMessage, DrcStatusNotifyMessage } from '/@/types/drone-control'
export interface UseDroneControlWsEventParams {
}
export function useDroneControlWsEvent (sn: string, payloadSn: string, funcs?: UseDroneControlWsEventParams) {
const droneControlSource = ref(ControlSource.A)
const payloadControlSource = ref(ControlSource.B)
function onControlSourceChange (data: ControlSourceChangeInfo) {
if (data.type === ControlSourceChangeType.Flight && data.sn === sn) {
droneControlSource.value = data.control_source
message.info(`Flight control is changed to ${droneControlSource.value}`)
return
}
if (data.type === ControlSourceChangeType.Payload && data.sn === payloadSn) {
payloadControlSource.value = data.control_source
message.info(`Payload control is changed to ${payloadControlSource.value}.`)
}
}
function handleProgress (key: string, message: string, error: number) {
if (error !== 0) {
notification.error({
key: key,
message: key + 'Error code:' + error,
description: message,
duration: null
})
} else {
notification.info({
key: key,
message: key,
description: message,
duration: 30
})
}
}
function handleDroneControlWsEvent (payload: any) {
if (!payload) {
return
}
switch (payload.biz_code) {
case EBizCode.ControlSourceChange: {
onControlSourceChange(payload.data)
break
}
case EBizCode.FlyToPointProgress: {
const { sn: deviceSn, result, message: msg } = payload.data as FlyToPointMessage
if (deviceSn !== sn) return
handleProgress(EBizCode.FlyToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result)
break
}
case EBizCode.TakeoffToPointProgress: {
const { sn: deviceSn, result, message: msg } = payload.data as TakeoffToPointMessage
if (deviceSn !== sn) return
handleProgress(EBizCode.TakeoffToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result)
break
}
case EBizCode.JoystickInvalidNotify: {
const { sn: deviceSn, result, message: msg } = payload.data as DrcModeExitNotifyMessage
if (deviceSn !== sn) return
handleProgress(EBizCode.JoystickInvalidNotify, `device(sn: ${deviceSn}) ${msg}`, result)
break
}
case EBizCode.DrcStatusNotify: {
const { sn: deviceSn, result, message: msg } = payload.data as DrcStatusNotifyMessage
// handleProgress(EBizCode.DrcStatusNotify, `device(sn: ${deviceSn}) ${msg}`, result)
break
}
}
// eslint-disable-next-line no-unused-expressions
// console.log('payload.biz_code', payload.data)
}
onMounted(() => {
EventBus.on('droneControlWs', handleDroneControlWsEvent)
})
onBeforeUnmount(() => {
EventBus.off('droneControlWs', handleDroneControlWsEvent)
})
return {
droneControlSource: droneControlSource,
payloadControlSource: payloadControlSource
}
}

40
src/components/g-map/use-drone-control.ts

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
import { ref } from 'vue'
import { postFlyToPoint, PostFlyToPointBody, deleteFlyToPoint, postTakeoffToPoint, PostTakeoffToPointBody } from '/@/api/drone-control/drone'
import { message } from 'ant-design-vue'
export function useDroneControl () {
const droneControlPanelVisible = ref(false)
function setDroneControlPanelVisible (visible: boolean) {
droneControlPanelVisible.value = visible
}
async function flyToPoint (sn: string, body: PostFlyToPointBody) {
const { code } = await postFlyToPoint(sn, body)
if (code === 0) {
message.success('Fly to')
}
}
async function stopFlyToPoint (sn: string) {
const { code } = await deleteFlyToPoint(sn)
if (code === 0) {
message.success('Stop fly to')
}
}
async function takeoffToPoint (sn: string, body: PostTakeoffToPointBody) {
const { code } = await postTakeoffToPoint(sn, body)
if (code === 0) {
message.success('Take off successfully')
}
}
return {
droneControlPanelVisible,
setDroneControlPanelVisible,
flyToPoint,
stopFlyToPoint,
takeoffToPoint
}
}

165
src/components/g-map/use-manual-control.ts

@ -0,0 +1,165 @@ @@ -0,0 +1,165 @@
import {
ref,
onUnmounted,
watch,
Ref,
} from 'vue'
import { message } from 'ant-design-vue'
import {
DRC_METHOD,
DroneControlProtocol,
} from '/@/types/drc'
import {
useMqtt,
DeviceTopicInfo
} from './use-mqtt'
let myInterval: any
export enum KeyCode {
KEY_W = 'KeyW',
KEY_A = 'KeyA',
KEY_S = 'KeyS',
KEY_D = 'KeyD',
KEY_Q = 'KeyQ',
KEY_E = 'KeyE',
ARROW_UP = 'ArrowUp',
ARROW_DOWN = 'ArrowDown',
}
export function useManualControl (deviceTopicInfo: DeviceTopicInfo, isCurrentFlightController: Ref<boolean>) {
const activeCodeKey = ref(null) as Ref<KeyCode | null>
const mqttHooks = useMqtt(deviceTopicInfo)
let seq = 0
function handlePublish (params: DroneControlProtocol) {
const body = {
method: DRC_METHOD.DRONE_CONTROL,
data: params,
}
handleClearInterval()
myInterval = setInterval(() => {
body.data.seq = seq++
seq++
window.console.log('keyCode>>>>', activeCodeKey.value, body)
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
}, 50)
}
function handleKeyup (keyCode: KeyCode) {
if (!deviceTopicInfo.pubTopic) {
message.error('请确保已经建立DRC链路')
return
}
const SPEED = 5 // check
const HEIGHT = 5 // check
const W_SPEED = 20 // 机头角速度
seq = 0
switch (keyCode) {
case 'KeyA':
if (activeCodeKey.value === keyCode) return
handlePublish({ y: -SPEED })
activeCodeKey.value = keyCode
break
case 'KeyW':
if (activeCodeKey.value === keyCode) return
handlePublish({ x: SPEED })
activeCodeKey.value = keyCode
break
case 'KeyS':
if (activeCodeKey.value === keyCode) return
handlePublish({ x: -SPEED })
activeCodeKey.value = keyCode
break
case 'KeyD':
if (activeCodeKey.value === keyCode) return
handlePublish({ y: SPEED })
activeCodeKey.value = keyCode
break
case 'ArrowUp':
if (activeCodeKey.value === keyCode) return
handlePublish({ h: HEIGHT })
activeCodeKey.value = keyCode
break
case 'ArrowDown':
if (activeCodeKey.value === keyCode) return
handlePublish({ h: -HEIGHT })
activeCodeKey.value = keyCode
break
case 'KeyQ':
if (activeCodeKey.value === keyCode) return
handlePublish({ w: -W_SPEED })
activeCodeKey.value = keyCode
break
case 'KeyE':
if (activeCodeKey.value === keyCode) return
handlePublish({ w: W_SPEED })
activeCodeKey.value = keyCode
break
default:
break
}
}
function handleClearInterval () {
clearInterval(myInterval)
myInterval = undefined
}
function resetControlState () {
activeCodeKey.value = null
seq = 0
handleClearInterval()
}
function onKeyup () {
resetControlState()
}
function onKeydown (e: KeyboardEvent) {
handleKeyup(e.code as KeyCode)
}
function startKeyboardManualControl () {
window.addEventListener('keydown', onKeydown)
window.addEventListener('keyup', onKeyup)
}
function closeKeyboardManualControl () {
resetControlState()
window.removeEventListener('keydown', onKeydown)
window.removeEventListener('keyup', onKeyup)
}
watch(() => isCurrentFlightController.value, (val) => {
if (val && deviceTopicInfo.pubTopic) {
startKeyboardManualControl()
} else {
closeKeyboardManualControl()
}
}, { immediate: true })
onUnmounted(() => {
closeKeyboardManualControl()
})
function handleEmergencyStop () {
if (!deviceTopicInfo.pubTopic) {
message.error('请确保已经建立DRC链路')
return
}
const body = {
method: DRC_METHOD.DRONE_EMERGENCY_STOP,
data: {}
}
resetControlState()
window.console.log('handleEmergencyStop>>>>', deviceTopicInfo.pubTopic, body)
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 1 })
}
return {
activeCodeKey,
handleKeyup,
handleEmergencyStop,
resetControlState,
}
}

134
src/components/g-map/use-mqtt.ts

@ -0,0 +1,134 @@ @@ -0,0 +1,134 @@
import {
ref,
reactive,
computed,
watch,
onUnmounted,
} from 'vue'
import {
IClientPublishOptions,
IPublishPacket,
} from '/@/mqtt'
import { useMyStore } from '/@/store'
import {
DRC_METHOD,
} from '/@/types/drc'
import EventBus from '/@/event-bus'
export interface DeviceTopicInfo{
sn: string
pubTopic: string
subTopic: string
}
type MessageMqtt = (topic: string, payload: Buffer, packet: IPublishPacket) => void | Promise<void>
export function useMqtt (deviceTopicInfo: DeviceTopicInfo) {
let cacheSubscribeArr: {
topic: string;
callback?: MessageMqtt;
}[] = []
const store = useMyStore()
const mqttState = computed(() => {
return store.state.mqttState
})
function publishMqtt (topic: string, body: object, ots?: IClientPublishOptions) {
// const buffer = Buffer.from(JSON.stringify(body))
mqttState.value?.publishMqtt(topic, JSON.stringify(body), ots)
}
function subscribeMqtt (topic: string, handleMessageMqtt?: MessageMqtt) {
mqttState.value?.subscribeMqtt(topic)
const handler = handleMessageMqtt || onMessageMqtt
mqttState.value?.on('onMessageMqtt', handler)
cacheSubscribeArr.push({
topic,
callback: handler,
})
}
function onMessageMqtt (message: any) {
if (cacheSubscribeArr.findIndex(item => item.topic === message?.topic) !== -1) {
const payloadStr = new TextDecoder('utf-8').decode(message?.payload)
const payloadObj = JSON.parse(payloadStr)
switch (payloadObj?.method) {
case DRC_METHOD.HEART_BEAT:
break
case DRC_METHOD.DELAY_TIME_INFO_PUSH:
case DRC_METHOD.HSI_INFO_PUSH:
case DRC_METHOD.OSD_INFO_PUSH:
case DRC_METHOD.DRONE_CONTROL:
case DRC_METHOD.DRONE_EMERGENCY_STOP:
EventBus.emit('droneControlMqttInfo', payloadObj)
break
default:
break
}
}
}
function unsubscribeDrc () {
// 销毁已订阅事件
cacheSubscribeArr.forEach(item => {
mqttState.value?.off('onMessageMqtt', item.callback)
mqttState.value?.unsubscribeMqtt(item.topic)
})
cacheSubscribeArr = []
}
// 心跳
const heartBeatSeq = ref(0)
const state = reactive({
heartState: new Map<string, {
pingInterval: any;
}>(),
})
// 监听云控控制权
watch(() => deviceTopicInfo, (val, oldVal) => {
if (val.subTopic !== '') {
// 1.订阅topic
subscribeMqtt(deviceTopicInfo.subTopic)
// 2.发心跳
publishDrcPing(deviceTopicInfo.sn)
} else {
clearInterval(state.heartState.get(deviceTopicInfo.sn)?.pingInterval)
state.heartState.delete(deviceTopicInfo.sn)
heartBeatSeq.value = 0
}
}, { immediate: true, deep: true })
function publishDrcPing (sn: string) {
const body = {
method: DRC_METHOD.HEART_BEAT,
data: {
ts: new Date().getTime(),
seq: heartBeatSeq.value,
},
}
const pingInterval = setInterval(() => {
if (!mqttState.value) return
heartBeatSeq.value += 1
body.data.ts = new Date().getTime()
body.data.seq = heartBeatSeq.value
publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
}, 1000)
state.heartState.set(sn, {
pingInterval,
})
}
onUnmounted(() => {
unsubscribeDrc()
heartBeatSeq.value = 0
})
return {
mqttState,
publishMqtt,
subscribeMqtt,
}
}

120
src/components/g-map/use-payload-control.ts

@ -0,0 +1,120 @@ @@ -0,0 +1,120 @@
import { message } from 'ant-design-vue'
import {
postPayloadAuth,
postPayloadCommands,
PayloadCommandsEnum,
PostCameraModeBody,
PostCameraFocalLengthBody,
PostGimbalResetBody,
PostCameraAimBody,
} from '/@/api/drone-control/payload'
import { ControlSource } from '/@/types/device'
export function usePayloadControl () {
function checkPayloadAuth (controlSource?: ControlSource) {
if (controlSource !== ControlSource.A) {
message.error('Get Payload Control first')
return false
}
return true
}
async function authPayload (sn: string, payloadIndx: string) {
const { code } = await postPayloadAuth(sn, {
payload_index: payloadIndx
})
if (code === 0) {
message.success('Get Payload Control successfully')
return true
}
return false
}
async function resetGimbal (sn: string, data: PostGimbalResetBody) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.GimbalReset,
data: data
})
if (code === 0) {
message.success('Gimbal Reset successfully')
}
}
async function switchCameraMode (sn: string, data: PostCameraModeBody) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.CameraModeSwitch,
data: data
})
if (code === 0) {
message.success('Camera Mode Switch successfully')
}
}
async function takeCameraPhoto (sn: string, payloadIndx: string) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.CameraPhotoTake,
data: {
payload_index: payloadIndx
}
})
if (code === 0) {
message.success('Take Photo successfully')
}
}
async function startCameraRecording (sn: string, payloadIndx: string) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.CameraRecordingStart,
data: {
payload_index: payloadIndx
}
})
if (code === 0) {
message.success('Start Recording successfully')
}
}
async function stopCameraRecording (sn: string, payloadIndx: string) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.CameraRecordingStop,
data: {
payload_index: payloadIndx
}
})
if (code === 0) {
message.success('Stop Recording successfully')
}
}
async function changeCameraFocalLength (sn: string, data: PostCameraFocalLengthBody) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.CameraFocalLengthSet,
data: data,
})
if (code === 0) {
message.success('Zoom successfully')
}
}
async function cameraAim (sn: string, data: PostCameraAimBody) {
const { code } = await postPayloadCommands(sn, {
cmd: PayloadCommandsEnum.CameraAim,
data: data,
})
if (code === 0) {
message.success('Zoom Aim successfully')
}
}
return {
checkPayloadAuth,
authPayload,
resetGimbal,
switchCameraMode,
takeCameraPhoto,
startCameraRecording,
stopCameraRecording,
changeCameraFocalLength,
cameraAim,
}
}

198
src/components/livestream-agora.vue

@ -1,17 +1,25 @@ @@ -1,17 +1,25 @@
<template>
<div class="mt20 flex-column flex-justify-start flex-align-center">
<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"
@select="onDroneSelect"
v-model:value="dronePara.droneSelected"
>
<a-select-option
v-for="item in dronePara.droneList"
:key="item.value"
:value="item.value"
@click="onDroneSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
@ -19,12 +27,13 @@ @@ -19,12 +27,13 @@
class="ml10"
style="width:150px"
placeholder="Select Camera"
@select="onCameraSelect"
v-model:value="dronePara.cameraSelected"
>
<a-select-option
v-for="item in dronePara.cameraList"
:key="item.value"
:value="item.value"
@click="onCameraSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
@ -35,13 +44,13 @@ @@ -35,13 +44,13 @@
@select="onVideoSelect"
>
<a-select-option
class="ml10"
v-for="item in dronePara.videoList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select> -->
</template>
<a-select
class="ml10"
style="width:150px"
@ -60,12 +69,15 @@ @@ -60,12 +69,15 @@
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"
@ -73,14 +85,15 @@ @@ -73,14 +85,15 @@
></a-input>
</div>
<div class="mt20 flex-row flex-justify-center flex-align-center">
<a-button type="primary" large @click="onStart">Play</a-button>
<a-button v-if="livePara.liveState && dronePara.isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
<a-button v-else type="primary" large @click="onStart">Play</a-button>
<a-button class="ml20" type="primary" large @click="onStop"
>Stop</a-button
>
<a-button class="ml20" type="primary" large @click="onUpdateQuality"
>Update Clarity</a-button
>
<a-button class="ml20" type="primary" large @click="onRefresh"
<a-button v-if="!livePara.liveState || !dronePara.isDockLive" class="ml20" type="primary" large @click="onRefresh"
>Refresh Live Capacity</a-button
>
</div>
@ -91,8 +104,9 @@ @@ -91,8 +104,9 @@
import AgoraRTC, { IAgoraRTCClient, IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
import { message } from 'ant-design-vue'
import { onMounted, reactive } from 'vue'
import { uuidv4 } from '../utils/uuid'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
const root = getRoot()
@ -120,6 +134,12 @@ const clarityList = [ @@ -120,6 +134,12 @@ const clarityList = [
}
]
interface SelectOption {
value: any,
label: string,
more?: any
}
let agoraClient = {} as IAgoraRTCClient
const agoraPara = reactive({
appid: config.agoraAPPID,
@ -130,13 +150,16 @@ const agoraPara = reactive({ @@ -130,13 +150,16 @@ const agoraPara = reactive({
})
const dronePara = reactive({
livestreamSource: [],
droneList: [] as any[],
cameraList: [] as any[],
videoList: [] as any[],
droneSelected: '',
cameraSelected: '',
videoSelected: '',
claritySelected: 0
droneList: [] as SelectOption[],
cameraList: [] as SelectOption[],
videoList: [] as SelectOption[],
droneSelected: undefined as string | undefined,
cameraSelected: undefined as string | undefined,
videoSelected: undefined as string | undefined,
claritySelected: 0,
lensList: [] as string[],
lensSelected: undefined as string | undefined,
isDockLive: false
})
const livePara = reactive({
url: '',
@ -144,14 +167,14 @@ const livePara = reactive({ @@ -144,14 +167,14 @@ const livePara = reactive({
videoId: '',
liveState: false
})
const nonSwitchable = 'normal'
const onRefresh = async () => {
dronePara.droneList = []
dronePara.cameraList = []
dronePara.videoList = []
dronePara.droneSelected = ''
dronePara.cameraSelected = ''
dronePara.videoSelected = ''
dronePara.droneSelected = undefined
dronePara.cameraSelected = undefined
dronePara.videoSelected = undefined
await getLiveCapacity({})
.then(res => {
if (res.code === 0) {
@ -166,12 +189,13 @@ const onRefresh = async () => { @@ -166,12 +189,13 @@ const onRefresh = async () => {
if (dronePara.livestreamSource) {
dronePara.livestreamSource.forEach((ele: any) => {
dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn })
dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
})
}
}
})
.catch(error => {
message.error(error)
console.error(error)
})
}
@ -179,6 +203,10 @@ const onRefresh = async () => { @@ -179,6 +203,10 @@ const onRefresh = async () => {
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')
@ -190,24 +218,24 @@ onMounted(() => { @@ -190,24 +218,24 @@ onMounted(() => {
// 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.
const remotePlayerContainer: any = document.getElementById('player')
// remotePlayerContainer.id = agoraPara.uid
remoteVideoTrack.play(remotePlayerContainer)
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)
message.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(
@ -222,25 +250,20 @@ const onStart = async () => { @@ -222,25 +250,20 @@ const onStart = async () => {
if (
dronePara.droneSelected == null ||
dronePara.cameraSelected == null ||
dronePara.videoSelected == null ||
dronePara.claritySelected == null
) {
message.warn('waring: not select live para!!!')
return
}
agoraClient.setClientRole('audience', { level: 1 })
agoraClient.setClientRole('audience', { level: 2 })
if (agoraClient.connectionState === 'DISCONNECTED') {
agoraClient
.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
await agoraClient.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
}
livePara.videoId =
dronePara.droneSelected +
'/' +
dronePara.cameraSelected +
'/' +
dronePara.videoSelected
dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
console.log(agoraPara)
agoraPara.token = encodeURIComponent(agoraPara.token)
livePara.url =
'channel=' +
@ -248,7 +271,7 @@ const onStart = async () => { @@ -248,7 +271,7 @@ const onStart = async () => {
'&sn=' +
dronePara.droneSelected +
'&token=' +
agoraPara.token +
encodeURIComponent(agoraPara.token) +
'&uid=' +
agoraPara.uid
@ -259,6 +282,9 @@ const onStart = async () => { @@ -259,6 +282,9 @@ const onStart = async () => {
video_quality: dronePara.claritySelected
})
.then(res => {
if (res.code !== 0) {
return
}
livePara.liveState = true
})
.catch(err => {
@ -266,65 +292,72 @@ const onStart = async () => { @@ -266,65 +292,72 @@ const onStart = async () => {
})
}
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
dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
stopLivestream({
video_id: livePara.videoId
}).then(res => {
if (res.code === 0) {
message.success(res.message)
livePara.liveState = false
dronePara.lensSelected = ''
console.log('stop play livestream')
}
livePara.liveState = false
console.log('stop play livestream')
})
}
const onDroneSelect = (val: any) => {
dronePara.droneSelected = val
if (dronePara.droneSelected) {
const droneTemp = dronePara.livestreamSource
dronePara.cameraList = []
const onDroneSelect = (val: SelectOption) => {
dronePara.cameraList = []
dronePara.videoList = []
dronePara.lensList = []
droneTemp.forEach((ele: any) => {
const drone = ele
if (drone.cameras_list && drone.sn === dronePara.droneSelected) {
const cameraListTemp = drone.cameras_list
cameraListTemp.forEach((ele: any) => {
dronePara.cameraList.push({ label: ele.name, value: ele.index })
})
}
})
dronePara.cameraSelected = undefined
dronePara.videoSelected = undefined
dronePara.lensSelected = undefined
dronePara.droneSelected = val.value
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
dronePara.cameraList.push({ label: ele.name, value: ele.index, more: ele.videos_list })
})
}
const onCameraSelect = (val: any) => {
dronePara.cameraSelected = val
const onCameraSelect = (val: SelectOption) => {
dronePara.cameraSelected = val.value
dronePara.videoSelected = undefined
dronePara.lensSelected = undefined
dronePara.videoList = []
dronePara.lensList = []
if (!val.more) {
return
}
if (dronePara.cameraSelected) {
const droneTemp = dronePara.livestreamSource
droneTemp.forEach((ele: any) => {
const drone = ele
if (drone.sn === dronePara.droneSelected) {
const cameraListTemp = drone.cameras_list
cameraListTemp.forEach((ele: any) => {
const camera = ele
if (camera.index === dronePara.cameraSelected) {
const videoListTemp = camera.videos_list
dronePara.videoList = []
videoListTemp.forEach((ele: any) => {
dronePara.videoList.push({ label: ele.type, value: ele.index })
})
dronePara.videoSelected = dronePara.videoList[0]?.value
}
})
}
})
val.more.forEach((ele: any) => {
dronePara.videoList.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
})
if (dronePara.videoList.length === 0) {
return
}
const firstVideo: SelectOption = dronePara.videoList[0]
dronePara.videoSelected = firstVideo.value
dronePara.lensList = firstVideo.more
dronePara.lensSelected = firstVideo.label
dronePara.isDockLive = dronePara.lensList?.length > 0
}
const onVideoSelect = (val: any) => {
dronePara.videoSelected = val
const onVideoSelect = (val: SelectOption) => {
dronePara.videoSelected = val.value
dronePara.lensList = val.more
dronePara.lensSelected = val.label
}
const onClaritySelect = (val: any) => {
dronePara.claritySelected = val
@ -343,6 +376,21 @@ const onUpdateQuality = () => { @@ -343,6 +376,21 @@ const onUpdateQuality = () => {
}
})
}
const onSwitch = () => {
if (dronePara.lensSelected === undefined || dronePara.lensSelected === nonSwitchable) {
message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
return
}
changeLivestreamLens({
video_id: livePara.videoId,
video_type: dronePara.lensSelected
}).then(res => {
if (res.code === 0) {
message.success('Switching live camera successfully.')
}
})
}
</script>
<style lang="scss" scoped>

213
src/components/livestream-others.vue

@ -1,18 +1,28 @@ @@ -1,18 +1,28 @@
<template>
<div class="flex-column flex-justify-start flex-align-center mt20">
<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"
@ -26,12 +36,13 @@ @@ -26,12 +36,13 @@
class="ml10"
style="width:150px"
placeholder="Select Drone"
@select="onDroneSelect"
v-model:value="droneSelected"
>
<a-select-option
v-for="item in droneList"
:key="item.value"
:value="item.value"
@click="onDroneSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
@ -39,12 +50,13 @@ @@ -39,12 +50,13 @@
class="ml10"
style="width:150px"
placeholder="Select Camera"
@select="onCameraSelect"
v-model:value="cameraSelected"
>
<a-select-option
v-for="item in cameraList"
:key="item.value"
:value="item.value"
@click="onCameraSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select>
@ -52,21 +64,23 @@ @@ -52,21 +64,23 @@
class="ml10"
style="width:150px"
placeholder="Select Lens"
@select="onVideoSelect"
v-model:value="videoSelected"
>
<a-select-option
class="ml10"
v-for="item in videoList"
:key="item.value"
:value="item.value"
@click="onVideoSelect(item)"
>{{ item.label }}</a-select-option
>
</a-select> -->
</template>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Clarity"
@select="onClaritySelect"
v-model:value="claritySelected"
>
<a-select-option
v-for="item in clarityList"
@ -85,14 +99,15 @@ @@ -85,14 +99,15 @@
</p>
</div>
<div class="mt10 flex-row flex-justify-center flex-align-center">
<a-button type="primary" large @click="onStart">Play</a-button>
<a-button v-if="liveState && isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
<a-button v-else type="primary" large @click="onStart">Play</a-button>
<a-button class="ml20" type="primary" large @click="onStop"
>Stop</a-button
>
<a-button class="ml20" type="primary" large @click="onUpdateQuality"
>Update Clarity</a-button
>
<a-button class="ml20" type="primary" large @click="onRefresh"
<a-button v-if="!liveState || !isDockLive" class="ml20" type="primary" large @click="onRefresh"
>Refresh Live Capacity</a-button
>
</div>
@ -103,12 +118,20 @@ @@ -103,12 +118,20 @@
import { message } from 'ant-design-vue'
import { onMounted, reactive, ref } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
import jswebrtc from '/@/vendors/jswebrtc.min.js'
import srs from '/@/vendors/srs.sdk.js'
const root = getRoot()
const liveTypeList = [
interface SelectOption {
value: any,
label: string,
more?: any
}
const liveTypeList: SelectOption[] = [
{
value: 1,
label: 'RTMP'
@ -120,9 +143,13 @@ const liveTypeList = [ @@ -120,9 +143,13 @@ const liveTypeList = [
{
value: 3,
label: 'GB28181'
},
{
value: 4,
label: 'WEBRTC'
}
]
const clarityList = [
const clarityList: SelectOption[] = [
{
value: 0,
label: 'Adaptive'
@ -152,12 +179,17 @@ const cameraList = ref() @@ -152,12 +179,17 @@ const cameraList = ref()
const videoList = ref()
const droneSelected = ref()
const cameraSelected = ref()
const videoSeleted = ref()
const claritySeleted = ref()
const videoSelected = ref()
const claritySelected = ref()
const videoId = ref()
const liveState = ref<boolean>(false)
const livetypeSelected = ref()
const rtspData = ref()
const lensList = ref<string[]>([])
const lensSelected = ref<String>()
const isDockLive = ref(false)
const nonSwitchable = 'normal'
let webrtc: any = null
const onRefresh = async () => {
droneList.value = []
@ -165,7 +197,7 @@ const onRefresh = async () => { @@ -165,7 +197,7 @@ const onRefresh = async () => {
videoList.value = []
droneSelected.value = null
cameraSelected.value = null
videoSeleted.value = null
videoSelected.value = null
await getLiveCapacity({})
.then(res => {
console.log(res)
@ -178,16 +210,17 @@ const onRefresh = async () => { @@ -178,16 +210,17 @@ const onRefresh = async () => {
console.log('live_capacity:', resData)
livestreamSource.value = resData
const temp: Array<{}> = []
const temp: Array<SelectOption> = []
if (livestreamSource.value) {
livestreamSource.value.forEach((ele: any) => {
temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn })
temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
})
droneList.value = temp
}
}
})
.catch(error => {
message.error(error)
console.error(error)
})
}
@ -201,22 +234,22 @@ const onStart = async () => { @@ -201,22 +234,22 @@ const onStart = async () => {
livetypeSelected.value,
droneSelected.value,
cameraSelected.value,
videoSeleted.value,
claritySeleted.value
videoSelected.value,
claritySelected.value
)
const timestamp = new Date().getTime().toString()
if (
livetypeSelected.value == null ||
droneSelected.value == null ||
cameraSelected.value == null ||
videoSeleted.value == null ||
claritySeleted.value == null
claritySelected.value == null
) {
message.warn('waring: not select live para!!!')
return
}
videoId.value =
droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
let liveURL = ''
switch (livetypeSelected.value) {
case 1: {
@ -233,6 +266,9 @@ const onStart = async () => { @@ -233,6 +266,9 @@ const onStart = async () => {
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
@ -241,9 +277,12 @@ const onStart = async () => { @@ -241,9 +277,12 @@ const onStart = async () => {
url: liveURL,
video_id: videoId.value,
url_type: livetypeSelected.value,
video_quality: claritySeleted.value
video_quality: claritySelected.value
})
.then(res => {
if (res.code !== 0) {
return
}
if (livetypeSelected.value === 3) {
const url = res.data.url
const videoElement = videowebrtc.value
@ -259,18 +298,11 @@ const onStart = async () => { @@ -259,18 +298,11 @@ const onStart = async () => {
console.log('start play livestream')
}
})
liveState.value = true
}
})
} else if (livetypeSelected.value === 2) {
console.log(res)
rtspData.value =
'url:' +
res.data.url +
'&username:' +
res.data.username +
'&password:' +
res.data.password
rtspData.value = 'url:' + res.data.url
} else if (livetypeSelected.value === 1) {
const url = res.data.url
const videoElement = videowebrtc.value
@ -281,10 +313,14 @@ const onStart = async () => { @@ -281,10 +313,14 @@ const onStart = async () => {
autoplay: true,
onPlay: (obj: any) => {
console.log('start play livestream')
liveState.value = true
}
})
} 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)
@ -292,13 +328,15 @@ const onStart = async () => { @@ -292,13 +328,15 @@ const onStart = async () => {
}
const onStop = () => {
videoId.value =
droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
stopLivestream({
video_id: videoId.value
}).then(res => {
if (res.code === 0) {
message.info(res.message)
message.success(res.message)
liveState.value = false
lensSelected.value = undefined
console.log('stop play livestream')
}
})
@ -311,10 +349,10 @@ const onUpdateQuality = () => { @@ -311,10 +349,10 @@ const onUpdateQuality = () => {
}
setLivestreamQuality({
video_id: videoId.value,
video_quality: claritySeleted.value
video_quality: claritySelected.value
}).then(res => {
if (res.code === 0) {
message.success('Set the clarity to ' + clarityList[claritySeleted.value].label)
message.success('Set the clarity to ' + clarityList[claritySelected.value].label)
}
})
}
@ -322,54 +360,79 @@ const onUpdateQuality = () => { @@ -322,54 +360,79 @@ const onUpdateQuality = () => {
const onLiveTypeSelect = (val: any) => {
livetypeSelected.value = val
}
const onDroneSelect = (val: any) => {
droneSelected.value = val
const temp: Array<{}> = []
const onDroneSelect = (val: SelectOption) => {
droneSelected.value = val.value
const temp: Array<SelectOption> = []
cameraList.value = []
if (droneSelected.value) {
const droneTemp = livestreamSource.value
droneTemp.forEach((ele: any) => {
const drone = ele
if (drone.cameras_list && drone.sn === droneSelected.value) {
const cameraListTemp = drone.cameras_list
console.info(ele)
cameraListTemp.forEach((ele: any) => {
temp.push({ label: ele.name, value: ele.index })
})
cameraList.value = temp
}
})
cameraSelected.value = undefined
videoSelected.value = undefined
videoList.value = []
lensList.value = []
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
temp.push({ label: ele.name, value: ele.index, more: ele.videos_list })
})
cameraList.value = temp
}
const onCameraSelect = (val: any) => {
cameraSelected.value = val
const result: Array<{}> = []
if (cameraSelected.value) {
const droneTemp = livestreamSource.value
droneTemp.forEach((ele: any) => {
const drone = ele
if (drone.sn === droneSelected.value) {
const cameraListTemp = drone.cameras_list
cameraListTemp.forEach((ele: any) => {
const camera = ele
if (camera.index === cameraSelected.value) {
const videoListTemp = camera.videos_list
videoListTemp.forEach((ele: any) => {
result.push({ label: ele.type, value: ele.index })
})
videoList.value = result
videoSeleted.value = videoList.value[0]?.value
}
})
}
})
const onCameraSelect = (val: SelectOption) => {
cameraSelected.value = val.value
const result: Array<SelectOption> = []
videoSelected.value = undefined
videoList.value = []
lensList.value = []
if (!val.more) {
return
}
val.more.forEach((ele: any) => {
result.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
})
videoList.value = result
if (videoList.value.length === 0) {
return
}
const firstVideo: SelectOption = videoList.value[0]
videoSelected.value = firstVideo.value
lensList.value = firstVideo.more
lensSelected.value = firstVideo.label
isDockLive.value = lensList.value?.length > 0
}
const onVideoSelect = (val: any) => {
videoSeleted.value = val
const onVideoSelect = (val: SelectOption) => {
videoSelected.value = val.value
lensList.value = val.more
lensSelected.value = val.label
}
const onClaritySelect = (val: any) => {
claritySeleted.value = val
claritySelected.value = val
}
const onSwitch = () => {
if (lensSelected.value === undefined || lensSelected.value === nonSwitchable) {
message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
return
}
changeLivestreamLens({
video_id: videoId.value,
video_type: lensSelected.value
}).then(res => {
if (res.code === 0) {
message.success('Switching live camera successfully.')
}
})
}
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>

450
src/components/task/CreatePlan.vue

@ -0,0 +1,450 @@ @@ -0,0 +1,450 @@
<template>
<div class="create-plan-wrapper">
<div class="header">
Create Plan
</div>
<div class="content">
<a-form ref="valueRef" layout="horizontal" :hideRequiredMark="true" :rules="rules" :model="planBody" labelAlign="left">
<a-form-item label="Plan Name" name="name" :labelCol="{span: 23}">
<a-input style="background: black;" placeholder="Please enter plan name" v-model:value="planBody.name"/>
</a-form-item>
<!-- 航线 -->
<a-form-item label="Flight Route" :wrapperCol="{offset: 7}" name="file_id">
<router-link
:to="{name: 'select-plan'}"
@click="selectRoute"
>
Select Route
</router-link>
</a-form-item>
<a-form-item v-if="planBody.file_id" style="margin-top: -15px;">
<div class="wayline-panel" style="padding-top: 5px;">
<div class="title">
<a-tooltip :title="wayline.name">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div>
</a-tooltip>
<div class="ml10"><UserOutlined /></div>
<a-tooltip :title="wayline.user_name">
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div>
</a-tooltip>
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ DEVICE_NAME[wayline.drone_model_key] }}</span>
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span>
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
{{ DEVICE_NAME[payload] }}
</span>
</div>
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span>
</div>
</div>
</a-form-item>
<!-- 设备 -->
<a-form-item label="Device" :wrapperCol="{offset: 10}" v-model:value="planBody.dock_sn" name="dock_sn">
<router-link
:to="{name: 'select-plan'}"
@click="selectDevice"
>Select Device</router-link>
</a-form-item>
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;">
<div class="panel" style="padding-top: 5px;">
<div class="title">
<a-tooltip :title="dock.nickname">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
</a-tooltip>
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ dock.children?.nickname ?? 'No drone' }}</span>
</div>
</div>
</a-form-item>
<!-- 任务类型 -->
<a-form-item label="Plan Timer" class="plan-timer-form-item">
<div style="white-space: nowrap;">
<a-radio-group v-model:value="planBody.task_type" button-style="solid">
<a-radio-button v-for="type in TaskTypeOptions" :value="type.value" :key="type.value">{{ type.label }}</a-radio-button>
</a-radio-group>
</div>
</a-form-item>
<!-- execute date -->
<a-form-item label="Date" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition" name="select_execute_date" :labelCol="{span: 23}">
<a-range-picker
v-model:value="planBody.select_execute_date"
:disabledDate="(current: Moment) => current < moment().subtract(1, 'days')"
format="YYYY-MM-DD"
:placeholder="['Start Time', 'End Time']"
style="width: 100%;"
/>
</a-form-item>
<!-- execute time -->
<a-form-item label="Time" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition"
name="select_execute_time" ref="select_execute_time" :labelCol="{span: 23}" :autoLink="false">
<div class="mb10 flex-row flex-align-center flex-justify-around" v-for="n in planBody.select_time_number" :key="n">
<a-time-picker
v-model:value="planBody.select_time[n - 1][0]"
format="HH:mm:ss"
show-time
placeholder="Start Time"
:style="planBody.task_type === TaskType.Condition ? 'width: 40%' : 'width: 82%'"
@change="() => $refs.select_execute_time.onFieldChange()"
/>
<template v-if="planBody.task_type === TaskType.Condition">
<div><span style="color: white;">-</span></div>
<a-time-picker
v-model:value="planBody.select_time[n - 1][1]"
format="HH:mm:ss"
show-time
placeholder="End Time"
style="width: 40%;"
/>
</template>
<div class="ml5" style="font-size:18px">
<PlusCircleOutlined class="mr5" style="color: #1890ff" @click="addTime"/>
<MinusCircleOutlined :style="planBody.select_time_number === 1 ? 'color: gray' : 'color: red;'" @click="removeTime"/>
</div>
</div>
</a-form-item>
<template v-if="planBody.task_type === TaskType.Condition">
<!-- battery capacity -->
<a-form-item label="Start task when battery level reaches" :labelCol="{span: 23}" name="min_battery_capacity">
<a-input-number class="width-100" v-model:value="planBody.min_battery_capacity" :min="50" :max="100"
:formatter="(value: number) => `${value}%`" :parser="(value: string) => value.replace('%', '')">
</a-input-number>
</a-form-item>
<!-- storage capacity -->
<a-form-item label="Start task when storage level reaches (MB)" :labelCol="{span: 23}" name="storage_capacity">
<a-input-number v-model:value="planBody.min_storage_capacity" class="width-100">
</a-input-number>
</a-form-item>
</template>
<!-- RTH Altitude Relative to Dock -->
<a-form-item label="RTH Altitude Relative to Dock (m)" :labelCol="{span: 23}" name="rth_altitude">
<a-input-number v-model:value="planBody.rth_altitude" :min="20" :max="1500" class="width-100" required>
</a-input-number>
</a-form-item>
<!-- Lost Action -->
<a-form-item label="Lost Action" :labelCol="{span: 23}" name="out_of_control_action">
<div style="white-space: nowrap;">
<a-radio-group v-model:value="planBody.out_of_control_action" button-style="solid">
<a-radio-button v-for="action in OutOfControlActionOptions" :value="action.value" :key="action.value">
{{ action.label }}
</a-radio-button>
</a-radio-group>
</div>
</a-form-item>
<a-form-item class="width-100" style="margin-bottom: 40px;">
<div class="footer">
<a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel
</a-button>
<a-button type="primary" @click="onSubmit" :disabled="disabled">OK
</a-button>
</div>
</a-form-item>
</a-form>
</div>
</div>
<div v-if="drawerVisible" style="position: absolute; left: 335px; width: 280px; height: 100vh; float: right; top: 0; z-index: 1000; color: white; background: #282828;">
<div>
<router-view :name="routeName"/>
</div>
<div style="position: absolute; top: 15px; right: 10px;">
<a style="color: white;" @click="closePanel"><CloseOutlined /></a>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, UnwrapRef } from 'vue'
import { CloseOutlined, RocketOutlined, CameraFilled, UserOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue'
import { ELocalStorageKey, ERouterName } from '/@/types'
import { useMyStore } from '/@/store'
import { WaylineType, WaylineFile } from '/@/types/wayline'
import { Device, DEVICE_NAME } from '/@/types/device'
import { createPlan, CreatePlan } from '/@/api/wayline'
import { getRoot } from '/@/root'
import { TaskType, OutOfControlActionOptions, OutOfControlAction, TaskTypeOptions } from '/@/types/task'
import moment, { Moment } from 'moment'
import { RuleObject } from 'ant-design-vue/es/form/interface'
const root = getRoot()
const store = useMyStore()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const wayline = computed<WaylineFile>(() => {
return store.state.waylineInfo
})
const dock = computed<Device>(() => {
return store.state.dockInfo
})
const disabled = ref(false)
const routeName = ref('')
const planBody = reactive({
name: '',
file_id: computed(() => store.state?.waylineInfo.id),
dock_sn: computed(() => store.state?.dockInfo.device_sn),
task_type: TaskType.Immediate,
select_execute_date: [moment(), moment()] as Moment[],
select_time_number: 1,
select_time: [[]] as Moment[][],
rth_altitude: '',
out_of_control_action: OutOfControlAction.ReturnToHome,
min_battery_capacity: 90 as number,
min_storage_capacity: undefined as number | undefined,
})
const drawerVisible = ref(false)
const valueRef = ref()
const rules = {
name: [
{ required: true, message: 'Please enter plan name.' },
{ max: 20, message: 'Length should be 1 to 20' }
],
file_id: [{ required: true, message: 'Select Route' }],
dock_sn: [{ required: true, message: 'Select Device' }],
select_execute_time: [{
validator: async (rule: RuleObject, value: Moment[]) => {
validEndTime()
validStartTime()
if (planBody.select_time.length < planBody.select_time_number) {
throw new Error('Select time')
}
validOverlapped()
}
}],
select_execute_date: [{ required: true, message: 'Select date' }],
rth_altitude: [
{
validator: async (rule: RuleObject, value: string) => {
if (!/^[0-9]{1,}$/.test(value)) {
throw new Error('RTH Altitude Require number')
}
},
}
],
min_battery_capacity: [
{
validator: async (rule: RuleObject, value: any) => {
if (TaskType.Condition === planBody.task_type && !value) {
throw new Error('Please enter battery capacity')
}
},
}
],
out_of_control_action: [{ required: true, message: 'Select Lost Action' }],
}
function validStartTime (): Error | void {
for (let i = 0; i < planBody.select_time.length; i++) {
if (!planBody.select_time[i][0]) {
throw new Error('Select start time')
}
}
}
function validEndTime (): Error | void {
if (TaskType.Condition !== planBody.task_type) return
for (let i = 0; i < planBody.select_time.length; i++) {
if (!planBody.select_time[i][1]) {
throw new Error('Select end time')
}
if (planBody.select_time[i][0] && planBody.select_time[i][1].isSameOrBefore(planBody.select_time[i][0])) {
throw new Error('End time should be later than start time')
}
}
}
function validOverlapped (): Error | void {
if (TaskType.Condition !== planBody.task_type) return
const arr = planBody.select_time.slice()
arr.sort((a, b) => a[0].unix() - b[0].unix())
arr.forEach((v, i, arr) => {
if (i > 0 && v[0] < arr[i - 1][1]) {
throw new Error('Overlapping time periods.')
}
})
}
function onSubmit () {
console.info(dock, '12131231')
valueRef.value.validate().then(() => {
disabled.value = true
const createPlanBody = { ...planBody } as unknown as CreatePlan
if (planBody.select_execute_date.length === 2) {
createPlanBody.task_days = []
for (let i = planBody.select_execute_date[0]; i.isSameOrBefore(planBody.select_execute_date[1]); i.add(1, 'days')) {
createPlanBody.task_days.push(i.unix())
}
}
createPlanBody.task_periods = []
if (TaskType.Immediate !== planBody.task_type) {
for (let i = 0; i < planBody.select_time.length; i++) {
const result = []
result.push(planBody.select_time[i][0].unix())
if (TaskType.Condition === planBody.task_type) {
result.push(planBody.select_time[i][1].unix())
}
createPlanBody.task_periods.push(result)
}
}
createPlanBody.rth_altitude = Number(createPlanBody.rth_altitude)
if (wayline.value && wayline.value.template_types && wayline.value.template_types.length > 0) {
createPlanBody.wayline_type = wayline.value.template_types[0]
}
createPlan(workspaceId, createPlanBody)
.then(res => {
disabled.value = false
}).finally(() => {
closePlan()
})
}).catch((e: any) => {
console.log('validate err', e)
})
}
function closePlan () {
root.$router.push('/' + ERouterName.TASK)
}
function closePanel () {
drawerVisible.value = false
routeName.value = ''
}
function selectRoute () {
drawerVisible.value = true
routeName.value = 'WaylinePanel'
}
function selectDevice () {
drawerVisible.value = true
routeName.value = 'DockPanel'
}
function addTime () {
valueRef.value.validateFields(['select_execute_time']).then(() => {
planBody.select_time_number++
planBody.select_time.push([])
})
}
function removeTime () {
if (planBody.select_time_number === 1) return
planBody.select_time_number--
planBody.select_time.splice(planBody.select_time_number)
}
</script>
<style lang="scss">
.create-plan-wrapper {
background-color: #232323;
color: fff;
padding-bottom: 0;
height: 100vh;
display: flex;
flex-direction: column;
width: 285px;
.header {
height: 52px;
border-bottom: 1px solid #4f4f4f;
font-weight: 700;
font-size: 16px;
padding-left: 10px;
display: flex;
align-items: center;
}
::-webkit-scrollbar {
display: none;
}
.content {
height: calc(100% - 54px);
overflow-y: auto;
form {
margin: 10px;
}
form label, input, .ant-input, .ant-calendar-range-picker-separator,
.ant-input:hover, .ant-time-picker .anticon, .ant-calendar-picker .anticon {
background-color: #232323;
color: #fff;
}
.ant-input-suffix {
color: #fff;
}
.plan-timer-form-item {
.ant-radio-button-wrapper{
background-color: #232323;
color: #fff;
width: 33%;
text-align: center;
&.ant-radio-button-wrapper-checked{
background-color: #1890ff;
}
}
}
}
.footer {
display: flex;
padding:10px 0;
button {
width: 45%;
color: #fff ;
border: 0;
}
}
}
.wayline-panel {
background: #3c3c3c;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
height: 90px;
width: 95%;
font-size: 13px;
border-radius: 2px;
cursor: pointer;
.title {
display: flex;
color: white;
flex-direction: row;
align-items: center;
height: 30px;
font-weight: bold;
margin: 0px 10px 0 10px;
}
}
.panel {
background: #3c3c3c;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
height: 70px;
width: 95%;
font-size: 13px;
border-radius: 2px;
cursor: pointer;
.title {
display: flex;
color: white;
flex-direction: row;
align-items: center;
height: 30px;
font-weight: bold;
margin: 0px 10px 0 10px;
}
}
</style>

365
src/components/task/TaskPanel.vue

@ -0,0 +1,365 @@ @@ -0,0 +1,365 @@
<template>
<div class="header">Task Plan Library</div>
<div class="plan-panel-wrapper">
<a-table class="plan-table" :columns="columns" :data-source="plansData.data" row-key="job_id"
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
<!-- 执行时间 -->
<template #duration="{ record }">
<div class="flex-row" style="white-space: pre-wrap">
<div>
<div>{{ formatTaskTime(record.begin_time) }}</div>
<div>{{ formatTaskTime(record.end_time) }}</div>
</div>
<div class="ml10">
<div>{{ formatTaskTime(record.execute_time) }}</div>
<div>{{ formatTaskTime(record.completed_time) }}</div>
</div>
</div>
</template>
<!-- 状态 -->
<template #status="{ record }">
<div>
<div class="flex-display flex-align-center">
<span class="circle-icon" :style="{backgroundColor: formatTaskStatus(record).color}"></span>
{{ formatTaskStatus(record).text }}
<a-tooltip v-if="!!record.code" placement="bottom" arrow-point-at-center >
<template #title>
<div>{{ getCodeMessage(record.code) }}</div>
</template>
<exclamation-circle-outlined class="ml5" :style="{color: commonColor.WARN, fontSize: '16px' }"/>
</a-tooltip>
</div>
<div v-if="record.status === TaskStatus.Carrying">
<a-progress :percent="record.progress || 0" />
</div>
</div>
</template>
<!-- 任务类型 -->
<template #taskType="{ record }">
<div>{{ formatTaskType(record) }}</div>
</template>
<!-- 失控动作 -->
<template #lostAction="{ record }">
<div>{{ formatLostAction(record) }}</div>
</template>
<!-- 媒体上传状态 -->
<template #media_upload="{ record }">
<div>
<div class="flex-display flex-align-center">
<span class="circle-icon" :style="{backgroundColor: formatMediaTaskStatus(record).color}"></span>
{{ formatMediaTaskStatus(record).text }}
</div>
<div class="pl15">
{{ formatMediaTaskStatus(record).number }}
<a-tooltip v-if="formatMediaTaskStatus(record).status === MediaStatus.ToUpload" placement="bottom" arrow-point-at-center >
<template #title>
<div>Upload now</div>
</template>
<UploadOutlined class="ml5" :style="{color: commonColor.BLUE, fontSize: '16px' }" @click="onUploadMediaFileNow(record.job_id)"/>
</a-tooltip>
</div>
</div>
</template>
<!-- 操作 -->
<template #action="{ record }">
<div class="action-area">
<a-popconfirm
v-if="record.status === TaskStatus.Wait"
title="Are you sure you want to delete flight task?"
ok-text="Yes"
cancel-text="No"
@confirm="onDeleteTask(record.job_id)"
>
<a-button type="primary" size="small">Delete</a-button>
</a-popconfirm>
<a-popconfirm
v-if="record.status === TaskStatus.Carrying"
title="Are you sure you want to suspend?"
ok-text="Yes"
cancel-text="No"
@confirm="onSuspendTask(record.job_id)"
>
<a-button type="primary" size="small">Suspend</a-button>
</a-popconfirm>
<a-popconfirm
v-if="record.status === TaskStatus.Paused"
title="Are you sure you want to resume?"
ok-text="Yes"
cancel-text="No"
@confirm="onResumeTask(record.job_id)"
>
<a-button type="primary" size="small">Resume</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { TableState } from 'ant-design-vue/lib/table/interface'
import { onMounted } from 'vue'
import { IPage } from '/@/api/http/type'
import { deleteTask, updateTaskStatus, UpdateTaskStatus, getWaylineJobs, Task, uploadMediaFileNow } from '/@/api/wayline'
import { useMyStore } from '/@/store'
import { ELocalStorageKey } from '/@/types/enums'
import { useFormatTask } from './use-format-task'
import { TaskStatus, TaskProgressInfo, TaskProgressStatus, TaskProgressWsStatusMap, MediaStatus, MediaStatusProgressInfo, TaskMediaHighestPriorityProgressInfo } from '/@/types/task'
import { useTaskWsEvent } from './use-task-ws-event'
import { getErrorMessage } from '/@/utils/error-code/index'
import { commonColor } from '/@/utils/color'
import { ExclamationCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
const store = useMyStore()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const body: IPage = {
page: 1,
total: 0,
page_size: 50
}
const paginationProp = reactive({
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
})
const columns = [
{
title: 'Planned/Actual Time',
dataIndex: 'duration',
width: 200,
slots: { customRender: 'duration' },
},
{
title: 'Status',
key: 'status',
width: 150,
slots: { customRender: 'status' }
},
{
title: 'Plan Name',
dataIndex: 'job_name',
width: 100,
},
{
title: 'Type',
dataIndex: 'taskType',
width: 100,
slots: { customRender: 'taskType' },
},
{
title: 'Flight Route Name',
dataIndex: 'file_name',
width: 100,
},
{
title: 'Dock Name',
dataIndex: 'dock_name',
width: 100,
ellipsis: true
},
{
title: 'RTH Altitude Relative to Dock (m)',
dataIndex: 'rth_altitude',
width: 120,
},
{
title: 'Lost Action',
dataIndex: 'out_of_control_action',
width: 120,
slots: { customRender: 'lostAction' },
},
{
title: 'Creator',
dataIndex: 'username',
width: 120,
},
{
title: 'Media File Upload',
key: 'media_upload',
width: 160,
slots: { customRender: 'media_upload' }
},
{
title: 'Action',
width: 120,
slots: { customRender: 'action' }
}
]
type Pagination = TableState['pagination']
const plansData = reactive({
data: [] as Task[]
})
const { formatTaskType, formatTaskTime, formatLostAction, formatTaskStatus, formatMediaTaskStatus } = useFormatTask()
//
function onTaskProgressWs (data: TaskProgressInfo) {
const { bid, output } = data
if (output) {
const { status, progress } = output || {}
const taskItem = plansData.data.find(task => task.job_id === bid)
if (!taskItem) return
if (status) {
taskItem.status = TaskProgressWsStatusMap[status]
//
if (status === TaskProgressStatus.Sent || status === TaskProgressStatus.inProgress) {
taskItem.progress = progress?.percent || 0
} else if ([TaskProgressStatus.Rejected, TaskProgressStatus.Canceled, TaskProgressStatus.Timeout, TaskProgressStatus.Failed, TaskProgressStatus.OK].includes(status)) {
getPlans()
}
}
}
}
//
function onTaskMediaProgressWs (data: MediaStatusProgressInfo) {
const { media_count: mediaCount, uploaded_count: uploadedCount, job_id: jobId } = data
if (isNaN(mediaCount) || isNaN(uploadedCount) || !jobId) {
return
}
const taskItem = plansData.data.find(task => task.job_id === jobId)
if (!taskItem) return
if (mediaCount === uploadedCount) {
taskItem.uploading = false
} else {
taskItem.uploading = true
}
taskItem.media_count = mediaCount
taskItem.uploaded_count = uploadedCount
}
function onoTaskMediaHighestPriorityWS (data: TaskMediaHighestPriorityProgressInfo) {
const { pre_job_id: preJobId, job_id: jobId } = data
const preTaskItem = plansData.data.find(task => task.job_id === preJobId)
const taskItem = plansData.data.find(task => task.job_id === jobId)
if (preTaskItem) {
preTaskItem.uploading = false
}
if (taskItem) {
taskItem.uploading = true
}
}
function getCodeMessage (code: number) {
return getErrorMessage(code) + `(code: ${code}`
}
useTaskWsEvent({
onTaskProgressWs,
onTaskMediaProgressWs,
onoTaskMediaHighestPriorityWS,
})
onMounted(() => {
getPlans()
})
function getPlans () {
getWaylineJobs(workspaceId, body).then(res => {
if (res.code !== 0) {
return
}
plansData.data = res.data.list
paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page
})
}
function refreshData (page: Pagination) {
body.page = page?.current!
body.page_size = page?.pageSize!
getPlans()
}
//
async function onDeleteTask (jobId: string) {
const { code } = await deleteTask(workspaceId, {
job_id: jobId
})
if (code === 0) {
message.success('Deleted successfully')
getPlans()
}
}
//
async function onSuspendTask (jobId: string) {
const { code } = await updateTaskStatus(workspaceId, {
job_id: jobId,
status: UpdateTaskStatus.Suspend
})
if (code === 0) {
message.success('Suspended successfully')
getPlans()
}
}
//
async function onResumeTask (jobId: string) {
const { code } = await updateTaskStatus(workspaceId, {
job_id: jobId,
status: UpdateTaskStatus.Resume
})
if (code === 0) {
message.success('Resumed successfully')
getPlans()
}
}
//
async function onUploadMediaFileNow (jobId: string) {
const { code } = await uploadMediaFileNow(workspaceId, jobId)
if (code === 0) {
message.success('Upload Media File successfully')
getPlans()
}
}
</script>
<style lang="scss" scoped>
.plan-panel-wrapper {
width: 100%;
padding: 16px;
.plan-table {
background: #fff;
margin-top: 10px;
}
.action-area {
&::v-deep {
.ant-btn {
margin-right: 10px;
margin-bottom: 10px;
}
}
}
.circle-icon {
display: inline-block;
width: 12px;
height: 12px;
margin-right: 3px;
border-radius: 50%;
vertical-align: middle;
flex-shrink: 0;
}
}
.header {
width: 100%;
height: 60px;
background: #fff;
padding: 16px;
font-size: 20px;
font-weight: bold;
text-align: start;
color: #000;
}
</style>

73
src/components/task/use-format-task.ts

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
import { DEFAULT_PLACEHOLDER } from '/@/utils/constants'
import { Task } from '/@/api/wayline'
import { TaskStatusColor, TaskStatusMap, TaskTypeMap, OutOfControlActionMap, MediaStatusMap, MediaStatusColorMap, MediaStatus } from '/@/types/task'
import { isNil } from 'lodash'
export function useFormatTask () {
function formatTaskType (task: Task) {
return TaskTypeMap[task.task_type] || DEFAULT_PLACEHOLDER
}
function formatTaskTime (time: string) {
return time || DEFAULT_PLACEHOLDER
}
function formatLostAction (task: Task) {
return OutOfControlActionMap[task.out_of_control_action] || DEFAULT_PLACEHOLDER
}
function formatTaskStatus (task: Task) {
const statusObj = {
text: '',
color: ''
}
const { status } = task
statusObj.text = TaskStatusMap[status]
statusObj.color = TaskStatusColor[status]
return statusObj
}
function formatMediaTaskStatus (task: Task) {
const statusObj = {
text: '',
color: '',
number: '',
status: MediaStatus.Empty,
}
const { media_count, uploaded_count, uploading } = task
if (isNil(media_count) || isNaN(media_count)) {
return statusObj
}
const expectedFileCount = media_count || 0
const uploadedFileCount = uploaded_count || 0
if (media_count === 0) {
statusObj.text = MediaStatusMap[MediaStatus.Empty]
statusObj.color = MediaStatusColorMap[MediaStatus.Empty]
} else if (media_count === uploaded_count) {
statusObj.text = MediaStatusMap[MediaStatus.Success]
statusObj.color = MediaStatusColorMap[MediaStatus.Success]
statusObj.number = `(${uploadedFileCount}/${expectedFileCount})`
statusObj.status = MediaStatus.Success
} else {
if (uploading) {
statusObj.text = MediaStatusMap[MediaStatus.Uploading]
statusObj.color = MediaStatusColorMap[MediaStatus.Uploading]
statusObj.status = MediaStatus.Uploading
} else {
statusObj.text = MediaStatusMap[MediaStatus.ToUpload]
statusObj.color = MediaStatusColorMap[MediaStatus.ToUpload]
statusObj.status = MediaStatus.ToUpload
}
statusObj.number = `(${uploadedFileCount}/${expectedFileCount})`
}
return statusObj
}
return {
formatTaskType,
formatTaskTime,
formatLostAction,
formatTaskStatus,
formatMediaTaskStatus,
}
}

43
src/components/task/use-task-ws-event.ts

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { TaskProgressInfo, MediaStatusProgressInfo, TaskMediaHighestPriorityProgressInfo } from '/@/types/task'
import { EBizCode } from '/@/types'
export interface UseTaskWsEventParams {
onTaskProgressWs: (data: TaskProgressInfo) => void,
onTaskMediaProgressWs: (data: MediaStatusProgressInfo) => void
onoTaskMediaHighestPriorityWS: (data: TaskMediaHighestPriorityProgressInfo) => void
}
export function useTaskWsEvent (funcs: UseTaskWsEventParams): void {
function handleTaskWsEvent (payload: any) {
if (!payload) {
return
}
switch (payload.biz_code) {
case EBizCode.FlightTaskProgress: {
funcs?.onTaskProgressWs(payload.data)
break
}
case EBizCode.FlightTaskMediaProgress: {
funcs?.onTaskMediaProgressWs(payload.data)
break
}
case EBizCode.FlightTaskMediaHighestPriority: {
funcs?.onoTaskMediaHighestPriorityWS(payload.data)
break
}
}
// eslint-disable-next-line no-unused-expressions
// console.log('payload', payload.data)
}
onMounted(() => {
EventBus.on('flightTaskWs', handleTaskWsEvent)
})
onBeforeUnmount(() => {
EventBus.off('flightTaskWs', handleTaskWsEvent)
})
}

123
src/components/wayline-panel.vue

@ -1,123 +0,0 @@ @@ -1,123 +0,0 @@
<template>
<div class="panel-wrapper" draggable="true">
<div class="header">Wayline Library</div>
<a-button type="primary" style="margin-top:20px" @click="onRefresh"
>Refresh</a-button
>
<a-table class="table" :columns="columns" :data-source="data">
<template #name="{ text, record }">
<a :href="record.preview_url">{{ text }}</a>
</template>
<template #action>
<span class="action-area">
action
</span>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { onMounted } from 'vue'
import { ELocalStorageKey } from '../types/enums'
import { getWaylineFiles } from '/@/api/wayline'
const columns = [
{
title: 'FileName',
dataIndex: 'name',
key: 'name',
slots: { customRender: 'name' }
},
{
title: 'TemplateType',
dataIndex: 'template_type',
key: 'template_type'
},
{
title: 'Favorited',
dataIndex: 'favorite',
key: 'favorite'
},
{
title: 'DroneType',
dataIndex: 'drone_type',
key: 'drone_type'
},
{
title: 'PayloadType',
dataIndex: 'payload_type',
key: 'payload_type'
},
{
title: 'User',
dataIndex: 'user',
key: 'user'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
]
const data = ref([
{
key: '1',
name: 'name1',
template_type: '0',
drone_type: '0-60-0',
payload_type: 'PM320_DUAL',
user: 'pilot',
favorited: 'true'
}
])
onMounted(() => {
onRefresh()
})
const onRefresh = async () => {
const wid: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)
data.value = []
const index = 1
const res = await getWaylineFiles(wid, {
page: 1, //
page_size: 9, //
order_by: 'update_time desc' // , xxx_column_desc, xxx_column_asc, xxx_column(default asc)
})
console.log(res)
res.data.list.forEach(ele => {
data.value.push({
key: index.toString(),
name: ele.name,
template_type: ele.template_types[0],
drone_type: ele.drone_model_key,
payload_type: ele.payload_model_keys[0],
user: ele.user_name,
favorite: ele.favorited.toString()
})
})
console.log('wayline files:', data.value)
}
</script>
<style lang="scss" scoped>
.panel-wrapper {
width: 100%;
.table {
background: #fff;
margin-top: 32px;
}
.header {
width: 100%;
height: 60px;
background: #fff;
padding: 16px 24px;
font-size: 20px;
text-align: start;
color: #000;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
</style>

15
src/components/workspace/DividerLine.vue

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
<template>
<Divider class="divider" />
</template>
<script lang="ts" setup>
import { Divider } from 'ant-design-vue'
</script>
<style lang="scss" scoped>
.divider {
margin: 10px 0;
height: 1px;
background-color: #4f4f4f;
}
</style>

21
src/components/workspace/Title.vue

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
<template>
<div style="height: 40px; line-height: 50px; font-weight: 450;">
<a-row>
<a-col :span="1"></a-col>
<a-col :span="(23 - (extSpan || 0))">{{ title }}</a-col>
<a-col :span="extSpan"><slot /></a-col>
</a-row>
</div>
<DividerLine />
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
import DividerLine from '/@/components/workspace/DividerLine.vue'
const props = defineProps < {
extSpan?: number,
title: string,
} >()
</script>

6
src/constants/map.ts

@ -11,8 +11,8 @@ export const MapElementDefaultColor = MapElementColor.Default @@ -11,8 +11,8 @@ export const MapElementDefaultColor = MapElementColor.Default
export enum MapDoodleColor {
PinColor = '#2D8CF0',
PolylineColor = '#3366FF',
PolygonColor = '#FF33FF'
PolylineColor = '#2D8CF0',
PolygonColor = '#2D8CF0'
}
export enum MapElementEnum {
@ -20,4 +20,4 @@ export enum MapElementEnum { @@ -20,4 +20,4 @@ export enum MapElementEnum {
LINE = 1,
POLY = 2
}
export type MapDoodleType = 'pin' | 'polyline' | 'polygon' | 'off'
export type MapDoodleType = 'pin' | 'polyline' | 'polygon' | 'off' | 'circle'

38
src/directives/drag-window.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { nextTick, App } from 'vue'
export default function useDragWindowDirective (app: App): void {
app.directive('drag-window', async (el) => {
await nextTick()
const modal = el
const header = el.getElementsByClassName('drag-title')[0]
let left = 0
let top = 0
header.style.cursor = 'move'
top = top || modal.offsetTop
header.onpointerdown = (e: { clientX: number; clientY: number; pointerId: number }) => {
const startX = e.clientX
const startY = e.clientY
header.left = header.offsetLeft
header.top = header.offsetTop
header.setPointerCapture(e.pointerId)
el.onpointermove = (event: { clientX: number; clientY: number }) => {
const endX = event.clientX
const endY = event.clientY
modal.left = header.left + (endX - startX) + left
modal.top = header.top + (endY - startY) + top
modal.style.left = modal.left + 'px'
modal.style.top = modal.top + 'px'
}
el.onpointerup = () => {
left = modal.left || 0
top = modal.top || 0
el.onpointermove = null
el.onpointerup = null
header.releasePointerCapture(e.pointerId)
}
}
})
}

6
src/directives/index.ts

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
import { App } from 'vue'
import useDragWindowDirective from './drag-window'
export function useDirectives (app: App): void {
useDragWindowDirective(app)
}

10
src/event-bus/index.ts

@ -1,8 +1,14 @@ @@ -1,8 +1,14 @@
import mitt, { Emitter } from 'mitt'
type Events = {
deviceUpgrade: any;
deviceLogUploadProgress: any
deviceUpgrade: any; // 设备升级
deviceLogUploadProgress: any // 设备日志上传
flightTaskWs: any // 机场任务消息
droneControlWs: any // 飞行指令信息
droneControlMqttInfo: any // drc 链路通知
flightAreasDroneLocationWs: any
flightAreasSyncProgressWs: any
flightAreasUpdateWs: any
};
const emitter: Emitter<Events> = mitt<Events>()

209
src/hooks/use-g-map-cover.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { EFlightAreaType } from '../types/flight-area'
import pin19be6b from '/@/assets/icons/pin-19be6b.svg'
import pin212121 from '/@/assets/icons/pin-212121.svg'
import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
@ -14,12 +15,16 @@ export function useGMapCover () { @@ -14,12 +15,16 @@ export function useGMapCover () {
const normalColor = '#2D8CF0'
const store = rootStore
const coverList = store.state.coverList
const coverMap = store.state.coverMap
const flightAreaColorMap = {
[EFlightAreaType.DFENCE]: '#19be6b',
[EFlightAreaType.NFZ]: '#ff0000',
}
const disableColor = '#b3b3b3'
function AddCoverToMap (cover :any) {
root.$map.add(cover)
coverList.push(cover)
// console.log('coverList:', store.state.coverList)
coverMap[cover.getExtData().id] = [cover]
}
function getPinIcon (color?:string) {
@ -29,10 +34,10 @@ export function useGMapCover () { @@ -29,10 +34,10 @@ export function useGMapCover () {
} = {
'2d8cf0': pin2d8cf0,
'19be6b': pin19be6b,
'212121': pin212121,
'b620e0': pinb620e0,
'e23c39': pine23c39,
'ffbb00': pineffbb00,
212121: pin212121,
b620e0: pinb620e0,
e23c39: pine23c39,
ffbb00: pineffbb00,
}
const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
return new AMap.Icon({
@ -44,7 +49,6 @@ export function useGMapCover () { @@ -44,7 +49,6 @@ export function useGMapCover () {
}
function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) {
console.log(name, coordinates[0], coordinates[1], color, data)
const pin = new AMap.Marker({
position: new AMap.LngLat(coordinates[0], coordinates[1]),
title: name,
@ -59,7 +63,8 @@ export function useGMapCover () { @@ -59,7 +63,8 @@ export function useGMapCover () {
function AddOverlayGroup (overlayGroup) {
root.$map.add(overlayGroup)
coverList.push(overlayGroup)
const id = overlayGroup.getExtData().id
coverMap[id] = [...(coverMap[id] || []), overlayGroup]
}
function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
const path = [] as GeojsonCoordinate[]
@ -78,9 +83,9 @@ export function useGMapCover () { @@ -78,9 +83,9 @@ export function useGMapCover () {
AddOverlayGroup(polyline)
}
function initPolygon (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
function initPolygon (name: string, coordinates:GeojsonCoordinate[][], color?:string, data?:{}) {
const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => {
coordinates[0].forEach(coordinate => {
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
})
// console.log('Polygon', path)
@ -98,36 +103,18 @@ export function useGMapCover () { @@ -98,36 +103,18 @@ export function useGMapCover () {
}
function removeCoverFromMap (id:string) {
for (let i = 0; i < coverList.length; i++) {
const ele = coverList[i]
// console.log(ele)
const extdata = ele?.getExtData()
if (extdata?.id === id) {
console.log(extdata)
root.$map.remove(ele)
coverList.slice(i, 1)
break
}
}
coverMap[id].forEach(cover => root.$map.remove(cover))
coverMap[id] = []
}
function getElementFromMap (id:string) {
// console.log('start', new Date().getTime())
const ele = coverList.find(ele => ele?.getExtData().id === id)
// console.log('end', new Date().getTime())
return ele
// coverList.forEach((ele:any) => {
// const extdata = ele?.getExtData()
// // console.log(extdata)
// if (extdata?.id === id) {
// return ele
// }
// })
function getElementFromMap (id:string): any[] {
return coverMap[id]
}
function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) {
const element = getElementFromMap(id) as any
if (element) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
const element = elements[0]
const icon = getPinIcon(color)
element.setPosition(new AMap.LngLat(coordinates[0], coordinates[1]))
element.setIcon(icon)
@ -141,12 +128,160 @@ export function useGMapCover () { @@ -141,12 +128,160 @@ export function useGMapCover () {
}
}
function updatePolylineElement (id:string, name: string, coordinates:GeojsonCoordinate[], color?:string) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
const element = elements[0]
const options = element.getOptions()
options.strokeColor = color || normalColor
element.setOptions(options)
} else {
initPolyline(name, coordinates, color, {
id: id,
name: name
})
}
}
function updatePolygonElement (id:string, name: string, coordinates:GeojsonCoordinate[][], color?:string) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
const element = elements[0]
const options = element.getOptions()
options.fillColor = color || normalColor
options.strokeColor = color || normalColor
element.setOptions(options)
} else {
initPolygon(name, coordinates, color, {
id: id,
name: name
})
}
}
function initTextInfo (content: string, coordinates: GeojsonCoordinate, id: string) {
const info = new AMap.Text({
text: content,
position: new AMap.LngLat(coordinates[0], coordinates[1]),
extData: { id: id, type: 'text' },
anchor: 'top-center',
style: {
background: 'none',
borderStyle: 'none',
fontSize: '16px',
},
})
AddOverlayGroup(info)
}
function initFlightAreaCircle (name: string, radius: number, position: GeojsonCoordinate, data: { id: string, type: EFlightAreaType, enable: boolean }) {
const circle = new AMap.Circle({
strokeColor: data.enable ? flightAreaColorMap[data.type] : disableColor,
strokeOpacity: 1,
strokeWeight: 6,
extData: data,
strokeStyle: 'dashed',
strokeDasharray: EFlightAreaType.NFZ === data.type ? [10, 2] : [10, 1, 2],
fillColor: flightAreaColorMap[data.type],
fillOpacity: EFlightAreaType.NFZ === data.type && data.enable ? 0.3 : 0,
radius: radius,
center: new AMap.LngLat(position[0], position[1]),
})
AddOverlayGroup(circle)
initTextInfo(name, position, data.id)
}
function updateFlightAreaCircle (id: string, name: string, radius: number, position: GeojsonCoordinate, enable: boolean, type: EFlightAreaType) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
let textIndex = elements.findIndex(ele => ele.getExtData()?.type === 'text')
if (textIndex === -1) {
textIndex = 1
initTextInfo(name, position, id)
} else {
const text = elements[textIndex]
text.setText(name)
text.setPosition(position)
}
const element = elements[textIndex ^ 1]
const options = element.getOptions()
options.fillOpacity = EFlightAreaType.NFZ === type && enable ? 0.3 : 0
options.strokeColor = enable ? flightAreaColorMap[type] : disableColor
options.radius = radius
options.center = new AMap.LngLat(position[0], position[1])
element.setOptions(options)
} else {
initFlightAreaCircle(name, radius, position, { id, type, enable })
}
}
function calcPolygonPosition (coordinate: GeojsonCoordinate[]): GeojsonCoordinate {
const index = coordinate.length - 1
return [(coordinate[0][0] + coordinate[index][0]) / 2.0, (coordinate[0][1] + coordinate[index][1]) / 2]
}
function initFlightAreaPolygon (name: string, coordinates: GeojsonCoordinate[], data: { id: string, type: EFlightAreaType, enable: boolean }) {
const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => {
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
})
const polygon = new AMap.Polygon({
path: path,
strokeColor: data.enable ? flightAreaColorMap[data.type] : disableColor,
strokeOpacity: 1,
strokeWeight: 4,
draggable: true,
extData: data,
strokeStyle: 'dashed',
strokeDasharray: EFlightAreaType.NFZ === data.type ? [10, 2] : [10, 1, 2],
fillColor: flightAreaColorMap[data.type],
fillOpacity: EFlightAreaType.NFZ === data.type && data.enable ? 0.3 : 0,
})
AddOverlayGroup(polygon)
initTextInfo(name, calcPolygonPosition(coordinates), data.id)
}
function updateFlightAreaPolygon (id: string, name: string, coordinates: GeojsonCoordinate[], enable: boolean, type: EFlightAreaType) {
const elements = getElementFromMap(id)
if (elements && elements.length > 0) {
let textIndex = elements.findIndex(ele => ele.getExtData()?.type === 'text')
if (textIndex === -1) {
textIndex = 1
initTextInfo(name, calcPolygonPosition(coordinates), id)
} else {
const text = elements[textIndex]
text.setText(name)
text.setPosition(calcPolygonPosition(coordinates))
}
const element = elements[textIndex ^ 1]
const options = element.getOptions()
const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => {
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
})
options.path = path
options.fillOpacity = EFlightAreaType.NFZ === type && enable ? 0.3 : 0
options.strokeColor = enable ? flightAreaColorMap[type] : disableColor
element.setOptions(options)
} else {
initFlightAreaPolygon(name, coordinates, { id, type, enable })
}
}
return {
init2DPin,
initPolyline,
initPolygon,
removeCoverFromMap,
getElementFromMap,
updatePinElement
updatePinElement,
updatePolylineElement,
updatePolygonElement,
initFlightAreaCircle,
initFlightAreaPolygon,
updateFlightAreaPolygon,
updateFlightAreaCircle,
calcPolygonPosition,
}
}

37
src/hooks/use-g-map-tsa.ts

@ -1,25 +1,24 @@ @@ -1,25 +1,24 @@
import store from '/@/store'
import { getRoot } from '/@/root'
import { ELocalStorageKey } from '/@/types'
import { ELocalStorageKey, EDeviceTypeName } from '/@/types'
import { getDeviceBySn } from '/@/api/manage'
import { message } from 'ant-design-vue'
import dockIcon from '/@/assets/icons/dock.png'
import rcIcon from '/@/assets/icons/rc.png'
import droneIcon from '/@/assets/icons/drone.png'
export function deviceTsaUpdate () {
const root = getRoot()
const AMap = root.$aMap
let AMap = root.$aMap
const icons: {
[key: string]: string
} = {
'sub-device': '/@/assets/icons/drone.png',
'gateway': '/@/assets/icons/rc.png',
'dock': '/@/assets/icons/dock.png'
}
const icons = new Map([
[EDeviceTypeName.Aircraft, droneIcon],
[EDeviceTypeName.Gateway, rcIcon],
[EDeviceTypeName.Dock, dockIcon]
])
const markers = store.state.markerInfo.coverMap
const paths = store.state.markerInfo.pathMap
// Fix: 航迹初始化报错
// TODO: 从时序上解决
let trackLine = null as any
function getTrackLineInstance () {
if (!trackLine) {
@ -31,26 +30,30 @@ export function deviceTsaUpdate () { @@ -31,26 +30,30 @@ export function deviceTsaUpdate () {
return trackLine
}
function initIcon (type: string) {
function initIcon (type: number) {
return new AMap.Icon({
image: icons[type],
imageSize: new AMap.Size(40, 40)
image: icons.get(type),
imageSize: new AMap.Size(40, 40),
size: new AMap.Size(40, 40)
})
}
function initMarker (type: string, name: string, sn: string, lng?: number, lat?: number) {
function initMarker (type: number, name: string, sn: string, lng?: number, lat?: number) {
if (markers[sn]) {
return
}
if (root.$aMap === undefined) {
return
}
AMap = root.$aMap
markers[sn] = new AMap.Marker({
position: new AMap.LngLat(lng || 113.935913, lat || 22.525335),
position: new AMap.LngLat(lng || 113.943225499, lat || 22.577673716),
icon: initIcon(type),
title: name,
anchor: 'top-center',
offset: [0, -20],
})
root.$map.add(markers[sn])
// markers[sn].on('moving', function (e: any) {
// let path = paths[sn]
// if (!path) {

4
src/hooks/use-g-map.ts

@ -15,8 +15,8 @@ export function useGMapManage () { @@ -15,8 +15,8 @@ export function useGMapManage () {
}).then((AMap) => {
state.aMap = AMap
state.map = new AMap.Map(container, {
center: [113.935913, 22.525335],
zoom: 15
center: [113.943225499, 22.577673716],
zoom: 20
})
state.mouseTool = new AMap.MouseTool(state.map)

16
src/hooks/use-map-tool.ts

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
import { GeojsonCoordinate } from '../utils/genjson'
import { getRoot } from '/@/root'
export function useMapTool () {
const root = getRoot()
const map = root.$map
const AMap = root.$aMap
function panTo (coordinate: GeojsonCoordinate) {
map.panTo(coordinate, 100)
map.setZoom(18, false, 100)
}
return {
panTo,
}
}

58
src/hooks/use-mouse-tool.ts

@ -3,6 +3,8 @@ import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg' @@ -3,6 +3,8 @@ import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
import { MapDoodleType } from '/@/constants/map'
import { getRoot } from '/@/root'
import { MapDoodleEnum } from '/@/types/map-enum'
import { EFlightAreaType } from '../types/flight-area'
import { message } from 'ant-design-vue'
export function useMouseTool () {
const root = getRoot()
@ -13,7 +15,10 @@ export function useMouseTool () { @@ -13,7 +15,10 @@ export function useMouseTool () {
PolygonNum: 0,
currentType: '',
})
const flightAreaColorMap = {
[EFlightAreaType.DFENCE]: '#19be6b',
[EFlightAreaType.NFZ]: '#ff0000',
}
function drawPin (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.marker({
title: type + state.pinNum,
@ -29,7 +34,6 @@ export function useMouseTool () { @@ -29,7 +34,6 @@ export function useMouseTool () {
strokeOpacity: 1,
strokeWeight: 2,
strokeStyle: 'solid',
draggable: true,
title: type + state.polylineNum++
})
root?.$mouseTool.on('draw', getDrawCallback)
@ -42,7 +46,6 @@ export function useMouseTool () { @@ -42,7 +46,6 @@ export function useMouseTool () {
strokeWeight: 2,
fillColor: '#1791fc',
fillOpacity: 0.4,
draggable: true,
title: type + state.PolygonNum++
})
root?.$mouseTool.on('draw', getDrawCallback)
@ -53,8 +56,55 @@ export function useMouseTool () { @@ -53,8 +56,55 @@ export function useMouseTool () {
root?.$mouseTool.off('draw')
}
function mouseTool (type: MapDoodleType, getDrawCallback: Function) {
function drawFlightAreaPolygon (type: EFlightAreaType, getDrawFlightAreaCallback: Function) {
root?.$mouseTool.polygon({
strokeColor: flightAreaColorMap[type],
strokeOpacity: 1,
strokeWeight: 4,
extData: {
type: type,
mapType: 'polygon',
},
strokeStyle: 'dashed',
strokeDasharray: EFlightAreaType.NFZ === type ? [10, 2] : [10, 1, 2],
fillColor: flightAreaColorMap[type],
fillOpacity: EFlightAreaType.NFZ === type ? 0.3 : 0,
})
root?.$mouseTool.on('draw', getDrawFlightAreaCallback)
}
function drawFlightAreaCircle (type: EFlightAreaType, getDrawFlightAreaCallback: Function) {
root?.$mouseTool.circle({
strokeColor: flightAreaColorMap[type],
strokeOpacity: 1,
strokeWeight: 6,
extData: {
type: type,
mapType: 'circle',
},
strokeStyle: 'dashed',
strokeDasharray: EFlightAreaType.NFZ === type ? [10, 2] : [10, 1, 2],
fillColor: flightAreaColorMap[type],
fillOpacity: EFlightAreaType.NFZ === type ? 0.3 : 0,
})
root?.$mouseTool.on('draw', getDrawFlightAreaCallback)
}
function mouseTool (type: MapDoodleType, getDrawCallback: Function, flightAreaType?: EFlightAreaType) {
state.currentType = type
if (flightAreaType) {
switch (type) {
case MapDoodleEnum.POLYGON:
drawFlightAreaPolygon(flightAreaType, getDrawCallback)
return
case MapDoodleEnum.CIRCLE:
drawFlightAreaCircle(flightAreaType, getDrawCallback)
return
default:
message.error(`Invalid type: ${flightAreaType}`)
return
}
}
switch (type) {
case MapDoodleEnum.PIN:
drawPin(type, getDrawCallback)

4
src/main.ts

@ -5,10 +5,14 @@ import { CommonComponents } from './use-common-components' @@ -5,10 +5,14 @@ import { CommonComponents } from './use-common-components'
import 'virtual:svg-icons-register'
import store, { storeKey } from './store'
import { createInstance } from '/@/root'
import { useDirectives } from './directives'
import '/@/styles/index.scss'
const app = createInstance(App)
app.use(store, storeKey)
app.use(router)
app.use(CommonComponents)
app.use(antComponents)
app.use(useDirectives)
app.mount('#demo-app')

12
src/mqtt/config.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import {
IClientOptions,
} from 'mqtt'
export const OPTIONS: IClientOptions = {
clean: true, // true: 清除会话, false: 保留会话
connectTimeout: 10000, // mqtt 超时时间
resubscribe: true, // 断开重连后,再次订阅原订阅
reconnectPeriod: 10000, // 重连间隔时间: 5s
keepalive: 1, // 心跳间隔时间:1s
}

117
src/mqtt/index.ts

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
import EventEmitter from 'eventemitter3'
import {
OPTIONS,
} from './config'
import {
connect,
MqttClient,
IClientPublishOptions,
IPublishPacket,
Packet,
ISubscriptionGrant,
IClientOptions,
} from 'mqtt/dist/mqtt.min'
export class UranusMqtt extends EventEmitter {
_url: string
_options?: IClientOptions
_client: MqttClient | null
_hasInit: boolean
constructor (url?: string, options?: IClientOptions) {
super()
this._url = url || ''
this._options = options
this._client = null
this._hasInit = false
}
initMqtt = () => {
// 仅初始化一次
if (this._hasInit) return
// 建立连接
this._client = connect(this._url, {
...OPTIONS,
...this._options,
})
this._hasInit = true
if (this._client) {
this._client.on('reconnect', this._onReconnect)
// 消息监听
this._client.on('message', this._onMessage)
// 连接关闭
this._client.on('close', this._onClose)
// 连接异常
this._client.on('error', this._onError)
}
}
// 发布
publishMqtt = (topic: string, body: string | Buffer, opts?: IClientPublishOptions) => {
if (!this._client?.connected) {
this.initMqtt()
}
this._client?.publish(topic, body, opts || {}, (error?: Error, packet?: Packet) => {
if (error) {
window.console.error('mqtt publish error,', error, packet)
}
})
}
// 订阅
subscribeMqtt = (topic: string) => {
if (!this._client?.connected) {
this.initMqtt()
}
window.console.log('subscribeMqtt>>>>>', topic)
this._client?.subscribe(topic, (error: Error, granted: ISubscriptionGrant[]) => {
window.console.log('mqtt subscribe,', error, granted)
})
}
// 取消订阅
unsubscribeMqtt = (topic: string) => {
window.console.log('mqtt unsubscribeMqtt,', topic)
this._client?.unsubscribe(topic)
}
// 关闭 mqtt 客户端
destroyed = () => {
window.console.log('mqtt destroyed')
this._client?.end()
}
_onReconnect = () => {
if (this._client) { window.console.error('mqtt reconnect,') }
}
_onMessage = (topic: string, payload: Buffer, packet: IPublishPacket) => {
this.emit('onMessageMqtt', { topic, payload, packet })
}
_onClose = () => {
// 连接异常关闭会自动重连
window.console.error('mqtt close,')
this.emit('onStatus', {
status: 'close',
})
}
_onError = (error: Error) => {
// 连接错误会自动重连
window.console.error('mqtt error,', error)
this.emit('onStatus', {
status: 'error',
data: error,
})
}
}
export {
IClientOptions,
IPublishPacket,
IClientPublishOptions,
}

48
src/pages/page-pilot/pilot-home.vue

@ -13,9 +13,9 @@ @@ -13,9 +13,9 @@
<RightOutlined style="float: right; margin-top: 5px; color: #8894a0" />
</div>
<div style="height: 50%;">
<CloudSyncOutlined v-if="state === EStatusValue.CONNECTED" style="color: #75c5f6" />
<CloudSyncOutlined v-if="thingState === EStatusValue.CONNECTED" style="color: #75c5f6" />
<SyncOutlined spin v-else/>
<span style="color: #737373; margin-left: 3px;">{{ state }}</span>
<span style="color: #737373; margin-left: 3px;">{{ thingState }}</span>
</div>
<a-drawer placement="right" v-model:visible="drawerVisible" width="340px">
<div class="mb10 flex-row flex-justify-center flex-align-center">
@ -67,7 +67,7 @@ @@ -67,7 +67,7 @@
<span style="color: #737373">{{ device.data.gateway_sn }}</span>
</a-col>
</a-row>
<a-row style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" v-if="device.data.online_status">
<a-row style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" v-if="device.data.online_status && device.data.sn">
<a-col :span="1"></a-col>
<a-col :span="9">Aircraft Sn</a-col>
<a-col :span="13" class="flex-align-end flex-column" >
@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
<span class="ml5" style="color: #939393;">Settings</span>
</div>
<div class="fz16" style="background-color: white; border-radius: 4px;">
<a-row v-if="device.data.online_status" style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" @click="bindingDevice">
<a-row v-if="device.data.online_status && device.data.sn" style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" @click="bindingDevice">
<a-col :span="1"></a-col>
<a-col :span="11">
Device Binding
@ -135,7 +135,6 @@ import { useConnectWebSocket } from '/@/hooks/use-connect-websocket' @@ -135,7 +135,6 @@ import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
const root = getRoot()
const gatewayState = ref<boolean>(localStorage.getItem(ELocalStorageKey.GatewayOnline) === 'true')
const state = ref(EStatusValue.DISCONNECT)
const thingState = ref(EStatusValue.DISCONNECT)
const apiState = ref(EStatusValue.DISCONNECT)
const liveState = ref(EStatusValue.DISCONNECT)
@ -150,14 +149,14 @@ const wsId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!) @@ -150,14 +149,14 @@ const wsId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!)
const components = apiPilot.init()
const exitVisible = ref(false)
const drawerVisible = ref(false)
let minitor = -1
let minitor: any
interface DeviceInfoData {
data: DeviceStatus
}
const device = reactive<DeviceInfoData>({
data: {
sn: EStatusValue.DISCONNECT,
sn: '',
online_status: false,
device_callsign: '',
user_id: '',
@ -165,7 +164,7 @@ const device = reactive<DeviceInfoData>({ @@ -165,7 +164,7 @@ const device = reactive<DeviceInfoData>({
bound_status: false,
model: '',
gateway_sn: EStatusValue.DISCONNECT,
domain: ''
domain: -1
}
})
const bindParam: BindBody = {
@ -218,9 +217,7 @@ const messageHandler = async (payload: any) => { @@ -218,9 +217,7 @@ const messageHandler = async (payload: any) => {
case EBizCode.DeviceOnline: {
console.info('online: ', payload)
if (payload.data.sn === device.data.gateway_sn) {
gatewayState.value = true
localStorage.setItem(ELocalStorageKey.GatewayOnline, gatewayState.value.toString())
state.value = gatewayState.value && thingState.value === EStatusValue.CONNECTED ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
break
}
if (payload.data.gateway_sn === device.data.gateway_sn) {
@ -246,21 +243,28 @@ const messageHandler = async (payload: any) => { @@ -246,21 +243,28 @@ const messageHandler = async (payload: any) => {
// ws
useConnectWebSocket(messageHandler)
let bindNum: number
let bindNum: any
onMounted(() => {
apiPilot.onBackClickReg()
apiPilot.onStopPlatform()
const oldDevice = localStorage.getItem(ELocalStorageKey.Device)
if (oldDevice) {
device.data = JSON.parse(oldDevice)
}
window.connectCallback = (arg: any) => {
connectCallback(arg)
}
window.wsConnectCallback = (arg: any) => {
wsConnectCallback(arg)
}
device.data.gateway_sn = apiPilot.getRemoteControllerSN()
if (device.data.gateway_sn === EStatusValue.DISCONNECT.toString()) {
message.warn('Data is not available, please restart the remote control.')
return
}
const oldDevice = localStorage.getItem(ELocalStorageKey.Device)
if (oldDevice) {
device.data = JSON.parse(oldDevice)
}
device.data.sn = apiPilot.getAircraftSN()
getDeviceInfo()
@ -296,12 +300,6 @@ onMounted(() => { @@ -296,12 +300,6 @@ onMounted(() => {
bindParam.user_id = res.data.user_id
bindParam.workspace_id = res.data.workspace_id
})
window.connectCallback = arg => {
connectCallback(arg)
}
window.wsConnectCallback = arg => {
wsConnectCallback(arg)
}
})
const connectCallback = async (arg: any) => {
@ -331,6 +329,11 @@ const connectCallback = async (arg: any) => { @@ -331,6 +329,11 @@ const connectCallback = async (arg: any) => {
apiPilot.loadComponent(EComponentName.Mission, {})
bindNum = setInterval(() => {
if (!bindParam.device_sn) {
device.data.gateway_sn = apiPilot.getRemoteControllerSN()
bindParam.device_sn = device.data.gateway_sn
return
}
bindDevice(bindParam).then(bindRes => {
if (bindRes.code !== 0) {
message.error(bindRes.message)
@ -435,7 +438,6 @@ function refreshStatus () { @@ -435,7 +438,6 @@ function refreshStatus () {
tsaState.value = apiPilot.isComponentLoaded(EComponentName.Tsa) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
mediaState.value = apiPilot.isComponentLoaded(EComponentName.Media) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
waylineState.value = apiPilot.isComponentLoaded(EComponentName.Mission) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
state.value = thingState.value === EStatusValue.CONNECTED && gatewayState.value ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
}
function moduleInstall (m: any) {
@ -481,7 +483,7 @@ function moduleUninstall (m: any) { @@ -481,7 +483,7 @@ function moduleUninstall (m: any) {
}
function getDeviceInfo () {
if (device.data.sn === EStatusValue.DISCONNECT) {
if (!device.data.sn || device.data.sn === EStatusValue.DISCONNECT) {
return
}
getDeviceBySn(bindParam.workspace_id, device.data.sn).then(res => {

28
src/pages/page-pilot/pilot-liveshare.vue

@ -54,7 +54,26 @@ @@ -54,7 +54,26 @@
<div class="width-100" style="margin-top: -10px;">
<div class="ml10" style="width: 97%;">
<span class="fz16">Param: </span>
<span v-if="liveStreamStatus.type === ELiveTypeValue.Agora" style="word-break: break-all; color: #75c5f6;">{{ agoraParam }}</span>
<span v-if="liveStreamStatus.type === ELiveTypeValue.Agora" style="word-break: break-all; color: #75c5f6;">
<div class="flex-col flex-justify-center flex-align-center">
<div>
<span class="ml10">Token:</span>
<a-input
class="ml10"
v-model:value="agoraParam.token"
placeholder="Token"
></a-input>
</div>
<div>
<span class="ml10">Channel:</span>
<a-input
class="ml10"
v-model:value="agoraParam.channelId"
placeholder="Channel"
></a-input>
</div>
</div>
</span>
<span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTMP" style="word-break: break-all; color: #75c5f6;">{{ rtmpParam }}</span>
<span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTSP" style="word-break: break-all; color: #75c5f6;">{{ rtspParam }}</span>
<span v-else-if="liveStreamStatus.type === ELiveTypeValue.GB28181" style="word-break: break-all; color: #75c5f6;">{{ gb28181Param }}</span>
@ -154,11 +173,11 @@ const liveTypeList = [ @@ -154,11 +173,11 @@ const liveTypeList = [
label: ELiveTypeName.GB28181
}
]
const agoraParam = {
const agoraParam = reactive({
uid: '2892130292',
token: config.agoraToken,
channelId: config.agoraChannel
}
})
const rtmpParam = {
url: config.rtmpURL + new Date().getTime()
}
@ -174,7 +193,7 @@ const gb28181Param: GB28181Param = { @@ -174,7 +193,7 @@ const gb28181Param: GB28181Param = {
agentId: CURRENT_CONFIG.gbAgentId,
password: CURRENT_CONFIG.gbPassword,
agentPort: CURRENT_CONFIG.gbAgentPort,
agentChannel: CURRENT_CONFIG.gbAgentChannel
agentChannel: CURRENT_CONFIG.gbAgentChannel,
}
const playVisiable = ref(false)
@ -253,6 +272,7 @@ const onPublishModeSelect = (val: string) => { @@ -253,6 +272,7 @@ const onPublishModeSelect = (val: string) => {
apiPilot.setVideoPublishType(publishModeSelected.value)
}
const onPlay = () => {
console.info(JSON.stringify(agoraParam))
if (!publishModeSelected.value) {
message.warn('Please select publish mode!')
return

325
src/pages/page-web/projects/Firmwares.vue

@ -0,0 +1,325 @@ @@ -0,0 +1,325 @@
<template>
<div class="ml20 mt20 mr20 flex-row flex-align-center flex-justify-between">
<div class="flex-row">
<a-button type="primary" @click="sVisible = true">
Click to Upload
</a-button>
<a-modal :visible="sVisible"
title="Import Firmware File"
:closable="false"
@cancel="onCancel"
@ok="uploadFile"
centered>
<a-form :rules="rules" ref="formRef" :model="uploadParam" :label-col="{ span: 6 }">
<a-form-item name="status" label="Avaliable" required>
<a-switch v-model:checked="uploadParam.status" />
</a-form-item>
<a-form-item name="device_name" label="Device Name" required>
<a-select
style="width: 220px"
mode="multiple"
placeholder="can choose multiple"
v-model:value="uploadParam.device_name">
<a-select-option
v-for="k in DeviceNameEnum"
:key="k"
:value="k"
>
{{ k }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="release_note" label="Release Note" required>
<a-textarea v-model:value="uploadParam.release_note" showCount :maxlength="300" />
</a-form-item>
<a-form-item label="File" required>
<a-upload
:multiple="false"
:before-upload="beforeUpload"
:show-upload-list="true"
:file-list="fileList"
:remove="removeFile"
>
<a-button type="primary">
<UploadOutlined />
Import Firmware File
</a-button>
</a-upload>
</a-form-item>
</a-form>
</a-modal>
</div>
<div class="flex-row">
<div class="ml5">
<a-select
style="width: 150px"
v-model:value="param.firmware_status"
@select="getAllFirmwares(pageParam)">
<a-select-option
v-for="(key, value) in FirmwareStatusEnum"
:key="key"
:value="value"
>
{{ key }}
</a-select-option>
</a-select>
</div>
<div class="ml5">
<a-select
style="width: 150px"
v-model:value="param.device_name"
@select="getAllFirmwares(pageParam)">
<a-select-option
v-for="item in deviceNameList"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="ml5">
<a-input-search
:enter-button="true"
v-model:value="param.product_version"
placeholder="input search verison"
style="width: 250px"
@search="getAllFirmwares(pageParam)"/>
</div>
</div>
</div>
<div class="table flex-display flex-column">
<a-table :columns="columns" :data-source="data.firmware" :pagination="paginationProp" @change="refreshData" row-key="firmware_id"
:rowClassName="(record, index) => ((index % 2) === 0 ? 'table-striped' : null)" :scroll="{ x: '100%', y: 600 }">
<template #device_name="{ record }">
<div v-for="text in record.device_name" :key="text">
{{ text }}
</div>
</template>
<template #file_size="{ record }">
<div>{{ bytesToSize(record.file_size) }}</div>
</template>
<template #firmware_status="{ record }">
<DeviceFirmwareStatus :firmware="record" />
</template>
<template v-for="col in ['file_name', 'release_note']" #[col]="{ text }" :key="col">
<a-tooltip :title="text">
<span>{{ text }}</span>
</a-tooltip>
</template>
</a-table>
</div>
</template>
<script lang="ts" setup>
import { message, notification, PaginationProps } from 'ant-design-vue'
import { TableState } from 'ant-design-vue/lib/table/interface'
import { onMounted, reactive, Ref, ref, UnwrapRef } from 'vue'
import { IPage } from '/@/api/http/type'
import { getFirmwares, importFirmareFile } from '/@/api/manage'
import DeviceFirmwareStatus from '/@/components/devices/DeviceFirmwareStatus.vue'
import { ELocalStorageKey } from '/@/types'
import { UploadOutlined } from '@ant-design/icons-vue'
import { Firmware, FirmwareQueryParam, FirmwareStatusEnum, DeviceNameEnum, FirmwareUploadParam } from '/@/types/device-firmware'
import { commonColor } from '/@/utils/color'
import { bytesToSize } from '/@/utils/bytes'
import moment from 'moment'
interface FirmwareData {
firmware: Firmware[]
}
const columns = [
{ title: 'Model', dataIndex: 'device_name', width: 120, ellipsis: true, className: 'titleStyle', slots: { customRender: 'device_name' } },
{ title: 'File Name', dataIndex: 'file_name', width: 220, ellipsis: true, className: 'titleStyle', slots: { customRender: 'file_name' } },
{ title: 'Firmware Version', dataIndex: 'product_version', width: 180, className: 'titleStyle' },
{ title: 'File Size', dataIndex: 'file_size', width: 150, className: 'titleStyle', slots: { customRender: 'file_size' } },
{ title: 'Creator', dataIndex: 'username', width: 100, className: 'titleStyle' },
{ title: 'Release Date', dataIndex: 'released_time', width: 160, sorter: (a: Firmware, b: Firmware) => a.released_time.localeCompare(b.released_time), className: 'titleStyle' },
{ title: 'Release Note', dataIndex: 'release_note', width: 300, ellipsis: true, className: 'titleStyle', slots: { customRender: 'release_note' } },
{ title: 'Status', dataIndex: 'firmware_status', width: 100, className: 'titleStyle', slots: { customRender: 'firmware_status' } },
]
const data = reactive<FirmwareData>({
firmware: []
})
const paginationProp = reactive({
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
})
const deviceNameList = ref<any[]>([{ label: 'All', value: '' }])
type Pagination = TableState['pagination']
const pageParam: IPage = {
page: 1,
total: 0,
page_size: 50
}
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const param = reactive<FirmwareQueryParam>({
product_version: '',
device_name: '',
firmware_status: FirmwareStatusEnum.NONE
})
onMounted(() => {
getAllFirmwares(pageParam)
for (const key in DeviceNameEnum) {
const value = DeviceNameEnum[key]
deviceNameList.value.push({ label: value, value: value })
}
})
function refreshData (page: Pagination) {
pageParam.page = page?.current!
pageParam.page_size = page?.pageSize!
getAllFirmwares(pageParam)
}
function getAllFirmwares (page: IPage) {
getFirmwares(workspaceId, page, param).then(res => {
const firmwareList: Firmware[] = res.data.list
data.firmware = firmwareList
paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page
})
}
const sVisible = ref(false)
const uploadParam = reactive<FirmwareUploadParam>({
device_name: [],
release_note: '',
status: true
})
const rules = {
status: [{ required: true }],
release_note: [{ required: true, message: 'Please input release note.' }],
device_name: [{ required: true, message: 'Please select which models this firmware belongs to.' }]
}
interface FileItem {
uid: string;
name?: string;
status?: string;
response?: string;
url?: string;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
const fileList = ref<FileItem[]>([])
function beforeUpload (file: FileItem) {
if (!file.name || !file.name?.endsWith('.zip')) {
message.error('Format error. Please select zip file.')
return false
}
fileList.value = [file]
return false
}
const formRef = ref()
function removeFile (file: FileItem) {
fileList.value = []
}
function onCancel () {
formRef.value.resetFields()
fileList.value = []
sVisible.value = false
}
const uploadFile = async () => {
if (fileList.value.length === 0) {
message.error('Please select at least one file.')
}
let uploading: string
formRef.value.validate().then(async () => {
const file: FileItem = fileList.value[0]
const fileData = new FormData()
fileData.append('file', file as any, file.name)
Object.keys(uploadParam).forEach((key) => {
const val = uploadParam[key as keyof FirmwareUploadParam]
if (val instanceof Array) {
val.forEach((value) => {
fileData.append(key, value)
})
} else {
fileData.append(key, val.toString())
}
})
const timestamp = new Date().getTime()
uploading = (file.name ?? 'uploding') + timestamp
notification.open({
key: uploading,
message: `Uploading ${moment().format()}`,
description: `[${file.name}] is uploading... `,
duration: null
})
importFirmareFile(workspaceId, fileData).then((res) => {
if (res.code === 0) {
notification.success({
message: `Uploaded ${moment().format()}`,
description: `[${file.name}] file uploaded successfully. Duration: ${moment.duration(new Date().getTime() - timestamp).asSeconds()}`,
duration: null
})
getAllFirmwares(pageParam)
} else {
notification.error({
message: `Failed to upload [${file.name}]. Check and try again.`,
description: `Error message: ${res.message} ${moment().format()}`,
style: { color: commonColor.FAIL },
duration: null,
})
}
}).finally(() => {
notification.close(uploading)
})
fileList.value = []
formRef.value.resetFields()
sVisible.value = false
})
}
</script>
<style>
.table {
background-color: white;
margin: 20px;
padding: 20px;
height: 88vh;
}
.table-striped {
background-color: #f7f9fa;
}
.ant-table {
border-top: 1px solid rgb(0,0,0,0.06);
border-bottom: 1px solid rgb(0,0,0,0.06);
}
.ant-table-tbody tr td {
border: 0;
}
.ant-table td {
white-space: nowrap;
}
.ant-table-thead tr th {
background: white !important;
border: 0;
}
th.ant-table-selection-column {
background-color: white !important;
}
.ant-table-header {
background-color: white !important;
}
</style>

251
src/pages/page-web/projects/create-plan.vue

@ -1,251 +0,0 @@ @@ -1,251 +0,0 @@
<template>
<div class="plan">
<div class="header">
Create Plan
</div>
<div class="content">
<a-form ref="valueRef" layout="horizontal" :hideRequiredMark="true" :rules="rules" :model="planBody">
<a-form-item label="Plan Name" name="name" :labelCol="{span: 24}">
<a-input style="background: black;" placeholder="Please enter plan name" v-model:value="planBody.name"/>
</a-form-item>
<a-form-item label="Flight Route" :wrapperCol="{offset: 7}" name="file_id">
<router-link
:to="{name: 'select-plan'}"
@click="selectRoute"
>
Select Route
</router-link>
</a-form-item>
<a-form-item v-if="planBody.file_id" style="margin-top: -15px;">
<div class="wayline-panel" style="padding-top: 5px;">
<div class="title">
<a-tooltip :title="wayline.name">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div>
</a-tooltip>
<div class="ml10"><UserOutlined /></div>
<a-tooltip :title="wayline.user_name">
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div>
</a-tooltip>
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span>
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span>
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }}
</span>
</div>
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span>
</div>
</div>
</a-form-item>
<a-form-item label="Device" :wrapperCol="{offset: 10}" v-model:value="planBody.dock_sn" name="dock_sn">
<router-link
:to="{name: 'select-plan'}"
@click="selectDevice"
>Select Device</router-link>
</a-form-item>
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;">
<div class="panel" style="padding-top: 5px;" @click="selectDock(dock)">
<div class="title">
<a-tooltip :title="dock.nickname">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
</a-tooltip>
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ dock.children?.nickname }}</span>
</div>
</div>
</a-form-item>
<a-form-item label="Immediate">
<a-switch v-model:checked="planBody.immediate">
<template #checkedChildren><CheckOutlined /></template>
<template #unCheckedChildren><CloseOutlined /></template>
</a-switch>
</a-form-item>
<a-form-item style="position: absolute; bottom: 0px; margin-bottom: 0; margin-left: -10px; width: 280px;">
<div class="footer">
<a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel
</a-button>
<a-button type="primary" @click="onSubmit" :disabled="disabled">OK
</a-button>
</div>
</a-form-item>
</a-form>
</div>
</div>
<div v-if="drawerVisible" style="position: absolute; left: 330px; width: 280px; height: 100vh; float: right; top: 0; z-index: 1000; color: white; background: #282828;">
<div>
<router-view :name="routeName"/>
</div>
<div style="position: absolute; top: 15px; right: 10px;">
<a style="color: white;" @click="closePanel"><CloseOutlined /></a>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, UnwrapRef } from 'vue'
import { CheckOutlined, CloseOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { ELocalStorageKey, ERouterName } from '/@/types'
import { useMyStore } from '/@/store'
import { WaylineFile } from '/@/types/wayline'
import { Device, EDeviceType } from '/@/types/device'
import { createPlan, CreatePlan } from '/@/api/wayline'
import { getRoot } from '/@/root'
const root = getRoot()
const store = useMyStore()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const wayline = computed<WaylineFile>(() => {
return store.state.waylineInfo
})
const dock = computed<Device>(() => {
return store.state.dockInfo
})
const disabled = ref(false)
const routeName = ref('')
const planBody: UnwrapRef<CreatePlan> = reactive({
name: '',
file_id: computed(() => store.state.waylineInfo.id),
dock_sn: computed(() => store.state.dockInfo.device_sn),
immediate: false,
type: 'wayline'
})
const drawerVisible = ref(false)
const valueRef = ref()
const rules = {
name: [
{ required: true, message: 'Please enter plan name.' },
{ max: 20, message: 'Length should be 1 to 20', trigger: 'blur' }
],
file_id: [{ required: true, message: 'Select Route' }],
dock_sn: [{ required: true, message: 'Select Device' }]
}
function onSubmit () {
valueRef.value.validate().then(() => {
disabled.value = true
createPlan(workspaceId, planBody)
.then(res => {
message.success('Saved Successfully')
setTimeout(() => {
disabled.value = false
}, 1500)
}).finally(() => {
closePlan()
})
})
}
function closePlan () {
root.$router.push('/' + ERouterName.TASK)
}
function closePanel () {
drawerVisible.value = false
routeName.value = ''
}
function selectRoute () {
drawerVisible.value = true
routeName.value = 'WaylinePanel'
}
function selectDevice () {
drawerVisible.value = true
routeName.value = 'DockPanel'
}
</script>
<style lang="scss">
.plan {
background-color: #232323;
color: white;
padding-bottom: 0;
height: 100vh;
display: flex;
flex-direction: column;
.header {
height: 53px;
border-bottom: 1px solid #4f4f4f;
font-weight: 700;
font-size: 16px;
padding-left: 10px;
display: flex;
align-items: center;
}
.content {
height: 100%;
form {
margin: 10px;
}
form label, input {
color: white;
}
}
.footer {
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #4f4f4f;
min-height: 65px;
margin-bottom: 0;
padding-bottom: 0;
button {
width: 45%;
color: white;
border: 0;
}
}
}
.wayline-panel {
background: #3c3c3c;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
height: 90px;
width: 95%;
font-size: 13px;
border-radius: 2px;
cursor: pointer;
.title {
display: flex;
color: white;
flex-direction: row;
align-items: center;
height: 30px;
font-weight: bold;
margin: 0px 10px 0 10px;
}
}
.panel {
background: #3c3c3c;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
height: 70px;
width: 95%;
font-size: 13px;
border-radius: 2px;
cursor: pointer;
.title {
display: flex;
color: white;
flex-direction: row;
align-items: center;
height: 30px;
font-weight: bold;
margin: 0px 10px 0 10px;
}
}
</style>

19
src/pages/page-web/projects/devices.vue

@ -20,7 +20,9 @@ @@ -20,7 +20,9 @@
style="margin: -5px 0"
/>
<template v-else>
{{ text }}
<a-tooltip :title="text">
{{ text }}
</a-tooltip>
</template>
</div>
</template>
@ -127,7 +129,7 @@ import { useDeviceUpgradeEvent } from '/@/components/devices/device-upgrade/use- @@ -127,7 +129,7 @@ import { useDeviceUpgradeEvent } from '/@/components/devices/device-upgrade/use-
import { DeviceCmdExecuteInfo, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
import DeviceLogUploadRecordDrawer from '/@/components/devices/device-log/DeviceLogUploadRecordDrawer.vue'
import DeviceHmsDrawer from '/@/components/devices/device-hms/DeviceHmsDrawer.vue'
import { message } from 'ant-design-vue'
import { message, notification } from 'ant-design-vue'
interface DeviceData {
device: Device[]
@ -268,13 +270,18 @@ function updateDevicesByWs (devices: Device[], payload: DeviceCmdExecuteInfo) { @@ -268,13 +270,18 @@ function updateDevicesByWs (devices: Device[], payload: DeviceCmdExecuteInfo) {
for (let i = 0; i < devices.length; i++) {
if (devices[i].device_sn === payload.sn) {
if (!payload.output) return
const { status, progress } = payload.output
const { status, progress, ext } = payload.output
if (status === DeviceCmdExecuteStatus.Sent || status === DeviceCmdExecuteStatus.InProgress) { //
const rate = ext?.rate ? (ext.rate / 1024).toFixed(2) + 'kb/s' : ''
devices[i].firmware_status = DeviceFirmwareStatusEnum.DuringUpgrade
devices[i].firmware_progress = progress?.percent || 0
devices[i].firmware_progress = (progress?.percent || 0) + '% ' + rate
} else { //
if (status === DeviceCmdExecuteStatus.Failed || status === DeviceCmdExecuteStatus.Timeout) {
message.error(`设备(${payload.sn}) 升级失败`)
notification.error({
message: `(${payload.sn}) Upgrade failed`,
description: `Error Code: ${payload.result}`,
duration: null
})
}
//
getDevices(current.value[0], true)
@ -290,7 +297,7 @@ function updateDevicesByWs (devices: Device[], payload: DeviceCmdExecuteInfo) { @@ -290,7 +297,7 @@ function updateDevicesByWs (devices: Device[], payload: DeviceCmdExecuteInfo) {
useDeviceUpgradeEvent(onDeviceUpgradeWs)
//
function getDevices (domain: string, closeLoading?: boolean) {
function getDevices (domain: number, closeLoading?: boolean) {
if (!closeLoading) {
loading.value = true
}

88
src/pages/page-web/projects/dock.vue

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<template>
<div>
<div class="height-100">
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;">
<a-row>
<a-col :span="1"></a-col>
@ -7,23 +7,25 @@ @@ -7,23 +7,25 @@
<a-col :span="1"></a-col>
</a-row>
</div>
<div v-if="docksData.data.length !== 0">
<div v-for="dock in docksData.data" :key="dock.device_sn">
<div v-if="dock.children" class="panel" style="padding-top: 5px;" @click="selectDock(dock)">
<div class="title">
<a-tooltip :title="dock.nickname">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
</a-tooltip>
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ dock.children?.nickname }}</span>
<div class="scrollbar height-100" :style="{ height: scorllHeight + 'px'}">
<div id="data" class=" uranus-scrollbar" v-if="docksData.data.length !== 0" @scroll="onScroll">
<div v-for="dock in docksData.data" :key="dock.device_sn">
<div class="panel" style="padding-top: 5px;" @click="selectDock(dock)">
<div class="title">
<a-tooltip :title="dock.nickname">
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
</a-tooltip>
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ dock.children?.nickname ?? 'No drone' }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
<div v-else>
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
</div>
</div>
</div>
</template>
@ -35,7 +37,7 @@ import { onMounted, ref } from 'vue' @@ -35,7 +37,7 @@ import { onMounted, ref } from 'vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline'
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { Device, EDeviceType } from '/@/types/device'
import { Device } from '/@/types/device'
import { useMyStore } from '/@/store'
import { getBindingDevices } from '/@/api/manage'
import { IPage } from '/@/api/http/type'
@ -47,30 +49,56 @@ const docksData = reactive({ @@ -47,30 +49,56 @@ const docksData = reactive({
})
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const scorllHeight = ref()
const canRefresh = ref(true)
onMounted(() => {
const parent = document.getElementsByClassName('scrollbar').item(0)?.parentNode as HTMLDivElement
scorllHeight.value = document.body.clientHeight - parent.firstElementChild!.clientHeight
getDocks()
const key = setInterval(() => {
const data = document.getElementById('data')?.lastElementChild as HTMLDivElement
if (body.total === 0 || Math.ceil(body.total / body.page_size) <= body.page || scorllHeight.value + 50 <= data?.clientHeight + data?.offsetTop) {
clearInterval(key)
return
}
body.page++
getDocks()
}, 1000)
})
const body: IPage = {
page: 1,
total: 0,
page_size: 100
total: -1,
page_size: 10,
}
function getDocks () {
getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => {
async function getDocks () {
if (!canRefresh.value) {
return
}
canRefresh.value = false
await getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => {
if (res.code !== 0) {
return
}
docksData.data = []
res.data.list.forEach((dock: any) => {
if (dock.child_device_sn) {
docksData.data.push(dock)
}
})
console.info(docksData.data)
docksData.data.push(...res.data.list)
body.page = res.data.pagination.page
body.page_size = res.data.pagination.page_size
body.total = res.data.pagination.total
}).finally(() => {
canRefresh.value = true
})
}
function onScroll (e: any) {
const element = e.srcElement
if (element.scrollTop + element.clientHeight >= element.scrollHeight - 5 && Math.ceil(body.total / body.page_size) > body.page && canRefresh.value) {
body.page++
getDocks()
}
}
function selectDock (dock: Device) {
store.commit('SET_SELECT_DOCK_INFO', dock)
}
@ -97,4 +125,10 @@ function selectDock (dock: Device) { @@ -97,4 +125,10 @@ function selectDock (dock: Device) {
margin: 0px 10px 0 10px;
}
}
.uranus-scrollbar {
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #c5c8cc transparent;
height: 100%;
}
</style>

98
src/pages/page-web/projects/flight-area.vue

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
<template>
<div class="project-flight-area-wrapper height-100">
<a-spin :spinning="loading" :delay="300" tip="loading" size="large" class="height-100">
<Title title="Custom Flight Area" />
<FlightAreaPanel :data="flightAreaList" @location-area="clickArea" @delete-area="deleteAreaById"/>
<DividerLine />
<FlightAreaSyncPanel />
</a-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import Title from '/@/components/workspace/Title.vue'
import DividerLine from '/@/components/workspace/DividerLine.vue'
import FlightAreaPanel from '/@/components/flight-area/FlightAreaPanel.vue'
import FlightAreaSyncPanel from '/@/components/flight-area/FlightAreaSyncPanel.vue'
import { GetFlightArea, deleteFlightArea, getFlightAreaList } from '/@/api/flight-area'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { useMapTool } from '/@/hooks/use-map-tool'
import { EFlightAreaType, EGeometryType, FlightAreaUpdate } from '/@/types/flight-area'
import { useFlightArea } from '/@/components/flight-area/use-flight-area'
import { useFlightAreaUpdateEvent } from '/@/components/flight-area/use-flight-area-update'
const loading = ref(false)
const flightAreaList = ref<GetFlightArea[]>([])
let useGMapCoverHook = useGMapCover()
let useMapToolHook = useMapTool()
onMounted(() => {
getDataList()
})
const { getGcj02 } = useFlightArea()
const initMapFlightArea = () => {
useMapToolHook = useMapTool()
useGMapCoverHook = useGMapCover()
flightAreaList.value.forEach(area => {
updateMapFlightArea(area)
})
}
const updateMapFlightArea = (area: GetFlightArea) => {
switch (area.content.geometry.type) {
case EGeometryType.CIRCLE:
useGMapCoverHook.updateFlightAreaCircle(area.area_id, area.name, area.content.geometry.radius, getGcj02(area.content.geometry.coordinates), area.status, area.type)
break
case 'Polygon':
useGMapCoverHook.updateFlightAreaPolygon(area.area_id, area.name, getGcj02(area.content.geometry.coordinates[0]), area.status, area.type)
break
}
}
const getDataList = () => {
loading.value = true
getFlightAreaList().then(res => {
flightAreaList.value = res.data
setTimeout(initMapFlightArea, 2000)
}).finally(() => {
loading.value = false
})
}
const deleteAreaById = (areaId: string) => {
deleteFlightArea(areaId)
}
const deleteArea = (area: FlightAreaUpdate) => {
flightAreaList.value = flightAreaList.value.filter(data => data.area_id !== area.area_id)
useGMapCoverHook.removeCoverFromMap(area.area_id)
}
const updateArea = (area: FlightAreaUpdate) => {
flightAreaList.value = flightAreaList.value.map(data => data.area_id === area.area_id ? area : data)
updateMapFlightArea(area as GetFlightArea)
}
const addArea = (area: FlightAreaUpdate) => {
flightAreaList.value.push(area as GetFlightArea)
updateMapFlightArea(area as GetFlightArea)
}
useFlightAreaUpdateEvent(addArea, deleteArea, updateArea)
const clickArea = (area: GetFlightArea) => {
console.info(area)
let coordinate
switch (area.content.geometry.type) {
case EGeometryType.CIRCLE:
coordinate = getGcj02(area.content.geometry.coordinates)
break
case 'Polygon':
coordinate = useGMapCoverHook.calcPolygonPosition(getGcj02(area.content.geometry.coordinates[0]))
break
}
useMapToolHook.panTo(coordinate)
}
</script>

50
src/pages/page-web/projects/layer.vue

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
<template>
<div class="project-layer-wrapper">
<div class="project-layer-wrapper height-100">
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;">
<a-row>
<a-col :span="1"></a-col>
@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
<a-col :span="1"></a-col>
</a-row>
</div>
<div class="scrollbar" :style="{ height: scorllHeight + 'px'}">
<LayersTree
:layer-data="mapLayers"
class="project-layer-content"
@ -15,6 +16,7 @@ @@ -15,6 +16,7 @@
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys"
/>
</div>
<a-drawer
title="Map Element"
placement="right"
@ -128,6 +130,7 @@ const colors = ref<Color[]>([ @@ -128,6 +130,7 @@ const colors = ref<Color[]>([
{ id: 5, name: 'RED', color: '#E23C39', selected: false },
{ id: 6, name: 'NAME_DEFAULT', color: '#212121', selected: false }
])
const scorllHeight = ref()
async function getAllElement () {
getElementGroups('init')
@ -168,7 +171,6 @@ function updateMapElement ( @@ -168,7 +171,6 @@ function updateMapElement (
const geoType = element.resource?.content.geometry.type
const id = element.id
const type = element.resource?.type as number
if (MapElementEnum.PIN === type) {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate
@ -176,17 +178,11 @@ function updateMapElement ( @@ -176,17 +178,11 @@ function updateMapElement (
} else if (MapElementEnum.LINE === type && geoType === 'LineString') {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[]
useGMapCoverHook.initPolyline(name, coordinates, color, {
id: id,
name: name
})
useGMapCoverHook.updatePolylineElement(id, name, coordinates, color)
} else if (MapElementEnum.POLY === type && geoType === 'Polygon') {
const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[]
useGMapCoverHook.initPolygon(name, coordinates, color, {
id: id,
name: name
})
.coordinates as GeojsonCoordinate[][]
useGMapCoverHook.updatePolygonElement(id, name, coordinates, color)
}
}
function checkLayer (keys: string[]) {
@ -232,10 +228,12 @@ function setBaseInfo () { @@ -232,10 +228,12 @@ function setBaseInfo () {
layerState.layerName = layer.name
layerState.layerId = layer.id
layerState.color = layer.resource?.content.properties.color
let coordinate: GeojsonCoordinate
switch (geoType) {
case GeoType.Point:
layerState.longitude = layer.resource?.content.geometry.coordinates[0]
layerState.latitude = layer.resource?.content.geometry.coordinates[1]
coordinate = gcj02towgs84(layer.resource?.content.geometry.coordinates[0], layer.resource?.content.geometry.coordinates[1]) as GeojsonCoordinate
layerState.longitude = coordinate[0]
layerState.latitude = coordinate[1]
break
case GeoType.LineString:
break
@ -245,6 +243,9 @@ function setBaseInfo () { @@ -245,6 +243,9 @@ function setBaseInfo () {
}
}
onMounted(() => {
const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
const parent = element?.parentNode as HTMLDivElement
scorllHeight.value = parent?.clientHeight - parent.firstElementChild!.clientHeight
getAllElement()
})
function closeDrawer () {
@ -350,39 +351,39 @@ function updateCoordinates (transformType: string, element: LayerResource) { @@ -350,39 +351,39 @@ function updateCoordinates (transformType: string, element: LayerResource) {
) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult
}
} else if (MapElementEnum.LINE === type && geoType === 'LineString') {
} else if (MapElementEnum.LINE === type) {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
}
element.resource.content.geometry.coordinates = coordinates
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') {
} else if (MapElementEnum.POLY === type) {
const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinates.forEach((coordinate, i, arr) => {
arr[i] = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
@ -439,4 +440,7 @@ function updateCoordinates (transformType: string, element: LayerResource) { @@ -439,4 +440,7 @@ function updateCoordinates (transformType: string, element: LayerResource) {
margin-bottom: 10px;
}
}
.scrollbar {
overflow: auto;
}
</style>

19
src/pages/page-web/projects/livestream.vue

@ -18,7 +18,8 @@ @@ -18,7 +18,8 @@
>
</router-link>
</div>
<div class="live" v-if="showLive">
<div class="live" v-if="showLive" v-drag-window>
<div style="height: 40px; width: 100%" class="drag-title"></div>
<a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="() => root.$router.push('/' + ERouterName.LIVESTREAM)"><CloseOutlined /></a>
<router-view :name="routeName" />
</div>
@ -31,8 +32,8 @@ import { CloseOutlined } from '@ant-design/icons-vue' @@ -31,8 +32,8 @@ import { CloseOutlined } from '@ant-design/icons-vue'
import { getRoot } from '/@/root'
import { ERouterName } from '/@/types'
const root = getRoot()
const routeName = ref<string>()
const showLive = ref<boolean>(false)
const routeName = ref<string>('LiveOthers')
const showLive = ref<boolean>(root.$route.name === ERouterName.LIVING)
const options = [
{ key: 0, label: 'Agora Live', path: '/' + ERouterName.LIVESTREAM + '/' + ERouterName.LIVING, routeName: 'LiveAgora' },
@ -40,6 +41,7 @@ const options = [ @@ -40,6 +41,7 @@ const options = [
]
const selectLivestream = (route: string) => {
showLive.value = root.$route.name === ERouterName.LIVING
routeName.value = route
}
@ -73,14 +75,13 @@ onMounted(() => { @@ -73,14 +75,13 @@ onMounted(() => {
.live {
position: absolute;
z-index: 1;
right: 50%;
left: 50%;
top: 50%;
margin: auto;
transform: translate(-50%, -50%);
left: 0;
top: 10px;
margin-left: 345px;
text-align: center;
width: 800px;
height: 700px;
height: 720px;
background: #232323;
}
</style>

28
src/pages/page-web/projects/task.vue

@ -5,34 +5,42 @@ @@ -5,34 +5,42 @@
<a-col :span="1"></a-col>
<a-col :span="20">Task Plan Library</a-col>
<a-col :span="2">
<span v-if="!createPlanTip">
<router-link :to="{ name: 'create-plan'}">
<PlusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = true"/>
<span v-if="taskRoute">
<router-link :to="{ name: ERouterName.CREATE_PLAN}">
<PlusOutlined class="route-icon"/>
</router-link>
</span>
<span v-else>
<router-link :to="{ name: 'task'}">
<MinusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = false"/>
<router-link :to="{ name: ERouterName.TASK}">
<MinusOutlined class="route-icon"/>
</router-link>
</span>
</a-col>
<a-col :span="1"></a-col>
</a-row>
</div>
<div v-if="createPlanTip">
<router-view />
<div v-if="!taskRoute">
<router-view/>
</div>
</div>
</template>
<script lang="ts" setup>
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { computed, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ERouterName } from '/@/types/enums'
const createPlanTip = ref(false)
const route = useRoute()
const taskRoute = computed(() => {
return route.name === ERouterName.TASK
})
</script>
<style lang="scss">
.route-icon {
color: #fff;
font-size: 16px;
}
</style>

97
src/pages/page-web/projects/tsa.vue

@ -8,7 +8,7 @@ @@ -8,7 +8,7 @@
<a-col :span="1"></a-col>
</a-row>
</div>
<div>
<div class="scrollbar" :style="{ height: scorllHeight + 'px'}">
<a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;">
<a-collapse-panel :key="EDeviceTypeName.Dock" header="Dock" style="border-bottom: 1px solid #4f4f4f;">
<div v-if="onlineDocks.data.length === 0" style="height: 150px; color: white;">
@ -19,16 +19,16 @@ @@ -19,16 +19,16 @@
<div style="border-radius: 2px; height: 100%; width: 100%;" class="flex-row flex-justify-between flex-align-center">
<div style="float: left; padding: 0px 5px 8px 8px; width: 88%">
<div style="width: 80%; height: 30px; line-height: 30px; font-size: 16px;">
<a-tooltip :title="dock.gateway.callsign">
<span class="text-hidden" style="max-width: 200px;">{{ dock.gateway.callsign }}</span>
<a-tooltip :title="`${dock.gateway.callsign} - ${dock.callsign ?? 'No Drone'}`">
<div class="text-hidden" style="max-width: 200px;">{{ dock.gateway.callsign }} - {{ dock.callsign ?? 'No Drone' }}</div>
</a-tooltip>
</div>
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
<div class="flex-align-center flex-row">
<span class="ml5 mr5"><RobotOutlined /></span>
<span class="font-bold text-hidden" style="max-width: 80px;" :style="dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'">
{{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].mode_code] : EDockModeCode[EDockModeCode.Disconnected] }}
</span>
<div class="font-bold text-hidden" style="max-width: 80px;" :style="dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].basic_osd?.mode_code !== EDockModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'">
{{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].basic_osd?.mode_code] : EDockModeCode[EDockModeCode.Disconnected] }}
</div>
</div>
<div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;">
<div v-if="hmsInfo[dock.gateway.sn]" class="flex-align-center flex-row">
@ -75,11 +75,11 @@ @@ -75,11 +75,11 @@
</div>
</div>
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
<div>
<div class="flex-row">
<span class="ml5 mr5"><RocketOutlined /></span>
<span class="font-bold" :style="deviceInfo[dock.sn] && deviceInfo[dock.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'">
<div class="font-bold text-hidden" style="max-width: 80px" :style="deviceInfo[dock.sn] && deviceInfo[dock.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'">
{{ deviceInfo[dock.sn] ? EModeCode[deviceInfo[dock.sn].mode_code] : EModeCode[EModeCode.Disconnected] }}
</span>
</div>
</div>
<div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;">
<div v-if="hmsInfo[dock.sn]" class="flex-align-center flex-row">
@ -126,7 +126,7 @@ @@ -126,7 +126,7 @@
</div>
</div>
<div style="float: right; background: #595959; height: 100%; width: 40px;" class="flex-row flex-justify-center flex-align-center">
<div class="fz16" @click="switchVisible($event, dock, true, dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected)">
<div class="fz16" @click="switchVisible($event, dock, true, dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].basic_osd?.mode_code !== EDockModeCode.Disconnected)">
<a v-if="osdVisible.gateway_sn === dock.gateway.sn && osdVisible.visible"><EyeOutlined /></a>
<a v-else><EyeInvisibleOutlined /></a>
</div>
@ -154,8 +154,8 @@ @@ -154,8 +154,8 @@
<div style="float: left; padding: 5px 5px 8px 8px; width: 88%">
<div style="width: 100%; height: 100%;">
<a-tooltip>
<template #title>{{ device.model }} - {{ device.callsign }}</template>
<span class="text-hidden" style="max-width: 200px; display: block; height: 20px;">{{ device.model }} - {{ device.callsign }}</span>
<template #title>{{ device.model ? `${device.model} - ${device.callsign}` : 'No Drone'}}</template>
<span class="text-hidden" style="max-width: 200px; display: block; height: 20px;">{{ device.model ? `${device.model} - ${device.callsign}` : 'No Drone'}}</span>
</a-tooltip>
</div>
<div class="mt5" style="background: #595959;">
@ -173,11 +173,11 @@ @@ -173,11 +173,11 @@
</div>
</div>
<div class="flex-row flex-justify-center flex-align-center" style="height: 40px;">
<div style="height: 20px; background: #595959; width: 94%;" >
<div class="flex-row" style="height: 20px; background: #595959; width: 94%;" >
<span class="mr5"><a-image style="margin-left: 2px; margin-top: -2px; height: 20px; width: 20px;" :src="rc" /></span>
<a-tooltip>
<template #title>{{ device.gateway.callsign }} </template>
<span>{{ device.gateway.callsign }}</span>
<template #title>{{ device.gateway.model }} - {{ device.gateway.callsign }} </template>
<div class="text-hidden" style="max-width: 200px;">{{ device.gateway.model }} - {{ device.gateway.callsign }}</div>
</a-tooltip>
</div>
</div>
@ -194,34 +194,18 @@ import { computed, onMounted, reactive, ref, watch, WritableComputedRef } from ' @@ -194,34 +194,18 @@ import { computed, onMounted, reactive, ref, watch, WritableComputedRef } from '
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
import noData from '/@/assets/icons/no-data.png'
import rc from '/@/assets/icons/rc.png'
import { DeviceStatus, EModeCode, OSDVisible, EDockModeCode, DeviceOsd } from '/@/types/device'
import { OnlineDevice, EModeCode, OSDVisible, EDockModeCode, DeviceOsd } from '/@/types/device'
import { useMyStore } from '/@/store'
import { getDeviceTopo, getUnreadDeviceHms, updateDeviceHms } from '/@/api/manage'
import { message } from 'ant-design-vue'
import { RocketOutlined, EyeInvisibleOutlined, EyeOutlined, RobotOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'
import { EHmsLevel } from '/@/types/enums'
const store = useMyStore()
const username = ref(localStorage.getItem(ELocalStorageKey.Username))
const workspaceId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!)
const osdVisible = ref({} as OSDVisible)
const osdVisible = computed(() => store.state.osdVisible)
const hmsVisible = new Map<string, boolean>()
interface OnlineDevice {
model: string,
callsign: string,
sn: string,
mode: number,
gateway: {
model: string,
callsign: string,
sn: string,
domain: string,
},
payload: {
model: string
}[]
}
const scorllHeight = ref()
const onlineDevices = reactive({
data: [] as OnlineDevice[]
@ -256,6 +240,9 @@ onMounted(() => { @@ -256,6 +240,9 @@ onMounted(() => {
)
getOnlineDeviceHms()
}, 3000)
const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
const parent = element?.parentNode as HTMLDivElement
scorllHeight.value = parent?.clientHeight - parent?.firstElementChild?.clientHeight
})
function getOnlineTopo () {
@ -265,12 +252,12 @@ function getOnlineTopo () { @@ -265,12 +252,12 @@ function getOnlineTopo () {
}
onlineDevices.data = []
onlineDocks.data = []
res.data.forEach((val: any) => {
const gateway = val.gateways_list.pop()
res.data.forEach((gateway: any) => {
const child = gateway.children
const device: OnlineDevice = {
model: val.device_name,
callsign: val.nickname,
sn: val.device_sn,
model: child?.device_name,
callsign: child?.nickname,
sn: child?.device_sn,
mode: EModeCode.Disconnected,
gateway: {
model: gateway?.device_name,
@ -280,17 +267,22 @@ function getOnlineTopo () { @@ -280,17 +267,22 @@ function getOnlineTopo () {
},
payload: []
}
val.payloads_list.forEach((payload: any) => {
child?.payloads_list.forEach((payload: any) => {
device.payload.push({
model: payload.payload_name
index: payload.index,
model: payload.model,
payload_name: payload.payload_name,
payload_sn: payload.payload_sn,
control_source: payload.control_source,
payload_index: payload.payload_index
})
})
if (gateway && EDeviceTypeName.Dock === gateway.domain) {
if (EDeviceTypeName.Dock === gateway.domain) {
hmsVisible.set(device.sn, false)
hmsVisible.set(device.gateway.sn, false)
onlineDocks.data.push(device)
}
if (val.status && EDeviceTypeName.Gateway === gateway.domain) {
if (gateway.status && EDeviceTypeName.Gateway === gateway.domain) {
onlineDevices.data.push(device)
}
})
@ -312,6 +304,7 @@ function switchVisible (e: any, device: OnlineDevice, isDock: boolean, isClick: @@ -312,6 +304,7 @@ function switchVisible (e: any, device: OnlineDevice, isDock: boolean, isClick:
osdVisible.value.gateway_sn = device.gateway.sn
osdVisible.value.is_dock = isDock
osdVisible.value.gateway_callsign = device.gateway.callsign
osdVisible.value.payloads = device.payload
}
store.commit('SET_OSD_VISIBLE_INFO', osdVisible)
}
@ -351,6 +344,15 @@ function readHms (visiable: boolean, sn: string) { @@ -351,6 +344,15 @@ function readHms (visiable: boolean, sn: string) {
})
}
}
function openLivestreamOthers () {
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', true)
}
function openLivestreamAgora () {
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', true)
}
</script>
<style lang="scss">
@ -360,6 +362,15 @@ function readHms (visiable: boolean, sn: string) { @@ -360,6 +362,15 @@ function readHms (visiable: boolean, sn: string) {
align-items: center;
border-bottom: 1px solid #4f4f4f;
}
.project-tsa-wrapper {
height: 100%;
.scrollbar {
overflow: auto;
}
::-webkit-scrollbar {
display: none;
}
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
color: white;
border: 0;

115
src/pages/page-web/projects/wayline.vue

@ -1,15 +1,27 @@ @@ -1,15 +1,27 @@
<template>
<div class="project-wayline-wrapper height-100">
<a-spin :spinning="loading" :delay="300" tip="downloading" size="large">
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;">
<a-row>
<a-col :span="1"></a-col>
<a-col :span="22">Flight Route Library</a-col>
<a-col :span="1"></a-col>
<a-col :span="15">Flight Route Library</a-col>
<a-col :span="8" v-if="importVisible" class="flex-row flex-justify-end flex-align-center">
<a-upload
name="file"
:multiple="false"
:before-upload="beforeUpload"
:show-upload-list="false"
:customRequest="uploadFile"
>
<a-button type="text" style="color: white;">
<SelectOutlined />
</a-button>
</a-upload>
</a-col>
</a-row>
</div>
<div class="height-100">
<a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
<div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
<div :style="{ height : height + 'px'}" class="scrollbar">
<div id="data" class="height-100 uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
<div v-for="wayline in waylinesData.data" :key="wayline.id">
<div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)">
<div class="title">
@ -40,10 +52,10 @@ @@ -40,10 +52,10 @@
</div>
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
<span><RocketOutlined /></span>
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span>
<span class="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">
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }}
{{ DEVICE_NAME[payload] }}
</span>
</div>
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
@ -63,8 +75,8 @@ @@ -63,8 +75,8 @@
</div>
</template>
</a-modal>
</a-spin>
</div>
</a-spin>
</div>
</template>
@ -72,20 +84,23 @@ @@ -72,20 +84,23 @@
import { reactive } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { onMounted, onUpdated, ref } from 'vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline'
import { ELocalStorageKey } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { EDeviceType } from '/@/types/device'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles, importKmzFile } from '/@/api/wayline'
import { ELocalStorageKey, ERouterName } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined, SelectOutlined } from '@ant-design/icons-vue'
import { DEVICE_NAME } from '/@/types/device'
import { useMyStore } from '/@/store'
import { WaylineFile } from '/@/types/wayline'
import { downloadFile } from '/@/utils/common'
import { IPage } from '/@/api/http/type'
import { CURRENT_CONFIG } from '/@/api/http/config'
import { load } from '@amap/amap-jsapi-loader'
import { getRoot } from '/@/root'
const loading = ref(false)
const store = useMyStore()
const pagination :IPage = {
page: 1,
total: 0,
total: -1,
page_size: 10
}
@ -93,28 +108,28 @@ const waylinesData = reactive({ @@ -93,28 +108,28 @@ const waylinesData = reactive({
data: [] as WaylineFile[]
})
const root = getRoot()
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const deleteTip = ref(false)
const deleteWaylineId = ref<string>('')
const canRefresh = ref(true)
const importVisible = ref<boolean>(root.$router.currentRoute.value.name === ERouterName.WAYLINE)
const height = ref()
onMounted(() => {
const parent = document.getElementsByClassName('scrollbar').item(0)?.parentNode as HTMLDivElement
height.value = document.body.clientHeight - parent.firstElementChild!.clientHeight
getWaylines()
})
onUpdated(() => {
const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
const parent = element?.parentNode as HTMLDivElement
setTimeout(() => {
if (element?.scrollHeight < parent?.clientHeight && pagination.total > waylinesData.data.length) {
if (canRefresh.value) {
pagination.page++
getWaylines()
}
} else if (element && element.className.indexOf('height-100') === -1) {
element.className = element.className + ' height-100'
const key = setInterval(() => {
const data = document.getElementById('data')?.lastElementChild as HTMLDivElement
if (pagination.total === 0 || Math.ceil(pagination.total / pagination.page_size) <= pagination.page || height.value <= data?.clientHeight + data?.offsetTop) {
clearInterval(key)
return
}
}, 300)
pagination.page++
getWaylines()
}, 1000)
})
function getWaylines () {
@ -130,7 +145,7 @@ function getWaylines () { @@ -130,7 +145,7 @@ function getWaylines () {
if (res.code !== 0) {
return
}
res.data.list.forEach((wayline: WaylineFile) => waylinesData.data.push(wayline))
waylinesData.data = [...waylinesData.data, ...res.data.list]
pagination.total = res.data.pagination.total
pagination.page = res.data.pagination.page
}).finally(() => {
@ -150,9 +165,10 @@ function deleteWayline () { @@ -150,9 +165,10 @@ function deleteWayline () {
}
deleteWaylineId.value = ''
deleteTip.value = false
pagination.total--
pagination.total = 0
pagination.page = 1
waylinesData.data = []
setTimeout(getWaylines, 500)
getWaylines()
})
}
@ -175,12 +191,51 @@ function selectRoute (wayline: WaylineFile) { @@ -175,12 +191,51 @@ function selectRoute (wayline: WaylineFile) {
function onScroll (e: any) {
const element = e.srcElement
if (element.scrollTop + element.clientHeight === element.scrollHeight && Math.ceil(pagination.total / pagination.page_size) > pagination.page && canRefresh.value) {
if (element.scrollTop + element.clientHeight >= element.scrollHeight - 5 && Math.ceil(pagination.total / pagination.page_size) > pagination.page && canRefresh.value) {
pagination.page++
getWaylines()
}
}
interface FileItem {
uid: string;
name?: string;
status?: string;
response?: string;
url?: string;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
const fileList = ref<FileItem[]>([])
function beforeUpload (file: FileItem) {
fileList.value = [file]
loading.value = true
return true
}
const uploadFile = async () => {
fileList.value.forEach(async (file: FileItem) => {
const fileData = new FormData()
fileData.append('file', file, file.name)
await importKmzFile(workspaceId, fileData).then((res) => {
if (res.code === 0) {
message.success(`${file.name} file uploaded successfully`)
canRefresh.value = true
pagination.total = 0
pagination.page = 1
waylinesData.data = []
getWaylines()
}
}).finally(() => {
loading.value = false
fileList.value = []
})
})
}
</script>
<style lang="scss" scoped>

83
src/pages/page-web/projects/workspace.vue

@ -13,22 +13,22 @@ @@ -13,22 +13,22 @@
<div class="media-wrapper" v-if="root.$route.name === ERouterName.MEDIA">
<MediaPanel />
</div>
<div class="media-wrapper" v-if="root.$route.name === ERouterName.TASK">
<div class="task-wrapper" v-if="root.$route.name === ERouterName.TASK">
<TaskPanel />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import Sidebar from '/@/components/common/sidebar.vue'
import MediaPanel from '/@/components/MediaPanel.vue'
import TaskPanel from '/@/components/TaskPanel.vue'
import TaskPanel from '/@/components/task/TaskPanel.vue'
import GMap from '/@/components/GMap.vue'
import { EBizCode, ERouterName } from '/@/types'
import { getRoot } from '/@/root'
import { useMyStore } from '/@/store'
import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
import EventBus from '/@/event-bus'
const root = getRoot()
const store = useMyStore()
@ -71,8 +71,10 @@ const messageHandler = async (payload: any) => { @@ -71,8 +71,10 @@ const messageHandler = async (payload: any) => {
store.commit('SET_DEVICE_OFFLINE', payload.data)
break
}
case EBizCode.FlightTaskProgress: {
store.commit('SET_FLIGHT_TASK_PROGRESS', payload.data)
case EBizCode.FlightTaskProgress:
case EBizCode.FlightTaskMediaProgress:
case EBizCode.FlightTaskMediaHighestPriority: {
EventBus.emit('flightTaskWs', payload)
break
}
case EBizCode.DeviceHms: {
@ -98,6 +100,27 @@ const messageHandler = async (payload: any) => { @@ -98,6 +100,27 @@ const messageHandler = async (payload: any) => {
})
break
}
case EBizCode.ControlSourceChange:
case EBizCode.FlyToPointProgress:
case EBizCode.TakeoffToPointProgress:
case EBizCode.JoystickInvalidNotify:
case EBizCode.DrcStatusNotify:
{
EventBus.emit('droneControlWs', payload)
break
}
case EBizCode.FlightAreasSyncProgress: {
EventBus.emit('flightAreasSyncProgressWs', payload.data)
break
}
case EBizCode.FlightAreasDroneLocation: {
EventBus.emit('flightAreasDroneLocationWs', payload)
break
}
case EBizCode.FlightAreasUpdate: {
EventBus.emit('flightAreasUpdateWs', payload.data)
break
}
default:
break
}
@ -112,42 +135,42 @@ useConnectWebSocket(messageHandler) @@ -112,42 +135,42 @@ useConnectWebSocket(messageHandler)
.project-app-wrapper {
display: flex;
position: absolute;
transition: width 0.2s ease;
height: 100%;
width: 100%;
.left {
width: 400px;
display: flex;
width: 335px;
flex: 0 0 335px;
background-color: #232323;
float: left;
.main-content {
flex: 1;
color: $text-white-basic;
width: 285px;
}
}
.right {
width: 100%;
height: 100%;
.map-wrapper {
flex-grow: 1;
position: relative;
.map-wrapper{
width: 100%;
height: 100%;
}
}
.main-content {
flex: 1;
color: $text-white-basic;
}
.media-wrapper {
position: absolute;
top: 0;
bottom: 0;
z-index: 100;
background: #f6f8fa;
}
.wayline-wrapper {
position: absolute;
top: 0;
bottom: 0;
z-index: 100;
background: #f6f8fa;
padding: 16px;
.media-wrapper,
.task-wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: #f6f8fa;
}
}
}
</style>

22
src/router/index.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { ERouterName } from '/@/types/index'
import CreatePlan from '../pages/page-web/projects/create-plan.vue'
import CreatePlan from '/@/components/task/CreatePlan.vue'
import WaylinePanel from '/@/pages/page-web/projects/wayline.vue'
import DockPanel from '/@/pages/page-web/projects/dock.vue'
import LiveAgora from '/@/components/livestream-agora.vue'
@ -32,6 +32,11 @@ const routes: Array<RouteRecordRaw> = [ @@ -32,6 +32,11 @@ const routes: Array<RouteRecordRaw> = [
path: '/' + ERouterName.DEVICES,
name: ERouterName.DEVICES,
component: () => import('/@/pages/page-web/projects/devices.vue')
},
{
path: '/' + ERouterName.FIRMWARES,
name: ERouterName.FIRMWARES,
component: () => import('../pages/page-web/projects/Firmwares.vue')
}
]
},
@ -42,6 +47,10 @@ const routes: Array<RouteRecordRaw> = [ @@ -42,6 +47,10 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('/@/pages/page-web/projects/workspace.vue'),
redirect: '/' + ERouterName.TSA,
children: [
{
path: '/' + ERouterName.TSA,
component: () => import('/@/pages/page-web/projects/tsa.vue')
},
{
path: '/' + ERouterName.LIVESTREAM,
name: ERouterName.LIVESTREAM,
@ -57,10 +66,6 @@ const routes: Array<RouteRecordRaw> = [ @@ -57,10 +66,6 @@ const routes: Array<RouteRecordRaw> = [
}
]
},
{
path: '/' + ERouterName.TSA,
component: () => import('/@/pages/page-web/projects/tsa.vue')
},
{
path: '/' + ERouterName.LAYER,
name: ERouterName.LAYER,
@ -98,7 +103,12 @@ const routes: Array<RouteRecordRaw> = [ @@ -98,7 +103,12 @@ const routes: Array<RouteRecordRaw> = [
}
]
}
},
{
path: '/' + ERouterName.FLIGHT_AREA,
name: ERouterName.FLIGHT_AREA,
component: () => import('/@/pages/page-web/projects/flight-area.vue')
},
]
},
// pilot

4
src/shims-mqtt.d.ts vendored

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
declare module 'mqtt/dist/mqtt.min' {
import MQTT from 'mqtt'
export = MQTT
}

73
src/store/index.ts

@ -4,8 +4,9 @@ import { EDeviceTypeName } from '../types' @@ -4,8 +4,9 @@ import { EDeviceTypeName } from '../types'
import { Device, DeviceHms, DeviceOsd, DeviceStatus, DockOsd, GatewayOsd, OSDVisible } from '../types/device'
import { getLayers } from '/@/api/layer'
import { LayerType } from '/@/types/mapLayer'
import { ETaskStatus, TaskInfo, WaylineFile } from '/@/types/wayline'
import { WaylineFile } from '/@/types/wayline'
import { DevicesCmdExecuteInfo } from '/@/types/device-cmd'
import { FlightAreaStatus } from '../api/flight-area'
const initStateFunc = () => ({
Layers: [
@ -32,9 +33,11 @@ const initStateFunc = () => ({ @@ -32,9 +33,11 @@ const initStateFunc = () => ({
[key:string]:string
},
drawVisible: false,
coverList: [
] as any,
livestreamOthersVisible: false,
livestreamAgoraVisible: false,
coverMap: {} as {
[key: string]: any[]
},
wsEvent: {
mapElementCreat: {},
mapElementUpdate: {},
@ -65,15 +68,16 @@ const initStateFunc = () => ({ @@ -65,15 +68,16 @@ const initStateFunc = () => ({
[sn: string]: DockOsd
},
currentSn: '',
currentType: ''
currentType: -1
},
osdVisible: {
osdVisible: { // osd 显示设备相关信息
sn: '',
callsign: '',
model: '',
visible: false,
gateway_sn: '',
is_dock: false,
payloads: null
} as OSDVisible,
waylineInfo: {
@ -81,17 +85,14 @@ const initStateFunc = () => ({ @@ -81,17 +85,14 @@ const initStateFunc = () => ({
dockInfo: {
} as Device,
taskProgressInfo: {
} as {
[bid: string]: TaskInfo
},
hmsInfo: {} as {
[sn: string]: DeviceHms[]
},
// 机场指令执行状态信息
devicesCmdExecuteInfo: {
} as DevicesCmdExecuteInfo
} as DevicesCmdExecuteInfo,
mqttState: null as any, // mqtt 实例
clientId: '', // mqtt 连接 唯一客户端id
})
export type RootStateType = ReturnType<typeof initStateFunc>
@ -113,23 +114,36 @@ const mutations: MutationTree<RootStateType> = { @@ -113,23 +114,36 @@ const mutations: MutationTree<RootStateType> = {
state.deviceState.currentType = EDeviceTypeName.Gateway
},
SET_DOCK_INFO (state, info) {
if (Object.keys(info.host).length === 0) {
return
}
if (!state.deviceState.dockInfo[info.sn]) {
state.deviceState.dockInfo[info.sn] = { } as DockOsd
}
state.deviceState.currentSn = info.sn
state.deviceState.currentType = EDeviceTypeName.Dock
const dock = state.deviceState.dockInfo[info.sn]
if (info.host.sdr && state.deviceState.dockInfo[info.sn]) {
dock.sdr = info.host.sdr
dock.media_file_detail = info.host.media_file_detail
if (info.host.mode_code !== undefined) {
dock.basic_osd = info.host
return
}
const sdr = dock?.sdr
const mediaFileDetail = dock?.media_file_detail
state.deviceState.dockInfo[info.sn] = info.host
state.deviceState.dockInfo[info.sn].sdr = sdr
state.deviceState.dockInfo[info.sn].media_file_detail = mediaFileDetail
if (info.host.wireless_link) {
dock.link_osd = info.host
return
}
if (info.host.job_number !== undefined) {
dock.work_osd = info.host
}
},
SET_DRAW_VISIBLE_INFO (state, bool) {
state.drawVisible = bool
},
SET_LIVESTREAM_OTHERS_VISIBLE (state, bool) {
state.livestreamOthersVisible = bool
},
SET_LIVESTREAM_AGORA_VISIBLE (state, bool) {
state.livestreamAgoraVisible = bool
},
SET_MAP_ELEMENT_CREATE (state, info) {
state.wsEvent.mapElementCreat = info
},
@ -160,17 +174,6 @@ const mutations: MutationTree<RootStateType> = { @@ -160,17 +174,6 @@ const mutations: MutationTree<RootStateType> = {
SET_SELECT_DOCK_INFO (state, info) {
state.dockInfo = info
},
SET_FLIGHT_TASK_PROGRESS (state, info) {
const taskInfo: TaskInfo = info.output
if (taskInfo.status === ETaskStatus.OK || taskInfo.status === ETaskStatus.FAILED) {
taskInfo.status = taskInfo.status.concat('(Code:').concat(info.result).concat(')')
setTimeout(() => {
delete state.taskProgressInfo[info.bid]
}, 60000)
}
state.taskProgressInfo[info.bid] = info.output
},
SET_DEVICE_HMS_INFO (state, info) {
const hmsList: Array<DeviceHms> = state.hmsInfo[info.sn]
state.hmsInfo[info.sn] = info.host.concat(hmsList ?? [])
@ -193,7 +196,13 @@ const mutations: MutationTree<RootStateType> = { @@ -193,7 +196,13 @@ const mutations: MutationTree<RootStateType> = {
} else {
state.devicesCmdExecuteInfo[info.sn] = [info]
}
}
},
SET_MQTT_STATE (state, mqttState) {
state.mqttState = mqttState
},
SET_CLIENT_ID (state, clientId) {
state.clientId = clientId
},
}
const actions: ActionTree<RootStateType, RootStateType> = {

42
src/types/airport-tsa.ts

@ -36,3 +36,45 @@ export enum SupplementLightStateEnum { @@ -36,3 +36,45 @@ export enum SupplementLightStateEnum {
Close = 0, // 关闭
Open = 1, // 打开
}
// 机场声光报警状态
export enum AlarmModeEnum {
CLOSE = 0, // 关闭
OPEN = 1, // 开启
}
// 电池保养
export enum BatteryStoreModeEnum {
BATTERY_PLAN_STORE = 1, // 电池计划存储策略
BATTERY_EMERGENCY_STORE = 2, // 电池应急存储策略
}
// 飞行器电池保养
export enum DroneBatteryStateEnum {
NoMaintenanceRequired = 0, // 0-无需保养
MaintenanceRequired = 1, // 1-待保养
MaintenanceInProgress = 2, // 2-正在保养
}
export enum DroneBatteryModeEnum {
CLOSE = 0, // 关闭
OPEN = 1, // 开启
}
// 4g链路连接状态
export enum FourGLinkStateEnum {
CLOSE = 0, // 断开
OPEN = 1, // 连接
}
// Sdr链路连接状态
export enum SdrLinkStateEnum {
CLOSE = 0, // 断开
OPEN = 1, // 连接
}
// 机场的图传链路模式
export enum LinkWorkModeEnum {
SDR = 0, // sdr模式
FourG_FUSION_MODE = 1, // 4G融合模式
}

112
src/types/device-cmd.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { AlarmModeEnum, BatteryStoreModeEnum, DroneBatteryModeEnum, LinkWorkModeEnum } from '/@/types/airport-tsa'
// 机场指令集
export enum DeviceCmd {
// 简单指令
@ -6,6 +7,7 @@ export enum DeviceCmd { @@ -6,6 +7,7 @@ export enum DeviceCmd {
SupplementLightOpen = 'supplement_light_open', // 打开补光灯
SupplementLightClose = 'supplement_light_close', // 关闭补光灯
ReturnHome = 'return_home', // 一键返航
ReturnHomeCancel = 'return_home_cancel', // 取消返航
// 复杂指令
DeviceReboot = 'device_reboot', // 机场重启
DroneOpen = 'drone_open', // 飞行器开机
@ -19,17 +21,43 @@ export enum DeviceCmd { @@ -19,17 +21,43 @@ export enum DeviceCmd {
PutterClose = 'putter_close', // 推杆闭合
ChargeOpen = 'charge_open', // 打开充电
ChargeClose = 'charge_close', // 关闭充电
AlarmStateSwitch = 'alarm_state_switch', // 机场声光报警
BatteryStoreModeSwitch = 'battery_store_mode_switch', // 电池保养
DroneBatteryModeSwitch = 'battery_maintenance_switch', // 飞行器电池保养
SdrWorkModeSwitch = 'sdr_workmode_switch', // 增强图传
}
export type DeviceCmdItemAction = AlarmModeEnum | BatteryStoreModeEnum | DroneBatteryModeEnum | LinkWorkModeEnum
export interface DeviceCmdItem{
label: string, // 标题
status: string, // 当前状态
operateText: string, // 按钮文字
cmdKey: DeviceCmd, // 请求指令
oppositeCmdKey?: DeviceCmd, // 相反状态指令
action?: DeviceCmdItemAction, // 参数
func: string, // 处理函数
loading: boolean // 按钮loading
disabled?: boolean // 按钮disabled
}
export const noDebugCmdList: DeviceCmdItem[] = [
{
label: 'Return Home',
status: '--',
operateText: 'Return Home',
cmdKey: DeviceCmd.ReturnHome,
func: 'returnHome',
loading: false,
},
{
label: 'Return Home Cancel',
status: '--',
operateText: 'Return Home Cancel',
cmdKey: DeviceCmd.ReturnHomeCancel,
func: 'returnHomeCancel',
loading: false,
}
]
// 机场指令
export const cmdList: DeviceCmdItem[] = [
@ -81,14 +109,6 @@ export const cmdList: DeviceCmdItem[] = [ @@ -81,14 +109,6 @@ export const cmdList: DeviceCmdItem[] = [
func: 'chargeStatus',
loading: false,
},
{
label: '一键返航',
status: '--',
operateText: '返航',
cmdKey: DeviceCmd.ReturnHome,
func: 'returnHome',
loading: false,
},
{
label: '机场存储',
status: '--',
@ -114,6 +134,43 @@ export const cmdList: DeviceCmdItem[] = [ @@ -114,6 +134,43 @@ export const cmdList: DeviceCmdItem[] = [
func: 'supplementLightStatus',
loading: false,
},
{
label: '机场声光报警',
status: '关',
operateText: '打开',
cmdKey: DeviceCmd.AlarmStateSwitch,
action: AlarmModeEnum.OPEN,
func: 'alarmState',
loading: false,
},
{
label: '机场电池存储模式',
status: '计划',
operateText: '应急',
cmdKey: DeviceCmd.BatteryStoreModeSwitch,
action: BatteryStoreModeEnum.BATTERY_EMERGENCY_STORE,
func: 'batteryStoreMode',
loading: false,
},
{
label: '飞机电池保养',
status: '--',
operateText: '保养',
cmdKey: DeviceCmd.DroneBatteryModeSwitch,
action: DroneBatteryModeEnum.OPEN,
func: 'droneBatteryMode',
loading: false,
disabled: true,
},
{
label: '4g 增强',
status: '--',
operateText: '开启',
cmdKey: DeviceCmd.SdrWorkModeSwitch,
action: LinkWorkModeEnum.FourG_FUSION_MODE,
func: 'sdrWorkMode',
loading: false,
},
]
export enum DeviceCmdStatusText {
@ -176,6 +233,42 @@ export enum DeviceCmdStatusText { @@ -176,6 +233,42 @@ export enum DeviceCmdStatusText {
DeviceSupplementLightCloseText = '关闭中...',
DeviceSupplementLightCloseFailedText = '开',
DeviceSupplementLightCloseBtnText = '打开',
AlarmStateOpenNormalText = '开',
AlarmStateOpenText = '开启中...',
AlarmStateOpenFailedText = '关',
AlarmStateOpenBtnText = '关闭',
AlarmStateCloseNormalText = '关',
AlarmStateCloseText = '关闭中...',
AlarmStateCloseFailedText = '开',
AlarmStateCloseBtnText = '打开',
BatteryStoreModePlanNormalText = '计划',
BatteryStoreModePlanText = '切换中...',
BatteryStoreModePlanFailedText = '应急',
BatteryStoreModePlanBtnText = '应急',
BatteryStoreModeEmergencyNormalText = '应急',
BatteryStoreModeEmergencyText = '切换中...',
BatteryStoreModeEmergencyFailedText = '计划',
BatteryStoreModeEmergencyBtnText = '计划',
DroneBatteryModeMaintenanceInProgressText = '保养中',
DroneBatteryModeMaintenanceNotNeedText = '无需保养',
DroneBatteryModeMaintenanceNeedText = '需保养',
DroneBatteryModeOpenBtnText = '保养',
DroneBatteryModeCloseBtnText = '关闭保养',
SdrWorkModeFourGOpenNormalText = '开',
SdrWorkModeFourGOpenText = '开启中...',
SdrWorkModeFourGOpenFailedText = '--',
SdrWorkModeFourGOpenBtnText = '关闭',
SdrWorkModeFourGCloseNormalText = '--',
SdrWorkModeFourGCloseText = '关闭中...',
SdrWorkModeFourGCloseFailedText = '开',
SdrWorkModeFourCloseBtnText = '开启',
}
// cmd ws 消息状态
@ -199,6 +292,9 @@ export interface DeviceCmdExecuteInfo { @@ -199,6 +292,9 @@ export interface DeviceCmdExecuteInfo {
percent: number,
step_key: string,
step_result: number
},
ext?: {
rate?: number
}
}
result: number,

38
src/types/device-firmware.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
export interface Firmware {
firmware_id: string
file_name: string
product_version: string
file_size: number
device_name: string[]
username: string
release_note: string
released_time: string
firmware_status: boolean
}
export enum FirmwareStatusEnum {
NONE = 'All',
FALSE = 'Disabled',
TRUE = 'Available'
}
export interface FirmwareQueryParam {
product_version: string
device_name: string
firmware_status: FirmwareStatusEnum
}
export interface FirmwareUploadParam {
device_name: string[]
release_note: string
status: boolean
}
export enum DeviceNameEnum {
DJI_DOCK = 'DJI Dock',
DJI_DOCK2 = 'DJI Dock2',
MATRICE_30 = 'Matrice 30',
MATRICE_30T = 'Matrice 30T',
M3D = 'M3D',
M3TD = 'M3TD',
}

148
src/types/device-setting.ts

@ -0,0 +1,148 @@ @@ -0,0 +1,148 @@
// 夜航灯开关
export enum NightLightsStateEnum {
CLOSE = 0, // 0-关闭
OPEN = 1, // 1-打开
}
// 限远开关
export enum DistanceLimitStatusEnum {
UNSET = 0, // 0-未设置
SET = 1, // 1-已设置
}
export interface DistanceLimitStatus {
state?: DistanceLimitStatusEnum;
distance_limit?: number; // 限远
}
// 避障
export enum ObstacleAvoidanceStatusEnum {
CLOSE = 0, // 0-关闭
OPEN = 1, // 1-开启
}
export interface ObstacleAvoidance {
horizon?: ObstacleAvoidanceStatusEnum;// 水平避障开关
upside?: ObstacleAvoidanceStatusEnum;// 上行方向避障开关
downside?: ObstacleAvoidanceStatusEnum;// 下行方向避障开关
}
// 设备管理设置key
export enum DeviceSettingKeyEnum {
NIGHT_LIGHTS_MODE_SET = 'night_lights_state', // 夜航灯开关
HEIGHT_LIMIT_SET = 'height_limit', // 限高设置
DISTANCE_LIMIT_SET = 'distance_limit_status', // 限远开关
OBSTACLE_AVOIDANCE_HORIZON = 'obstacle_avoidance_horizon', // 水平避障状态
OBSTACLE_AVOIDANCE_UPSIDE = 'obstacle_avoidance_upside', // 上视避障状态
OBSTACLE_AVOIDANCE_DOWNSIDE = 'obstacle_avoidance_downside', // 下视避障状态
}
export type DeviceSettingType = Record<DeviceSettingKeyEnum, any>
export const initDeviceSetting = {
[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET]:
{
label: '飞行器夜航灯',
value: '',
trueValue: NightLightsStateEnum.CLOSE,
editable: false,
popConfirm: {
visible: false,
loading: false,
// content: '为保证飞行器的作业安全,建议打开夜航灯',
label: '飞行器夜航灯',
},
settingKey: DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET,
},
[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET]:
{
label: '限高',
value: '',
trueValue: 120,
editable: false,
popConfirm: {
visible: false,
loading: false,
// content: '限高:20 - 1500m',
// info: '修改限高会影响当前机场的所有作业任务,建议确认作业情况后再进行修改',
label: '限高',
},
settingKey: DeviceSettingKeyEnum.HEIGHT_LIMIT_SET,
},
[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET]:
{
label: '限远',
value: '',
trueValue: DistanceLimitStatusEnum.UNSET,
// info: '限远(15 - 8000m)是约束飞行器相对机场的最大作业距离',
editable: false,
popConfirm: {
visible: false,
loading: false,
// content: '限远 (15- 8000m) 是约束飞行器相对机场的最大作业距离',
// info: '修改限远会影响当前机场的所有作业任务,建议确认作业情况后再进行修改',
label: '限远',
},
settingKey: DeviceSettingKeyEnum.DISTANCE_LIMIT_SET,
},
[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON]:
{
label: '水平避障',
value: '',
trueValue: ObstacleAvoidanceStatusEnum.CLOSE,
// info: '飞行器的避障工作状态显示,可以快速开启/关闭飞行器避障,如需进一步设置请在设备运维页面设置',
editable: false,
popConfirm: {
visible: false,
loading: false,
// content: '飞行器避障是保障飞行作业安全的基础功能,建议保持飞行器避障开启',
label: '水平避障',
},
settingKey: DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON,
},
[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE]:
{
label: '上视避障',
value: '',
trueValue: ObstacleAvoidanceStatusEnum.CLOSE,
// info: '飞行器的避障工作状态显示,可以快速开启/关闭飞行器避障,如需进一步设置请在设备运维页面设置',
editable: false,
popConfirm: {
visible: false,
loading: false,
// content: '飞行器避障是保障飞行作业安全的基础功能,建议保持飞行器避障开启',
label: '上视避障',
},
settingKey: DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE,
},
[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE]:
{
label: '下视避障',
value: '',
trueValue: ObstacleAvoidanceStatusEnum.CLOSE,
// info: '飞行器的避障工作状态显示,可以快速开启/关闭飞行器避障,如需进一步设置请在设备运维页面设置',
editable: false,
popConfirm: {
visible: false,
loading: false,
// content: '飞行器避障是保障飞行作业安全的基础功能,建议保持飞行器避障开启',
label: '下视避障',
},
settingKey: DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE,
},
} as DeviceSettingType
export const initDeviceSettingFormModel = {
nightLightsState: false, // 夜航灯开关
heightLimit: 20, // 限高设置
distanceLimitStatus: { state: false, distanceLimit: 15 }, // 限远开关
obstacleAvoidanceHorizon: false, // 飞行器避障-水平开关设置
obstacleAvoidanceUpside: false, // 飞行器避障-上视开关设置
obstacleAvoidanceDownside: false, // 飞行器避障-下视开关设置
}
export type DeviceSettingFormModel = typeof initDeviceSettingFormModel

274
src/types/device.ts

@ -1,4 +1,7 @@ @@ -1,4 +1,7 @@
import { commonColor } from '/@/utils/color'
import { NightLightsStateEnum, DistanceLimitStatus, ObstacleAvoidance } from './device-setting'
import { AlarmModeEnum, BatteryStoreModeEnum, DroneBatteryStateEnum, FourGLinkStateEnum, SdrLinkStateEnum, LinkWorkModeEnum } from './airport-tsa'
import { CameraMode } from '/@/types/live-stream'
export interface DeviceValue {
key: string; // 'domain-type-subtype'
@ -19,11 +22,9 @@ export enum DOMAIN { @@ -19,11 +22,9 @@ export enum DOMAIN {
export enum DRONE_TYPE {
M30 = 67,
M300 = 60,
Phantom4 = 11,
Phantom4Pro = 18,
Phantom4RTK = 59,
Phantom4Advanced = 27,
Mavic3EnterpriseAdvanced= 77,
M350 = 89,
M3D = 91,
}
// DJI负载类型枚举值
@ -43,6 +44,8 @@ export enum PAYLOAD_TYPE { @@ -43,6 +44,8 @@ export enum PAYLOAD_TYPE {
M3E = 66,
M3T = 67,
M3D = 80,
M3TD = 81,
// UNKNOWN = 65535
}
@ -56,6 +59,7 @@ export enum RC_TYPE { @@ -56,6 +59,7 @@ export enum RC_TYPE {
// DOCK type
export enum DOCK_TYPE {
Dock = 1,
Dock2 = 2,
}
// 设备sub_type 从0升序
@ -75,10 +79,10 @@ export const DEVICE_MODEL_KEY = { @@ -75,10 +79,10 @@ export const DEVICE_MODEL_KEY = {
M3T: `${DOMAIN.DRONE}-${DRONE_TYPE.Mavic3EnterpriseAdvanced}-${DEVICE_SUB_TYPE.ONE}`,
M300: `${DOMAIN.DRONE}-${DRONE_TYPE.M300}-${DEVICE_SUB_TYPE.ZERO}`,
Phantom4: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4}-${DEVICE_SUB_TYPE.ZERO}`,
Phantom4Pro: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4Pro}-${DEVICE_SUB_TYPE.ZERO}`,
Phantom4RTK: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4RTK}-${DEVICE_SUB_TYPE.ZERO}`,
Phantom4Advanced: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4Advanced}-${DEVICE_SUB_TYPE.ZERO}`,
M350: `${DOMAIN.DRONE}-${DRONE_TYPE.M350}-${DEVICE_SUB_TYPE.ZERO}`,
M3D: `${DOMAIN.DRONE}-${DRONE_TYPE.M3D}-${DEVICE_SUB_TYPE.ZERO}`,
M3TD: `${DOMAIN.DRONE}-${DRONE_TYPE.M3D}-${DEVICE_SUB_TYPE.ONE}`,
FPV: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.FPV}-${DEVICE_SUB_TYPE.ZERO}`,
H20: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20}-${DEVICE_SUB_TYPE.ZERO}`,
@ -91,6 +95,8 @@ export const DEVICE_MODEL_KEY = { @@ -91,6 +95,8 @@ export const DEVICE_MODEL_KEY = {
M3ECamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3E}-${DEVICE_SUB_TYPE.ZERO}`,
M3TCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3T}-${DEVICE_SUB_TYPE.ZERO}`,
M3DCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3D}-${DEVICE_SUB_TYPE.ZERO}`,
M3TDCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3TD}-${DEVICE_SUB_TYPE.ZERO}`,
// M3MCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3M}-${DEVICE_SUB_TYPE.ZERO}`,
XT2: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.XT2}-${DEVICE_SUB_TYPE.ZERO}`,
@ -102,6 +108,7 @@ export const DEVICE_MODEL_KEY = { @@ -102,6 +108,7 @@ export const DEVICE_MODEL_KEY = {
RCPlus: `${DOMAIN.RC}-${RC_TYPE.RCPlus}-${DEVICE_SUB_TYPE.ZERO}`,
Dock: `${DOMAIN.DOCK}-${DOCK_TYPE.Dock}-${DEVICE_SUB_TYPE.ZERO}`,
Dock2: `${DOMAIN.DOCK}-${DOCK_TYPE.Dock2}-${DEVICE_SUB_TYPE.ZERO}`,
}
export const DEVICE_NAME = {
@ -112,10 +119,9 @@ export const DEVICE_NAME = { @@ -112,10 +119,9 @@ export const DEVICE_NAME = {
[DEVICE_MODEL_KEY.M3T]: 'Mavic 3T',
// [DEVICE_MODEL_KEY.M3M]: 'Mavic 3M',
[DEVICE_MODEL_KEY.M300]: 'M300 RTK',
[DEVICE_MODEL_KEY.Phantom4]: 'Phantom 4',
[DEVICE_MODEL_KEY.Phantom4Pro]: 'Phantom 4 Pro',
[DEVICE_MODEL_KEY.Phantom4RTK]: 'Phantom 4 RTK',
[DEVICE_MODEL_KEY.Phantom4Advanced]: 'Phantom 4 Advanced',
[DEVICE_MODEL_KEY.M350]: 'M350 RTK',
[DEVICE_MODEL_KEY.M3D]: 'M3D',
[DEVICE_MODEL_KEY.M3TD]: 'M3TD',
// payload
[DEVICE_MODEL_KEY.FPV]: 'FPV',
@ -133,6 +139,8 @@ export const DEVICE_NAME = { @@ -133,6 +139,8 @@ export const DEVICE_NAME = {
[DEVICE_MODEL_KEY.XTS]: 'XTS',
[DEVICE_MODEL_KEY.Z30]: 'Z30',
[DEVICE_MODEL_KEY.DockTopCamera]: 'Dock Camera',
[DEVICE_MODEL_KEY.M3DCamera]: 'M3D Camera',
[DEVICE_MODEL_KEY.M3TDCamera]: 'M3TD Camera',
// rc
[DEVICE_MODEL_KEY.RC]: 'RC',
@ -140,6 +148,37 @@ export const DEVICE_NAME = { @@ -140,6 +148,37 @@ export const DEVICE_NAME = {
// dock
[DEVICE_MODEL_KEY.Dock]: 'Dock',
[DEVICE_MODEL_KEY.Dock2]: 'Dock2',
}
// 控制权
export enum ControlSource {
A = 'A',
B = 'B'
}
export interface PayloadInfo {
index: number,
model: string,
control_source?: ControlSource,
payload_sn?: string,
payload_index?: string,
payload_name?: string,
}
// 设备信息
export interface OnlineDevice {
model: string,
callsign: string,
sn: string,
mode: number,
gateway: {
model: string,
callsign: string,
sn: string,
domain: string,
},
payload: PayloadInfo[]
}
// 固件升级类型
@ -181,8 +220,9 @@ export interface Device { @@ -181,8 +220,9 @@ export interface Device {
bound_time: string,
login_time: string,
children?: Device[],
domain: string,
firmware_progress?: number, // 升级进度
domain: number,
type: number,
firmware_progress?: string, // 升级进度
}
export interface DeviceStatus {
@ -194,7 +234,7 @@ export interface DeviceStatus { @@ -194,7 +234,7 @@ export interface DeviceStatus {
bound_status: boolean,
model: string,
gateway_sn: string,
domain: string
domain: number
}
export interface OSDVisible {
@ -205,6 +245,7 @@ export interface OSDVisible { @@ -205,6 +245,7 @@ export interface OSDVisible {
is_dock: boolean,
gateway_sn: string,
gateway_callsign: string,
payloads: null | PayloadInfo [],
}
export interface GatewayOsd {
@ -213,6 +254,24 @@ export interface GatewayOsd { @@ -213,6 +254,24 @@ export interface GatewayOsd {
longitude: number,
latitude: number,
}
export interface OsdCameraLiveview {
bottom: number,
left: number,
right: number,
top: number,
}
export interface DeviceOsdCamera {
camera_mode: CameraMode,
payload_index: string,
photo_state: number,
record_time: number,
recording_state: number,
remain_photo_num: number,
remain_record_duration: number,
liveview_world_region: OsdCameraLiveview
}
export interface DeviceOsd {
longitude: number,
latitude: number,
@ -235,65 +294,160 @@ export interface DeviceOsd { @@ -235,65 +294,160 @@ export interface DeviceOsd {
landing_power: string,
remain_flight_time: number,
return_home_power: string,
}
},
night_lights_state?: NightLightsStateEnum;// 夜航灯开关
height_limit?: number;// 限高设置
distance_limit_status?: DistanceLimitStatus;// 限远开关
obstacle_avoidance?: ObstacleAvoidance;// 飞行器避障开关设置
cameras?: DeviceOsdCamera[]
}
export interface DockOsd {
media_file_detail: {
remain_upload: number
},
sdr: {
up_quality: string,
down_quality: string,
frequency_band: number,
},
network_state: {
type: number,
export enum NetworkStateTypeEnum {
FOUR_G = 1,
ETHERNET = 2,
}
export enum NetworkStateQualityEnum {
NO_SIGNAL = 0,
BAD = 1,
POOR = 2,
FAIR = 3,
GOOD = 4,
EXCELLENT = 5,
}
export enum RainfallEnum {
NONE = 0,
LIGHT_RAIN = 1,
MODERATE_RAIN = 2,
HEAVY_RAIN = 3,
}
export enum DroneInDockEnum {
OUTSIDE, INSIDE
}
export interface DockBasicOsd {
network_state?: {
type: NetworkStateTypeEnum,
quality: number,
rate: number,
},
drone_in_dock: number,
drone_charge_state: {
drone_charge_state?: {
state: number,
capacity_percent: string,
capacity_percent: number,
},
rainfall: string,
wind_speed: string,
environment_temperature: string,
environment_humidity: string
temperature: string,
humidity: string,
drone_in_dock: boolean,
rainfall: RainfallEnum,
wind_speed: number,
environment_temperature: number,
temperature: number,
humidity: number,
latitude: number,
longitude: number,
height: number,
job_number: number,
acc_time: number,
alternate_land_point?: {
latitude: number,
longitude: number,
height: number,
safe_land_height: number,
is_configured: number
}
first_power_on: number,
positionState: {
gps_number: string,
positionState?: {
gps_number: number,
is_fixed: number,
rtk_number: string,
rtk_number: number,
is_calibration: number,
quality: number,
},
storage: {
storage?: {
total: number,
used: number,
},
electric_supply_voltage: number,
working_voltage: string,
working_current: string,
backup_battery_voltage: number,
mode_code: number,
cover_state: number,
supplement_light_state: number,
emergency_stop_state: number,
air_conditioner?: {
air_conditioner_state: number,
switch_time: number,
}
battery_store_mode?: BatteryStoreModeEnum; // 电池保养(存储)模式
alarm_state?: AlarmModeEnum; // 机场声光报警状态
putter_state: number,
sub_device: {
device_sn: string,
device_model_key: string,
sub_device?: {
device_sn?: string,
device_model_key?: string,
device_online_status: number,
device_paired: number,
},
// live_capacity?: LiveCapacity; // 直播能力
// live_status?: Array<LiveStatus>; // 直播状态
}
export enum DrcStateEnum {
DISCONNECT = 0,
CONNECTING = 1,
CONNECTED = 2
}
export interface DockLinkOsd {
drc_state: DrcStateEnum,
flighttask_prepare_capacity: number,
flighttask_step_code: number,
media_file_detail?: {
remain_upload: number
},
sdr?: {
up_quality: string,
down_quality: string,
frequency_band: number,
},
wireless_link?:{ // 图传链路<会包括4G和sdr信息
dongle_number: number, // dongle 数量
['4g_link_state']: FourGLinkStateEnum, // 4g_link_state
sdr_link_state: SdrLinkStateEnum, // sdr链路连接状态
link_workmode: LinkWorkModeEnum, // 图传链路模式
sdr_quality: number, // sdr信号质量 0-5
['4g_quality']: number, // 4G信号质量 0-5
['4g_freq_band']: number,
['4g_gnd_quality']: number,
['4g_uav_quality']: number,
sdr_freq_band: number,
}
}
export interface MaintainStatus {
state: number,
last_maintain_type: number,
last_maintain_time: number,
last_maintain_work_sorties: number,
}
export interface DockWorkOsd {
job_number: number,
acc_time: number,
activation_time: number,
maintain_status?: {
maintain_status_array: MaintainStatus[]
}
electric_supply_voltage: number,
working_voltage: string,
working_current: string,
backup_battery?: {
voltage: number,
temperature: number,
switch: number,
}
drone_battery_maintenance_info?: { // 飞行器电池保养信息
maintenance_state: DroneBatteryStateEnum, // 保养状态
maintenance_time_left: number, // 电池保养剩余时间(小时)
}
}
export interface DockOsd {
basic_osd: DockBasicOsd,
link_osd: DockLinkOsd,
work_osd: DockWorkOsd
}
export enum EModeCode {
@ -327,24 +481,6 @@ export enum EGear { @@ -327,24 +481,6 @@ export enum EGear {
T
}
export enum EDeviceType {
M30 = '0-67-0' as any,
M30T = '0-67-1' as any,
M300 = '0-60-0' as any,
Z30 = '1-20-0' as any,
XT2 = '1-26-0' as any,
FPV = '1-39-0' as any,
XTS = '1-41-0' as any,
H20 = '1-42-0' as any,
H20T = '1-43-0' as any,
P1 = '1-50-65535' as any,
M30_Camera = '1-52-0' as any,
M30T_Camera = '1-53-0' as any,
H20N = '1-61-0' as any,
DJI_Dock_Camera = '1-165-0' as any,
L1 = '1-90742-0' as any,
}
export enum EDockModeCode {
Disconnected = -1,
Idle,
@ -366,11 +502,11 @@ export interface DeviceHms { @@ -366,11 +502,11 @@ export interface DeviceHms {
message_zh: string,
create_time: string,
update_time: string,
domain: string
domain: number
}
// TODO: 设备拓扑管理优化
// 设备信息
// 设备osd信息
export interface DeviceInfoType {
gateway: GatewayOsd, // 遥控器
dock: DockOsd, // 机场

72
src/types/drc.ts

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
export enum DRC_METHOD {
HEART_BEAT = 'heart_beat',
DRONE_CONTROL = 'drone_control', // 飞行控制-虚拟摇杆
DRONE_EMERGENCY_STOP = 'drone_emergency_stop', // 急停
OSD_INFO_PUSH = 'osd_info_push', // 高频osd信息上报
HSI_INFO_PUSH = 'hsi_info_push', // 避障信息上报
DELAY_TIME_INFO_PUSH = 'delay_info_push', // 图传链路延时信息上报
}
// 手动控制
export interface DroneControlProtocol {
x?: number; // 水平方向速度,正值为A指令 负值为D指令 单位:m/s
y?: number; // 前进后退方向速度,正值为W指令 负值为S指令 单位:m/s
h?: number;// 上下高度值,正值为上升指令 负值为下降指令 单位:m
w?: number; // 机头角速度,正值为顺时针,负值为逆时针 单位:degree/s (web端暂无此设计)
seq?: number; // 从0计时
}
// 低延时osd
export interface DRCOsdInfo {
attitude_head: number;// 飞机姿态head角,单位:度
latitude: number;// 飞机经纬度
longitude: number;
altitude: number;
speed_x: number;
speed_y: number;
speed_z: number;
gimbal_pitch: number;// 云台pitch角
gimbal_roll: number;// 云台roll角
gimbal_yaw: number;// 云台yaw角
}
// 态势感知-HSI
export interface DRCHsiInfo {
up_distance: number;// 上方的障碍物距离,单位:mm
down_distance: number;// 下方的障碍物距离,单位:mm
around_distances: number[]; // 水平方向观察点,分布在[0,360)区间,表示障碍物与飞机距离,单位为mm。 0对应机头方向正前方,顺时针分布,例如0度为机头正前方,90度为飞机正右方
up_enable: boolean; // 上视避障开关状态,true:已开启 false:已关闭
up_work: boolean; // 上视避障工作状态,true:正常工作 false:异常或离线
down_enable: boolean; // 下视避障开关状态,true:已开启 false:已关闭
down_work: boolean; // 下视避障工作状态,true:正常工作 false:异常或离线
left_enable: boolean; // 左视避障开关状态,true:已开启 false:已关闭
left_work: boolean; // 左视避障工作状态,true:正常工作 false:异常或离线
right_enable: boolean; // 右视避障开关状态,true:已开启 false:已关闭
right_work: boolean; // 右视避障工作状态,true:正常工作 false:异常或离线
front_enable: boolean; // 前视避障开关状态,true:已开启 false:已关闭
front_work: boolean; // 前视避障工作状态,true:正常工作 false:异常或离线
back_enable: boolean; // 后视避障开关状态,true:已开启 false:已关闭
back_work: boolean; // 后视避障工作状态,true:正常工作 false:异常或离线
vertical_enable: boolean; // 垂直方向综合开关状态,当本协议中上、下视开关状态均为true时,输出true:已开启,否则输出false:已关闭
vertical_work: boolean; // 垂直方向避障工作状态,当本协议中上、下视工作均为true时,输出true:正常工作,否则输出false:异常或离线
horizontal_enable: boolean; // 水平方向综合开关状态,当本协议中前、后、左、右、开关状态均为true时,输出true:已开启,否则输出false:已关闭
horizontal_work: boolean; // 水平方向避障工作综合状态,当本协议中前、后、左、右视工作均为true时,输出true:正常工作,否则输出false:异常或离线
}
export interface LiveViewDelayItem {
video_id: string;
liveview_delay_time: number;
}
// 链路时延信息
export interface DRCDelayTimeInfo {
sdr_cmd_delay: number; // sdr链路命令延时,单位:ms
liveview_delay_list: LiveViewDelayItem[];
}
export interface DrcResponseInfo {
result: number;
output: {
seq: number
}
}

83
src/types/drone-control.ts

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
import { ControlSource } from './device'
import { ECommanderModeLostAction, ERthMode, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone'
export enum ControlSourceChangeType {
Flight = 1,
Payload = 2,
}
// 控制权变化消息
export interface ControlSourceChangeInfo {
sn: string,
type: ControlSourceChangeType,
control_source: ControlSource
}
// 飞向目标点结果
export interface FlyToPointMessage {
sn: string,
result: number,
message: string,
}
// 一键起飞结果
export interface TakeoffToPointMessage {
sn: string,
result: number,
message: string,
}
// 设备端退出drc模式
export interface DrcModeExitNotifyMessage {
sn: string,
result: number,
message: string,
}
// 飞行控制模式状态
export interface DrcStatusNotifyMessage {
sn: string,
result: number,
message: string,
}
export const WaylineLostControlActionInCommandFlightOptions = [
{ label: 'Continue', value: WaylineLostControlActionInCommandFlight.CONTINUE },
{ label: 'Execute Lost Action', value: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION }
]
export const LostControlActionInCommandFLightOptions = [
{ label: 'Return Home', value: LostControlActionInCommandFLight.RETURN_HOME },
{ label: 'Hover', value: LostControlActionInCommandFLight.HOVER },
{ label: 'Landing', value: LostControlActionInCommandFLight.Land }
]
export const RthModeInCommandFlightOptions = [
{ label: 'Smart Height', value: ERthMode.SMART },
{ label: 'Setting Height', value: ERthMode.SETTING }
]
export const CommanderModeLostActionInCommandFlightOptions = [
{ label: 'Continue', value: ECommanderModeLostAction.CONTINUE },
{ label: 'Execute Lost Action', value: ECommanderModeLostAction.EXEC_LOST_ACTION }
]
export const CommanderFlightModeInCommandFlightOptions = [
{ label: 'Smart Height', value: ERthMode.SMART },
{ label: 'Setting Height', value: ERthMode.SETTING }
]
// 云台重置模式
export enum GimbalResetMode {
Recenter = 0,
Down = 1,
RecenterGimbalPan = 2,
PitchDown = 3,
}
export const GimbalResetModeOptions = [
{ label: 'Gimbal Recenter', value: GimbalResetMode.Recenter },
{ label: 'Gimbal down', value: GimbalResetMode.Down },
{ label: 'Recenter Gimbal Pan', value: GimbalResetMode.RecenterGimbalPan },
{ label: 'Gimbal Pitch Down', value: GimbalResetMode.PitchDown }
]

28
src/types/enums.ts

@ -14,6 +14,8 @@ export enum ERouterName { @@ -14,6 +14,8 @@ export enum ERouterName {
TASK = 'task',
CREATE_PLAN = 'create-plan',
SELECT_PLAN = 'select-plan',
FIRMWARES = 'firmwares',
FLIGHT_AREA = 'flight-area',
PILOT = 'pilot-login',
PILOT_HOME = 'pilot-home',
@ -90,9 +92,13 @@ export enum EBizCode { @@ -90,9 +92,13 @@ export enum EBizCode {
MapElementDelete = 'map_element_delete',
DeviceOnline = 'device_online',
DeviceOffline = 'device_offline',
FlightTaskProgress = 'flighttask_progress',
DeviceHms = 'device_hms',
// 机场任务
FlightTaskProgress = 'flighttask_progress', // 机场任务执行进度
FlightTaskMediaProgress = 'file_upload_callback', // 机场任务媒体上传进度
FlightTaskMediaHighestPriority = 'highest_priority_upload_flighttask_media', // 机场任务媒体优先级上报
// 设备指令
DeviceReboot = 'device_reboot', // 机场重启
DroneOpen = 'drone_open', // 飞行器开机
@ -110,13 +116,25 @@ export enum EBizCode { @@ -110,13 +116,25 @@ export enum EBizCode {
DeviceUpgrade = 'ota_progress', // 设备升级
// 设备日志
DeviceLogUploadProgress = 'fileupload_progress' // 设备日志上传上传
DeviceLogUploadProgress = 'fileupload_progress', // 设备日志上传
// 飞行指令消息
ControlSourceChange = 'control_source_change', // 控制权更新
FlyToPointProgress = 'fly_to_point_progress', // 飞向目标点
TakeoffToPointProgress = 'takeoff_to_point_progress', // 一键起飞
JoystickInvalidNotify = 'joystick_invalid_notify', // 设备端退出drc模式
DrcStatusNotify = 'drc_status_notify', // 飞行控制模式状态
// custom flight area
FlightAreasSyncProgress = 'flight_areas_sync_progress',
FlightAreasDroneLocation = 'flight_areas_drone_location',
FlightAreasUpdate = 'flight_areas_update',
}
export enum EDeviceTypeName {
Aircraft = 'sub-device',
Gateway = 'gateway',
Dock = 'dock',
Aircraft = 0,
Gateway = 2,
Dock = 3,
}
export enum EHmsLevel {

80
src/types/flight-area.ts

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
import { GeojsonCoordinate, GeojsonPolygon } from '../utils/genjson'
export enum EFlightAreaType {
NFZ = 'nfz',
DFENCE = 'dfence',
}
export enum EGeometryType {
CIRCLE = 'Circle',
POLYGON = 'Polygon',
}
export enum EFlightAreaUpdate {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}
export enum ESyncStatus {
WAIT_SYNC = 'wait_sync',
SWITCH_FAIL = 'switch_fail',
SYNCHRONIZING = 'synchronizing',
SYNCHRONIZED = 'synchronized',
FAIL = 'fail',
}
export interface GeojsonCircle {
type: 'Feature'
properties: {
color: string
clampToGround?: boolean
}
geometry: {
type: EGeometryType.CIRCLE
coordinates: GeojsonCoordinate
radius: number
}
}
export interface DroneLocation {
area_distance: number,
area_id: string,
is_in_area: boolean,
}
export interface FlightAreasDroneLocation {
drone_locations: DroneLocation[]
}
export type FlightAreaContent = GeojsonCircle | GeojsonPolygon
export interface FlightAreaUpdate {
operation: EFlightAreaUpdate,
area_id: string,
name: string,
type: EFlightAreaType,
content: FlightAreaContent,
status: boolean,
username: string,
create_time: number,
update_time: number,
}
export interface FlightAreaSyncProgress {
sn: string,
result: number,
status: ESyncStatus,
message: string,
}
export const FlightAreaTypeTitleMap = {
[EFlightAreaType.NFZ]: {
[EGeometryType.CIRCLE]: 'Circular GEO Zone',
[EGeometryType.POLYGON]: 'Polygonal GEO Zone',
},
[EFlightAreaType.DFENCE]: {
[EGeometryType.CIRCLE]: 'Circular Task Area',
[EGeometryType.POLYGON]: 'Polygonal Task Area',
},
}

71
src/types/live-stream.ts

@ -53,3 +53,74 @@ export enum ELiveTypeName { @@ -53,3 +53,74 @@ export enum ELiveTypeName {
RTSP = 'RTSP',
GB28181 = 'GB28181'
}
export enum CameraMode {
Photo = 0, // 拍照
Video = 1, // 录像
}
// 镜头类型
export enum VideoType {
NORMAL = 'normal',
WIDE = 'wide',
ZOOM = 'zoom',
IR = 'ir'
}
// 镜头类型
export enum CameraType {
WIDE = 'wide',
ZOOM = 'zoom',
IR = 'ir'
}
export const CameraTypeOptions = [
{ label: CameraType.WIDE, value: CameraType.WIDE },
{ label: CameraType.ZOOM, value: CameraType.ZOOM },
{ label: CameraType.IR, value: CameraType.IR },
]
export const ZoomCameraTypeOptions = [
{ label: CameraType.ZOOM, value: CameraType.ZOOM },
{ label: CameraType.IR, value: CameraType.IR },
]
export interface VideoListItem {
video_index: string;
video_type: VideoType;
switchable_video_types?: Array<VideoType>;
}
export interface CameraListItem {
available_video_number: number;
camera_index: string;
camera_name: string;
coexist_video_number_max: number;
video_list: VideoListItem[];
// 自定义
switchCamera?: boolean;
content?: string;
// 该camera由哪个控上报的
camera_carrier_sns?: string[];
}
export interface DeviceListItem {
sn: string;
available_video_number: number;
coexist_video_number_max: number;
camera_list: CameraListItem[];
}
// export interface LiveCapacity {
// available_video_number: number;
// coexist_video_number_max: number;
// device_list: DeviceListItem[];
// }
// export interface LiveStatus {
// live_time: number; // 直播时间 该路码流已推流时间 unit: s
// live_trendline: number; // 直播带宽的使用状态 代表直播性能趋势,0-4表示overuse,其中,数值越小,表示overuse程度越大,5表示normal状态,6~10表示underuse,其中,数值越大,表示有更多比例的带宽未能充分利用
// video_id: string; // 直播码流标识符 某路在推视频码流的标识符,格式为 #{uav_sn}/#{camera_id}/#{video_index}
// video_quality: number; // 直播码流的质量 0: 自动, 1: 流畅, 2: 高清, 3: 超清
// error_status?: number; // 设备端当前状态,是错误码,需要匹配到文案上
// }

3
src/types/map-enum.ts

@ -2,5 +2,6 @@ export enum MapDoodleEnum { @@ -2,5 +2,6 @@ export enum MapDoodleEnum {
PIN = 'pin',
POLYLINE = 'polyline',
POLYGON = 'polygon',
Close = 'off'
Close = 'off',
CIRCLE = 'circle',
}

144
src/types/task.ts

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
import { commonColor } from '/@/utils/color'
// 任务类型
export enum TaskType {
Immediate = 0, // 立即执行
Timed = 1, // 单次定时任务
Condition = 2,
}
export const TaskTypeMap = {
[TaskType.Immediate]: 'Immediate',
[TaskType.Timed]: 'Timed',
[TaskType.Condition]: 'Continuous',
}
export const TaskTypeOptions = [
{ value: TaskType.Immediate, label: TaskTypeMap[TaskType.Immediate] },
{ value: TaskType.Timed, label: TaskTypeMap[TaskType.Timed] },
{ value: TaskType.Condition, label: TaskTypeMap[TaskType.Condition] },
]
// 失控动作
export enum OutOfControlAction {
ReturnToHome = 0,
Hover = 1,
Land = 2,
}
export const OutOfControlActionMap = {
[OutOfControlAction.ReturnToHome]: 'Return to Home',
[OutOfControlAction.Hover]: 'Hover',
[OutOfControlAction.Land]: 'Land',
}
export const OutOfControlActionOptions = [
{ value: OutOfControlAction.ReturnToHome, label: OutOfControlActionMap[OutOfControlAction.ReturnToHome] },
{ value: OutOfControlAction.Hover, label: OutOfControlActionMap[OutOfControlAction.Hover] },
{ value: OutOfControlAction.Land, label: OutOfControlActionMap[OutOfControlAction.Land] },
]
// 任务状态
export enum TaskStatus {
Wait = 1, // 待执行
Carrying = 2, // 执行中
Success = 3, // 完成
CanCel = 4, // 取消
Fail = 5, // 失败
Paused = 6, // 暂停
}
export const TaskStatusMap = {
[TaskStatus.Wait]: 'To be performed',
[TaskStatus.Carrying]: 'In progress',
[TaskStatus.Success]: 'Task completed',
[TaskStatus.CanCel]: 'Task canceled',
[TaskStatus.Fail]: 'Task failed',
[TaskStatus.Paused]: 'Paused',
}
export const TaskStatusColor = {
[TaskStatus.Wait]: commonColor.BLUE,
[TaskStatus.Carrying]: commonColor.BLUE,
[TaskStatus.Success]: commonColor.NORMAL,
[TaskStatus.CanCel]: commonColor.FAIL,
[TaskStatus.Fail]: commonColor.FAIL,
[TaskStatus.Paused]: commonColor.BLUE,
}
// 任务执行 ws 消息状态
export enum TaskProgressStatus {
Sent = 'sent', // 已下发
inProgress = 'in_progress', // 执行中
Paused = 'paused', // 暂停
Rejected = 'rejected', // 拒绝
Canceled = 'canceled', // 取消或终止
Timeout = 'timeout', // 超时
Failed = 'failed', // 失败
OK = 'ok', // 上传成功
}
// 任务进度消息
export interface TaskProgressInfo {
bid: string,
output:{
ext: {
current_waypoint_index: number,
media_count: number // 媒体文件
},
progress:{
current_step: number,
percent: number
},
status: TaskProgressStatus
},
result: number,
}
// ws status => log status
export const TaskProgressWsStatusMap = {
[TaskProgressStatus.Sent]: TaskStatus.Carrying,
[TaskProgressStatus.inProgress]: TaskStatus.Carrying,
[TaskProgressStatus.Rejected]: TaskStatus.Fail,
[TaskProgressStatus.OK]: TaskStatus.Success,
[TaskProgressStatus.Failed]: TaskStatus.Fail,
[TaskProgressStatus.Canceled]: TaskStatus.CanCel,
[TaskProgressStatus.Timeout]: TaskStatus.Fail,
[TaskProgressStatus.Paused]: TaskStatus.Paused,
}
// 根据媒体文件上传进度信息,前端自己判断出的状态
export enum MediaStatus { // 媒体上传进度
ToUpload = 1, // 待上传
Uploading = 2, // 上传中
Empty = 3, // 无媒体文件
Success = 4, // 上传成功
}
export const MediaStatusMap = {
[MediaStatus.ToUpload]: 'Waiting to upload',
[MediaStatus.Uploading]: 'Uploading…',
[MediaStatus.Success]: 'Uploaded',
[MediaStatus.Empty]: 'No media files',
}
export const MediaStatusColorMap = {
[MediaStatus.ToUpload]: commonColor.BLUE,
[MediaStatus.Uploading]: commonColor.BLUE,
[MediaStatus.Success]: commonColor.NORMAL,
[MediaStatus.Empty]: commonColor.WARN,
}
// 媒体上传进度消息
export interface MediaStatusProgressInfo {
job_id: string,
media_count: number
uploaded_count: number,
}
// 媒体上传优先级消息
export interface TaskMediaHighestPriorityProgressInfo {
pre_job_id: string,
job_id: string,
}

29
src/types/wayline.ts

@ -1,30 +1,15 @@ @@ -1,30 +1,15 @@
// 航线类型
export enum WaylineType {
NormalWaypointWayline = 0, // 普通航点航线
AccurateReshootingWayline = 1 // 精准复拍航线
}
export interface WaylineFile {
id: string,
name: string,
drone_model_key: any,
payload_model_keys: string[],
template_types: number[],
template_types: WaylineType[],
update_time: number,
user_name: string,
}
export interface TaskExt {
current_waypoint_index: number,
media_count: number,
}
export interface TaskProgress {
current_step: number,
percent: number,
}
export interface TaskInfo {
status: string,
progress: TaskProgress,
ext: TaskExt,
}
export enum ETaskStatus {
OK = 'ok',
FAILED = 'failed'
}

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

Loading…
Cancel
Save