From 4dccbb68399da2d46943b8d613585afb49aa5455 Mon Sep 17 00:00:00 2001
From: sean.zhou <sean.zhou@dji.com>
Date: Fri, 22 Jul 2022 20:48:54 +0800
Subject: [PATCH] V1.1.0 for dock
---
src/api/websocket.ts | 21
src/pages/project-app/home.vue | 57
src/api/media.ts | 17
src/types/wayline.ts | 30
src/pages/project-app/topbar.vue | 97
src/types/device.ts | 198 +
src/components/wayline-panel.vue | 5
src/api/wayline.ts | 52
src/components/livestream-others.vue | 378 +++
src/router/index.ts | 118
src/pages/project-app/projects/devices.vue | 518 ++++
src/pages/project-app/sidebar.vue | 50
src/styles/fonts.scss | 2
src/pages/project-app/projects/layer.vue | 10
src/assets/icons/rc.png | 0
src/styles/common.scss | 14
README.md | 6
src/pages/project-app/index.vue | 164 -
src/types/live-stream.ts | 55
src/assets/icons/cloudapi.png | 0
src/pages/page-pilot/pilot-index.vue | 96
src/api/pilot-bridge.ts | 264 +
src/assets/icons/no-data.png | 0
tsconfig.json | 5
yarn.lock | 15
src/api/manage.ts | 113
src/pages/project-app/projects/workspace.vue | 140 +
src/assets/icons/drone.png | 0
src/assets/icons/dock.png | 0
src/components/GMap.vue | 624 +++++
package-lock.json | 30
src/api/layer.ts | 3
src/utils/common.ts | 8
src/pages/project-app/projects/livestream.vue | 96
src/pages/page-pilot/pilot-home.vue | 601 ++++
src/types/enums.ts | 110
vite.config.ts | 2
src/pages/project-app/projects/task.vue | 38
src/api/http/config.ts | 44
src/components/livestream-agora.vue | 350 +++
src/assets/icons/dji_logo.png | 0
src/components/MediaPanel.vue | 194 +
src/hooks/use-g-map-cover.ts | 10
src/pages/project-app/projects/wayline.vue | 204 +
src/constants/index.ts | 7
src/hooks/use-g-map-tsa.ts | 96
src/pages/project-app/projects/tsa.vue | 458 ++++
src/assets/icons/m30.png | 0
src/pages/project-app/projects/create-plan.vue | 251 ++
src/pages/project-app/projects/members.vue | 169 +
src/store/index.ts | 121 +
src/api/http/request.ts | 40
src/pages/page-pilot/pilot-liveshare.vue | 301 +
src/pages/page-pilot/pilot-media.vue | 96
src/pages/project-app/projects/media.vue | 1
src/pages/project-app/projects/dock.vue | 100
package.json | 20
src/components/TaskPanel.vue | 182 +
src/pages/page-pilot/pilot-bind.vue | 54
59 files changed, 5,835 insertions(+), 800 deletions(-)
diff --git a/README.md b/README.md
index 2071047..faf73ce 100644
--- a/README.md
+++ b/README.md
@@ -6,15 +6,15 @@
## Docker
-If you don't want to install the development environment, you can try deploying with docker. [Click the link to download.](https://terra-sz-hc1pro-cloudapi.oss-cn-shenzhen.aliyuncs.com/c0af9fe0d7eb4f35a8fe5b695e4d0b96/docker/cloud_api_sample_docker_1.0.0.zip)
+If you don't want to install the development environment, you can try deploying with docker. [Click the link to download.](https://terra-sz-hc1pro-cloudapi.oss-cn-shenzhen.aliyuncs.com/c0af9fe0d7eb4f35a8fe5b695e4d0b96/docker/cloud_api_sample_docker.zip)
## Usage
-For more documentation, please visit the [DJI Developer Documentation](https://developer.dji.com/cn/document/209883f1-f2ad-406e-b99c-be7498df7f10).
+For more documentation, please visit the [DJI Developer Documentation](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
## Latest Release
-Cloud API 1.0.0 was released on 21 March 2022. For more information, please visit the [Release Note](https://developer.dji.com/cn/document/87026f9b-e906-4809-9aba-870f569061b5).
+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/).
## License
diff --git a/package-lock.json b/package-lock.json
index 5ae7c05..ccce315 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/icons-vue": "^6.0.1",
"@vitejs/plugin-legacy": "^1.6.2",
- "agora-rtc-sdk-ng": "latest",
+ "agora-rtc-sdk-ng": "^4.12.1",
"ant-design-vue": "^2.2.8",
"axios": "^0.21.1",
"query-string": "^7.0.1",
@@ -1226,9 +1226,17 @@
}
},
"node_modules/agora-rtc-sdk-ng": {
- "version": "4.9.1",
- "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.9.1.tgz",
- "integrity": "sha512-Jogn3TQC7VdA7uZjGYmaAs0XzgYBgGs6nGA67/dQVjqC7kiwAfkQsAuvbevE/qxrVJmLfqtDTNxP40IFvnTlgQ=="
+ "version": "4.12.1",
+ "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.12.1.tgz",
+ "integrity": "sha512-kmc+ZyKDdnY/BN3iAwBs+MSgTX8Zkc6THFSIAXN9WebjZ/F+N/JXItoNEcgQe3MdTABUli6w3pZ+iObnDqVkBw==",
+ "dependencies": {
+ "agora-rte-extension": "^1.0.22"
+ }
+ },
+ "node_modules/agora-rte-extension": {
+ "version": "1.0.23",
+ "resolved": "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.0.23.tgz",
+ "integrity": "sha512-X2cGBg+L5ZJIFU91qvMASvRsBfg1HXTktVG3YROw9wxHsILSI7jgF9R9XraLc3fNX/UjovaYAlUW+hiJe0v6Xw=="
},
"node_modules/ajv": {
"version": "6.12.6",
@@ -8774,9 +8782,17 @@
"requires": {}
},
"agora-rtc-sdk-ng": {
- "version": "4.9.1",
- "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.9.1.tgz",
- "integrity": "sha512-Jogn3TQC7VdA7uZjGYmaAs0XzgYBgGs6nGA67/dQVjqC7kiwAfkQsAuvbevE/qxrVJmLfqtDTNxP40IFvnTlgQ=="
+ "version": "4.12.1",
+ "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.12.1.tgz",
+ "integrity": "sha512-kmc+ZyKDdnY/BN3iAwBs+MSgTX8Zkc6THFSIAXN9WebjZ/F+N/JXItoNEcgQe3MdTABUli6w3pZ+iObnDqVkBw==",
+ "requires": {
+ "agora-rte-extension": "^1.0.22"
+ }
+ },
+ "agora-rte-extension": {
+ "version": "1.0.23",
+ "resolved": "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.0.23.tgz",
+ "integrity": "sha512-X2cGBg+L5ZJIFU91qvMASvRsBfg1HXTktVG3YROw9wxHsILSI7jgF9R9XraLc3fNX/UjovaYAlUW+hiJe0v6Xw=="
},
"ajv": {
"version": "6.12.6",
diff --git a/package.json b/package.json
index 85f213a..89acf7c 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/icons-vue": "^6.0.1",
"@vitejs/plugin-legacy": "^1.6.2",
- "agora-rtc-sdk-ng": "latest",
+ "agora-rtc-sdk-ng": "^4.12.1",
"ant-design-vue": "^2.2.8",
"axios": "^0.21.1",
"query-string": "^7.0.1",
@@ -61,21 +61,39 @@
"agora-rtc-sdk-ng",
"ant-design-vue",
"ant-design-vue/es",
+ "ant-design-vue/es/avatar/style/css",
+ "ant-design-vue/es/breadcrumb/style/css",
"ant-design-vue/es/button/style/css",
+ "ant-design-vue/es/checkbox/style/css",
+ "ant-design-vue/es/col/style/css",
+ "ant-design-vue/es/collapse/style/css",
+ "ant-design-vue/es/date-picker/style/css",
"ant-design-vue/es/divider/style/css",
"ant-design-vue/es/drawer/style/css",
+ "ant-design-vue/es/dropdown/style/css",
+ "ant-design-vue/es/empty/style/css",
"ant-design-vue/es/form/style/css",
"ant-design-vue/es/image/style/css",
"ant-design-vue/es/input/style/css",
+ "ant-design-vue/es/layout/style/css",
+ "ant-design-vue/es/menu/style/css",
"ant-design-vue/es/message/style/css",
"ant-design-vue/es/modal/style/css",
+ "ant-design-vue/es/pagination/style/css",
+ "ant-design-vue/es/popconfirm/style/css",
+ "ant-design-vue/es/popover/style/css",
+ "ant-design-vue/es/progress/style/css",
"ant-design-vue/es/radio/style/css",
+ "ant-design-vue/es/row/style/css",
"ant-design-vue/es/select/style/css",
+ "ant-design-vue/es/space/style/css",
+ "ant-design-vue/es/spin/style/css",
"ant-design-vue/es/switch/style/css",
"ant-design-vue/es/table/style/css",
"ant-design-vue/es/tooltip/style/css",
"ant-design-vue/es/tree/style/css",
"axios",
+ "moment",
"reconnecting-websocket",
"vconsole",
"vue",
diff --git a/src/api/http/config.ts b/src/api/http/config.ts
index 6b12882..fafe023 100644
--- a/src/api/http/config.ts
+++ b/src/api/http/config.ts
@@ -1,20 +1,36 @@
export const CURRENT_CONFIG = {
- 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'
-
- rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
- gb28181Para:
- 'serverIP=Please enter the server ip.&serverPort=Please enter the server port.&serverID=Please enter the server id.' +
- '&agentID=Please enter the agent id.&agentPassword=Please enter the agent password' +
- '&localPort=Please enter the local port.&channel=Please enter the channel.',
- rtspPara: 'userName=Please enter the username.&password=Please enter the password&port=Please enter the port.',
- amapKey: 'Please enter the amap key.',
- agoraAPPID: 'Please enter the agora app id.',
- agoraToken: 'Please enter the agora token.',
- agoraChannel: 'Please enter the agora channel.',
-
+ // 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.
+
+ // 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'
+
+ // 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/'
+ // GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
+ gbServerIp: 'Please enter the server ip.',
+ gbServerPort: 'Please enter the server port.',
+ gbServerId: 'Please enter the server id.',
+ gbAgentId: 'Please enter the agent id',
+ gbPassword: 'Please enter the agent password',
+ gbAgentPort: 'Please enter the local port.',
+ gbAgentChannel: 'Please enter the channel.',
+ // RTSP
+ rtspUserName: 'Please enter the username.',
+ rtspPassword: 'Please enter the password.',
+ rtspPort: '8554',
+ // Agora
+ agoraAPPID: 'Please enter the agora app id.',
+ agoraToken: 'Please enter the agora temporary token.',
+ agoraChannel: 'Please enter the agora channel.',
+
+ // map
+ // You can apply on the AMap website.
+ amapKey: 'Please enter the amap key.',
+
}
diff --git a/src/api/http/request.ts b/src/api/http/request.ts
index d1a0d6f..d668b89 100644
--- a/src/api/http/request.ts
+++ b/src/api/http/request.ts
@@ -1,10 +1,14 @@
import axios from 'axios'
import { uuidv4 } from '/@/utils/uuid'
import { CURRENT_CONFIG } from './config'
+import { message } from 'ant-design-vue'
+import router from '/@/router'
+import { ELocalStorageKey, ERouterName, EUserType } from '/@/types/enums'
export * from './type'
+
const REQUEST_ID = 'X-Request-Id'
function getAuthToken () {
- return localStorage.getItem('x-auth-token')
+ return localStorage.getItem(ELocalStorageKey.Token)
}
const instance = axios.create({
@@ -17,7 +21,7 @@
instance.interceptors.request.use(
config => {
- config.headers['X-Auth-Token'] = getAuthToken()
+ config.headers[ELocalStorageKey.Token] = getAuthToken()
// config.headers[REQUEST_ID] = uuidv4()
config.baseURL = CURRENT_CONFIG.baseURL
return config
@@ -28,10 +32,15 @@
)
instance.interceptors.response.use(
- response => response,
+ response => {
+ console.info('URL: ' + response.config.baseURL + response.config.url, '\nData: ', response.data, '\nResponse:', response)
+ if (response.data.code && response.data.code !== 0) {
+ message.error(response.data.message)
+ }
+ return response
+ },
err => {
const requestId = err?.config?.headers && err?.config?.headers[REQUEST_ID]
- console.info('')
if (requestId) {
console.info(REQUEST_ID, ':', requestId)
}
@@ -46,15 +55,26 @@
}
// @See: https://github.com/axios/axios/issues/383
if (!err.response || !err.response.status) {
- console.log('The network is abnormal, please check the network and try again')
- } else if (err.response?.status !== 200) {
- console.log(`ERROR_CODE: ${err.response?.status}`)
+ message.error('The network is abnormal, please check the backend service and try again')
+ return
}
- if (err.response?.status === 403) {
- // window.location.href = '/'
+ if (err.response?.status !== 200) {
+ message.error(`ERROR_CODE: ${err.response?.status}`)
}
+ // if (err.response?.status === 403) {
+ // // window.location.href = '/'
+ // }
if (err.response?.status === 401) {
- console.log(err.response)
+ console.error(err.response)
+ const flag: number = Number(localStorage.getItem(ELocalStorageKey.Flag))
+ switch (flag) {
+ case EUserType.Web:
+ router.push(ERouterName.PROJECT)
+ break
+ case EUserType.Pilot:
+ router.push(ERouterName.PILOT)
+ break
+ }
}
return Promise.reject(err)
diff --git a/src/api/layer.ts b/src/api/layer.ts
index 1c97320..9501201 100644
--- a/src/api/layer.ts
+++ b/src/api/layer.ts
@@ -1,8 +1,9 @@
+import { ELocalStorageKey } from '../types/enums'
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { mapLayers } from '/@/constants/mock-layers'
import { elementGroupsReq, PostElementsBody, PutElementsBody } from '/@/types/mapLayer'
const PREFIX = '/map/api/v1'
-const workspace_id = localStorage.getItem('workspace-id')
+const workspace_id = localStorage.getItem(ELocalStorageKey.WorkspaceId)
type UnknownResponse = Promise<IWorkspaceResponse<unknown>>
// get elements group
// export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
diff --git a/src/api/manage.ts b/src/api/manage.ts
index 305fcb0..1fe6b06 100644
--- a/src/api/manage.ts
+++ b/src/api/manage.ts
@@ -1,12 +1,31 @@
-import request, { IWorkspaceResponse } from '/@/api/http/request'
+import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/manage/api/v1'
// login
-interface loginBody {
+export interface LoginBody {
username: string,
- password: string
+ password: string,
+ flag: number,
}
-export const login = async function (body: loginBody): Promise<IWorkspaceResponse<any>> {
+export interface BindBody {
+ device_sn: string,
+ user_id: string,
+ workspace_id: string,
+ domain?: string
+}
+export interface HmsQueryBody {
+ sns: string[],
+ children_sn: string,
+ device_sn: string,
+ language: string,
+ level: number | string,
+ begin_time: number,
+ end_time: number,
+ message: string,
+ domain: string,
+}
+
+export const login = async function (body: LoginBody): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/login`
const result = await request.post(url, body)
return result.data
@@ -20,23 +39,23 @@
}
// Get Platform Info
-export const getPlatformInfo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
+export const getPlatformInfo = async function (): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/current`
- const result = await request.get(url, body)
+ const result = await request.get(url)
return result.data
}
// Get User Info
-export const getUserInfo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
+export const getUserInfo = async function (): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/users/current`
- const result = await request.get(url, body)
+ const result = await request.get(url)
return result.data
}
// Get Device Topo
-export const getDeviceTopo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
- const url = `${HTTP_PREFIX}/devices/devices`
- const result = await request.get(url, body)
+export const getDeviceTopo = async function (workspace_id: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices`
+ const result = await request.get(url)
return result.data
}
@@ -60,3 +79,75 @@
const result = await request.post(url, body)
return result.data
}
+// Update Quality
+export const setLivestreamQuality = async function (body: {}): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/live/streams/update`
+ const result = await request.post(url, body)
+ return result.data
+}
+
+export const getAllUsersInfo = async function (wid: string, body: IPage): Promise<CommonListResponse<any>> {
+ const url = `${HTTP_PREFIX}/users/${wid}/users?&page=${body.page}&page_size=${body.page_size}`
+ const result = await request.get(url)
+ return result.data
+}
+
+export const updateUserInfo = async function (wid: string, user_id: string, body: {}): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/users/${wid}/users/${user_id}`
+ const result = await request.put(url, body)
+ return result.data
+}
+
+export const bindDevice = async function (body: BindBody): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${body.device_sn}/binding`
+ const result = await request.post(url, body)
+ return result.data
+}
+
+export const unbindDevice = async function (device_sn: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${device_sn}/unbinding`
+ const result = await request.delete(url)
+ return result.data
+}
+
+export const getDeviceBySn = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
+ const result = await request.get(url)
+ return result.data
+}
+
+export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}&domain=${domain}`
+ const result = await request.get(url)
+ return result.data
+}
+
+export const updateDevice = async function (body: {}, workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
+ const result = await request.put(url, body)
+ return result.data
+}
+
+export const getUnreadDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
+ const result = await request.get(url)
+ return result.data
+}
+
+export const updateDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
+ const result = await request.put(url)
+ return result.data
+}
+
+export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: string, pagination: IPage): Promise<IListWorkspaceResponse<any>> {
+ let url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms?page=${pagination.page}&pageSize=${pagination.page_size}` +
+ `&level=${body.level ?? ''}&beginTime=${body.begin_time ?? ''}&endTime=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}`
+ body.sns.forEach((sn: string) => {
+ if (sn !== '') {
+ url = url.concat(`&deviceSn=${sn}`)
+ }
+ })
+ const result = await request.get(url)
+ return result.data
+}
\ No newline at end of file
diff --git a/src/api/media.ts b/src/api/media.ts
index 8cdec5c..9cf48d9 100644
--- a/src/api/media.ts
+++ b/src/api/media.ts
@@ -1,9 +1,18 @@
-import request from '/@/api/http/request'
+import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/media/api/v1'
// Get Media Files
-export const getMediaFiles = async function (wid: string, body: {}): Promise<any> {
- const url = `${HTTP_PREFIX}/files/${wid}/files`
- const result = await request.get(url, body)
+export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}`
+ const result = await request.get(url)
return result.data
}
+// Download Media File
+export const downloadMediaFile = async function (workspaceId: string, fingerprint: string): Promise<any> {
+ const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fingerprint}/url`
+ const result = await request.get(url, { responseType: 'blob' })
+ if (result.data.code) {
+ return result.data
+ }
+ return result
+}
diff --git a/src/api/pilot-bridge.ts b/src/api/pilot-bridge.ts
index a126522..61bdf4e 100644
--- a/src/api/pilot-bridge.ts
+++ b/src/api/pilot-bridge.ts
@@ -1,95 +1,151 @@
+import { message } from 'ant-design-vue'
+import { EComponentName, EPhotoType, ERouterName } from '../types'
+import { CURRENT_CONFIG } from './http/config'
+import { EVideoPublishType, LiveStreamStatus } from '../types/live-stream'
import { getRoot } from '/@/root'
const root = getRoot()
-const components = new Map()
-
+export const components = new Map()
declare let window:any
-
interface JsResponse{
code:number,
message:string,
- data:{}
+ data:any
+}
+
+export interface ThingParam {
+ host: string,
+ username: string,
+ password: string,
+ connectCallback: string
+}
+
+export interface LiveshareParam {
+ videoPublishType: string, // video-on-demand、video-by-manual、video-demand-aux-manual
+ statusCallback: string
+}
+
+export interface MapParam {
+ userName: string,
+ elementPreName: string
+}
+
+export interface WsParam {
+ host: string,
+ token: string,
+ connectCallback: string
+}
+
+export interface ApiParam {
+ host: string,
+ token: string
+}
+
+export interface MediaParam {
+ autoUploadPhoto: boolean, // 是否自动上传图片, 非必需
+ autoUploadPhotoType: number, // 自动上传的照片类型,0:原图, 1:缩略图, 非必需
+ autoUploadVideo: boolean // 是否自动上传视频, 非必需
+}
+
+function returnBool (response: string): boolean {
+ const res: JsResponse = JSON.parse(response)
+ const isError = errorHint(res)
+ if (JSON.stringify(res.data) !== '{}') {
+ return isError && res.data
+ }
+ return isError
+}
+
+function returnString (response: string): string {
+ const res: JsResponse = JSON.parse(response)
+ return errorHint(res) ? res.data : ''
+}
+
+function returnNumber (response: string): number {
+ const res: JsResponse = JSON.parse(response)
+ return errorHint(res) ? res.data : -1
+}
+
+function errorHint (response: JsResponse): boolean {
+ if (response.code !== 0) {
+ message.error(response.message)
+ console.error(response.message)
+ return false
+ }
+ return true
}
export default {
- init () {
- components.set('thing', {
+ init (): Map<EComponentName, any> {
+ const thingParam: ThingParam = {
host: '',
connectCallback: '',
username: '',
password: ''
- })
- components.set('liveshare', {
- videoPublishType: 'video-demand-aux-manual', // video-on-demand、video-by-manual、video-demand-aux-manual
- statusCallback: ''
- })
- components.set('map', {
+ }
+ components.set(EComponentName.Thing, thingParam)
+ const liveshareParam: LiveshareParam = {
+ videoPublishType: EVideoPublishType.VideoDemandAuxManual,
+ statusCallback: 'liveStatusCallback'
+ }
+ components.set(EComponentName.Liveshare, liveshareParam)
+ const mapParam: MapParam = {
userName: '',
- elementPreName: ''
- })
- components.set('ws', {
- host: '',
+ elementPreName: 'PILOT'
+ }
+ components.set(EComponentName.Map, mapParam)
+ const wsParam: WsParam = {
+ host: CURRENT_CONFIG.websocketURL,
token: '',
- connectCallback: ''
- })
- components.set('api', {
+ connectCallback: 'wsConnectCallback'
+ }
+ components.set(EComponentName.Ws, wsParam)
+ const apiParam: ApiParam = {
host: '',
token: ''
- })
- components.set('tsa', {
- })
- components.set('media', {
- autoUploadPhoto: true, // 是否自动上传图片, 非必需
- autoUploadPhotoType: 1, // 自动上传的照片类型,0:原图, 1:缩略图, 非必需
- autoUploadVideo: true // 是否自动上传视频, 非必需
- })
- components.set('mission', {
- })
+ }
+ components.set(EComponentName.Api, apiParam)
+ components.set(EComponentName.Tsa, {})
+ const mediaParam: MediaParam = {
+ autoUploadPhoto: true,
+ autoUploadPhotoType: EPhotoType.Preview,
+ autoUploadVideo: true
+ }
+ components.set(EComponentName.Media, mediaParam)
+ components.set(EComponentName.Mission, {})
+
+ return components
},
- getComponentParam (key:string) {
+
+ getComponentParam (key:EComponentName): any {
return components.get(key)
},
- setComponentParam (key:string, value:any) {
+ setComponentParam (key:EComponentName, value:any) {
components.set(key, value)
},
loadComponent (name:string, param:any):string {
- return window.djiBridge.platformLoadComponent(name, JSON.stringify(param))
+ return returnString(window.djiBridge.platformLoadComponent(name, JSON.stringify(param)))
},
unloadComponent (name:string) :string {
- return window.djiBridge.platformUnloadComponent(name)
+ return returnString(window.djiBridge.platformUnloadComponent(name))
},
- isComponentLoaded (module:string):string {
- return window.djiBridge.platformIsComponentLoaded(module)
+ isComponentLoaded (module:string): boolean {
+ return returnBool(window.djiBridge.platformIsComponentLoaded(module))
},
setWorkspaceId (uuid:string):string {
- return window.djiBridge.platformSetWorkspaceId(uuid)
+ return returnString(window.djiBridge.platformSetWorkspaceId(uuid))
},
- setPlatformMessage (platformName:string, title:string, desc:string):string {
- return window.djiBridge.platformSetInformation(platformName, title, desc)
+ setPlatformMessage (platformName:string, title:string, desc:string): boolean {
+ return returnBool(window.djiBridge.platformSetInformation(platformName, title, desc))
},
getRemoteControllerSN () :string {
- return window.djiBridge.platformGetRemoteControllerSN()
+ return returnString(window.djiBridge.platformGetRemoteControllerSN())
},
getAircraftSN ():string {
- return window.djiBridge.platformGetAircraftSN()
+ return returnString(window.djiBridge.platformGetAircraftSN())
},
stopwebview ():string {
- return window.djiBridge.platformStopSelf()
- },
- getToken () :string {
- const res:string = this.isComponentLoaded('api')
- const resObj = JSON.parse(res)
- console.log('api load status:', resObj)
- if (resObj.data === true) {
- const tokenRes = JSON.parse(window.djiBridge.apiGetToken())
- return tokenRes.data
- } else {
- console.warn('warning: not api component loaded!!')
- return ''
- }
- },
- setToken (token:string):string {
- return window.djiBridge.apiSetToken(token)
+ return returnString(window.djiBridge.platformStopSelf())
},
setLogEncryptKey (key:string):string {
return window.djiBridge.platformSetLogEncryptKey(key)
@@ -98,14 +154,42 @@
return window.djiBridge.platformClearLogEncryptKey()
},
getLogPath ():string {
- return window.djiBridge.platformGetLogPath()
+ return returnString(window.djiBridge.platformGetLogPath())
},
- platformVerifyLicense (appId:string, appKey:string, appLicense:string):string {
- return window.djiBridge.platformVerifyLicense(appId, appKey, appLicense)
+ platformVerifyLicense (appId:string, appKey:string, appLicense:string): boolean {
+ return returnBool(window.djiBridge.platformVerifyLicense(appId, appKey, appLicense))
},
- isPlatformVerifySuccess ():string {
- return window.djiBridge.platformIsVerified()
+ isPlatformVerifySuccess (): boolean {
+ return returnBool(window.djiBridge.platformIsVerified())
},
+ isAppInstalled (pkgName: string): boolean {
+ return returnBool(window.djiBridge.platformIsAppInstalled(pkgName))
+ },
+ getVersion (): string {
+ return window.djiBridge.platformGetVersion()
+ },
+
+ // thing
+ thingGetConnectState (): boolean {
+ return returnBool(window.djiBridge.thingGetConnectState())
+ },
+
+ thingGetConfigs (): ThingParam {
+ const thingParam = JSON.parse(window.djiBridge.thingGetConfigs())
+ return thingParam.code === 0 ? JSON.parse(thingParam.data) : {}
+ },
+
+ // api
+ getToken () : string {
+ return returnString(window.djiBridge.apiGetToken())
+ },
+ setToken (token:string):string {
+ return returnString(window.djiBridge.apiSetToken(token))
+ },
+ getHost (): string {
+ return returnString(window.djiBridge.apiGetHost())
+ },
+
// liveshare
/**
*
@@ -114,8 +198,8 @@
* video-by-manual:手动点播,配置好直播类型参数之后,在图传页面可修改直播参数,停止直播
* video-demand-aux-manual: 混合模式,支持服务器点播,以及图传页面修改直播参数,停止直播
*/
- setVideoPublishType (type:string):string {
- return window.djiBridge.liveshareSetVideoPublishType(type)
+ setVideoPublishType (type:string): boolean {
+ return returnBool(window.djiBridge.liveshareSetVideoPublishType(type))
},
/**
@@ -123,8 +207,8 @@
* @returns
* type: liveshare type, 0:unknown, 1:agora, 2:rtmp, 3:rtsp, 4:gb28181
*/
- getLiveshareConfig () {
- return window.djiBridge.liveshareGetConfig()
+ getLiveshareConfig (): string {
+ return returnString(window.djiBridge.liveshareGetConfig())
},
setLiveshareConfig (type:number, params:string):string {
@@ -134,50 +218,66 @@
setLiveshareStatusCallback (callbackFunc:string) :string {
return window.djiBridge.liveshareSetStatusCallback(callbackFunc)
},
- getLiveshareStatus () {
- return window.djiBridge.liveshareGetStatus()
+ getLiveshareStatus (): LiveStreamStatus {
+ return JSON.parse(JSON.parse(window.djiBridge.liveshareGetStatus()).data)
},
- startLiveshare ():string {
- return window.djiBridge.liveshareStartLive()
+ startLiveshare (): boolean {
+ return returnBool(window.djiBridge.liveshareStartLive())
},
- stopLiveshare ():string {
- return window.djiBridge.liveshareStopLive()
+ stopLiveshare (): boolean {
+ return returnBool(window.djiBridge.liveshareStopLive())
+ },
+ // WebSocket
+ wsGetConnectState (): boolean {
+ return returnBool(window.djiBridge.wsGetConnectState())
+ },
+ wsConnect (host: string, token: string, callback: string): string {
+ return window.djiBridge.wsConnect(host, token, callback)
+ },
+ wsDisconnect (): string {
+ return window.djiBridge.wsConnect()
+ },
+ wsSend (message: string): string {
+ return window.djiBridge.wsSend(message)
},
// media
setAutoUploadPhoto (auto:boolean):string {
return window.djiBridge.mediaSetAutoUploadPhoto(auto)
},
- getAutoUploadPhoto () {
- return window.djiBridge.mediaGetAutoUploadPhoto()
+ getAutoUploadPhoto (): boolean {
+ return returnBool(window.djiBridge.mediaGetAutoUploadPhoto())
},
setUploadPhotoType (type:number):string {
return window.djiBridge.mediaSetUploadPhotoType(type)
},
- getUploadPhotoType () {
- return window.djiBridge.mediaGetUploadPhotoType()
+ getUploadPhotoType (): number {
+ return returnNumber(window.djiBridge.mediaGetUploadPhotoType())
},
setAutoUploadVideo (auto:boolean):string {
return window.djiBridge.mediaSetAutoUploadVideo(auto)
},
- getAutoUploadVideo () {
- return window.djiBridge.mediaGetAutoUploadVideo()
+ getAutoUploadVideo (): boolean {
+ return returnBool(window.djiBridge.mediaGetAutoUploadVideo())
},
setDownloadOwner (rcIndex:number):string {
return window.djiBridge.mediaSetDownloadOwner(rcIndex)
},
- getDownloadOwner () {
- return window.djiBridge.mediaGetDownloadOwner()
+ getDownloadOwner (): number {
+ return returnNumber(window.djiBridge.mediaGetDownloadOwner())
},
onBackClickReg () {
window.djiBridge.onBackClick = () => {
- if (root.$router.currentRoute.value.path === '/pilot-home') {
- console.log(root.$router.currentRoute.value.path)
+ if (root.$router.currentRoute.value.path === '/' + ERouterName.PILOT_HOME) {
return false
} else {
- console.log(root.$router.currentRoute.value.path)
history.go(-1)
return true
}
}
+ },
+ onStopPlatform () {
+ window.djiBridge.onStopPlatform = () => {
+ localStorage.clear()
+ }
}
}
diff --git a/src/api/wayline.ts b/src/api/wayline.ts
index 2963b6c..87c9c31 100644
--- a/src/api/wayline.ts
+++ b/src/api/wayline.ts
@@ -1,9 +1,55 @@
-import request from '/@/api/http/request'
+import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1'
+export interface CreatePlan {
+ name: string,
+ file_id: string,
+ dock_sn: string,
+ immediate: boolean,
+ type: string,
+}
+
// Get Wayline Files
-export const getWaylineFiles = async function (wid: string, body: {}): Promise<any> {
- const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?` + 'order_by=' + body.order_by + '&page=' + body.page + '&page_size=' + body.page_size
+export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?order_by=${body.order_by}&page=${body.page}&page_size=${body.page_size}`
const result = await request.get(url)
return result.data
}
+
+// Download Wayline File
+export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> {
+ const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url`
+ const result = await request.get(url, { responseType: 'blob' })
+ if (result.data.code) {
+ return result.data
+ }
+ return result
+}
+
+// Delete Wayline File
+export const deleteWaylineFile = async function (workspaceId: string, waylineId: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}`
+ const result = await request.delete(url)
+ return result.data
+}
+
+// Create Wayline Job
+export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks`
+ const result = await request.post(url, plan)
+ return result.data
+}
+
+// Get Wayline Jobs
+export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}`
+ const result = await request.get(url)
+ return result.data
+}
+
+// Execute Wayline Job
+export const executeWaylineJobs = async function (workspaceId: string, plan_id: string): Promise<IWorkspaceResponse<any>> {
+ const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${plan_id}`
+ const result = await request.post(url)
+ return result.data
+}
diff --git a/src/api/websocket.ts b/src/api/websocket.ts
index 4613457..0d5a08f 100644
--- a/src/api/websocket.ts
+++ b/src/api/websocket.ts
@@ -1,14 +1,14 @@
import ReconnectingWebSocket from 'reconnecting-websocket'
+import { ELocalStorageKey } from '../types/enums'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
-let socket = {}
+let socket: ReconnectingWebSocket
export default {
- init (getMsgFunc) {
- const token = localStorage.getItem('x-auth-token')
- const wspath =
- config.websocketURL + '?x-auth-token=' + escape(token)
- socket = new ReconnectingWebSocket(wspath)
+ init (getMsgFunc: any) {
+ const token: string = localStorage.getItem(ELocalStorageKey.Token)!
+ const wspath = config.websocketURL + '?x-auth-token=' + encodeURI(token)
+ socket = new ReconnectingWebSocket(wspath, '', { maxRetries: 5 })
socket.onopen = this.onOpen
socket.onerror = this.onError
socket.onmessage = getMsgFunc
@@ -18,13 +18,16 @@
onOpen () {
console.log('ws opened')
},
- onError (err) {
+ onError (err: any) {
console.error(err)
},
onClose () {
console.log('ws closed')
},
- sendMsg (data) {
- this.socket.send(data)
+ sendMsg (data: any) {
+ socket.send(data)
+ },
+ close () {
+ socket.close()
}
}
diff --git a/src/assets/icons/cloudapi.png b/src/assets/icons/cloudapi.png
new file mode 100644
index 0000000..c4e398b
--- /dev/null
+++ b/src/assets/icons/cloudapi.png
Binary files differ
diff --git a/src/assets/icons/dji_logo.png b/src/assets/icons/dji_logo.png
new file mode 100644
index 0000000..c6d0e22
--- /dev/null
+++ b/src/assets/icons/dji_logo.png
Binary files differ
diff --git a/src/assets/icons/dock.png b/src/assets/icons/dock.png
new file mode 100644
index 0000000..7c0958b
--- /dev/null
+++ b/src/assets/icons/dock.png
Binary files differ
diff --git a/src/assets/icons/drone.png b/src/assets/icons/drone.png
new file mode 100644
index 0000000..cf5f3b2
--- /dev/null
+++ b/src/assets/icons/drone.png
Binary files differ
diff --git a/src/assets/icons/m30.png b/src/assets/icons/m30.png
new file mode 100644
index 0000000..a5932c7
--- /dev/null
+++ b/src/assets/icons/m30.png
Binary files differ
diff --git a/src/assets/icons/no-data.png b/src/assets/icons/no-data.png
new file mode 100644
index 0000000..36a8fd8
--- /dev/null
+++ b/src/assets/icons/no-data.png
Binary files differ
diff --git a/src/assets/icons/rc.png b/src/assets/icons/rc.png
new file mode 100644
index 0000000..a4ca3c5
--- /dev/null
+++ b/src/assets/icons/rc.png
Binary files differ
diff --git a/src/components/GMap.vue b/src/components/GMap.vue
index 8121e47..80d540b 100644
--- a/src/components/GMap.vue
+++ b/src/components/GMap.vue
@@ -5,18 +5,377 @@
class="g-action-panle"
:style="{ right: drawVisible ? '316px' : '16px' }"
>
- <div class="g-action-item" @click="draw('pin', true)">
- <a-button type="primary">PIN</a-button>
+ <div :class="state.currentType === 'pin' ? 'g-action-item selection' : 'g-action-item'" @click="draw('pin', true)">
+ <a><a-image :src="pin" :preview="false" /></a>
</div>
- <div class="g-action-item" @click="draw('polyline', true)">
- <a-button type="primary">Line</a-button>
+ <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="g-action-item" @click="draw('polygon', true)">
- <a-button type="primary">Poly</a-button>
+ <div :class="state.currentType === 'polygon' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polygon', true)">
+ <a><BorderOutlined class="fz18" /></a>
</div>
<div v-if="mouseMode" class="g-action-item" @click="draw('off', false)">
- <a-button type="primary" danger>X</a-button>
+ <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%;">
+ <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;">
+ <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>
+ <span>{{ osdVisible.model }}</span>
+ </div>
+ </a-tooltip>
+ </div>
+ <div class="osd">
+ <a-row>
+ <a-col span="16" :style="deviceInfo.device.mode_code === EModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">{{ EModeCode[deviceInfo.device.mode_code] }}</a-col>
+ </a-row>
+ <a-row>
+ <a-col span="6">
+ <a-tooltip title="Signal strength">
+ <span>HD</span>
+ <span class="ml10">{{ deviceInfo.gateway?.transmission_signal_quality }}</span>
+ </a-tooltip>
+ </a-col>
+ <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>
+ </a-tooltip>
+ </a-col>
+
+ <a-col span="6">
+ <a-tooltip title="Drone Battery Level">
+ <span><ThunderboltOutlined class="fz14"/></span>
+ <span class="ml10">{{ deviceInfo.device.battery.capacity_percent !== str ? deviceInfo.device.battery.capacity_percent + ' %' : deviceInfo.device.battery.capacity_percent }}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-tooltip title="RTK Fixed">
+ <a-col span="6" class="flex-row flex-align-center flex-justify-start">
+ <span>Fixed</span>
+ <span class="ml10 circle" :style="deviceInfo.device.position_state.is_fixed === 1 ? 'backgroud: rgb(25,190,107);' : ' background: red;'"></span>
+ </a-col>
+ </a-tooltip>
+ <a-col span="6">
+ <a-tooltip title="GPS">
+ <span>GPS</span>
+ <span class="ml10">{{ deviceInfo.device.position_state.gps_number }}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="RTK">
+ <span><TrademarkOutlined class="fz14"/></span>
+ <span class="ml10">{{ deviceInfo.device.position_state.rtk_number }}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-col span="6">
+ <a-tooltip title="Flight Mode">
+ <span><ControlOutlined class="fz16" /></span>
+ <span class="ml10">{{ EGear[deviceInfo.device.gear] }}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Altitude above sea level">
+ <span>ASL</span>
+ <span class="ml10">{{ deviceInfo.device.height === str ? str : deviceInfo.device.height.toFixed(2) + ' m'}}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Altitude above takeoff level">
+ <span>ALT</span>
+ <span class="ml10">{{ deviceInfo.device.elevation === str ? str : deviceInfo.device.elevation.toFixed(2) + ' m' }}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Distance to Home Point">
+ <span>H</span>
+ <span class="ml10">{{ deviceInfo.device.home_distance === str ? str : deviceInfo.device.home_distance.toFixed(2) + ' m' }}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-col span="6">
+ <a-tooltip title="Horizontal Speed">
+ <span>H.S</span>
+ <span class="ml10">{{ deviceInfo.device.horizontal_speed === str ? str : deviceInfo.device.horizontal_speed.toFixed(2) + ' m/s'}}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Vertical Speed">
+ <span>V.S</span>
+ <span class="ml10">{{ deviceInfo.device.vertical_speed === str ? str : deviceInfo.device.vertical_speed.toFixed(2) + ' m/s'}}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Wind Speed">
+ <span>W.S</span>
+ <span class="ml10">{{ deviceInfo.device.wind_speed === str ? str : (deviceInfo.device.wind_speed / 10).toFixed(2) + ' m/s'}}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ </div>
+ </div>
+ <div class="battery-slide" v-if="deviceInfo.device.battery.remain_flight_time !== 0">
+ <div style="background: #535759;" class="width-100"></div>
+ <div class="capacity-percent" :style="{ width: deviceInfo.device.battery.capacity_percent + '%'}"></div>
+ <div class="return-home" :style="{ width: deviceInfo.device.battery.return_home_power + '%'}"></div>
+ <div class="landing" :style="{ width: deviceInfo.device.battery.landing_power + '%'}"></div>
+ <div class="white-point" :style="{ left: deviceInfo.device.battery.landing_power + '%'}"></div>
+ <div class="battery" :style="{ left: deviceInfo.device.battery.capacity_percent + '%' }">
+ {{ Math.floor(deviceInfo.device.battery.remain_flight_time / 60) }}:
+ {{ 10 > (deviceInfo.device.battery.remain_flight_time % 60) ? '0' : ''}}{{deviceInfo.device.battery.remain_flight_time % 60 }}
+ </div>
+ </div>
+ </div>
+ <div v-if="osdVisible.visible && osdVisible.is_dock" class="osd-panel fz12" style="height: 280px;">
+ <div class="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>
+ <div style="height: 45%; border-bottom: 1px solid #515151;">
+ <div class="flex-column flex-align-center flex-justify-center" style="float: left; width: 60px; height: 100%; background: #2d2d2d;">
+ <a-tooltip :title="osdVisible.model">
+ <div class="flex-column flex-align-center flex-justify-center" style="width: 90%;">
+ <span><RobotFilled style="font-size: 48px;"/></span>
+ <span class="mt10">Dock</span>
+ </div>
+ </a-tooltip>
+ </div>
+ <div class="osd">
+ <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-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>
+ </a-tooltip>
+ </a-col>
+ <a-col span="12">
+ <a-tooltip title="Last login">
+ <span><FieldTimeOutlined /></span>
+ <span class="ml10">{{ new Date(deviceInfo.dock.first_power_on).toLocaleString() }}
+ </span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-col span="12">
+ <a-tooltip title="Network State">
+ <span :style="deviceInfo.dock.network_state.quality === 2 ? 'color: #00ee8b' :
+ deviceInfo.dock.network_state.quality === 1 ? 'color: yellow' : 'color: red'">
+ <span v-if="deviceInfo.dock.network_state.type === 1"><SignalFilled /></span>
+ <span v-else><GlobalOutlined /></span>
+ </span>
+ <span class="ml10" >{{ deviceInfo.dock.network_state.rate }} KB/S</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>
+ </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>
+ </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>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <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>
+ </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>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Environment Temperature">
+ <span>°C</span>
+ <span class="ml10">{{ deviceInfo.dock.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>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Dock Humidity">
+ <span>💦</span>
+ <span class="ml10">{{ deviceInfo.dock.humidity === str ? str : deviceInfo.dock.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>
+ </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>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ </div>
+ </div>
+ <div style="height: 45%;">
+ <div class="flex-column flex-align-center flex-justify-center" style="float: left; width: 60px; height: 100%; 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>
+ <span>M30</span>
+ </div>
+ </a-tooltip>
+ </div>
+ <div class="osd">
+ <a-row>
+ <a-col span="16" :style="!deviceInfo.device || deviceInfo.device?.mode_code === EModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">
+ {{ !deviceInfo.device ? EModeCode[EModeCode.Disconnected] : EModeCode[deviceInfo.device?.mode_code] }}</a-col>
+ </a-row>
+ <a-row>
+ <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>
+ </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>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Drone Battery Level">
+ <span><ThunderboltOutlined class="fz14"/></span>
+ <span class="ml10">{{ deviceInfo.device && deviceInfo.device.battery.capacity_percent !== str ? deviceInfo.device?.battery.capacity_percent + ' %' : str }}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-tooltip title="RTK Fixed">
+ <a-col span="6" class="flex-row flex-align-center flex-justify-start">
+ <span>Fixed</span>
+ <span class="ml10 circle" :style="deviceInfo.device?.position_state.is_fixed === 1 ? 'backgroud: rgb(25,190,107);' : ' background: red;'"></span>
+ </a-col>
+ </a-tooltip>
+ <a-col span="6">
+ <a-tooltip title="GPS">
+ <span>GPS</span>
+ <span class="ml10">{{ deviceInfo.device ? deviceInfo.device.position_state.gps_number : str }}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="RTK">
+ <span><TrademarkOutlined class="fz14"/></span>
+ <span class="ml10">{{ deviceInfo.device ? deviceInfo.device.position_state.rtk_number : str }}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-col span="6">
+ <a-tooltip title="Flight Mode">
+ <span><ControlOutlined class="fz16" /></span>
+ <span class="ml10">{{ deviceInfo.device ? EGear[deviceInfo.device?.gear] : str }}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Altitude above sea level">
+ <span>ASL</span>
+ <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.height === str ? str : deviceInfo.device?.height.toFixed(2) + ' m'}}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Altitude above takeoff level">
+ <span>ALT</span>
+ <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.elevation === str ? str : deviceInfo.device?.elevation.toFixed(2) + ' m' }}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Distance to Home Point">
+ <span style="border: 1px solid; border-radius: 50%; width: 18px; height: 18px; line-height: 15px; text-align: center; display: block; float: left;" >H</span>
+ <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.home_distance === str ? str : deviceInfo.device?.home_distance.toFixed(2) + ' m' }}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ <a-row>
+ <a-col span="6">
+ <a-tooltip title="Horizontal Speed">
+ <span>H.S</span>
+ <span class="ml10">{{ !deviceInfo.device || deviceInfo.device?.horizontal_speed === str ? str : deviceInfo.device?.horizontal_speed.toFixed(2) + ' m/s'}}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Vertical Speed">
+ <span>V.S</span>
+ <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.vertical_speed === str ? str : deviceInfo.device?.vertical_speed.toFixed(2) + ' m/s'}}</span>
+ </a-tooltip>
+ </a-col>
+ <a-col span="6">
+ <a-tooltip title="Wind Speed">
+ <span>W.S</span>
+ <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.wind_speed === str ? str : (deviceInfo.device?.wind_speed / 10).toFixed(2) + ' m/s'}}</span>
+ </a-tooltip>
+ </a-col>
+ </a-row>
+ </div>
+ </div>
+ <div class="battery-slide" v-if="deviceInfo.device && deviceInfo.device.battery.remain_flight_time !== 0" style="border: 1px solid red">
+ <div style="background: #535759;" class="width-100"></div>
+ <div class="capacity-percent" :style="{ width: deviceInfo.device.battery.capacity_percent + '%'}"></div>
+ <div class="return-home" :style="{ width: deviceInfo.device.battery.return_home_power + '%'}"></div>
+ <div class="landing" :style="{ width: deviceInfo.device.battery.landing_power + '%'}"></div>
+ <div class="white-point" :style="{ left: deviceInfo.device.battery.landing_power + '%'}"></div>
+ <div class="battery" :style="{ left: deviceInfo.device.battery.capacity_percent + '%' }">
+ {{ Math.floor(deviceInfo.device.battery.remain_flight_time / 60) }}:
+ {{ 10 > (deviceInfo.device.battery.remain_flight_time % 60) ? '0' : ''}}{{deviceInfo.device.battery.remain_flight_time % 60 }}
+ </div>
+ </div>
+
</div>
</div>
</template>
@@ -40,19 +399,129 @@
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 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
+} from '@ant-design/icons-vue'
+import { EDeviceTypeName } from '../types'
export default defineComponent({
+ components: {
+ BorderOutlined,
+ LineOutlined,
+ CloseOutlined,
+ ControlOutlined,
+ TrademarkOutlined,
+ ThunderboltOutlined,
+ SignalFilled,
+ GlobalOutlined,
+ HistoryOutlined,
+ CloudUploadOutlined,
+ FieldTimeOutlined,
+ CloudOutlined,
+ CloudFilled,
+ FolderOpenOutlined,
+ RobotFilled,
+ ArrowUpOutlined,
+ ArrowDownOutlined
+ },
name: 'GMap',
props: {},
setup () {
const useMouseToolHook = useMouseTool()
const useGMapManageHook = useGMapManage()
+ const deviceTsaUpdateHook = ref()
const mouseMode = ref(false)
const store = useMyStore()
const state = reactive({
currentType: '',
coverIndex: 0
+ })
+ const str: string = '--'
+ const deviceInfo = reactive({
+ gateway: {
+ capacity_percent: str,
+ 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: {
+ gear: -1,
+ mode_code: EModeCode.Disconnected,
+ height: str,
+ home_distance: str,
+ horizontal_speed: str,
+ vertical_speed: str,
+ wind_speed: str,
+ wind_direction: str,
+ elevation: str,
+ position_state: {
+ gps_number: str,
+ is_fixed: 0,
+ rtk_number: str
+ },
+ battery: {
+ capacity_percent: str,
+ landing_power: str,
+ remain_flight_time: 0,
+ return_home_power: str,
+ },
+ latitude: 0,
+ longitude: 0,
+ } as DeviceOsd
})
const shareId = computed(() => {
return store.state.layerBaseInfo.share
@@ -63,6 +532,57 @@
const drawVisible = computed(() => {
return store.state.drawVisible
})
+ const osdVisible = computed(() => {
+ return store.state.osdVisible
+ })
+
+ 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)
+ store.state.deviceStatusEvent.deviceOnline = {} as DeviceStatus
+ }
+ if (Object.keys(data.deviceOffline).length !== 0) {
+ deviceTsaUpdateHook.value.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)
+ }
+ store.state.deviceStatusEvent.deviceOffline = {}
+ }
+ },
+ {
+ deep: true
+ }
+ )
+
+ 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)
+ 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)
+ if (osdVisible.value.visible && osdVisible.value.sn !== '') {
+ deviceInfo.device = data.deviceInfo[osdVisible.value.sn]
+ }
+ }
+ if (data.currentType === EDeviceTypeName.Dock && data.dockInfo[data.currentSn]) {
+ 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]
+ }
+ }
+ }, {
+ deep: true
+ })
+
watch(
() => store.state.wsEvent,
newData => {
@@ -144,7 +664,9 @@
async function postPinPositionResource (obj) {
const req = getPinPositionResource(obj)
setLayers(req)
- updateCoordinates('gcj02-wgs84', req)
+ const coordinates = req.resource.content.geometry.coordinates
+ updateCoordinates('gcj02-wgs84', req);
+ (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)
@@ -281,7 +803,16 @@
return {
draw,
mouseMode,
- drawVisible
+ drawVisible,
+ osdVisible,
+ pin,
+ state,
+ M30,
+ deviceInfo,
+ EGear,
+ EModeCode,
+ str,
+ EDockModeCode,
}
}
})
@@ -296,16 +827,77 @@
top: 16px;
right: 16px;
.g-action-item {
- padding-top: 8px;
+ width: 28px;
+ height: 28px;
+ background: white;
+ color: $primary;
+ border-radius: 2px;
+ line-height: 28px;
+ text-align: center;
+ margin-bottom: 2px;
+ }
+ .g-action-item:hover {
+ border: 1px solid $primary;
+ border-radius: 2px;
}
}
+ .selection {
+ border: 1px solid $primary;
+ border-radius: 2px;
+ }
}
-</style>
-<style lang="scss">
-.amap-logo {
- opacity: 0;
+.osd-panel {
+ position: absolute;
+ left: 350px;
+ top: 10px;
+ width: 480px;
+ height: 160px;
+ background: black;
+ color: white;
+ border-radius: 2px;
+ opacity: 0.7;
}
-.amap-copyright {
- opacity: 0;
+.osd > div {
+ padding-top: 5px;
+ padding-left: 5px;
+}
+
+.circle {
+ border-radius: 50%;
+ width: 10px;
+ height: 10px;
+}
+
+.battery-slide {
+ .capacity-percent {
+ background: #00ee8b;
+ }
+ .return-home {
+ background: #ff9f0a;
+ }
+ .landing {
+ background: #f5222d;
+ }
+ .white-point {
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: white;
+ bottom: -0.5px;
+ }
+ .battery {
+ background: #141414;
+ color: #00ee8b;
+ margin-top: -10px;
+ height: 20px;
+ width: auto;
+ border-left: 1px solid #00ee8b;
+ padding: 0 5px;
+ }
+}
+.battery-slide > div {
+ position: absolute;
+ min-height: 2px;
+ border-radius: 2px;
}
</style>
diff --git a/src/components/MediaPanel.vue b/src/components/MediaPanel.vue
index 969342a..32b7714 100644
--- a/src/components/MediaPanel.vue
+++ b/src/components/MediaPanel.vue
@@ -1,92 +1,168 @@
<template>
- <div class="media-panel-wrapper">
- <div class="header">Media</div>
- <a-button type="primary" style="margin-top:20px" @click="onRefresh"
- >Refresh</a-button
- >
- <a-table class="media-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>
+ <div class="header">Media Files</div>
+ <a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
+ <div class="media-panel-wrapper">
+ <a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint"
+ :pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
+ <template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col">
+ <a-tooltip :title="text">
+ <a v-if="col === 'name'">{{ text }}</a>
+ <span v-else>{{ text }}</span>
+ </a-tooltip>
+ </template>
+ <template #original="{ text }">
+ {{ text }}
+ </template>
+ <template #action="{ record }">
+ <a-tooltip title="download">
+ <a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a>
+ </a-tooltip>
+ </template>
+ </a-table>
+ </div>
+ </a-spin>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
-import { getMediaFiles } from '/@/api/media'
+import { TableState } from 'ant-design-vue/lib/table/interface'
+import { onMounted, reactive } from 'vue'
+import { IPage } from '../api/http/type'
+import { ELocalStorageKey } from '../types/enums'
+import { downloadFile } from '../utils/common'
+import { downloadMediaFile, getMediaFiles } from '/@/api/media'
+import { DownloadOutlined } from '@ant-design/icons-vue'
+import { Pagination } from 'ant-design-vue'
+import { load } from '@amap/amap-jsapi-loader'
+
+const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+const loading = ref(false)
+
const columns = [
{
- title: 'FileName',
- dataIndex: 'name',
- key: 'name',
+ title: 'File Name',
+ dataIndex: 'file_name',
+ ellipsis: true,
slots: { customRender: 'name' }
},
{
- title: 'FileSize',
- dataIndex: 'size',
- key: 'size'
+ title: 'File Path',
+ dataIndex: 'file_path',
+ ellipsis: true,
+ slots: { customRender: 'path' }
+ },
+ // {
+ // title: 'FileSize',
+ // dataIndex: 'size',
+ // },
+ {
+ title: 'Drone',
+ dataIndex: 'drone'
},
{
- title: 'PayloadType',
- dataIndex: 'payload_type',
- key: 'payload_type',
- ellipsis: true
+ title: 'Payload Type',
+ dataIndex: 'payload'
+ },
+ {
+ title: 'Original',
+ dataIndex: 'is_original',
+ slots: { customRender: 'original' }
+ },
+ {
+ title: 'Created',
+ dataIndex: 'create_time'
},
{
title: 'Action',
- key: 'action',
slots: { customRender: 'action' }
}
]
-const data = ref([
- {
- key: '1',
- name: 'name1',
- size: 32,
- payload_type: 'PM320_DUAL',
- preview_url: ''
- }
-])
-const onRefresh = async () => {
- const wid = localStorage.getItem('workspace-id')
- data.value = []
- const index = 1
- const res = await getMediaFiles(wid, {})
- res.data.forEach(ele => {
- data.value.push({
- key: index.toString(),
- name: ele.file_name
- })
- })
- console.log(res)
+const body: IPage = {
+ page: 1,
+ total: 0,
+ page_size: 50
}
+const paginationProp = reactive({
+ pageSizeOptions: ['20', '50', '100'],
+ showQuickJumper: true,
+ showSizeChanger: true,
+ pageSize: 50,
+ current: 1,
+ total: 0
+})
+
+type Pagination = TableState['pagination']
+
+interface MediaFile {
+ fingerprint: string,
+ drone: string,
+ payload: string,
+ is_original: string,
+ file_name: string,
+ file_path: string,
+ create_time: string,
+}
+
+const mediaData = reactive({
+ data: [] as MediaFile[]
+})
+
+onMounted(() => {
+ getFiles()
+})
+
+function getFiles () {
+ getMediaFiles(workspaceId, body).then(res => {
+ mediaData.data = res.data.list
+ paginationProp.total = res.data.pagination.total
+ paginationProp.current = res.data.pagination.page
+ console.info(mediaData.data[0])
+ })
+}
+
+function refreshData (page: Pagination) {
+ body.page = page?.current!
+ body.page_size = page?.pageSize!
+ getFiles()
+}
+
+function downloadMedia (media: MediaFile) {
+ loading.value = true
+ downloadMediaFile(workspaceId, media.fingerprint).then(res => {
+ if (res.code && res.code !== 0) {
+ return
+ }
+ const suffix = media.file_name.substring(media.file_name.lastIndexOf('.'))
+ const data = new Blob([res.data], { type: suffix === '.mp4' ? 'video/mp4' : 'image/jpeg' })
+ downloadFile(data, media.file_name)
+ }).finally(() => {
+ loading.value = false
+ })
+}
+
</script>
<style lang="scss" scoped>
.media-panel-wrapper {
width: 100%;
+ padding: 16px;
.media-table {
background: #fff;
- margin-top: 32px;
- }
- .header {
- width: 100%;
- height: 60px;
- background: #fff;
- padding: 16px 24px;
- font-size: 20px;
- text-align: start;
- color: #000;
+ margin-top: 10px;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
+.header {
+ width: 100%;
+ height: 60px;
+ background: #fff;
+ padding: 16px;
+ font-size: 20px;
+ font-weight: bold;
+ text-align: start;
+ color: #000;
+}
</style>
diff --git a/src/components/TaskPanel.vue b/src/components/TaskPanel.vue
new file mode 100644
index 0000000..ce880e0
--- /dev/null
+++ b/src/components/TaskPanel.vue
@@ -0,0 +1,182 @@
+<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 === ETaskStatus.FAILED ? 'exception' : taskProgressMap[record.bid]?.status === ETaskStatus.OK ? 'success' : 'normal'">
+ <template #format="percent">
+ <a-tooltip :title="taskProgressMap[record.bid]?.status">
+ <div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;">
+ {{ percent }}% {{ taskProgressMap[record.bid]?.status }}
+ </div>
+ </a-tooltip>
+ </template>
+ </a-progress>
+ </span>
+ </template>
+ <template #action="{ record }">
+ <span class="action-area">
+ <a-popconfirm
+ title="Are you sure execute this task?"
+ ok-text="Yes"
+ cancel-text="No"
+ @confirm="executePlan(record.job_id)"
+ >
+ <a-button type="primary" size="small">Execute</a-button>
+ </a-popconfirm>
+ </span>
+ </template>
+ </a-table>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from '@vue/reactivity'
+import { message } from 'ant-design-vue'
+import { TableState } from 'ant-design-vue/lib/table/interface'
+import { computed, onMounted, watch } from 'vue'
+import { IPage } from '../api/http/type'
+import { executeWaylineJobs, getWaylineJobs } from '../api/wayline'
+import { getRoot } from '../root'
+import { useMyStore } from '../store'
+import { ELocalStorageKey, ERouterName } from '../types/enums'
+import router from '/@/router'
+import { ETaskStatus } from '/@/types/wayline'
+
+const store = useMyStore()
+
+const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+
+const root = getRoot()
+const body: IPage = {
+ page: 1,
+ total: 0,
+ page_size: 50
+}
+const paginationProp = reactive({
+ pageSizeOptions: ['20', '50', '100'],
+ showQuickJumper: true,
+ showSizeChanger: true,
+ pageSize: 50,
+ current: 1,
+ total: 0
+})
+
+const columns = [
+ {
+ title: 'Plan Name',
+ dataIndex: 'job_name'
+ },
+ {
+ title: 'Flight Route Name',
+ dataIndex: 'file_name',
+ ellipsis: true
+ },
+ {
+ title: 'Dock Name',
+ dataIndex: 'dock_name',
+ ellipsis: true
+ },
+ {
+ title: 'Creator',
+ dataIndex: 'username',
+ },
+ {
+ title: 'Updated',
+ dataIndex: 'update_time'
+ },
+ {
+ title: 'Status',
+ key: 'status',
+ width: 200,
+ slots: { customRender: 'status' }
+ },
+
+ {
+ title: 'Action',
+ slots: { customRender: 'action' }
+ }
+]
+type Pagination = TableState['pagination']
+
+interface TaskPlan {
+ bid: string,
+ job_id: string,
+ job_name: string,
+ file_name: string,
+ dock_name: string,
+ username: string,
+ create_time: string,
+}
+
+const plansData = reactive({
+ data: [] as TaskPlan[]
+})
+
+function createPlan () {
+ root.$router.push('/' + ERouterName.CREATE_PLAN)
+}
+
+const taskProgressMap = computed(() => store.state.taskProgressInfo)
+
+onMounted(() => {
+ getPlans()
+})
+
+function getPlans () {
+ getWaylineJobs(workspaceId, body).then(res => {
+ if (res.code !== 0) {
+ return
+ }
+ plansData.data = res.data.list
+ paginationProp.total = res.data.pagination.total
+ paginationProp.current = res.data.pagination.page
+ })
+}
+
+function refreshData (page: Pagination) {
+ body.page = page?.current!
+ body.page_size = page?.pageSize!
+ getPlans()
+}
+
+function executePlan (jobId: string) {
+ executeWaylineJobs(workspaceId, jobId).then(res => {
+ if (res.code === 0) {
+ message.success('Executed Successfully')
+ getPlans()
+ }
+ })
+}
+</script>
+
+<style lang="scss" scoped>
+.plan-panel-wrapper {
+ width: 100%;
+ padding: 16px;
+ .plan-table {
+ background: #fff;
+ margin-top: 10px;
+ }
+ .action-area {
+ color: $primary;
+ cursor: pointer;
+ }
+}
+.header {
+ width: 100%;
+ height: 60px;
+ background: #fff;
+ padding: 16px;
+ font-size: 20px;
+ font-weight: bold;
+ text-align: start;
+ color: #000;
+}
+</style>
diff --git a/src/components/livestream-agora.vue b/src/components/livestream-agora.vue
new file mode 100644
index 0000000..41463f3
--- /dev/null
+++ b/src/components/livestream-agora.vue
@@ -0,0 +1,350 @@
+<template>
+ <div class="mt20 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">
+ <a-select
+ style="width:150px"
+ placeholder="Select Drone"
+ @select="onDroneSelect"
+ >
+ <a-select-option
+ v-for="item in dronePara.droneList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select>
+ <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Camera"
+ @select="onCameraSelect"
+ >
+ <a-select-option
+ v-for="item in dronePara.cameraList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select>
+ <!-- <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Lens"
+ @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> -->
+ <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Clarity"
+ @select="onClaritySelect"
+ >
+ <a-select-option
+ v-for="item in clarityList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select>
+ </div>
+ <p class="fz16 mt10">
+ Note: Obtain The Following Parameters From https://console.agora.io
+ </p>
+ <div class="flex-row flex-justify-center flex-align-center">
+ <a-input v-model:value="agoraPara.appid" placeholder="APP ID"></a-input>
+ <a-input
+ class="ml10"
+ v-model:value="agoraPara.token"
+ placeholder="Token"
+ ></a-input>
+ <a-input
+ class="ml10"
+ v-model:value="agoraPara.channel"
+ placeholder="Channel"
+ ></a-input>
+ </div>
+ <div class="mt20 flex-row flex-justify-center flex-align-center">
+ <a-button 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"
+ >Refresh Live Capacity</a-button
+ >
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import AgoraRTC, { IAgoraRTCClient, IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
+import { message } from 'ant-design-vue'
+import { onMounted, reactive } from 'vue'
+import { CURRENT_CONFIG as config } from '/@/api/http/config'
+import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
+import { getRoot } from '/@/root'
+
+const root = getRoot()
+
+const clarityList = [
+ {
+ value: 0,
+ label: 'Adaptive'
+ },
+ {
+ value: 1,
+ label: 'Smooth'
+ },
+ {
+ value: 2,
+ label: 'Standard'
+ },
+ {
+ value: 3,
+ label: 'HD'
+ },
+ {
+ value: 4,
+ label: 'Super Clear'
+ }
+]
+
+let agoraClient = {} as IAgoraRTCClient
+const agoraPara = reactive({
+ appid: config.agoraAPPID,
+ token: config.agoraToken,
+ channel: config.agoraChannel,
+ uid: 123456,
+ stream: {}
+})
+const dronePara = reactive({
+ livestreamSource: [],
+ droneList: [] as any[],
+ cameraList: [] as any[],
+ videoList: [] as any[],
+ droneSelected: '',
+ cameraSelected: '',
+ videoSelected: '',
+ claritySelected: 0
+})
+const livePara = reactive({
+ url: '',
+ webrtc: {} as any,
+ videoId: '',
+ liveState: false
+})
+
+const onRefresh = async () => {
+ dronePara.droneList = []
+ dronePara.cameraList = []
+ dronePara.videoList = []
+ dronePara.droneSelected = ''
+ dronePara.cameraSelected = ''
+ dronePara.videoSelected = ''
+ await getLiveCapacity({})
+ .then(res => {
+ if (res.code === 0) {
+ if (res.data === null) {
+ console.warn('warning: get live capacity is null!!!')
+ return
+ }
+ dronePara.livestreamSource = res.data
+ dronePara.droneList = []
+
+ console.log('live_capacity:', dronePara.livestreamSource)
+
+ if (dronePara.livestreamSource) {
+ dronePara.livestreamSource.forEach((ele: any) => {
+ dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn })
+ })
+ }
+ }
+ })
+ .catch(error => {
+ console.error(error)
+ })
+}
+
+onMounted(() => {
+ onRefresh()
+ agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'vp8' })
+ // Subscribe when a remote user publishes a stream
+ agoraClient.on('user-joined', async (user: IAgoraRTCRemoteUser) => {
+ message.info('user[' + user.uid + '] join')
+ })
+ agoraClient.on('user-published', async (user: IAgoraRTCRemoteUser, mediaType: 'audio' | 'video') => {
+ await agoraClient.subscribe(user, mediaType)
+ if (mediaType === 'video') {
+ console.log('subscribe success')
+ // Get `RemoteVideoTrack` in the `user` object.
+ const remoteVideoTrack = user.videoTrack!
+ // Dynamically create a container in the form of a DIV element for playing the remote video track.
+ const remotePlayerContainer: any = document.getElementById('player')
+ // remotePlayerContainer.id = agoraPara.uid
+ remoteVideoTrack.play(remotePlayerContainer)
+ }
+ })
+ agoraClient.on('user-unpublished', async (user: any) => {
+ console.log('unpublish live:', user)
+ message.info('unpublish live')
+ })
+})
+
+const handleError = (err: any) => {
+ console.error(err)
+}
+const handleJoinChannel = (uid: any) => {
+ agoraPara.uid = uid
+}
+
+const onStart = async () => {
+ const that = this
+ console.log(
+ 'drone parameter:',
+ dronePara.droneSelected,
+ dronePara.cameraSelected,
+ dronePara.videoSelected,
+ dronePara.claritySelected
+ )
+ const timestamp = new Date().getTime().toString()
+ const liveTimestamp = timestamp
+ if (
+ dronePara.droneSelected == null ||
+ dronePara.cameraSelected == null ||
+ dronePara.videoSelected == null ||
+ dronePara.claritySelected == null
+ ) {
+ message.warn('waring: not select live para!!!')
+ return
+ }
+ agoraClient.setClientRole('audience', { level: 1 })
+ if (agoraClient.connectionState === 'DISCONNECTED') {
+ agoraClient
+ .join(agoraPara.appid, agoraPara.channel, agoraPara.token)
+ }
+ livePara.videoId =
+ dronePara.droneSelected +
+ '/' +
+ dronePara.cameraSelected +
+ '/' +
+ dronePara.videoSelected
+ console.log(agoraPara)
+ agoraPara.token = encodeURIComponent(agoraPara.token)
+
+ livePara.url =
+ 'channel=' +
+ agoraPara.channel +
+ '&sn=' +
+ dronePara.droneSelected +
+ '&token=' +
+ agoraPara.token +
+ '&uid=' +
+ agoraPara.uid
+
+ startLivestream({
+ url: livePara.url,
+ video_id: livePara.videoId,
+ url_type: 0,
+ video_quality: dronePara.claritySelected
+ })
+ .then(res => {
+ livePara.liveState = true
+ })
+ .catch(err => {
+ console.error(err)
+ })
+}
+const onStop = async () => {
+ livePara.videoId =
+ dronePara.droneSelected +
+ '/' +
+ dronePara.cameraSelected +
+ '/' +
+ dronePara.videoSelected
+ stopLivestream({
+ video_id: livePara.videoId
+ }).then(res => {
+ if (res.code === 0) {
+ message.success(res.message)
+ }
+ livePara.liveState = false
+ console.log('stop play livestream')
+ })
+}
+const onDroneSelect = (val: any) => {
+ dronePara.droneSelected = val
+ if (dronePara.droneSelected) {
+ const droneTemp = dronePara.livestreamSource
+ dronePara.cameraList = []
+
+ 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 })
+ })
+ }
+ })
+ }
+}
+const onCameraSelect = (val: any) => {
+ dronePara.cameraSelected = val
+
+ 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
+ }
+ })
+ }
+ })
+ }
+}
+const onVideoSelect = (val: any) => {
+ dronePara.videoSelected = val
+}
+const onClaritySelect = (val: any) => {
+ dronePara.claritySelected = val
+}
+const onUpdateQuality = () => {
+ if (!livePara.liveState) {
+ message.info('Please turn on the livestream first.')
+ return
+ }
+ setLivestreamQuality({
+ video_id: livePara.videoId,
+ video_quality: dronePara.claritySelected
+ }).then(res => {
+ if (res.code === 0) {
+ message.success('Set the clarity to ' + clarityList[dronePara.claritySelected].label)
+ }
+ })
+}
+</script>
+
+<style lang="scss" scoped>
+@import '/@/styles/index.scss';
+</style>
diff --git a/src/components/livestream-others.vue b/src/components/livestream-others.vue
new file mode 100644
index 0000000..a77bcc0
--- /dev/null
+++ b/src/components/livestream-others.vue
@@ -0,0 +1,378 @@
+<template>
+ <div class="flex-column flex-justify-start flex-align-center mt20">
+ <video
+ :style="{ width: '720px', height: '480px' }"
+ id="video-webrtc"
+ ref="videowebrtc"
+ controls
+ class="mt20"
+ ></video>
+ <p class="fz24">Live streaming source selection</p>
+ <div class="flex-row flex-justify-center flex-align-center mt10">
+ <a-select
+ style="width: 150px"
+ placeholder="Select Live Type"
+ @select="onLiveTypeSelect"
+ >
+ <a-select-option
+ v-for="item in liveTypeList"
+ :key="item.label"
+ :value="item.value"
+ >
+ {{ item.label }}
+ </a-select-option>
+ </a-select>
+ <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Drone"
+ @select="onDroneSelect"
+ >
+ <a-select-option
+ v-for="item in droneList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select>
+ <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Camera"
+ @select="onCameraSelect"
+ >
+ <a-select-option
+ v-for="item in cameraList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select>
+ <!-- <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Lens"
+ @select="onVideoSelect"
+ >
+ <a-select-option
+ class="ml10"
+ v-for="item in videoList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select> -->
+ <a-select
+ class="ml10"
+ style="width:150px"
+ placeholder="Select Clarity"
+ @select="onClaritySelect"
+ >
+ <a-select-option
+ v-for="item in clarityList"
+ :key="item.value"
+ :value="item.value"
+ >{{ item.label }}</a-select-option
+ >
+ </a-select>
+ </div>
+ <div class="mt20">
+ <p class="fz10" v-if="livetypeSelected == 2">
+ Please use VLC media player to play the RTSP livestream !!!
+ </p>
+ <p class="fz10" v-if="livetypeSelected == 2">
+ RTSP Parameter:{{ rtspData }}
+ </p>
+ </div>
+ <div class="mt10 flex-row flex-justify-center flex-align-center">
+ <a-button 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"
+ >Refresh Live Capacity</a-button
+ >
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { message } from 'ant-design-vue'
+import { onMounted, reactive, ref } from 'vue'
+import { CURRENT_CONFIG as config } from '/@/api/http/config'
+import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
+import { getRoot } from '/@/root'
+import jswebrtc from '/@/vendors/jswebrtc.min.js'
+const root = getRoot()
+
+const liveTypeList = [
+ {
+ value: 1,
+ label: 'RTMP'
+ },
+ {
+ value: 2,
+ label: 'RTSP'
+ },
+ {
+ value: 3,
+ label: 'GB28181'
+ }
+]
+const clarityList = [
+ {
+ value: 0,
+ label: 'Adaptive'
+ },
+ {
+ value: 1,
+ label: 'Smooth'
+ },
+ {
+ value: 2,
+ label: 'Standard'
+ },
+ {
+ value: 3,
+ label: 'HD'
+ },
+ {
+ value: 4,
+ label: 'Super Clear'
+ }
+]
+
+const videowebrtc = ref(null)
+const livestreamSource = ref()
+const droneList = ref()
+const cameraList = ref()
+const videoList = ref()
+const droneSelected = ref()
+const cameraSelected = ref()
+const videoSeleted = ref()
+const claritySeleted = ref()
+const videoId = ref()
+const liveState = ref<boolean>(false)
+const livetypeSelected = ref()
+const rtspData = ref()
+
+const onRefresh = async () => {
+ droneList.value = []
+ cameraList.value = []
+ videoList.value = []
+ droneSelected.value = null
+ cameraSelected.value = null
+ videoSeleted.value = null
+ await getLiveCapacity({})
+ .then(res => {
+ console.log(res)
+ if (res.code === 0) {
+ if (res.data === null) {
+ console.warn('warning: get live capacity is null!!!')
+ return
+ }
+ const resData: Array<[]> = res.data
+ console.log('live_capacity:', resData)
+ livestreamSource.value = resData
+
+ const temp: Array<{}> = []
+ if (livestreamSource.value) {
+ livestreamSource.value.forEach((ele: any) => {
+ temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn })
+ })
+ droneList.value = temp
+ }
+ }
+ })
+ .catch(error => {
+ console.error(error)
+ })
+}
+
+onMounted(() => {
+ onRefresh()
+})
+const onStart = async () => {
+ console.log(
+ 'Param:',
+ livetypeSelected.value,
+ droneSelected.value,
+ cameraSelected.value,
+ videoSeleted.value,
+ claritySeleted.value
+ )
+ const timestamp = new Date().getTime().toString()
+ if (
+ livetypeSelected.value == null ||
+ droneSelected.value == null ||
+ cameraSelected.value == null ||
+ videoSeleted.value == null ||
+ claritySeleted.value == null
+ ) {
+ message.warn('waring: not select live para!!!')
+ return
+ }
+ videoId.value =
+ droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
+ let liveURL = ''
+ switch (livetypeSelected.value) {
+ case 1: {
+ // RTMP
+ liveURL = config.rtmpURL + timestamp
+ break
+ }
+ case 2: {
+ // RTSP
+ liveURL = `userName=${config.rtspUserName}&password=${config.rtspPassword}&port=${config.rtspPort}`
+ break
+ }
+ case 3: {
+ liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}`
+ break
+ }
+ default:
+ console.warn('warning: live type is not correct!!!')
+ break
+ }
+ await startLivestream({
+ url: liveURL,
+ video_id: videoId.value,
+ url_type: livetypeSelected.value,
+ video_quality: claritySeleted.value
+ })
+ .then(res => {
+ if (livetypeSelected.value === 3) {
+ const url = res.data.url
+ const videoElement = videowebrtc.value
+ // gb28181,it will fail if not wait.
+ message.loading({
+ content: 'Loding...',
+ duration: 4,
+ onClose () {
+ const player = new jswebrtc.Player(url, {
+ video: videoElement,
+ autoplay: true,
+ onPlay: (obj: any) => {
+ console.log('start play livestream')
+ }
+ })
+ 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
+ } else if (livetypeSelected.value === 1) {
+ const url = res.data.url
+ const videoElement = videowebrtc.value
+ console.log('start live:', url)
+ console.log(videoElement)
+ const player = new jswebrtc.Player(url, {
+ video: videoElement,
+ autoplay: true,
+ onPlay: (obj: any) => {
+ console.log('start play livestream')
+ liveState.value = true
+ }
+ })
+ }
+ })
+ .catch(err => {
+ console.error(err)
+ })
+}
+const onStop = () => {
+ videoId.value =
+ droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
+ stopLivestream({
+ video_id: videoId.value
+ }).then(res => {
+ if (res.code === 0) {
+ message.info(res.message)
+ liveState.value = false
+ console.log('stop play livestream')
+ }
+ })
+}
+
+const onUpdateQuality = () => {
+ if (!liveState.value) {
+ message.info('Please turn on the livestream first.')
+ return
+ }
+ setLivestreamQuality({
+ video_id: videoId.value,
+ video_quality: claritySeleted.value
+ }).then(res => {
+ if (res.code === 0) {
+ message.success('Set the clarity to ' + clarityList[claritySeleted.value].label)
+ }
+ })
+}
+
+const onLiveTypeSelect = (val: any) => {
+ livetypeSelected.value = val
+}
+const onDroneSelect = (val: any) => {
+ droneSelected.value = val
+ const temp: Array<{}> = []
+ 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
+ }
+ })
+ }
+}
+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 onVideoSelect = (val: any) => {
+ videoSeleted.value = val
+}
+const onClaritySelect = (val: any) => {
+ claritySeleted.value = val
+}
+</script>
+
+<style lang="scss" scoped>
+@import '/@/styles/index.scss';
+</style>
diff --git a/src/components/wayline-panel.vue b/src/components/wayline-panel.vue
index 784fa3c..aad0a49 100644
--- a/src/components/wayline-panel.vue
+++ b/src/components/wayline-panel.vue
@@ -1,5 +1,5 @@
<template>
- <div class="panel-wrapper">
+ <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
@@ -20,6 +20,7 @@
<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 = [
{
@@ -74,7 +75,7 @@
onRefresh()
})
const onRefresh = async () => {
- const wid: string = localStorage.getItem('workspace-id')
+ const wid: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)
data.value = []
const index = 1
const res = await getWaylineFiles(wid, {
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 3fa5692..c576509 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -1,5 +1,7 @@
+import { CURRENT_CONFIG } from '/@/api/http/config'
+
export const AMapConfig = {
- key: '26d54da6733de88435c68d1a2e88b682',
+ key: CURRENT_CONFIG.amapKey,
version: '2.0',
plugins: [
'AMap.Scale',
@@ -14,6 +16,7 @@
'AMap.PolyEditor',
'AMap.RangingTool',
'AMap.Weather',
- 'AMap.MouseTool'
+ 'AMap.MouseTool',
+ 'AMap.MoveAnimation'
]
}
diff --git a/src/hooks/use-g-map-cover.ts b/src/hooks/use-g-map-cover.ts
index ee539bc..1ddf152 100644
--- a/src/hooks/use-g-map-cover.ts
+++ b/src/hooks/use-g-map-cover.ts
@@ -11,10 +11,10 @@
export function useGMapCover () {
const root = getRoot()
const AMap = root.$aMapObj
+
const normalColor = '#2D8CF0'
const store = rootStore
const coverList = store.state.coverList
-
function AddCoverToMap (cover :any) {
root.$aMap.add(cover)
coverList.push(cover)
@@ -27,10 +27,10 @@
} = {
'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()
diff --git a/src/hooks/use-g-map-tsa.ts b/src/hooks/use-g-map-tsa.ts
new file mode 100644
index 0000000..55b1b59
--- /dev/null
+++ b/src/hooks/use-g-map-tsa.ts
@@ -0,0 +1,96 @@
+import store from '/@/store'
+import { getRoot } from '/@/root'
+import { ELocalStorageKey } from '/@/types'
+import { getDeviceBySn } from '/@/api/manage'
+import { message } from 'ant-design-vue'
+
+export function deviceTsaUpdate () {
+ const root = getRoot()
+ const AMap = root.$aMapObj
+
+ const map = root.$aMap
+ const icons: {
+ [key: string]: string
+ } = {
+ 'sub-device' : '/@/assets/icons/drone.png',
+ 'gateway': '/@/assets/icons/rc.png',
+ 'dock': '/@/assets/icons/dock.png'
+ }
+ const markers = store.state.markerInfo.coverMap
+ const paths = store.state.markerInfo.pathMap
+
+ const passedPolyline = new AMap.Polyline({
+ map: map,
+ strokeColor: '#939393' // 线颜色
+ })
+
+ function initIcon (type: string) {
+ return new AMap.Icon({
+ image: icons[type],
+ imageSize: new AMap.Size(40, 40)
+ })
+ }
+
+ function initMarker (type: string, name: string, sn: string, lng?: number, lat?: number) {
+ if (markers[sn]) {
+ return
+ }
+ markers[sn] = new AMap.Marker({
+ position: new AMap.LngLat(lng ? lng : 113.935913, lat ? lat : 22.525335),
+ icon: initIcon(type),
+ title: name,
+ anchor: 'top-center',
+ offset: [0, -20],
+ })
+ root.$aMap.add(markers[sn])
+
+ // markers[sn].on('moving', function (e: any) {
+ // let path = paths[sn]
+ // if (!path) {
+ // paths[sn] = e.passedPath
+ // return
+ // }
+ // path.push(e.passedPath[0])
+ // path.push(e.passedPath[1])
+ // passedPolyline.setPath(path)
+ // })
+ }
+ function removeMarker (sn: string) {
+ if (!markers[sn]) {
+ return
+ }
+ root.$aMap.remove(markers[sn])
+ passedPolyline.setPath([])
+ delete markers[sn]
+ delete paths[sn]
+ }
+ function addMarker(sn: string, lng?: number, lat?: number) {
+ getDeviceBySn(localStorage.getItem(ELocalStorageKey.WorkspaceId)!, sn)
+ .then(data => {
+ if (data.code !== 0) {
+ message.error(data.message)
+ return
+ }
+ initMarker(data.data.domain, data.data.nickname, sn, lng, lat)
+ })
+ }
+ function moveTo(sn: string, lng: number, lat: number) {
+ let marker = markers[sn]
+ if (!marker) {
+ addMarker(sn, lng, lat)
+ marker = markers[sn]
+ return
+ }
+ marker.moveTo([lng, lat], {
+ duration: 1800,
+ autoRotation: true
+ })
+ }
+
+ return {
+ marker: markers,
+ initMarker,
+ removeMarker,
+ moveTo
+ }
+}
diff --git a/src/pages/page-pilot/pilot-bind.vue b/src/pages/page-pilot/pilot-bind.vue
new file mode 100644
index 0000000..0f3e9e8
--- /dev/null
+++ b/src/pages/page-pilot/pilot-bind.vue
@@ -0,0 +1,54 @@
+<template>
+ <a-layout class="flex-display" style="height: 100vh; background-color: white;">
+ <div class="height100 width100 flex-column flex-justify-start flex-align-start">
+ <a-row class="pt20 pl20" style="height: 45px; width: 100vw" align="middle">
+ <a-col :span="1">
+ <span style="color: #1fa3f6" class="fz26"><SendOutlined rotate="90" /></span>
+ </a-col>
+ <a-col :span="20">
+ <span class="fz20 pl5">{{ drone.data.model }}</span>
+ </a-col>
+ <a-col :span="3">
+ <span class="fz16" v-if="drone.data.bound_status" style="color: #737373">Bound</span>
+ <a-button type="primary" @click="onBindDevice" v-else>Bind</a-button>
+ </a-col>
+ </a-row>
+ </div>
+ </a-layout>
+</template>
+
+<script lang="ts" setup>
+import { SendOutlined } from '@ant-design/icons-vue'
+import { message } from 'ant-design-vue'
+import { onMounted, reactive, ref } from 'vue'
+import { BindBody, bindDevice } from '/@/api/manage'
+import apiPilot from '/@/api/pilot-bridge'
+import { getRoot } from '/@/root'
+import { ELocalStorageKey } from '/@/types'
+import { DeviceStatus } from '/@/types/device'
+
+const root = getRoot()
+interface DeviceStatusData {
+ data: DeviceStatus
+}
+const drone = reactive<DeviceStatusData>({
+ data: JSON.parse(localStorage.getItem(ELocalStorageKey.Device)!)
+})
+
+function onBindDevice () {
+ const bindParam: BindBody = {
+ device_sn: drone.data.sn,
+ user_id: localStorage.getItem(ELocalStorageKey.UserId)!,
+ workspace_id: localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+ }
+ bindDevice(bindParam).then(bindRes => {
+ if (bindRes.code !== 0) {
+ message.error('bind failed:' + bindRes.message)
+ console.error(bindRes.message)
+ return
+ }
+ drone.data.bound_status = true
+ localStorage.setItem(ELocalStorageKey.Device, JSON.stringify(drone.data))
+ })
+}
+</script>
diff --git a/src/pages/page-pilot/pilot-home.vue b/src/pages/page-pilot/pilot-home.vue
index 0d3f434..42416fb 100644
--- a/src/pages/page-pilot/pilot-home.vue
+++ b/src/pages/page-pilot/pilot-home.vue
@@ -1,132 +1,501 @@
<template>
- <div class="page">
- <div class="left flex-column flex-justify-start flex-align-center">
- <p class="fz26 mb0 mt10" style="color: #727272">
- {{ platformName }}
- </p>
- <p class="fz16 ml10 mb0 mt10" style="color: #2d8cf0">
- status:{{ connect }}
- </p>
- <p class="fz32 mb0 mt50" style="color: #000000">{{ workspaceName }}</p>
- <a-button
- class="fz20 mt20 flex-column flex-justify-center flex-align-center"
- style="width: 30vw; height: 12vh;"
- type="default"
- @click="onOpen3rdApp"
- >Open 3rd Party APP</a-button
- >
- <a-button
- class="fz20"
- style="width: 15vw; height: 12vh; position: fixed; bottom: 7vh"
- type="primary"
- @click="onExit"
- >Quit</a-button
- >
- </div>
- <div class="right flex-column flex-justify-start flex-align-center">
- <p class="fz24 mb0 mt10 ">Setting</p>
- <a-button class="mt10 fz16" style="width:90%" @click="onMediaSetting"
- >Media File Upload Setting</a-button
- >
- <a-button class="mt10 fz16" style="width:90%" @click="onLiveshareSetting"
- >Manual Live Share Setting</a-button
- >
- </div>
- </div>
+ <a-layout class="page">
+ <a-layout-sider class="left" width="40%" style="border-radius: 4px;">
+ <div style="width:90%; height: 90%; margin: 4vh">
+ <a-layout style="height: 20%; margin-top: 3vh; background-color: white; ">
+ <a-layout-sider width="25%" theme="light" align="center">
+ <a-avatar :size="60" :src="cloudapi">
+ </a-avatar>
+ </a-layout-sider>
+ <a-layout-content style="margin-left: 1vw;" @click="showStatus">
+ <div style="height: 50%;">
+ <span style="font-size: 16px; font-weight: bolder">{{ workspaceName }}</span>
+ <RightOutlined style="float: right; margin-top: 5px; color: #8894a0" />
+ </div>
+ <div style="height: 50%;">
+ <CloudSyncOutlined v-if="state === EStatusValue.CONNECTED" style="color: #75c5f6" />
+ <SyncOutlined spin v-else/>
+ <span style="color: #737373; margin-left: 3px;">{{ state }}</span>
+ </div>
+ <a-drawer placement="right" v-model:visible="drawerVisible" width="340px">
+ <div class="mb10 flex-row flex-justify-center flex-align-center">
+ <p class="fz14" style="font-weight: 100;">Module State</p>
+ </div>
+ <div class= "width-100 mb10 flex-align-start" v-for="m in modules" :key="m.name" style="height: 30px;">
+
+ <div class="ml5" style="float: left; color: #000000;">{{m.name}}:</div>
+ <div class="ml10" style="float: right; margin-bottom: 8px;">
+ <span :key="m.state" :class="m.state.value === EStatusValue.CONNECTED ? 'green' : 'red'">{{ m.state.value }} </span>
+ <a-button-group >
+ <a-button class="ml5" type="primary" size="small" @click.stop="moduleInstall(m)">install</a-button>
+ <a-button class="ml5 mr5" type="danger" size="small" @click.stop="moduleUninstall(m)">uninstall</a-button>
+ </a-button-group>
+ </div>
+ <a-divider />
+
+ </div>
+ </a-drawer>
+ </a-layout-content>
+
+ </a-layout>
+ <a-divider style="height: 2px; background-color: #f5f5f5; margin-top: 3vh;" />
+
+ <a-button id="exitBtn" class="fz18" @click="confirmAgain"
+ style="width: 10vw; height: 10vh; position: fixed; bottom: 13vh; left: 15vw; background-color: #e6e6e6; color: red; border: 0;"
+ type="primary">Exit
+ </a-button>
+ <a-modal v-model:visible="exitVisible" width="300px" :closable="false">
+ <template #footer>
+ <a-button type="text" style="width: 48%; float: left;" @click="onBack">Cancel</a-button>
+ <a-button type="text" style="width: 48%;" @click="onExit">Exit</a-button>
+ </template>
+ <p>Data will not be synchronized between DJI Pilot and this server after exiting.</p>
+ </a-modal>
+ </div>
+ </a-layout-sider>
+ <a-layout-content class="right flex-column">
+ <div class="mb5">
+ <span class="ml5" style="color: #939393;">Serial Number</span>
+ </div>
+ <div class="fz16" style="background-color: white; border-radius: 4px;">
+ <a-row style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle">
+ <a-col :span="1"></a-col>
+ <a-col :span="9">
+ Remote Control Sn
+ </a-col>
+ <a-col :span="13" class="flex-align-end flex-column">
+ <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-col :span="1"></a-col>
+ <a-col :span="9">Aircraft Sn</a-col>
+ <a-col :span="13" class="flex-align-end flex-column" >
+ <span style="color: #737373">{{ device.data.sn }}</span>
+ </a-col>
+ </a-row>
+ </div>
+ <div class="mt5 mb5">
+ <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-col :span="1"></a-col>
+ <a-col :span="11">
+ Device Binding
+ </a-col>
+ <a-col :span="10" style="text-align: right">
+ <span v-if="device.data.bound_status" style="color: #737373">Aircraft bound</span>
+ <span v-else style="color: #737373">Aircraft not bound</span>
+ </a-col>
+ <a-col :span="2" class="flex-align-center flex-column" >
+ <RightOutlined style="color: #8894a0; font-size: 20px;" />
+ </a-col>
+ </a-row>
+ <a-row style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" @click="onMediaSetting">
+ <a-col :span="1"></a-col>
+ <a-col :span="21">
+ Media File Upload
+ </a-col>
+ <a-col :span="2" class="flex-align-center flex-column" >
+ <RightOutlined style="color: #8894a0; font-size: 20px;" />
+ </a-col>
+ </a-row>
+ <a-row style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" @click="onLiveshareSetting">
+ <a-col :span="1"></a-col>
+ <a-col :span="21">Livestream Manually</a-col>
+ <a-col :span="2" class="flex-align-center flex-column">
+ <RightOutlined style="color: #8894a0; font-size: 20px;" />
+ </a-col>
+ </a-row>
+ <a-row style="border-bottom: 1px solid #f4f8f9; height: 45px;" align="middle" @click="onOpen3rdApp">
+ <a-col :span="1"></a-col>
+ <a-col :span="21">Open 3rd Party APP</a-col>
+ <a-col :span="2" class="flex-align-center flex-column">
+ <RightOutlined style="color: #8894a0; font-size: 20px;" />
+ </a-col>
+ </a-row>
+ </div>
+ </a-layout-content>
+ </a-layout>
</template>
<script lang="ts" setup>
-import { message } from 'ant-design-vue'
-import { onMounted, ref } from 'vue'
+import { message, Popconfirm } from 'ant-design-vue'
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { CURRENT_CONFIG } from '/@/api/http/config'
-import { getPlatformInfo, getUserInfo } from '/@/api/manage'
-import apiPilot from '/@/api/pilot-bridge'
+import { BindBody, bindDevice, getDeviceBySn, getPlatformInfo, getUserInfo } from '/@/api/manage'
+import apiPilot, { ApiParam, MapParam, ThingParam, WsParam } from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
+import { EBizCode, EComponentName, EDownloadOwner, ELocalStorageKey, ERouterName, EStatusValue } from '/@/types'
+import cloudapi from '/@/assets/icons/cloudapi.png'
+import { RightOutlined, CloudOutlined, CloudSyncOutlined, SyncOutlined } from '@ant-design/icons-vue'
+import { useMyStore } from '/@/store'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import websocket from '/@/api/websocket'
+import { DeviceStatus } from '/@/types/device'
const root = getRoot()
-const connect = ref('Disconnect')
-const platformName = ref('Unknown')
-const workspaceName = ref('Unknown')
-const workspaceDesc = ref('Unknown')
-const wsId = ref()
+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)
+const wsState = ref(EStatusValue.DISCONNECT)
+const mapState = ref(EStatusValue.DISCONNECT)
+const tsaState = ref(EStatusValue.DISCONNECT)
+const mediaState = ref(EStatusValue.DISCONNECT)
+const waylineState = ref(EStatusValue.DISCONNECT)
+const workspaceName = ref<string>(localStorage.getItem(ELocalStorageKey.WorkspaceName)!)
+const username = ref(localStorage.getItem(ELocalStorageKey.Username)!)
+const wsId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!)
+const components = apiPilot.init()
+const exitVisible = ref(false)
+const drawerVisible = ref(false)
+let minitor = -1
+
+interface DeviceInfoData {
+ data: DeviceStatus
+}
+const device = reactive<DeviceInfoData>({
+ data: {
+ sn: EStatusValue.DISCONNECT,
+ online_status: false,
+ device_callsign: '',
+ user_id: '',
+ user_callsign: '',
+ bound_status: false,
+ model: '',
+ gateway_sn: EStatusValue.DISCONNECT,
+ domain: ''
+ }
+})
+const bindParam: BindBody = {
+ device_sn: '',
+ user_id: '',
+ workspace_id: wsId.value
+}
+
+const modules = [{
+ name: 'Cloud',
+ state: thingState,
+ module: EComponentName.Thing
+}, {
+ name: 'Api',
+ state: apiState,
+ module: EComponentName.Api
+}, {
+ name: 'Live',
+ state: liveState,
+ module: EComponentName.Liveshare
+}, {
+ name: 'Ws',
+ state: wsState,
+ module: EComponentName.Ws
+}, {
+ name: 'Map',
+ state: mapState,
+ module: EComponentName.Map
+}, {
+ name: 'Tsa',
+ state: tsaState,
+ module: EComponentName.Tsa
+}, {
+ name: 'Media',
+ state: mediaState,
+ module: EComponentName.Media
+}, {
+ name: 'Wayline',
+ state: waylineState,
+ module: EComponentName.Mission
+}]
+
+const store = useMyStore()
+
+const wsGetMsg = async (res: any) => {
+ const payload = JSON.parse(res.data)
+ switch (payload.biz_code) {
+ 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) {
+ device.data = payload.data
+ localStorage.setItem(ELocalStorageKey.Device, JSON.stringify(device.data))
+ }
+ break
+ }
+ case EBizCode.DeviceOffline: {
+ console.info('offline: ', payload)
+ if (payload.data.sn === device.data.sn) {
+ device.data.online_status = payload.data.online_status
+ localStorage.setItem(ELocalStorageKey.Device, JSON.stringify(device.data))
+ }
+ break
+ }
+
+ default:
+ break
+ }
+}
+let bindNum: number
+let socket: ReconnectingWebSocket
onMounted(() => {
- apiPilot.init()
- const token = apiPilot.getToken()
- if (token) {
- getPlatformInfo({}).then(res => {
- console.log(res)
- platformName.value = res.data.platform_name
- workspaceName.value = res.data.workspace_name
- workspaceDesc.value = res.data.workspace_desc
- wsId.value = res.data.workspace_id
- apiPilot.setPlatformMessage(
- platformName.value,
- workspaceName.value,
- workspaceDesc.value
- )
- apiPilot.setWorkspaceId(wsId.value)
- })
+ apiPilot.onBackClickReg()
+ apiPilot.onStopPlatform()
+
+ 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
}
- if (JSON.parse(apiPilot.isComponentLoaded('thing')).data === false || token) {
- getUserInfo({}).then(res => {
- const param = {
- host: res.data.mqtt_addr,
- username: res.data.mqtt_username,
- password: res.data.mqtt_password,
- connectCallback: 'connectCallback'
- }
- apiPilot.setComponentParam('thing', param)
- apiPilot.loadComponent('thing', apiPilot.getComponentParam('thing'))
- })
- } else {
- const connectState = JSON.parse(window.djiBridge.thingGetConnectState())
- if (connectState.code === 0 && connectState.data) {
- connect.value = 'Connected'
- } else {
- connect.value = 'Disconnect'
+ const oldDevice = localStorage.getItem(ELocalStorageKey.Device)
+ if (oldDevice) {
+ device.data = JSON.parse(oldDevice)
+ }
+ device.data.sn = apiPilot.getAircraftSN()
+ getDeviceInfo()
+
+ socket = websocket.init(wsGetMsg)
+
+ const isLoaded = apiPilot.isComponentLoaded(EComponentName.Thing)
+ if (isLoaded) {
+ username.value = '' + localStorage.getItem(ELocalStorageKey.Username)
+ workspaceName.value = '' + localStorage.getItem(ELocalStorageKey.WorkspaceName)
+ refreshStatus()
+ apiPilot.setPlatformMessage(
+ '' + localStorage.getItem(ELocalStorageKey.PlatformName),
+ workspaceName.value,
+ '' + localStorage.getItem(ELocalStorageKey.WorkspaceDesc)
+ )
+ return
+ }
+
+ setWorkspaceInfo()
+
+ getUserInfo().then(res => {
+ username.value = res.data.username
+ localStorage.setItem(ELocalStorageKey.Username, username.value)
+ // thing
+ const param: ThingParam = {
+ host: res.data.mqtt_addr,
+ username: username.value,
+ password: res.data.mqtt_password,
+ connectCallback: 'connectCallback'
}
- }
- apiPilot.loadComponent('liveshare', apiPilot.getComponentParam('liveshare'))
- console.log('ws token:', token)
- apiPilot.setComponentParam('ws', {
- host: CURRENT_CONFIG.websocketURL,
- token: token
+ components.set(EComponentName.Thing, param)
+ apiPilot.loadComponent(EComponentName.Thing, components.get(EComponentName.Thing))
+
+ bindParam.device_sn = device.data.gateway_sn
+ bindParam.user_id = res.data.user_id
+ bindParam.workspace_id = res.data.workspace_id
})
- apiPilot.loadComponent('ws', apiPilot.getComponentParam('ws'))
- apiPilot.setComponentParam('map', {
- userName: 'pilot1',
- elementPreName: 'PILOT'
- })
- apiPilot.loadComponent('map', apiPilot.getComponentParam('map'))
- apiPilot.loadComponent('tsa', apiPilot.getComponentParam('tsa'))
- apiPilot.loadComponent('media', apiPilot.getComponentParam('media'))
- apiPilot.loadComponent('mission', {})
window.connectCallback = arg => {
connectCallback(arg)
}
- apiPilot.onBackClickReg()
+ window.wsConnectCallback = arg => {
+ wsConnectCallback(arg)
+ }
})
-const connectCallback = (arg: any) => {
- console.info('into callback', arg)
+
+onUnmounted(() => {
+ socket.close()
+})
+
+const connectCallback = async (arg: any) => {
if (arg) {
- connect.value = 'Connected'
- window.djiBridge.mediaSetDownloadOwner(0)
- window.djiBridge.mediaSetUploadPhotoType(1)
+ thingState.value = EStatusValue.CONNECTED
+ // liveshare
+ apiPilot.loadComponent(EComponentName.Liveshare, components.get(EComponentName.Liveshare))
+
+ // ws
+ const wsParam: WsParam = components.get(EComponentName.Ws)
+ wsParam.token = apiPilot.getToken()
+ apiPilot.loadComponent(EComponentName.Ws, components.get(EComponentName.Ws))
+
+ // map
+ const mapParam: MapParam = components.get(EComponentName.Map)
+ mapParam.userName = username.value
+ apiPilot.loadComponent(EComponentName.Map, components.get(EComponentName.Map))
+
+ // tsa
+ apiPilot.loadComponent(EComponentName.Tsa, components.get(EComponentName.Tsa))
+
+ // media
+ apiPilot.loadComponent(EComponentName.Media, components.get(EComponentName.Media))
+ apiPilot.setDownloadOwner(EDownloadOwner.Mine.valueOf())
+
+ // mission
+ apiPilot.loadComponent(EComponentName.Mission, {})
+
+ bindNum = setInterval(() => {
+ bindDevice(bindParam).then(bindRes => {
+ if (bindRes.code !== 0) {
+ message.error(bindRes.message)
+ console.error(bindRes.message)
+ } else {
+ clearInterval(bindNum)
+ }
+ })
+ }, 2000)
+ setTimeout(getDeviceInfo, 3000)
} else {
- connect.value = 'Disconnect'
+ thingState.value = EStatusValue.DISCONNECT
+ }
+ refreshStatus()
+}
+const wsConnectCallback = async (arg: any) => {
+ if (arg) {
+ wsState.value = EStatusValue.CONNECTED
+ } else {
+ wsState.value = EStatusValue.DISCONNECT
}
}
-const onExit = async (e: any) => {
+
+const confirmAgain = () => {
+ exitVisible.value = true
+}
+
+const onBack = () => {
+ exitVisible.value = false
+}
+
+const onExit = () => {
+ localStorage.clear()
apiPilot.stopwebview()
}
+
+const bindingDevice = async () => {
+ root.$router.push(ERouterName.PILOT_BIND)
+}
+
const onMediaSetting = async (e: any) => {
- root.$router.push('/pilot-media')
+ root.$router.push(ERouterName.PILOT_MEDIA)
}
const onLiveshareSetting = async (e: any) => {
- root.$router.push('/pilot-liveshare')
+ root.$router.push(ERouterName.PILOT_LIVESHARE)
}
const onOpen3rdApp = () => {
- window.open('mydjischeme://www.dji.com')
+ const packageName = 'com.dji.sample'
+ const isInstalled = apiPilot.isAppInstalled(packageName)
+ if (isInstalled) {
+ window.open('https://www.dji.com')
+ } else {
+ message.error(packageName + ' is not installed.')
+ }
+}
+
+const showStatus = async () => {
+ minitor = setInterval(() => {
+ refreshStatus()
+ if (!drawerVisible.value) {
+ clearInterval(minitor)
+ }
+ }, 2000)
+ drawerVisible.value = true
+}
+
+function setWorkspaceInfo () {
+ if (localStorage.getItem(ELocalStorageKey.WorkspaceName)) {
+ apiPilot.setPlatformMessage(
+ '' + localStorage.getItem(ELocalStorageKey.PlatformName),
+ workspaceName.value,
+ '' + localStorage.getItem(ELocalStorageKey.WorkspaceDesc)
+ )
+ apiPilot.setWorkspaceId(wsId.value)
+
+ return
+ }
+ getPlatformInfo().then(res => {
+ console.log(res)
+ workspaceName.value = res.data.workspace_name
+ wsId.value = res.data.workspace_id
+ localStorage.setItem(ELocalStorageKey.PlatformName, res.data.platform_name)
+ localStorage.setItem(ELocalStorageKey.WorkspaceName, workspaceName.value)
+ localStorage.setItem(ELocalStorageKey.WorkspaceDesc, res.data.workspace_desc)
+ apiPilot.setPlatformMessage(
+ res.data.platform_name,
+ workspaceName.value,
+ res.data.workspace_desc
+ )
+ apiPilot.setWorkspaceId(wsId.value)
+ })
+}
+
+function refreshStatus () {
+ thingState.value = apiPilot.thingGetConnectState() ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
+ apiState.value = apiPilot.isComponentLoaded(EComponentName.Api) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
+ liveState.value = apiPilot.isComponentLoaded(EComponentName.Liveshare) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
+ wsState.value = apiPilot.isComponentLoaded(EComponentName.Ws) && apiPilot.wsGetConnectState()
+ ? EStatusValue.CONNECTED
+ : EStatusValue.DISCONNECT
+ mapState.value = apiPilot.isComponentLoaded(EComponentName.Map) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
+ 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) {
+ let param
+ switch (m.module) {
+ case EComponentName.Thing:
+ param = apiPilot.thingGetConfigs()
+ break
+ case EComponentName.Api: {
+ const apiParam: ApiParam = {
+ host: apiPilot.getHost(),
+ token: apiPilot.getToken()
+ }
+ param = apiParam
+ break
+ }
+ case EComponentName.Map: {
+ const mapParam: MapParam = components.get(EComponentName.Map)
+ mapParam.userName = '' + localStorage.getItem(ELocalStorageKey.Username)
+ param = mapParam
+ break
+ }
+ case EComponentName.Ws: {
+ const wsParam: WsParam = components.get(EComponentName.Ws)
+ wsParam.token = '' + localStorage.getItem(ELocalStorageKey.Token)
+ param = wsParam
+ break
+ }
+ default:
+ param = components.get(m.module)
+ }
+
+ components.set(m.module, param)
+ console.info(components.get(m.module))
+ apiPilot.loadComponent(m.module, components.get(m.module))
+ refreshStatus()
+}
+
+function moduleUninstall (m: any) {
+ message.info('uninstall ' + m.module)
+ apiPilot.unloadComponent(m.module)
+ refreshStatus()
+}
+
+function getDeviceInfo () {
+ if (device.data.sn === EStatusValue.DISCONNECT) {
+ return
+ }
+ getDeviceBySn(bindParam.workspace_id, device.data.sn).then(res => {
+ if (res.code !== 0) {
+ return
+ }
+ device.data.online_status = res.data.status
+ device.data.bound_status = res.data.bound_status
+ device.data.device_callsign = res.data.nickname
+ device.data.model = res.data.device_name
+ localStorage.setItem(ELocalStorageKey.Device, JSON.stringify(device.data))
+ })
}
</script>
@@ -139,12 +508,32 @@
height: 100%;
width: 100%;
.left {
- width: 50%;
- border-right: red solid 2px;
+ height: 90%;
+ background-color: white;
+ margin-top: 6vh;
+ margin-left: 2vh;
}
.right {
- width: 100%;
- height: 100%;
+ height: 90%;
+ margin-top: 6vh;
+ margin-left: 5vh;
+ margin-right: 5vh;
}
}
+.green {
+ color: green
+}
+.red {
+ color: red;
+}
+#exitBtn:hover :active {
+ background-color: rgb(77, 75, 75);
+ width: 10vw;
+ height: 10vh;
+ position: fixed;
+ bottom: 13vh;
+ left: 15vw;
+ line-height: 10vh;
+}
+
</style>
diff --git a/src/pages/page-pilot/pilot-index.vue b/src/pages/page-pilot/pilot-index.vue
index a4181fe..5a5d56b 100644
--- a/src/pages/page-pilot/pilot-index.vue
+++ b/src/pages/page-pilot/pilot-index.vue
@@ -2,7 +2,7 @@
<div class="login flex-column flex-justify-center flex-align-center m0 b0">
<a-image
style="width: 17vw; height: 10vw; margin-bottom: 50px"
- src="http://lofrev.net/wp-content/photos/2016/09/dji_logo_png.png"
+ :src="djiLogo"
/>
<p class="logo fz35 pb50">Pilot Cloud API Demo</p>
<a-form
@@ -11,7 +11,7 @@
class="flex-row flex-justify-center flex-align-center"
>
<a-form-item>
- <a-input v-model:value="formState.user" placeholder="Username">
+ <a-input v-model:value="formState.username" placeholder="Username">
<template #prefix
><UserOutlined style="color: rgba(0, 0, 0, 0.25)"
/></template>
@@ -44,93 +44,97 @@
</template>
<script lang="ts" setup>
-import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
-import { onMounted, reactive, UnwrapRef } from 'vue'
+import { onMounted, reactive, ref, UnwrapRef } from 'vue'
import { CURRENT_CONFIG } from '/@/api/http/config'
-import { login, refreshToken } from '/@/api/manage'
+import { login, LoginBody, refreshToken } from '/@/api/manage'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
+import router from '/@/router'
+import { EComponentName, ELocalStorageKey, ERouterName, EUserType } from '/@/types'
+import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
+import djiLogo from '/@/assets/icons/dji_logo.png'
-interface FormState {
- user: string
- password: string
-}
const root = getRoot()
-const formState: UnwrapRef<FormState> = reactive({
- user: 'pilot',
- password: 'pilot123'
+const formState: UnwrapRef<LoginBody> = reactive({
+ username: 'pilot',
+ password: 'pilot123',
+ flag: EUserType.Pilot,
})
-let isVerified:any
+const isVerified = ref<boolean>(false)
onMounted(async () => {
- const verifyLicense = JSON.parse(apiPilot.platformVerifyLicense(CURRENT_CONFIG.appId,
- CURRENT_CONFIG.appKey, CURRENT_CONFIG.appLicense))
- const platformVerify = JSON.parse(apiPilot.isPlatformVerifySuccess())
- isVerified = platformVerify.data
- if (platformVerify.data === true) {
- message.success('The license verification is successful.')
- } else {
- message.error('Filed to verify the license. message is ' + verifyLicense.data)
+ verifyLicense()
+ if (!isVerified.value) {
return
}
- const token = apiPilot.getToken()
- console.log('api token:', token)
+
apiPilot.setPlatformMessage('Cloud Api Platform', '', '')
- if (token && token !== undefined) {
+
+ const token = localStorage.getItem(ELocalStorageKey.Token)
+ if (token) {
await refreshToken({})
.then(res => {
- apiPilot.setComponentParam('api', {
+ apiPilot.setComponentParam(EComponentName.Api, {
host: CURRENT_CONFIG.baseURL,
token: res.data.access_token
})
- const jsres = JSON.parse(
- apiPilot.loadComponent('api', apiPilot.getComponentParam('api'))
- )
- console.log('load api module status:', jsres)
+ const jsres = apiPilot.loadComponent(EComponentName.Api, apiPilot.getComponentParam(EComponentName.Api))
+ if (!jsres) {
+ message.error('Failed to load api module.')
+ return
+ }
apiPilot.setToken(res.data.access_token)
- localStorage.setItem('x-auth-token', res.data.access_token)
- message.success('Login Success')
- root.$router.push('/pilot-home')
+ localStorage.setItem(ELocalStorageKey.Token, res.data.access_token)
+ root.$router.push(ERouterName.PILOT_HOME)
})
.catch(err => {
- console.error(err)
+ message.error(err)
})
}
})
const onSubmit = async (e: any) => {
- await login({
- username: formState.user,
- password: formState.password
- })
+ await login(formState)
.then(res => {
- if (!isVerified) {
+ if (!isVerified.value) {
message.error('Please verify the license firstly.')
return
}
console.log('login res:', res)
if (res.code === 0) {
- apiPilot.setComponentParam('api', {
+ apiPilot.setComponentParam(EComponentName.Api, {
host: CURRENT_CONFIG.baseURL,
token: res.data.access_token
})
const jsres = apiPilot.loadComponent(
- 'api',
- apiPilot.getComponentParam('api')
+ EComponentName.Api,
+ apiPilot.getComponentParam(EComponentName.Api)
)
console.log('load api module res:', jsres)
apiPilot.setToken(res.data.access_token)
- localStorage.setItem('x-auth-token', res.data.access_token)
- localStorage.setItem('workspace-id', res.data.workspace_id)
- localStorage.setItem('username', res.data.username)
+ localStorage.setItem(ELocalStorageKey.Token, res.data.access_token)
+ localStorage.setItem(ELocalStorageKey.WorkspaceId, res.data.workspace_id)
+ localStorage.setItem(ELocalStorageKey.UserId, res.data.user_id)
+ localStorage.setItem(ELocalStorageKey.Username, res.data.username)
+ localStorage.setItem(ELocalStorageKey.Flag, EUserType.Pilot.toString())
message.success('Login Success')
- root.$router.push('/pilot-home')
+ root.$router.push(ERouterName.PILOT_HOME)
}
})
.catch(err => {
- console.error(err)
+ message.error(err)
})
}
+
+function verifyLicense () {
+ isVerified.value = apiPilot.platformVerifyLicense(CURRENT_CONFIG.appId, CURRENT_CONFIG.appKey, CURRENT_CONFIG.appLicense) &&
+ apiPilot.isPlatformVerifySuccess()
+ if (isVerified.value) {
+ message.success('The license verification is successful.')
+ } else {
+ message.error('Filed to verify the license. Please check license whether the license is correct, or apply again.')
+ }
+}
</script>
<style lang="scss" scoped>
diff --git a/src/pages/page-pilot/pilot-liveshare.vue b/src/pages/page-pilot/pilot-liveshare.vue
index da54d73..2c778a5 100644
--- a/src/pages/page-pilot/pilot-liveshare.vue
+++ b/src/pages/page-pilot/pilot-liveshare.vue
@@ -1,16 +1,17 @@
<template>
- <div
- class="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
- >
+ <div class="width100 flex-column flex-justify-start flex-align-start" style="background-color: white;">
+
+ <p class="fz16 ml10 mt15 mb10 color-text-title color-font-bold" style="color: #939393">
+ Before starting manually, please select the publish mode and livestream type
+ </p>
<div
- class="mt20 flex-row flex-align-center flex-justify-between"
- style="width: 100%"
- >
+ class="mt15 flex-row flex-align-center flex-justify-between"
+ style="width: 100%;">
<p class="ml10 mb0 fz16" style="color: black">
Select Video Publish Mode:
</p>
<a-select
- style="width: 200px"
+ style="width: 200px; margin-right: 20px;"
placeholder="Select Mode"
@select="onPublishModeSelect"
>
@@ -24,16 +25,18 @@
</a-select>
</div>
- <a-divider dashed class="mt10 mb0"></a-divider>
-
+ <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
+ <a-divider />
+ </div>
<div
- class="flex-row flex-align-center flex-justify-between mt10"
- style="width: 100%"
+ class="flex-row flex-align-center flex-justify-between"
+ style="width: 100%; margin-top: -10px;"
>
- <p class="ml10 mb0 fz16" style="color: black">Select Live Share Type:</p>
+ <p class="ml10 mb0 fz16">Select Livestream Type:</p>
<a-select
- style="width: 200px"
+ style="width: 200px; margin-right: 20px;"
placeholder="Select Live Type"
+ :value="liveStreamStatus.type"
@select="onLiveTypeSelect"
>
<a-select-option
@@ -45,141 +48,265 @@
</a-select-option>
</a-select>
</div>
- <a-divider dashed class="mt10 mb0"></a-divider>
+ <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
+ <a-divider />
+ </div>
+ <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-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>
+ <span v-else></span>
+ </div>
- <div
- class="flex-row flex-align-center flex-justify-center mt20"
- style="width: 100%"
- >
- <p>Live Share State: {{ liveState }}</p>
</div>
- <div
- class="flex-row flex-align-center flex-justify-center mt20"
- style="width: 100%"
- >
- <a-button type="primary" @click="onPlay">Play</a-button>
- <a-button class="ml20" type="primary" @click="onGetConfig"
- >Get Config</a-button
- >
- <a-button class="ml20" type="primary" @click="onGetStatus"
- >Get Status</a-button
- >
+ <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
+ <a-divider />
</div>
+ <div class="mb20 flex-row flex-align-center flex-justify-center"
+ style="width: 100%; ">
+ <a-button class="flex-column fz20 flex-align-center flex-justify-center" style="width: 100px;" type="ghost" @click="onPlay">Play</a-button>
+ <a-button class="flex-column fz20 flex-align-center flex-justify-center ml40" style="width: 100px;" type="ghost" @click="onStop">Stop</a-button>
+ </div>
+ <a-button v-if="playVisiable" class="flex-column flex-align-center" shape="circle" @click="showLivingStatus"
+ style="position: fixed; top: 13vh; left: 5vw; opacity: 0.8; background-color: rgb(0,0,0,0)">
+ <template #icon><CaretRightFilled style="font-size: 26px; color: " /></template>
+ </a-button>
+
+ <a-drawer placement="right" v-model:visible="drawerVisible" width="280px" :mask="false" @close="closeDrawer">
+ <div class="fz16 width-100">
+ <div class="mt20" style=" margin-bottom: -10px;">
+ <span class="fz20 flex-row flex-align-center flex-justify-center">
+ <font :color="liveState === EStatusValue.LIVING ? 'green' : liveState === EStatusValue.CONNECTED ? 'blue' : 'red'">{{ liveState }}</font></span>
+ </div>
+ <a-divider />
+ <div style=" margin-top: -10px; margin-bottom: -15px;">
+ <span>Frame Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.fps }}<span v-if="liveStreamStatus.fps != -1"> fps</span></span><br/>
+ </div>
+ <a-divider />
+ <div style=" margin-top: -10px; margin-bottom: -10px;">
+ <span>Video Bit Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.videoBitRate }}<span v-if="liveStreamStatus.videoBitRate != -1"> kbps</span></span><br/>
+ </div>
+ <a-divider />
+ <div style=" margin-top: -10px; margin-bottom: -10px;">
+ <span>Audio Bit Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.audioBitRate }}<span v-if="liveStreamStatus.audioBitRate != -1"> kbps</span></span><br/>
+ </div>
+ <a-divider />
+ <div style=" margin-top: -10px; margin-bottom: -10px;">
+ <span>Packet Loss Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.dropRate }}<span v-if="liveStreamStatus.dropRate != -1"> %</span></span><br/>
+ </div>
+ <a-divider />
+ <div style=" margin-top: -10px; margin-bottom: -10px;">
+ <span>RTT:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.rtt }}<span v-if="liveStreamStatus.rtt != -1"> ms</span></span><br/>
+ </div>
+ <a-divider />
+ <div style=" margin-top: -10px;">
+ <span >Jitter:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.jitter }}</span><br/>
+ </div>
+ </div>
+ </a-drawer>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
-import { onMounted, ref } from 'vue'
-import { CURRENT_CONFIG as config } from '/@/api/http/config'
+import { onMounted, reactive, ref, UnwrapRef } from 'vue'
+import { CURRENT_CONFIG as config, CURRENT_CONFIG } from '/@/api/http/config'
+import { ELiveTypeName, ELiveTypeValue, GB28181Param, LiveConfigParam, LiveStreamStatus, RTSPParam, EVideoPublishType } from '/@/api/live-stream'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
+import { ELiveStatusValue, EStatusValue } from '/@/types'
+import { CaretRightFilled } from '@ant-design/icons-vue'
const root = getRoot()
const publishModeList = [
{
- value: 'video-on-demand',
- label: 'video-on-demand'
+ value: EVideoPublishType.VideoOnDemand,
+ label: EVideoPublishType.VideoOnDemand
},
{
- value: 'video-by-manual',
- label: 'video-by-manual'
+ value: EVideoPublishType.VideoByManual,
+ label: EVideoPublishType.VideoByManual
},
{
- value: 'video-demand-aux-manual',
- label: 'video-demand-aux-manual'
+ value: EVideoPublishType.VideoDemandAuxManual,
+ label: EVideoPublishType.VideoDemandAuxManual
}
]
const liveTypeList = [
{
- value: 1,
- label: 'AGORA'
+ value: ELiveTypeValue.Agora,
+ label: ELiveTypeName.Agora
},
{
- value: 2,
- label: 'RTMP'
+ value: ELiveTypeValue.RTMP,
+ label: ELiveTypeName.RTMP
},
{
- value: 3,
- label: 'RTSP'
+ value: ELiveTypeValue.RTSP,
+ label: ELiveTypeName.RTSP
},
{
- value: 4,
- label: 'GB28181'
+ value: ELiveTypeValue.GB28181,
+ label: ELiveTypeName.GB28181
}
]
const agoraParam = {
- uid: config.agoraAPPID,
+ uid: '2892130292',
token: config.agoraToken,
channelId: config.agoraChannel
}
const rtmpParam = {
- url: config.rtmpURL + '12345'
+ url: config.rtmpURL + new Date().getTime()
}
-const liveState = ref<string>('STOP')
-const livetypeSelected = ref<number>(1)
-const publishModeSelected = ref<string>('video-demand-aux-manual')
+const rtspParam: RTSPParam = {
+ userName: CURRENT_CONFIG.rtspUserName,
+ password: CURRENT_CONFIG.rtspPassword,
+ port: CURRENT_CONFIG.rtspPort
+}
+const gb28181Param: GB28181Param = {
+ serverIp: CURRENT_CONFIG.gbServerIp,
+ serverPort: CURRENT_CONFIG.gbServerPort,
+ serverId: CURRENT_CONFIG.gbServerId,
+ agentId: CURRENT_CONFIG.gbAgentId,
+ password: CURRENT_CONFIG.gbPassword,
+ agentPort: CURRENT_CONFIG.gbAgentPort,
+ agentChannel: CURRENT_CONFIG.gbAgentChannel
+}
+
+const playVisiable = ref(false)
+const drawerVisible = ref(false)
+const liveState = ref(EStatusValue.DISCONNECT)
+const liveTypeSelected = ref<string>()
+const publishModeSelected = ref<string>()
+const liveStreamStatus: LiveStreamStatus = reactive({
+ audioBitRate: -1,
+ dropRate: -1,
+ fps: -1,
+ jitter: -1,
+ quality: -1,
+ rtt: -1,
+ status: -1,
+ type: -1,
+ videoBitRate: -1
+})
onMounted(() => {
- const status: any = apiPilot.getLiveshareStatus()
- console.log(status)
- // liveState.value =
- // status.status === 0
- // ? 'Cannot connect to server'
- // : status.status === 1
- // ? 'Connect to server'
- // : 'Playing'
+ const config: LiveConfigParam = JSON.parse(apiPilot.getLiveshareConfig())
+ liveStreamStatus.type = config.type
+ refreshLiveType()
- // console.log(liveState.value)
+ window.liveStatusCallback = arg => {
+ liveStatusCallback(arg)
+ }
})
-const onLiveTypeSelect = (val: any) => {
- livetypeSelected.value = val
- message.info('set livetype:' + livetypeSelected.value, 5)
+
+const liveStatusCallback = async (arg: LiveStreamStatus) => {
+ liveStreamStatus.fps = arg.fps
+ liveStreamStatus.audioBitRate = arg.audioBitRate
+ liveStreamStatus.dropRate = arg.dropRate
+ liveStreamStatus.jitter = arg.jitter
+ liveStreamStatus.rtt = arg.rtt
+ liveStreamStatus.videoBitRate = arg.videoBitRate
+ liveStreamStatus.quality = arg.quality
+ liveStreamStatus.type = arg.type
+ liveStreamStatus.status = arg.status
+
+ switch (liveStreamStatus.status) {
+ case ELiveStatusValue.LIVING:
+ liveState.value = EStatusValue.LIVING
+ break
+ case ELiveStatusValue.CONNECTED:
+ liveState.value = EStatusValue.CONNECTED
+ break
+ default:
+ liveState.value = EStatusValue.DISCONNECT
+ }
+}
+function refreshLiveType () {
+ switch (liveStreamStatus.type) {
+ case ELiveTypeValue.Agora:
+ liveTypeSelected.value = ELiveTypeName.Agora
+ break
+ case ELiveTypeValue.RTMP:
+ liveTypeSelected.value = ELiveTypeName.RTMP
+ break
+ case ELiveTypeValue.RTSP:
+ liveTypeSelected.value = ELiveTypeName.RTSP
+ break
+ case ELiveTypeValue.GB28181:
+ liveTypeSelected.value = ELiveTypeName.GB28181
+ break
+ default:
+ liveTypeSelected.value = ELiveTypeName.Unknown
+ }
+}
+const onLiveTypeSelect = (val: number) => {
+ liveStreamStatus.type = val
+ refreshLiveType()
}
const onPublishModeSelect = (val: string) => {
publishModeSelected.value = val
- message.info(
- 'set publish mode res:' +
- apiPilot.setVideoPublishType(publishModeSelected.value),
- 5
- )
+ apiPilot.setVideoPublishType(publishModeSelected.value)
}
const onPlay = () => {
- switch (livetypeSelected.value) {
+ if (!publishModeSelected.value) {
+ message.warn('Please select publish mode!')
+ return
+ }
+ if (liveTypeSelected.value === ELiveTypeName.Unknown) {
+ message.warn('Please select livestream type!')
+ return
+ }
+ switch (liveStreamStatus.type) {
case 1: {
- message.info('agoraParam:' + JSON.stringify(agoraParam))
- apiPilot.setLiveshareConfig(1, JSON.stringify(agoraParam))
- apiPilot.startLiveshare()
+ apiPilot.setLiveshareConfig(ELiveTypeValue.Agora, JSON.stringify(agoraParam))
break
}
case 2: {
- message.info('rtmpParam:' + JSON.stringify(rtmpParam))
- apiPilot.setLiveshareConfig(2, JSON.stringify(rtmpParam))
- message.info(apiPilot.startLiveshare())
+ apiPilot.setLiveshareConfig(ELiveTypeValue.RTMP, JSON.stringify(rtmpParam))
break
}
case 3: {
- message.info('rtspParam:' + config.rtspPara)
- apiPilot.setLiveshareConfig(3, config.rtspPara)
- apiPilot.startLiveshare()
+ apiPilot.setLiveshareConfig(ELiveTypeValue.RTSP, JSON.stringify(rtspParam))
break
}
case 4: {
- message.info('gb28181Param:' + config.gb28181Para)
- apiPilot.setLiveshareConfig(4, config.gb28181Para)
- apiPilot.startLiveshare()
+ apiPilot.setLiveshareConfig(ELiveTypeValue.GB28181, JSON.stringify(gb28181Param))
break
}
}
+ const status = apiPilot.startLiveshare()
+ if (status) {
+ playVisiable.value = true
+ drawerVisible.value = true
+ message.success('success')
+ }
}
-const onGetStatus = () => {
- const status = apiPilot.getLiveshareStatus()
- message.info(status, 5)
+
+const showLivingStatus = () => {
+ drawerVisible.value = !drawerVisible.value
}
-const onGetConfig = () => {
- const status = apiPilot.getLiveshareConfig()
- message.info(status, 5)
+
+const onStop = () => {
+ const status = apiPilot.stopLiveshare()
+ if (status) {
+ message.success('success')
+ playVisiable.value = false
+ drawerVisible.value = false
+ setTimeout(() => {
+ let key: (keyof LiveStreamStatus)
+ for (key in liveStreamStatus) {
+ if (key === 'type') {
+ continue
+ }
+ liveStreamStatus[key] = -1
+ }
+ }, 2000)
+ }
}
</script>
diff --git a/src/pages/page-pilot/pilot-media.vue b/src/pages/page-pilot/pilot-media.vue
index 48b73ad..11b9c20 100644
--- a/src/pages/page-pilot/pilot-media.vue
+++ b/src/pages/page-pilot/pilot-media.vue
@@ -1,18 +1,16 @@
<template>
- <div
- class="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
- >
- <p class="fz16 ml10 mt10 mb10 color-text-title color-font-bold">
- If Enabled, Pilot will upload photos or videos to the server
- automatically.
+ <a-layout>
+ <div class="width100 flex-column flex-justify-start flex-align-start" style="background-color: white;">
+
+ <p class="fz16 ml10 mt15 mb10 color-text-title color-font-bold" style="color: #939393">
+ When enabled, photos and videos will be automatically uploaded to this server
</p>
<div
- class="flex-row flex-align-center flex-justify-between"
- style="width: 100%"
+ class="flex-row flex-align-center mt20"
+ style="width: 100%;"
>
- <p class="ml10 mb0 fz16" style="color: black">Auto Upload Photos</p>
+ <p class="ml10 mb0 fz16" style="margin-right: 73vw;">Auto Photo Upload</p>
<a-switch
- class="mt0 mb0"
v-model:checked="enablePhotoUpload"
@change="onPhotoUpload"
></a-switch>
@@ -23,34 +21,36 @@
>
<a-radio-group
class="mt10 ml20"
- v-if="enablePhotoUpload == true"
+ v-if="enablePhotoUpload === true"
v-model:value="photoType"
defaultChecked="0"
@change="onPhototype"
>
- <a-radio :value="0">Original Photo</a-radio>
- <a-radio class="ml20" :value="1">Preview Photo</a-radio>
+ <a-radio :value="EPhotoType.Original">Original Photo</a-radio>
+ <a-radio class="ml20" :value="EPhotoType.Preview">Preview Photo</a-radio>
</a-radio-group>
</div>
- <a-divider dashed class="mt10 mb0"></a-divider>
-
+ <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
+ <a-divider />
+ </div>
<div
- class="flex-row flex-align-center flex-justify-between mt10"
- style="width: 100%"
+ class="flex-row flex-align-center"
+ style="width: 100%; margin-top: -10px;"
>
- <p class="ml10 mb0 fz16" style="color: black">Auto Upload Video</p>
+ <p class="ml10 mb0 fz16" style="margin-right: 73vw;">Auto Video Upload</p>
<a-switch
@change="onVideoUpload"
v-model:checked="enableVideoUpload"
></a-switch>
</div>
- <a-divider dashed class="mt10 mb0"></a-divider>
-
+ <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
+ <a-divider />
+ </div>
<div
- class="flex-row flex-align-center flex-justify-between mt20"
- style="width: 100%"
+ class="flex-row flex-align-center flex-justify-between mb15"
+ style="width: 100%; margin-top: -10px;"
>
- <p class="ml10 mb0 fz16 color-font-bold" style="color: black">
+ <p class="ml10 mb0 fz16 color-font-bold">
Path for uploading media resources in dual-controller mode
</p>
<a-radio-group
@@ -59,11 +59,12 @@
button-style="solid"
@change="onUploadPath"
>
- <a-radio-button :value="0">Mine</a-radio-button>
- <a-radio-button :value="1">Another</a-radio-button>
+ <a-radio-button :value="EDownloadOwner.Mine">Mine</a-radio-button>
+ <a-radio-button :value="EDownloadOwner.Others">Another</a-radio-button>
</a-radio-group>
</div>
</div>
+ </a-layout>
</template>
<script lang="ts" setup>
@@ -71,58 +72,31 @@
import { onMounted, ref } from 'vue'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
+import { EComponentName, EPhotoType, EDownloadOwner } from '/@/types'
const root = getRoot()
-const enablePhotoUpload = ref<boolean>(true)
-const enableVideoUpload = ref<boolean>(false)
-const photoType = ref<number>(1)
-const uploadPath = ref<number>(0)
+const enablePhotoUpload = ref<boolean>(apiPilot.getAutoUploadPhoto())
+const enableVideoUpload = ref<boolean>(apiPilot.getAutoUploadVideo())
+const photoType = ref<number>(apiPilot.getUploadPhotoType())
+const uploadPath = ref<number>(apiPilot.getDownloadOwner())
-onMounted(() => {
- message.info('After setting, please use the physical button of the remote control to return, otherwise the setting is invalid.')
-
- enablePhotoUpload.value =
- apiPilot.getAutoUploadPhoto() === undefined
- ? true
- : apiPilot.getAutoUploadPhoto()
- enableVideoUpload.value =
- apiPilot.getAutoUploadVideo() === undefined
- ? false
- : apiPilot.getAutoUploadVideo()
- photoType.value =
- apiPilot.getUploadPhotoType() === undefined
- ? 1
- : apiPilot.getUploadPhotoType()
- uploadPath.value =
- apiPilot.getDownloadOwner() === undefined ? 0 : apiPilot.getDownloadOwner()
-
- console.log(
- enablePhotoUpload.value,
- enableVideoUpload.value,
- photoType.value,
- uploadPath.value
- )
- apiPilot.setComponentParam('media', {
- autoUploadPhoto: enablePhotoUpload.value,
- autoUploadPhotoType: photoType.value,
- autoUploadVideo: enableVideoUpload.value
- })
-})
const onPhotoUpload = () => {
apiPilot.setAutoUploadPhoto(enablePhotoUpload.value)
}
const onVideoUpload = () => {
- console.log(enableVideoUpload.value)
apiPilot.setAutoUploadVideo(enableVideoUpload.value)
}
const onPhototype = () => {
- console.log(photoType.value)
apiPilot.setUploadPhotoType(photoType.value)
}
const onUploadPath = (e: any) => {
apiPilot.setDownloadOwner(uploadPath.value)
}
+onMounted(() => {
+ console.error(apiPilot.getUploadPhotoType())
+ console.error(apiPilot.getAutoUploadVideo())
+})
</script>
<style lang="scss" scoped>
diff --git a/src/pages/project-app/home.vue b/src/pages/project-app/home.vue
new file mode 100644
index 0000000..c06bc5c
--- /dev/null
+++ b/src/pages/project-app/home.vue
@@ -0,0 +1,57 @@
+<template>
+ <a-layout class="width-100 flex-display" style="height: 100vh">
+ <a-layout-header class="header">
+ <Topbar />
+ </a-layout-header>
+ <a-layout-content>
+ <router-view />
+ </a-layout-content>
+
+ </a-layout>
+</template>
+
+<script lang="ts" setup>
+import Topbar from './topbar.vue'
+import { message } from 'ant-design-vue'
+import { onMounted, reactive, ref, UnwrapRef, watch } from 'vue'
+import { getPlatformInfo, getUserInfo } from '/@/api/manage'
+import websocket from '/@/api/websocket'
+import { useGMapCover } from '/@/hooks/use-g-map-cover'
+import { getRoot } from '/@/root'
+import { useMyStore } from '/@/store'
+import { ELocalStorageKey, ERouterName } from '/@/types'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+
+interface FormState {
+ user: string
+ password: string
+}
+
+const root = getRoot()
+const showLogin = ref(true)
+
+onMounted(() => {
+ const token = localStorage.getItem(ELocalStorageKey.Token)
+ if (!token) {
+ root.$router.push(ERouterName.PROJECT)
+ }
+})
+
+</script>
+
+<style lang="scss" scoped>
+@import '/@/styles/index.scss';
+
+.fontBold {
+ font-weight: 500;
+ font-size: 18px;
+}
+
+.header {
+ background-color: black;
+ color: white;
+ height: 60px;
+ font-size: 15px;
+ padding: 0 20px;
+}
+</style>
diff --git a/src/pages/project-app/index.vue b/src/pages/project-app/index.vue
index d8359da..b5a855d 100644
--- a/src/pages/project-app/index.vue
+++ b/src/pages/project-app/index.vue
@@ -1,20 +1,18 @@
<template>
<div
- v-if="showLogin"
- class="login flex-column flex-justify-center flex-align-center m0 b0"
- >
+ class="login flex-column flex-justify-center flex-align-center m0 b0">
<a-image
style="width: 17vw; height: 10vw; margin-bottom: 50px"
- src="http://lofrev.net/wp-content/photos/2016/09/dji_logo_png.png"
+ :src="djiLogo"
/>
- <p class="logo fz35 pb50">Cloud API Demo</p>
+ <p class="fz35 pb50" style="color: #2d8cf0">Cloud API Demo</p>
<a-form
layout="inline"
:model="formState"
class="flex-row flex-justify-center flex-align-center"
>
<a-form-item>
- <a-input v-model:value="formState.user" placeholder="Username">
+ <a-input v-model:value="formState.username" placeholder="Username">
<template #prefix
><UserOutlined style="color: rgba(0, 0, 0, 0.25)"
/></template>
@@ -44,115 +42,41 @@
</a-form-item>
</a-form>
</div>
- <div v-else class="project-app-wrapper">
- <div class="left">
- <Sidebar />
- <div class="main-content uranus-scrollbar dark">
- <router-view />
- </div>
- </div>
- <div class="right">
- <div class="map-wrapper">
- <GMap />
- </div>
- <div class="media-wrapper" v-if="getMediaRoute()">
- <MediaPanel />
- </div>
- <div class="wayline-wrapper" v-if="getWaylineRoute()">
- <WaylinePanel />
- </div>
- </div>
- </div>
+
</template>
<script lang="ts" setup>
+import djiLogo from '/@/assets/icons/dji_logo.png'
import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { reactive, ref, UnwrapRef } from 'vue'
-import Sidebar from './sidebar.vue'
-import { login } from '/@/api/manage'
-import websocket from '/@/api/websocket'
-import GMap from '/@/components/GMap.vue'
-import MediaPanel from '/@/components/MediaPanel.vue'
-import WaylinePanel from '/@/components/wayline-panel.vue'
-import { useGMapCover } from '/@/hooks/use-g-map-cover'
+import { login, LoginBody } from '/@/api/manage'
import { getRoot } from '/@/root'
-import { useMyStore } from '/@/store'
-
-interface FormState {
- user: string
- password: string
-}
+import { ELocalStorageKey, ERouterName, EUserType } from '/@/types'
+import router from '/@/router'
const root = getRoot()
-const showLogin = ref(true)
-const store = useMyStore()
-const formState: UnwrapRef<FormState> = reactive({
- user: 'adminPC',
- password: 'adminPC'
+const formState: UnwrapRef<LoginBody> = reactive({
+ username: 'adminPC',
+ password: 'adminPC',
+ flag: EUserType.Web,
})
-let socket = {} as any
-const gMapCoverHook = useGMapCover()
const onSubmit = async (e: any) => {
- const result = await login({
- username: formState.user,
- password: formState.password
- })
+ const result = await login(formState)
if (result.code === 0) {
console.log(result)
- localStorage.setItem('x-auth-token', result.data.access_token)
- localStorage.setItem('workspace-id', result.data.workspace_id)
- localStorage.setItem('username', result.data.username)
- showLogin.value = false
- message.info('login success')
- socket = websocket.init(wsGetMsg)
+ localStorage.setItem(ELocalStorageKey.Token, result.data.access_token)
+ localStorage.setItem(ELocalStorageKey.WorkspaceId, result.data.workspace_id)
+ localStorage.setItem(ELocalStorageKey.Username, result.data.username)
+ localStorage.setItem(ELocalStorageKey.UserId, result.data.user_id)
+ localStorage.setItem(ELocalStorageKey.Flag, EUserType.Web.toString())
+ root.$router.push(ERouterName.MEMBERS)
+ } else {
+ message.error(result.message)
}
}
-// function wsInfo (data) {
-// store.commit('SET_DEVICE_INFO', data)
-// }
-// function getDeviceInfo () {
-// const info = store.state.DeviceInfo
-// console.log(info)
-const wsGetMsg = async (res: any) => {
- const payload = JSON.parse(res.data)
- // console.log(payload)
- switch (payload.biz_code) {
- case 'gateway_osd': {
- store.commit('SET_GATEWAY_INFO', payload.data)
- break
- }
- case 'device_osd': {
- store.commit('SET_DEVICE_INFO', payload.data)
- break
- }
- case 'map_element_create': {
- store.commit('SET_MAP_ELEMENT_CREATE', payload.data)
- break
- }
- case 'map_element_update': {
- store.commit('SET_MAP_ELEMENT_UPDATE', payload.data)
- break
- }
- case 'map_element_delete': {
- store.commit('SET_MAP_ELEMENT_DELETE', payload.data)
- break
- }
-
- default:
- break
- }
-}
-
-function getMediaRoute () {
- return root.$route.name === 'media'
-}
-
-function getWaylineRoute () {
- return root.$route.name === 'wayline'
-}
</script>
<style lang="scss" scoped>
@@ -160,49 +84,5 @@
.login {
background-color: $dark-highlight;
height: 100vh;
-}
-.logo {
- color: $primary;
-}
-.project-app-wrapper {
- display: flex;
- position: absolute;
- transition: width 0.2s ease;
- height: 100%;
- width: 100%;
- .left {
- width: 450px;
- display: flex;
- background-color: #232323;
- float: left;
- }
- .right {
- width: 100%;
- height: 100%;
- .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;
- padding: 16px;
- }
- .wayline-wrapper {
- position: absolute;
- top: 0;
- bottom: 0;
- z-index: 100;
- background: #f6f8fa;
- padding: 16px;
- }
}
</style>
diff --git a/src/pages/project-app/projects/create-plan.vue b/src/pages/project-app/projects/create-plan.vue
new file mode 100644
index 0000000..3891f2a
--- /dev/null
+++ b/src/pages/project-app/projects/create-plan.vue
@@ -0,0 +1,251 @@
+<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>
diff --git a/src/pages/project-app/projects/devices.vue b/src/pages/project-app/projects/devices.vue
new file mode 100644
index 0000000..71201ec
--- /dev/null
+++ b/src/pages/project-app/projects/devices.vue
@@ -0,0 +1,518 @@
+
+<template>
+ <a-menu v-model:selectedKeys="current" mode="horizontal" @select="select">
+ <a-menu-item :key="EDeviceTypeName.Aircraft" class="ml20">
+ Aircraft
+ </a-menu-item>
+ <a-menu-item :key="EDeviceTypeName.Dock">
+ Dock
+ </a-menu-item>
+ </a-menu>
+ <div class="table flex-display flex-column">
+ <a-table :columns="columns" :data-source="data.device" :pagination="paginationProp" @change="refreshData" row-key="device_sn" :expandedRowKeys="expandRows"
+ :row-selection="rowSelection" :rowClassName="rowClassName" :scroll="{ x: '100%', y: 600 }"
+ :expandIcon="expandIcon" :loading="loading">
+ <template v-for="col in ['nickname']" #[col]="{ text, record }" :key="col">
+ <div>
+ <a-input
+ v-if="editableData[record.device_sn]"
+ v-model:value="editableData[record.device_sn][col]"
+ style="margin: -5px 0"
+ />
+ <template v-else>
+ {{ text }}
+ </template>
+ </div>
+ </template>
+ <template v-for="col in ['sn', 'workspace']" #[col]="{ text }" :key="col">
+ <a-tooltip :title="text">
+ <span>{{ text }}</span>
+ </a-tooltip>
+ </template>
+ <template #status="{ text }">
+ <span v-if="text" class="flex-row flex-align-center">
+ <span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: green;" />
+ <span>Online</span>
+ </span>
+ <span class="flex-row flex-align-center" v-else>
+ <span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: red;" />
+ <span>Offline</span>
+ </span>
+ </template>
+ <template #action="{ record }">
+ <div class="editable-row-operations">
+ <span v-if="editableData[record.device_sn]">
+ <a-tooltip title="Confirm changes">
+ <span @click="save(record)" style="color: #28d445;"><CheckOutlined /></span>
+ </a-tooltip>
+ <a-tooltip title="Modification canceled">
+ <span @click="() => delete editableData[record.device_sn]" class="ml15" style="color: #e70102;"><CloseOutlined /></span>
+ </a-tooltip>
+ </span>
+ <span v-else class="flex-align-center flex-row" style="color: #2d8cf0">
+ <a-tooltip v-if="current.indexOf(EDeviceTypeName.Dock) !== -1" title="Hms Info">
+ <FileSearchOutlined @click="showHms(record)"/>
+ </a-tooltip>
+ <a-tooltip title="Edit">
+ <EditOutlined @click="edit(record)" class="ml10" />
+ </a-tooltip>
+ <a-tooltip title="Delete">
+ <DeleteOutlined @click="() => { deleteTip = true, deleteSn = record.device_sn }" class="ml15" />
+ </a-tooltip>
+ </span>
+ </div>
+ </template>
+
+ </a-table>
+ <a-modal v-model:visible="deleteTip" width="450px" :closable="false" centered :okButtonProps="{ danger: true }" @ok="unbind">
+ <p class="pt10 pl20" style="height: 50px;">Delete device from workspace?</p>
+ <template #title>
+ <div class="flex-row flex-justify-center">
+ <span>Delete devices</span>
+ </div>
+ </template>
+ </a-modal>
+
+ <a-drawer
+ title="Hms Info"
+ placement="right"
+ v-model:visible="hmsVisible"
+ :width="800">
+ <div class="flex-row flex-align-center">
+ <div style="width: 240px;">
+ <a-range-picker
+ v-model:value="time"
+ format="YYYY-MM-DD"
+ :placeholder="['Start Time', 'End Time']"
+ @change="onTimeChange"/>
+ </div>
+ <div class="ml5">
+ <a-select
+ style="width: 150px"
+ v-model:value="param.level"
+ @select="onLevelSelect">
+ <a-select-option
+ v-for="item in levels"
+ :key="item.label"
+ :value="item.value"
+ >
+ {{ item.label }}
+ </a-select-option>
+ </a-select>
+ </div>
+ <div class="ml5">
+ <a-select
+ v-model:value="param.domain"
+ :disabled="!param.children_sn || !param.device_sn"
+ style="width: 150px"
+ @select="onDeviceTypeSelect">
+ <a-select-option
+ v-for="item in deviceTypes"
+ :key="item.label"
+ :value="item.value"
+ >
+ {{ item.label }}
+ </a-select-option>
+ </a-select>
+ </div>
+ <div class="ml5">
+ <a-input-search
+ v-model:value="param.message"
+ placeholder="input search message"
+ style="width: 200px"
+ @search="getHms"/>
+ </div>
+ </div>
+ <div>
+ <a-table :columns="hmsColumns" :scroll="{ x: '100%', y: 600 }" :data-source="hmsData.data" :pagination="hmsPaginationProp" @change="refreshHmsData" row-key="hms_id"
+ :rowClassName="rowClassName" :loading="loading">
+ <template #time="{ record }">
+ <div>{{ record.create_time }}</div>
+ <div :style="record.update_time ? '' : record.level === EHmsLevel.CAUTION ? 'color: orange;' :
+ record.level === EHmsLevel.WARN ? 'color: red;' : 'color: #28d445;'">{{ record.update_time ?? 'It is happening...' }}</div>
+ </template>
+ <template #level="{ text }">
+ <div class="flex-row flex-align-center">
+ <div :class="text === EHmsLevel.CAUTION ? 'caution' : text === EHmsLevel.WARN ? 'warn' : 'notice'" style="width: 10px; height: 10px; border-radius: 50%;"></div>
+ <div style="margin-left: 3px;">{{ EHmsLevel[text] }}</div>
+ </div>
+ </template>
+ <template v-for="col in ['code', 'message']" #[col]="{ text }" :key="col">
+ <a-tooltip :title="text">
+ <span>{{ text }}</span>
+ </a-tooltip>
+ </template>
+ </a-table>
+ </div>
+ </a-drawer>
+ </div>
+</template>
+<script lang="ts" setup>
+import { message } from 'ant-design-vue'
+import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
+import { h, onMounted, reactive, ref, UnwrapRef } from 'vue'
+import { IPage } from '/@/api/http/type'
+import { BindBody, bindDevice, getBindingDevices, getDeviceHms, HmsQueryBody, unbindDevice, updateDevice } from '/@/api/manage'
+import { EDeviceTypeName, EHmsLevel, ELocalStorageKey } from '/@/types'
+import { EditOutlined, CheckOutlined, CloseOutlined, DeleteOutlined, FileSearchOutlined } from '@ant-design/icons-vue'
+import { Device, DeviceHms } from '/@/types/device'
+import moment, { Moment } from 'moment'
+
+interface DeviceData {
+ device: Device[]
+}
+const loading = ref(true)
+const deleteTip = ref<boolean>(false)
+const deleteSn = ref<string>()
+const hmsVisible = ref<boolean>(false)
+const columns: ColumnProps[] = [
+ { title: 'Model', dataIndex: 'device_name', width: '10%', className: 'titleStyle' },
+ { title: 'SN', dataIndex: 'device_sn', width: '10%', className: 'titleStyle', ellipsis: true, slots: { customRender: 'sn' } },
+ {
+ title: 'Name',
+ dataIndex: 'nickname',
+ width: '15%',
+ sorter: (a: Device, b: Device) => a.nickname.localeCompare(b.nickname),
+ className: 'titleStyle',
+ ellipsis: true,
+ slots: { customRender: 'nickname' }
+ },
+ { title: 'Firmware Version', dataIndex: 'firmware_version', width: '10%', className: 'titleStyle' },
+ { title: 'Status', dataIndex: 'status', width: '100px', className: 'titleStyle', slots: { customRender: 'status' } },
+ {
+ title: 'Workspace',
+ dataIndex: 'workspace_name',
+ width: '10%',
+ className: 'titleStyle',
+ ellipsis: true,
+ slots: { customRender: 'workspace' },
+ customRender: ({ text, record, index }) => {
+ const obj = {
+ children: text,
+ props: {} as any,
+ }
+ if (current.value.indexOf(EDeviceTypeName.Aircraft) !== -1 || (!record.child_device_sn && record.domain === EDeviceTypeName.Dock)) {
+ return obj
+ }
+
+ obj.props.rowSpan = record.domain === EDeviceTypeName.Dock ? 2 : 0
+ return obj
+ }
+ },
+ { title: 'Joined', dataIndex: 'bound_time', width: '15%', sorter: (a: Device, b: Device) => a.bound_time.localeCompare(b.bound_time), className: 'titleStyle' },
+ { title: 'Last Online', dataIndex: 'login_time', width: '15%', sorter: (a: Device, b: Device) => a.login_time.localeCompare(b.login_time), className: 'titleStyle' },
+ {
+ title: 'Actions',
+ dataIndex: 'actions',
+ className: 'titleStyle',
+ slots: { customRender: 'action' }
+ },
+]
+const expandIcon = (props: any) => {
+ if (judgeCurrentType(EDeviceTypeName.Dock) && !props.expanded) {
+ return h('div',
+ {
+ style: 'border-left: 2px solid rgb(200,200,200); border-bottom: 2px solid rgb(200,200,200); height: 16px; width: 16px; float: left;',
+ class: 'mt-5 ml0',
+ })
+ }
+}
+const rowClassName = (record: any, index: number) => {
+ const className = []
+ if ((index & 1) === 0) {
+ className.push('table-striped')
+ }
+ if (record.domain !== EDeviceTypeName.Dock) {
+ className.push('child-row')
+ }
+ return className.toString().replaceAll(',', ' ')
+}
+
+const expandRows = ref<string[]>([])
+const data = reactive<DeviceData>({
+ device: []
+})
+
+const paginationProp = reactive({
+ pageSizeOptions: ['20', '50', '100'],
+ showQuickJumper: true,
+ showSizeChanger: true,
+ pageSize: 50,
+ current: 1,
+ total: 0
+})
+
+const rowSelection = {
+ onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => {
+ console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
+ },
+ onSelect: (record: any, selected: boolean, selectedRows: []) => {
+ console.log(record, selected, selectedRows)
+ },
+ onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => {
+ console.log(selected, selectedRows, changeRows)
+ },
+ getCheckboxProps: (record: any) => ({
+ disabled: judgeCurrentType(EDeviceTypeName.Dock) && record.domain !== EDeviceTypeName.Dock,
+ style: judgeCurrentType(EDeviceTypeName.Dock) && record.domain !== EDeviceTypeName.Dock ? 'display: none' : ''
+ }),
+}
+type Pagination = TableState['pagination']
+
+const body: IPage = {
+ page: 1,
+ total: 0,
+ page_size: 50
+}
+const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+
+const editableData: UnwrapRef<Record<string, Device>> = reactive({})
+
+const current = ref([EDeviceTypeName.Aircraft])
+
+onMounted(() => {
+ getDevices(workspaceId, body, current.value[0])
+})
+
+function judgeCurrentType (type: EDeviceTypeName): boolean {
+ return current.value.indexOf(type) !== -1
+}
+
+function getDevices (workspaceId: string, body: IPage, domain: string) {
+ loading.value = true
+ getBindingDevices(workspaceId, body, domain).then(res => {
+ if (res.code !== 0) {
+ return
+ }
+ const resData: Device[] = res.data.list
+ expandRows.value = []
+ resData.forEach((val: any) => {
+ if (val.children) {
+ val.children = [val.children]
+ }
+ if (judgeCurrentType(EDeviceTypeName.Dock)) {
+ expandRows.value.push(val.device_sn)
+ }
+ })
+ data.device = resData
+ paginationProp.total = res.data.pagination.total
+ paginationProp.current = res.data.pagination.page
+ loading.value = false
+ })
+}
+
+function refreshData (page: Pagination) {
+ body.page = page?.current!
+ body.page_size = page?.pageSize!
+ getDevices(workspaceId, body, current.value[0])
+}
+
+function edit (record: Device) {
+ editableData[record.device_sn] = record
+}
+
+function save (record: Device) {
+ delete editableData[record.device_sn]
+ updateDevice({ nickname: record.nickname }, workspaceId, record.device_sn)
+}
+
+function showDeleteTip (sn: any) {
+ deleteTip.value = true
+}
+
+function unbind () {
+ deleteTip.value = false
+ unbindDevice(deleteSn.value?.toString()!).then(res => {
+ if (res.code !== 0) {
+ return
+ }
+ getDevices(workspaceId, body, current.value[0])
+ })
+}
+
+function select (item: any) {
+ getDevices(workspaceId, body, item.key)
+}
+
+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: 'Hms Message', dataIndex: 'message_en', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } },
+]
+
+interface DeviceHmsData {
+ data: DeviceHms[]
+}
+const hmsData = reactive<DeviceHmsData>({
+ data: []
+})
+
+const hmsPaginationProp = reactive({
+ pageSizeOptions: ['20', '50', '100'],
+ showQuickJumper: true,
+ showSizeChanger: true,
+ pageSize: 50,
+ current: 1,
+ total: 0
+})
+
+const hmsPage: IPage = {
+ page: 1,
+ total: 0,
+ page_size: 50
+}
+
+function showHms (dock: Device) {
+ hmsVisible.value = true
+ if (dock.domain === EDeviceTypeName.Dock) {
+ param.domain = ''
+ getDeviceHmsBySn(dock.device_sn, dock.children?.[0].device_sn ?? '')
+ }
+ if (dock.domain === EDeviceTypeName.Aircraft) {
+ param.domain = EDeviceTypeName.Aircraft
+ getDeviceHmsBySn('', dock.device_sn)
+ }
+}
+
+function refreshHmsData (page: Pagination) {
+ hmsPage.page = page?.current!
+ hmsPage.page_size = page?.pageSize!
+ getHms()
+}
+
+const param = reactive<HmsQueryBody>({
+ sns: [],
+ device_sn: '',
+ children_sn: '',
+ language: 'en',
+ begin_time: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0),
+ end_time: new Date().setHours(23, 59, 59, 999),
+ domain: '',
+ level: '',
+ message: ''
+})
+
+const levels = [
+ {
+ label: 'All',
+ value: ''
+ }, {
+ label: EHmsLevel[0],
+ value: EHmsLevel.NOTICE
+ }, {
+ label: EHmsLevel[1],
+ value: EHmsLevel.CAUTION
+ }, {
+ label: EHmsLevel[2],
+ value: EHmsLevel.WARN
+ }
+]
+
+const deviceTypes = [
+ {
+ label: 'All',
+ value: ''
+ }, {
+ label: EDeviceTypeName.Aircraft,
+ value: EDeviceTypeName.Aircraft
+ }, {
+ label: EDeviceTypeName.Dock,
+ value: EDeviceTypeName.Dock
+ }
+]
+
+const time = ref([moment(param.begin_time), moment(param.end_time)])
+
+function getHms () {
+ getDeviceHms(param, workspaceId, hmsPage)
+ .then(res => {
+ hmsPaginationProp.total = res.data.pagination.total
+ hmsPaginationProp.current = res.data.pagination.page
+ hmsData.data = res.data.list
+ hmsData.data.forEach(hms => {
+ hms.domain = hms.sn === param.children_sn ? EDeviceTypeName.Aircraft : EDeviceTypeName.Dock
+ })
+ })
+}
+
+function getDeviceHmsBySn (sn: string, childSn: string) {
+ param.device_sn = sn
+ param.children_sn = childSn
+ param.sns = [param.device_sn, param.children_sn]
+ getHms()
+}
+
+function onTimeChange (newTime: [Moment, Moment]) {
+ param.begin_time = newTime[0].valueOf()
+ param.end_time = newTime[1].valueOf()
+ getHms()
+}
+
+function onDeviceTypeSelect (val: string) {
+ param.sns = [param.device_sn, param.children_sn]
+ if (val === EDeviceTypeName.Dock) {
+ param.sns = [param.device_sn, '']
+ }
+ if (val === EDeviceTypeName.Aircraft) {
+ param.sns = ['', param.children_sn]
+ }
+ getHms()
+}
+
+function onLevelSelect (val: number) {
+ param.level = val
+ getHms()
+}
+</script>
+<style lang="scss">
+
+.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;
+}
+.child-row {
+ height: 70px;
+}
+.notice {
+ background: $success;
+ overflow: hidden;
+ cursor: pointer;
+}
+.caution {
+ background: orange;
+ cursor: pointer;
+ overflow: hidden;
+}
+.warn {
+ background: red;
+ cursor: pointer;
+ overflow: hidden;
+}
+</style>
diff --git a/src/pages/project-app/projects/dock.vue b/src/pages/project-app/projects/dock.vue
new file mode 100644
index 0000000..72fd32f
--- /dev/null
+++ b/src/pages/project-app/projects/dock.vue
@@ -0,0 +1,100 @@
+<template>
+ <div>
+ <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">Devices</a-col>
+ <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>
+ </div>
+ </div>
+ </div>
+ <div v-else>
+ <a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive } from '@vue/reactivity'
+import { message } from 'ant-design-vue'
+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 { useMyStore } from '/@/store'
+import { getBindingDevices } from '/@/api/manage'
+import { IPage } from '/@/api/http/type'
+
+const store = useMyStore()
+
+const docksData = reactive({
+ data: [] as Device[]
+})
+
+const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+
+onMounted(() => {
+ getDocks()
+})
+const body: IPage = {
+ page: 1,
+ total: 0,
+ page_size: 100
+}
+function getDocks () {
+ 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)
+ })
+}
+
+function selectDock (dock: Device) {
+ store.commit('SET_SELECT_DOCK_INFO', dock)
+}
+
+</script>
+
+<style lang="scss" scoped>
+.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;
+ flex-direction: row;
+ align-items: center;
+ height: 30px;
+ font-weight: bold;
+ margin: 0px 10px 0 10px;
+ }
+}
+</style>
diff --git a/src/pages/project-app/projects/layer.vue b/src/pages/project-app/projects/layer.vue
index 89b4020..325fd86 100644
--- a/src/pages/project-app/projects/layer.vue
+++ b/src/pages/project-app/projects/layer.vue
@@ -1,5 +1,12 @@
<template>
<div class="project-layer-wrapper">
+ <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">Annotations</a-col>
+ <a-col :span="1"></a-col>
+ </a-row>
+ </div>
<LayersTree
:layer-data="mapLayers"
class="project-layer-content"
@@ -389,9 +396,6 @@
<style lang="scss" scoped>
@import '/@/styles/index.scss';
-.project-layer-wrapper {
- padding-top: 16px;
-}
</style>
<style lang="scss">
.drawer-element-wrapper {
diff --git a/src/pages/project-app/projects/livestream.vue b/src/pages/project-app/projects/livestream.vue
index 5794bab..ad9a9f9 100644
--- a/src/pages/project-app/projects/livestream.vue
+++ b/src/pages/project-app/projects/livestream.vue
@@ -1,65 +1,56 @@
<template>
<div class="flex-column flex-justify-start flex-align-center">
- <a-button
- class="mt10 "
- style="width:90%"
- type="primary"
- @click="onAgoraLiveStream"
- >Agora Live</a-button
+ <router-link
+ style="width: 90%; margin: auto;"
+ v-for="item in options"
+ :key="item.key"
+ :to="item.path"
+ :class="{
+ 'menu-item': true,
+ }"
>
<a-button
class="mt10"
- style="width:90%"
+ style="width:100%;"
type="primary"
- @click="onOthersLive"
- >RTMP/GB28181 Live</a-button
+ @click="selectLivestream(item.routeName)"
+ >{{ item.label }}</a-button
>
+ </router-link>
</div>
- <div v-if="enableAgoraLive">
- <a-modal
- style="top:0"
- v-model:visible="enableAgoraLive"
- title="Agora Live"
- width="100%"
- :maskClosable="false"
- wrapClassName="full-modal"
- :footer="null"
- >
- <LiveAgora />
- </a-modal>
- </div>
- <div v-if="enableOthersLive">
- <a-modal
- style="top:0"
- v-model:visible="enableOthersLive"
- title="RTMP/GB28181/RTSP Live"
- width="100%"
- :maskClosable="false"
- wrapClassName="full-modal"
- :footer="null"
- >
- <LiveOthers />
- </a-modal>
+ <div class="live" v-if="showLive">
+ <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>
</template>
<script lang="ts" setup>
-import { ref } from 'vue'
-import LiveAgora from './livestream-agora.vue'
-import LiveOthers from './livestream-others.vue'
+import { message } from 'ant-design-vue'
+import { onMounted, ref, watch } from 'vue'
+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 enableAgoraLive = ref(false)
-const enableOthersLive = ref(false)
-const onAgoraLiveStream = () => {
- console.log('agora')
- enableAgoraLive.value = true
+const options = [
+ { key: 0, label: 'Agora Live', path: '/' + ERouterName.LIVESTREAM + '/' + ERouterName.LIVING, routeName: 'LiveAgora' },
+ { key: 1, label: 'RTMP/GB28181 Live', path: '/' + ERouterName.LIVESTREAM + '/' + ERouterName.LIVING, routeName: 'LiveOthers' }
+]
+
+const selectLivestream = (route: string) => {
+ routeName.value = route
}
-const onOthersLive = () => {
- console.log('liveview')
- enableOthersLive.value = true
-}
+
+onMounted(() => {
+ watch(() => root.$route.name, data => {
+ showLive.value = data === ERouterName.LIVING
+ },
+ {
+ deep: true
+ })
+})
</script>
<style lang="scss">
@@ -79,4 +70,17 @@
flex: 1;
}
}
+.live {
+ position: absolute;
+ z-index: 1;
+ right: 50%;
+ left: 50%;
+ top: 50%;
+ margin: auto;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ width: 800px;
+ height: 700px;
+ background: #232323;
+}
</style>
diff --git a/src/pages/project-app/projects/media.vue b/src/pages/project-app/projects/media.vue
index 2923dfe..6995bcd 100644
--- a/src/pages/project-app/projects/media.vue
+++ b/src/pages/project-app/projects/media.vue
@@ -1,6 +1,5 @@
<template>
<div class="project-media-wrapper">
- Media
</div>
</template>
diff --git a/src/pages/project-app/projects/members.vue b/src/pages/project-app/projects/members.vue
new file mode 100644
index 0000000..be0ad1e
--- /dev/null
+++ b/src/pages/project-app/projects/members.vue
@@ -0,0 +1,169 @@
+
+<template>
+ <div class="table flex-display flex-column">
+ <a-table :columns="columns" :data-source="data.member" :pagination="paginationProp" @change="refreshData" row-key="user_id"
+ :row-selection="rowSelection" :rowClassName="(record, index) => ((index % 2) === 0 ? 'table-striped' : null)" :scroll="{ x: '100%', y: 600 }">
+ <template v-for="col in ['mqtt_username', 'mqtt_password']" #[col]="{ text, record }" :key="col">
+ <div>
+ <a-input
+ v-if="editableData[record.user_id]"
+ v-model:value="editableData[record.user_id][col]"
+ style="margin: -5px 0"
+ />
+ <template v-else>
+ {{ text }}
+ </template>
+ </div>
+ </template>
+ <template #action="{ record }">
+ <div class="editable-row-operations">
+ <span v-if="editableData[record.user_id]">
+ <a-tooltip title="Confirm changes">
+ <span @click="save(record)" style="color: #28d445;"><CheckOutlined /></span>
+ </a-tooltip>
+ <a-tooltip title="Modification canceled">
+ <span @click="() => delete editableData[record.user_id]" class="ml15" style="color: #e70102;"><CloseOutlined /></span>
+ </a-tooltip>
+ </span>
+ <span v-else class="fz18 flex-align-center flex-row" style="color: #2d8cf0">
+ <EditOutlined @click="edit(record)" />
+ </span>
+ </div>
+ </template>
+
+ </a-table>
+ </div>
+</template>
+<script lang="ts" setup>
+import { message, 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 { getAllUsersInfo, updateUserInfo } from '/@/api/manage'
+import { ELocalStorageKey } from '/@/types'
+import { EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
+
+export interface Member {
+ user_id: string
+ username: string
+ user_type: string
+ workspace_name: string
+ create_time: string
+ mqtt_username: string
+ mqtt_password: string
+}
+
+interface MemberData {
+ member: Member[]
+}
+const columns = [
+ { title: 'Account', dataIndex: 'username', width: 250, sorter: (a: Member, b: Member) => a.username.localeCompare(b.username), className: 'titleStyle' },
+ { title: 'User Type', dataIndex: 'user_type', width: 250, className: 'titleStyle' },
+ { title: 'Workspace Name', dataIndex: 'workspace_name', width: 250, className: 'titleStyle' },
+ { title: 'Mqtt Username', dataIndex: 'mqtt_username', width: 250, className: 'titleStyle', slots: { customRender: 'mqtt_username' } },
+ { title: 'Mqtt Password', dataIndex: 'mqtt_password', width: 250, className: 'titleStyle', slots: { customRender: 'mqtt_password' } },
+ { title: 'Joined', dataIndex: 'create_time', width: 250, sorter: (a: Member, b: Member) => a.create_time.localeCompare(b.create_time), className: 'titleStyle' },
+ { title: 'Action', dataIndex: 'action', className: 'titleStyle', slots: { customRender: 'action' } },
+]
+
+const data = reactive<MemberData>({
+ member: []
+})
+
+const editableData: UnwrapRef<Record<string, Member>> = reactive({})
+
+const paginationProp = reactive({
+ pageSizeOptions: ['20', '50', '100'],
+ showQuickJumper: true,
+ showSizeChanger: true,
+ pageSize: 50,
+ current: 1,
+ total: 0
+})
+
+const rowSelection = {
+ onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => {
+ console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
+ },
+ onSelect: (record: any, selected: boolean, selectedRows: []) => {
+ console.log(record, selected, selectedRows)
+ },
+ onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => {
+ console.log(selected, selectedRows, changeRows)
+ },
+}
+type Pagination = TableState['pagination']
+
+const body: IPage = {
+ page: 1,
+ total: 0,
+ page_size: 50
+}
+const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+
+onMounted(() => {
+ getAllUsers(workspaceId, body)
+})
+
+function refreshData (page: Pagination) {
+ body.page = page?.current!
+ body.page_size = page?.pageSize!
+ getAllUsers(workspaceId, body)
+}
+
+function getAllUsers (workspaceId: string, page: IPage) {
+ getAllUsersInfo(workspaceId, page).then(res => {
+ const userList: Member[] = res.data.list
+ data.member = userList
+ paginationProp.total = res.data.pagination.total
+ paginationProp.current = res.data.pagination.page
+
+ })
+}
+
+function edit (record: Member) {
+ editableData[record.user_id] = record
+}
+
+function save (record: Member) {
+ delete editableData[record.user_id]
+ updateUserInfo(workspaceId, record.user_id, record).then(res => {
+ if (res.code !== 0) {
+ message.error(res.message)
+ }
+ })
+}
+
+</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>
diff --git a/src/pages/project-app/projects/task.vue b/src/pages/project-app/projects/task.vue
new file mode 100644
index 0000000..e594a8d
--- /dev/null
+++ b/src/pages/project-app/projects/task.vue
@@ -0,0 +1,38 @@
+<template>
+ <div>
+ <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="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"/>
+ </router-link>
+ </span>
+ <span v-else>
+ <router-link :to="{ name: 'task'}">
+ <MinusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = false"/>
+ </router-link>
+ </span>
+ </a-col>
+ <a-col :span="1"></a-col>
+ </a-row>
+ </div>
+ <div v-if="createPlanTip">
+ <router-view />
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue'
+import { onMounted, onUnmounted, ref } from 'vue'
+
+const createPlanTip = ref(false)
+
+</script>
+
+<style lang="scss">
+
+</style>
diff --git a/src/pages/project-app/projects/tsa.vue b/src/pages/project-app/projects/tsa.vue
index 5b8286e..f1d1d30 100644
--- a/src/pages/project-app/projects/tsa.vue
+++ b/src/pages/project-app/projects/tsa.vue
@@ -1,11 +1,463 @@
<template>
- <div class="project-tsa-wrapper">
- TSA
+ <div class="project-tsa-wrapper ">
+ <div>
+ <a-row>
+ <a-col :span="1"></a-col>
+ <a-col :span="11">My Username</a-col>
+ <a-col :span="11" align="right" style="font-weight: 700">{{ username }}</a-col>
+ <a-col :span="1"></a-col>
+ </a-row>
+ </div>
+ <div>
+ <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;">
+ <a-empty :image="noData" :image-style="{ height: '60px' }" />
+ </div>
+ <div v-else class="fz12" style="color: white;">
+ <div v-for="dock in onlineDocks.data" :key="dock.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;">
+ <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>
+ </div>
+ <div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
+ <div>
+ <span class="ml5 mr5"><RobotOutlined /></span>
+ <span class="font-bold" :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>
+ <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">
+ <div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' :
+ hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;">
+ <span :style="hmsInfo[dock.gateway.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.gateway.sn].length }}</span>
+ <span class="fz10">{{ hmsInfo[dock.gateway.sn].length > 99 ? '+' : ''}}</span>
+ </div>
+ <a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.gateway.sn]"
+ @visibleChange="readHms(hmsVisible[dock.gateway.sn], dock.gateway.sn)"
+ :overlayStyle="{width: '200px', height: '300px'}">
+ <div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution' :
+ hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;">
+ <span class="word-loop">{{ hmsInfo[dock.gateway.sn][0].message_en }}</span>
+ </div>
+ <template #content>
+ <a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true">
+ <a-collapse-panel v-for="hms in hmsInfo[dock.gateway.sn]" :key="hms.hms_id" :showArrow="false"
+ style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px"
+ :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
+ >
+ <template #header="{ isActive }">
+ <div class="flex-row flex-align-center" style="width: 130px;">
+ <div style="width: 110px;">
+ <span class="word-loop">{{ hms.message_en }}</span>
+ </div>
+ <div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center"
+ :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
+ >
+ <DoubleRightOutlined :rotate="isActive ? 90 : 0" />
+ </div>
+ </div>
+ </template>
+
+ <a-tooltip :title="hms.create_time">
+ <div style="color: white;" class="text-hidden">{{ hms.create_time }}</div>
+ </a-tooltip>
+ </a-collapse-panel>
+ </a-collapse>
+ </template>
+ </a-popover>
+ </div>
+ <div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div>
+ </div>
+ </div>
+ <div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
+ <div>
+ <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;'">
+ {{ deviceInfo[dock.sn] ? EModeCode[deviceInfo[dock.sn].mode_code] : EModeCode[EModeCode.Disconnected] }}
+ </span>
+ </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">
+ <div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' :
+ hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;">
+ <span :style="hmsInfo[dock.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.sn].length }}</span>
+ <span class="fz10">{{ hmsInfo[dock.sn].length > 99 ? '+' : ''}}</span>
+ </div>
+ <a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.sn]" @visibleChange="readHms(hmsVisible[dock.sn], dock.sn)"
+ :overlayStyle="{width: '200px', height: '300px'}">
+ <div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution' :
+ hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;">
+ <span class="word-loop">{{ hmsInfo[dock.sn][0].message_en }}</span>
+ </div>
+ <template #content>
+ <a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true">
+ <a-collapse-panel v-for="hms in hmsInfo[dock.sn]" :key="hms.hms_id" :showArrow="false"
+ style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px"
+ :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
+ >
+ <template #header="{ isActive }">
+ <div class="flex-row flex-align-center" style="width: 130px;">
+ <div style="width: 110px;">
+ <span class="word-loop">{{ hms.message_en }}</span>
+ </div>
+ <div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center"
+ :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
+ >
+ <DoubleRightOutlined :rotate="isActive ? 90 : 0" />
+ </div>
+ </div>
+ </template>
+
+ <a-tooltip :title="hms.create_time">
+ <div style="color: white;" class="text-hidden">{{ hms.create_time }}</div>
+ </a-tooltip>
+ </a-collapse-panel>
+ </a-collapse>
+ </template>
+ </a-popover>
+ </div>
+ <div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div>
+ </div>
+ </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)">
+ <a v-if="osdVisible.gateway_sn === dock.gateway.sn && osdVisible.visible"><EyeOutlined /></a>
+ <a v-else><EyeInvisibleOutlined /></a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </a-collapse-panel>
+ </a-collapse>
+ <a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;">
+ <a-collapse-panel :key="EDeviceTypeName.Aircraft" header="Online Devices" style="border-bottom: 1px solid #4f4f4f;">
+ <div v-if="onlineDevices.data.length === 0" style="height: 150px; color: white;">
+ <a-empty :image="noData" :image-style="{ height: '60px' }" />
+ </div>
+ <div v-else class="fz12" style="color: white;">
+ <div v-for="device in onlineDevices.data" :key="device.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;">
+ <div class="battery-slide" v-if="deviceInfo[device.sn]">
+ <div style="background: #535759; width: 100%;"></div>
+ <div class="capacity-percent" :style="{ width: deviceInfo[device.sn].battery.capacity_percent + '%'}"></div>
+ <div class="return-home" :style="{ width: deviceInfo[device.sn].battery.return_home_power + '%'}"></div>
+ <div class="landing" :style="{ width: deviceInfo[device.sn].battery.landing_power + '%'}"></div>
+ <div class="battery" :style="{ left: deviceInfo[device.sn].battery.capacity_percent + '%' }"></div>
+ </div>
+ <div style="border-bottom: 1px solid #515151; border-radius: 2px; height: 50px; width: 100%;" class="flex-row flex-justify-between flex-align-center">
+ <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>
+ </a-tooltip>
+ </div>
+ <div class="mt5" style="background: #595959;">
+ <span class="ml5 mr5"><RocketOutlined /></span>
+ <span class="font-bold" :style="deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'">
+ {{ deviceInfo[device.sn] ? EModeCode[deviceInfo[device.sn].mode_code] : EModeCode[EModeCode.Disconnected] }}
+ </span>
+ </div>
+ </div>
+ <div style="float: right; background: #595959; height: 50px; width: 40px;" class="flex-row flex-justify-center flex-align-center">
+ <div class="fz16" @click="switchVisible($event, device, false, deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected)">
+ <a v-if="osdVisible.sn === device.sn && osdVisible.visible"><EyeOutlined /></a>
+ <a v-else><EyeInvisibleOutlined /></a>
+ </div>
+ </div>
+ </div>
+ <div class="flex-row flex-justify-center flex-align-center" style="height: 40px;">
+ <div 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>
+ </a-tooltip>
+ </div>
+ </div>
+ </div>
+ </div>
+ </a-collapse-panel>
+ </a-collapse>
+ </div>
</div>
</template>
<script lang="ts" setup>
+import { computed, onMounted, reactive, ref, watch, WritableComputedRef } from 'vue'
+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 { 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 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 onlineDevices = reactive({
+ data: [] as OnlineDevice[]
+})
+
+const onlineDocks = reactive({
+ data: [] as OnlineDevice[]
+})
+
+const deviceInfo = computed(() => store.state.deviceState.deviceInfo)
+const dockInfo = computed(() => store.state.deviceState.dockInfo)
+const hmsInfo = computed({
+ get: () => store.state.hmsInfo,
+ set: (val) => {
+ return val
+ }
+})
+
+onMounted(() => {
+ getOnlineTopo()
+ setTimeout(() => {
+ watch(() => store.state.deviceStatusEvent,
+ data => {
+ getOnlineTopo()
+ if (data.deviceOnline.sn) {
+ getUnreadHms(data.deviceOnline.sn)
+ }
+ },
+ {
+ deep: true
+ }
+ )
+ getOnlineDeviceHms()
+ }, 3000)
+})
+
+function getOnlineTopo () {
+ getDeviceTopo(workspaceId.value).then((res) => {
+ if (res.code !== 0) {
+ return
+ }
+ onlineDevices.data = []
+ onlineDocks.data = []
+ res.data.forEach((val: any) => {
+ const gateway = val.gateways_list.pop()
+ const device: OnlineDevice = {
+ model: val.device_name,
+ callsign: val.nickname,
+ sn: val.device_sn,
+ mode: EModeCode.Disconnected,
+ gateway: {
+ model: gateway?.device_name,
+ callsign: gateway?.nickname,
+ sn: gateway?.device_sn,
+ domain: gateway?.domain
+ },
+ payload: []
+ }
+ val.payloads_list.forEach((payload: any) => {
+ device.payload.push({
+ model: payload.payload_name
+ })
+ })
+ if (gateway && 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) {
+ onlineDevices.data.push(device)
+ }
+ })
+ })
+}
+
+function switchVisible (e: any, device: OnlineDevice, isDock: boolean, isClick: boolean) {
+ if (!isClick) {
+ e.target.style.cursor = 'not-allowed'
+ return
+ }
+ if (device.sn === osdVisible.value.sn) {
+ osdVisible.value.visible = !osdVisible.value.visible
+ } else {
+ osdVisible.value.sn = device.sn
+ osdVisible.value.callsign = device.callsign
+ osdVisible.value.model = device.model
+ osdVisible.value.visible = true
+ osdVisible.value.gateway_sn = device.gateway.sn
+ osdVisible.value.is_dock = isDock
+ osdVisible.value.gateway_callsign = device.gateway.callsign
+ }
+ store.commit('SET_OSD_VISIBLE_INFO', osdVisible)
+}
+
+function getUnreadHms (sn: string) {
+ getUnreadDeviceHms(workspaceId.value, sn).then(res => {
+ if (res.data.length !== 0) {
+ hmsInfo.value[sn] = res.data
+ }
+ })
+ console.info(hmsInfo.value)
+}
+
+function getOnlineDeviceHms () {
+ const snList = Object.keys(dockInfo.value)
+ if (snList.length === 0) {
+ return
+ }
+ snList.forEach(sn => {
+ getUnreadHms(sn)
+ })
+ const deviceSnList = Object.keys(deviceInfo.value)
+ if (deviceSnList.length === 0) {
+ return
+ }
+ deviceSnList.forEach(sn => {
+ getUnreadHms(sn)
+ })
+}
+
+function readHms (visiable: boolean, sn: string) {
+ if (!visiable) {
+ updateDeviceHms(workspaceId.value, sn).then(res => {
+ if (res.code === 0) {
+ delete hmsInfo.value[sn]
+ }
+ })
+ }
+}
</script>
-<style lang="scss" scoped>
+<style lang="scss">
+.project-tsa-wrapper > :first-child {
+ height: 50px;
+ line-height: 50px;
+ align-items: center;
+ border-bottom: 1px solid #4f4f4f;
+}
+.ant-collapse > .ant-collapse-item > .ant-collapse-header {
+ color: white;
+ border: 0;
+ padding-left: 14px;
+}
+
+.text-hidden {
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap;
+ -o-text-overflow: ellipsis;
+}
+.font-bold {
+ font-weight: 700;
+}
+
+.battery-slide {
+ width: 100%;
+ .capacity-percent {
+ background: #00ee8b;
+ }
+ .return-home {
+ background: #ff9f0a;
+ }
+ .landing {
+ background: #f5222d;
+ }
+ .battery {
+ background: white;
+ border-radius: 1px;
+ width: 8px;
+ height: 4px;
+ margin-top: -3px;
+ }
+}
+.battery-slide > div {
+ position: relative;
+ margin-top: -2px;
+ min-height: 2px;
+ border-radius: 2px;
+ white-space: nowrap;
+}
+.disable {
+ cursor: not-allowed;
+}
+
+.notice-blink {
+ background: $success;
+ animation: blink 500ms infinite;
+}
+.caution-blink {
+ background: orange;
+ animation: blink 500ms infinite;
+}
+.warn-blink {
+ background: red;
+ animation: blink 500ms infinite;
+}
+.notice {
+ background: $success;
+ overflow: hidden;
+ cursor: pointer;
+}
+.caution {
+ background: orange;
+ cursor: pointer;
+ overflow: hidden;
+}
+.warn {
+ background: red;
+ cursor: pointer;
+ overflow: hidden;
+}
+.word-loop {
+ white-space: nowrap;
+ display: inline-block;
+ animation: 10s loop linear infinite normal;
+}
+@keyframes blink {
+ from {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.35;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes loop {
+ 0% {
+ transform: translateX(20px);
+ -webkit-transform: translateX(20px);
+ }
+ 100% {
+ transform: translateX(-100%);
+ -webkit-transform: translateX(-100%);
+ }
+}
+
</style>
diff --git a/src/pages/project-app/projects/wayline.vue b/src/pages/project-app/projects/wayline.vue
index 09caaf5..3ef8e32 100644
--- a/src/pages/project-app/projects/wayline.vue
+++ b/src/pages/project-app/projects/wayline.vue
@@ -1,9 +1,205 @@
<template>
- <div class="project-wayline-wrapper">
- wayline
+ <div class="project-wayline-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>
+ <a-col :span="22">Flight Route Library</a-col>
+ <a-col :span="1"></a-col>
+ </a-row>
+ </div>
+ <div class="height-100">
+ <div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
+ <div v-for="wayline in waylinesData.data" :key="wayline.id">
+ <div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)">
+ <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 class="fz20">
+ <a-dropdown>
+ <a style="color: white;">
+ <EllipsisOutlined />
+ </a>
+ <template #overlay>
+ <a-menu theme="dark" class="more" style="background: #3c3c3c;">
+ <a-menu-item @click="downloadWayline(wayline.id, wayline.name)">
+ <span>Download</span>
+ </a-menu-item>
+ <a-menu-item @click="showWaylineTip(wayline.id)">
+ <span>Delete</span>
+ </a-menu-item>
+ </a-menu>
+ </template>
+ </a-dropdown>
+ </div>
+ </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>
+ </div>
+ </div>
+ <div v-else>
+ <a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
+ </div>
+ <a-modal v-model:visible="deleteTip" width="450px" :closable="false" :maskClosable="false" centered :okButtonProps="{ danger: true }" @ok="deleteWayline">
+ <p class="pt10 pl20" style="height: 50px;">Wayline file is unrecoverable once deleted. Continue?</p>
+ <template #title>
+ <div class="flex-row flex-justify-center">
+ <span>Delete</span>
+ </div>
+ </template>
+ </a-modal>
+ </div>
</div>
</template>
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+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 { useMyStore } from '/@/store'
+import { WaylineFile } from '/@/types/wayline'
+import { downloadFile } from '/@/utils/common'
+import { IPage } from '/@/api/http/type'
-<style lang="scss" scoped></style>
+const store = useMyStore()
+const pagination :IPage = {
+ page: 1,
+ total: 0,
+ page_size: 10
+}
+
+const waylinesData = reactive({
+ data: [] as WaylineFile[]
+})
+
+const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
+const deleteTip = ref(false)
+const deleteWaylineId = ref<string>('')
+const canRefresh = ref(true)
+
+onMounted(() => {
+ 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'
+ }
+ }, 300)
+})
+
+function getWaylines () {
+ if (!canRefresh.value) {
+ return
+ }
+ canRefresh.value = false
+ getWaylineFiles(workspaceId, {
+ page: pagination.page,
+ page_size: pagination.page_size,
+ order_by: 'update_time desc'
+ }).then(res => {
+ if (res.code !== 0) {
+ return
+ }
+ res.data.list.forEach((wayline: WaylineFile) => waylinesData.data.push(wayline))
+ pagination.total = res.data.pagination.total
+ pagination.page = res.data.pagination.page
+ }).finally(() => {
+ canRefresh.value = true
+ })
+}
+
+function showWaylineTip (waylineId: string) {
+ deleteWaylineId.value = waylineId
+ deleteTip.value = true
+}
+
+function deleteWayline () {
+ deleteWaylineFile(workspaceId, deleteWaylineId.value).then(res => {
+ if (res.code === 0) {
+ message.success('Wayline file deleted')
+ }
+ deleteWaylineId.value = ''
+ deleteTip.value = false
+ pagination.total--
+ waylinesData.data = []
+ setTimeout(getWaylines, 500)
+ })
+}
+
+function downloadWayline (waylineId: string, fileName: string) {
+ downloadWaylineFile(workspaceId, waylineId).then(res => {
+ if (res.code && res.code !== 0) {
+ return
+ }
+ const data = new Blob([res.data], { type: 'application/zip' })
+ downloadFile(data, fileName + '.kmz')
+ })
+}
+
+function selectRoute (wayline: WaylineFile) {
+ store.commit('SET_SELECT_WAYLINE_INFO', wayline)
+}
+
+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) {
+ pagination.page++
+ getWaylines()
+ }
+}
+
+</script>
+
+<style lang="scss" scoped>
+.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;
+ flex-direction: row;
+ align-items: center;
+ height: 30px;
+ font-weight: bold;
+ margin: 0px 10px 0 10px;
+ }
+}
+.uranus-scrollbar {
+ overflow: auto;
+ scrollbar-width: thin;
+ scrollbar-color: #c5c8cc transparent;
+}
+</style>
diff --git a/src/pages/project-app/projects/workspace.vue b/src/pages/project-app/projects/workspace.vue
new file mode 100644
index 0000000..17d94a6
--- /dev/null
+++ b/src/pages/project-app/projects/workspace.vue
@@ -0,0 +1,140 @@
+<template>
+ <div class="project-app-wrapper">
+ <div class="left">
+ <Sidebar />
+ <div class="main-content uranus-scrollbar dark">
+ <router-view />
+ </div>
+ </div>
+ <div class="right">
+ <div class="map-wrapper">
+ <GMap />
+ </div>
+ <div class="media-wrapper" v-if="root.$route.name === ERouterName.MEDIA">
+ <MediaPanel />
+ </div>
+ <div class="media-wrapper" v-if="root.$route.name === ERouterName.TASK">
+ <TaskPanel />
+ </div>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+
+import Sidebar from '../sidebar.vue'
+import MediaPanel from '@/components/MediaPanel.vue'
+import TaskPanel from '@/components/TaskPanel.vue'
+import GMap from '/@/components/GMap.vue'
+import { EBizCode, ERouterName } from '/@/types'
+import { getRoot } from '/@/root'
+import { onMounted, onUnmounted, watch } from 'vue'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import { useMyStore } from '/@/store'
+import websocket from '/@/api/websocket'
+// import { enableAgoraLive, enableOthersLive } from '/@/pages/project-app/projects/livestream.vue'
+
+const root = getRoot()
+
+const wsGetMsg = async (res: any) => {
+ const payload = JSON.parse(res.data)
+ switch (payload.biz_code) {
+ case EBizCode.GatewayOsd: {
+ store.commit('SET_GATEWAY_INFO', payload.data)
+ break
+ }
+ case EBizCode.DeviceOsd: {
+ store.commit('SET_DEVICE_INFO', payload.data)
+ break
+ }
+ case EBizCode.DockOsd: {
+ store.commit('SET_DOCK_INFO', payload.data)
+ break
+ }
+ case EBizCode.MapElementCreate: {
+ store.commit('SET_MAP_ELEMENT_CREATE', payload.data)
+ break
+ }
+ case EBizCode.MapElementUpdate: {
+ store.commit('SET_MAP_ELEMENT_UPDATE', payload.data)
+ break
+ }
+ case EBizCode.MapElementDelete: {
+ store.commit('SET_MAP_ELEMENT_DELETE', payload.data)
+ break
+ }
+ case EBizCode.DeviceOnline: {
+ store.commit('SET_DEVICE_ONLINE', payload.data)
+ break
+ }
+ case EBizCode.DeviceOffline: {
+ store.commit('SET_DEVICE_OFFLINE', payload.data)
+ break
+ }
+ case EBizCode.FlightTaskProgress: {
+ store.commit('SET_FLIGHT_TASK_PROGRESS', payload.data)
+ break
+ }
+ case EBizCode.DeviceHms: {
+ store.commit('SET_DEVICE_HMS_INFO', payload.data)
+ break
+ }
+ default:
+ break
+ }
+}
+
+const store = useMyStore()
+
+let socket: ReconnectingWebSocket
+
+onMounted(() => {
+ socket = websocket.init(wsGetMsg)
+})
+onUnmounted(() => {
+ socket.close()
+})
+</script>
+<style lang="scss" scoped>
+@import '/@/styles/index.scss';
+
+.project-app-wrapper {
+ display: flex;
+ position: absolute;
+ transition: width 0.2s ease;
+ height: 100%;
+ width: 100%;
+ .left {
+ width: 400px;
+ display: flex;
+ background-color: #232323;
+ float: left;
+ }
+ .right {
+ width: 100%;
+ height: 100%;
+ .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;
+ }
+}
+</style>
diff --git a/src/pages/project-app/sidebar.vue b/src/pages/project-app/sidebar.vue
index 023a6d0..c042e6e 100644
--- a/src/pages/project-app/sidebar.vue
+++ b/src/pages/project-app/sidebar.vue
@@ -1,5 +1,6 @@
<template>
- <div class="demo-project-sidebar-wrapper">
+ <div class="demo-project-sidebar-wrapper flex-justify-between">
+ <div>
<router-link
v-for="item in options"
:key="item.key"
@@ -7,19 +8,28 @@
:class="{
'menu-item': true,
selected: selectedRoute(item),
- disabled: item.key > 6
}"
>
<a-tooltip :title="item.label" placement="right">
- <span>{{ item.label }}</span>
+ <Icon class="fz20" style="width: 50px;" :icon="item.icon"/>
</a-tooltip>
</router-link>
+ </div>
+ <div class="mb20 flex-display flex-column flex-align-center flex-justify-between">
+ <a-tooltip title="Back to home" placement="right">
+ <a @click="goHome"> <Icon icon="ImportOutlined" style="font-size: 22px; color: white"/></a>
+ </a-tooltip>
+ </div>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue'
+import { createVNode, defineComponent } from 'vue'
import { getRoot } from '/@/root'
+import * as icons from '@ant-design/icons-vue'
+import { ERouterName } from '/@/types'
+import websocket from '/@/api/websocket'
+
interface IOptions {
key: number
label: string
@@ -32,26 +42,38 @@
icon: string
}
+const Icon = (props: {icon: string}) => {
+ return createVNode((icons as any)[props.icon])
+}
+
export default defineComponent({
+ components: {
+ Icon,
+ },
name: 'Sidebar',
setup () {
const root = getRoot()
const options = [
- { key: 0, label: 'livestream', path: '/livestream', icon: 'livestream' },
- { key: 1, label: 'tsa', path: '/tsa', icon: 'tsa' },
- { key: 2, label: 'layer', path: '/layer', icon: 'layer' },
- { key: 3, label: 'media', path: '/media', icon: 'media' },
- { key: 4, label: 'wayline', path: '/wayline', icon: 'wayline' }
+ { key: 0, label: 'Tsa', path: '/' + ERouterName.TSA, icon: 'TeamOutlined' },
+ { key: 1, label: 'Livestream', path: '/' + ERouterName.LIVESTREAM, icon: 'VideoCameraOutlined' },
+ { key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
+ { key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' },
+ { key: 4, label: 'Fligth Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
+ { key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' }
]
function selectedRoute (item: IOptions) {
const path = typeof item.path === 'string' ? item.path : item.path.path
return root.$route.path?.indexOf(path) === 0
}
-
+ function goHome () {
+ root.$router.push('/' + ERouterName.MEMBERS)
+ websocket.close()
+ }
return {
options,
- selectedRoute
+ selectedRoute,
+ goHome,
}
}
})
@@ -62,7 +84,7 @@
display: flex;
flex-direction: column;
align-items: center;
- width: 80px;
+ width: 50px;
border-right: 1px solid #4f4f4f;
color: $text-white-basic;
// flex: 1;
@@ -76,7 +98,7 @@
color: $text-white-basic;
cursor: pointer;
&.selected {
- background-color: $dark-highlight;
+ background-color: #101010;
color: $primary;
}
&.disabled {
@@ -95,8 +117,6 @@
}
}
-</style>
-<style>
.ant-tooltip-open {
border: 0;
}
diff --git a/src/pages/project-app/topbar.vue b/src/pages/project-app/topbar.vue
new file mode 100644
index 0000000..1555372
--- /dev/null
+++ b/src/pages/project-app/topbar.vue
@@ -0,0 +1,97 @@
+<template>
+ <div class="width-100 flex-row flex-justify-between flex-align-center" style="height: 60px;">
+ <div class="height-100">
+ <a-avatar :size="40" shape="square" :src="cloudapi" />
+ <span class="ml10 fontBold">{{ workspaceName }}</span>
+ </div>
+
+ <a-space class="fz16 height-100" size="large">
+ <router-link
+ v-for="item in options"
+ :key="item.key"
+ :to="item.path"
+ :class="{
+ 'menu-item': true,
+ }">
+ <span @click="selectedRoute(item.path)" :style="selected === item.path ? 'color: #2d8cf0;' : 'color: white'">{{ item.label }}</span>
+ </router-link>
+ </a-space>
+
+ <div class="height-100 fz16 flex-row flex-justify-between flex-align-center">
+ <a-dropdown>
+ <div class="height-100">
+ <span class="fz20 mt20" style="border: 2px solid white; border-radius: 50%; display: inline-flex;"><UserOutlined /></span>
+ <span class="ml10 mr10" style="float: right;">{{ username }}</span>
+ </div>
+ <template #overlay>
+ <a-menu theme="dark" class="flex-column flex-justify-between flex-align-center">
+ <a-menu-item>
+ <span class="mr10" style="font-size: 16px;"><ExportOutlined /></span>
+ <span @click="logout">Log Out</span>
+ </a-menu-item>
+ </a-menu>
+ </template>
+ </a-dropdown>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { message } from 'ant-design-vue'
+import { defineComponent, onMounted, ref } from 'vue'
+import { getRoot } from '/@/root'
+import { getPlatformInfo } from '/@/api/manage'
+import { ELocalStorageKey, ERouterName } from '/@/types'
+import { UserOutlined, ExportOutlined } from '@ant-design/icons-vue'
+import cloudapi from '/@/assets/icons/cloudapi.png'
+import ReconnectingWebSocket from 'reconnecting-websocket'
+import websocket from '/@/api/websocket'
+
+const root = getRoot()
+
+interface IOptions {
+ key: number
+ label: string
+ path:
+ | string
+ | {
+ path: string
+ query?: any
+ }
+ icon: string
+}
+const username = ref(localStorage.getItem(ELocalStorageKey.Username))
+const workspaceName = ref('')
+const options = [
+ { key: 0, label: ERouterName.WORKSPACE.charAt(0).toUpperCase() + ERouterName.WORKSPACE.substr(1), path: '/' + ERouterName.WORKSPACE },
+ { key: 1, label: ERouterName.MEMBERS.charAt(0).toUpperCase() + ERouterName.MEMBERS.substr(1), path: '/' + ERouterName.MEMBERS },
+ { key: 2, label: ERouterName.DEVICES.charAt(0).toUpperCase() + ERouterName.DEVICES.substr(1), path: '/' + ERouterName.DEVICES }
+]
+
+const selected = ref<string>(root.$route.path)
+
+onMounted(() => {
+ getPlatformInfo().then(res => {
+ workspaceName.value = res.data.workspace_name
+ })
+})
+
+function selectedRoute (path: string) {
+ selected.value = path
+}
+
+const logout = () => {
+ localStorage.clear()
+ root.$router.push(ERouterName.PROJECT)
+}
+</script>
+
+<style lang="scss" scoped>
+@import '/@/styles/index.scss';
+
+.fontBold {
+ font-weight: 500;
+ font-size: 18px;
+}
+
+</style>
diff --git a/src/router/index.ts b/src/router/index.ts
index 574c3a9..78f8bb5 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,63 +1,127 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { ERouterName } from '/@/types/index'
+import CreatePlan from '../pages/project-app/projects/create-plan.vue'
+import WaylinePanel from '/@/pages/project-app/projects/wayline.vue'
+import DockPanel from '/@/pages/project-app/projects/dock.vue'
+import LiveAgora from '/@/components/livestream-agora.vue'
+import LiveOthers from '/@/components/livestream-others.vue'
const routes: Array<RouteRecordRaw> = [
{
- path: '/' + ERouterName.Project,
- name: ERouterName.Project,
- // redirect: {
- // name: ERouterName.Project
- // },
- component: () => import('/@/pages/project-app/index.vue'),
+ path: '/',
+ redirect: '/' + ERouterName.PROJECT
+ },
+ {
+ path: '/' + ERouterName.PROJECT,
+ name: ERouterName.PROJECT,
+ component: () => import('/@/pages/project-app/index.vue')
+ },
+ {
+ path: '/' + ERouterName.HOME,
+ name: ERouterName.HOME,
+ component: () => import('/@/pages/project-app/home.vue'),
children: [
{
- path: '/' + ERouterName.Livestream,
- component: () => import('/@/pages/project-app/projects/livestream.vue')
+ path: '/' + ERouterName.MEMBERS,
+ name: ERouterName.MEMBERS,
+ component: () => import('/@/pages/project-app/projects/members.vue')
},
{
- path: '/' + ERouterName.Tsa,
+ path: '/' + ERouterName.DEVICES,
+ name: ERouterName.DEVICES,
+ component: () => import('/@/pages/project-app/projects/devices.vue')
+ }
+ ]
+ },
+ {
+ path: '/' + ERouterName.WORKSPACE,
+ name: ERouterName.WORKSPACE,
+ component: () => import('/@/pages/project-app/projects/workspace.vue'),
+ redirect: '/' + ERouterName.TSA,
+ children: [
+ {
+ path: '/' + ERouterName.LIVESTREAM,
+ name: ERouterName.LIVESTREAM,
+ component: () => import('/@/pages/project-app/projects/livestream.vue'),
+ children: [
+ {
+ path: ERouterName.LIVING,
+ name: ERouterName.LIVING,
+ components: {
+ LiveAgora,
+ LiveOthers
+ }
+ }
+ ]
+ },
+ {
+ path: '/' + ERouterName.TSA,
component: () => import('/@/pages/project-app/projects/tsa.vue')
},
{
- path: '/' + ERouterName.Layer,
- name: ERouterName.Layer,
+ path: '/' + ERouterName.LAYER,
+ name: ERouterName.LAYER,
component: () => import('/@/pages/project-app/projects/layer.vue')
},
{
- path: '/' + ERouterName.Media,
- name: ERouterName.Media,
+ path: '/' + ERouterName.MEDIA,
+ name: ERouterName.MEDIA,
component: () => import('/@/pages/project-app/projects/media.vue')
},
{
- path: '/' + ERouterName.Wayline,
- name: ERouterName.Wayline,
+ path: '/' + ERouterName.WAYLINE,
+ name: ERouterName.WAYLINE,
component: () => import('/@/pages/project-app/projects/wayline.vue')
},
- ]
- },
- {
- path: '/' + ERouterName.Pilot,
- name: ERouterName.Pilot,
- component: () => import('/@/pages/page-pilot/pilot-index.vue'),
- children: [
+ {
+ path: '/' + ERouterName.TASK,
+ name: ERouterName.TASK,
+ component: () => import('/@/pages/project-app/projects/task.vue'),
+ children: [
+ {
+ path: ERouterName.CREATE_PLAN,
+ name: ERouterName.CREATE_PLAN,
+ component: CreatePlan,
+ children: [
+ {
+ path: ERouterName.SELECT_PLAN,
+ name: ERouterName.SELECT_PLAN,
+ components: {
+ WaylinePanel,
+ DockPanel
+ }
+ }
+ ]
+ }
+ ]
+ }
]
},
{
- path: '/' + ERouterName.PilotHome,
+ path: '/' + ERouterName.PILOT,
+ name: ERouterName.PILOT,
+ component: () => import('/@/pages/page-pilot/pilot-index.vue'),
+ },
+ {
+ path: '/' + ERouterName.PILOT_HOME,
component: () => import('/@/pages/page-pilot/pilot-home.vue')
},
{
- path: '/' + ERouterName.PilotMedia,
+ path: '/' + ERouterName.PILOT_MEDIA,
component: () => import('/@/pages/page-pilot/pilot-media.vue')
},
{
- path: '/' + ERouterName.PilotLiveshare,
+ path: '/' + ERouterName.PILOT_LIVESHARE,
component: () => import('/@/pages/page-pilot/pilot-liveshare.vue')
},
{
- path: '/' + ERouterName.Element,
- name: ERouterName.Element,
+ path: '/' + ERouterName.PILOT_BIND,
+ component: () => import('/@/pages/page-pilot/pilot-bind.vue')
+ },
+ {
+ path: '/' + ERouterName.ELEMENT,
+ name: ERouterName.ELEMENT,
component: () => import('/@/pages/elements/elements.vue')
}
]
diff --git a/src/store/index.ts b/src/store/index.ts
index ac46e37..bdcd972 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,7 +1,11 @@
import { InjectionKey } from 'vue'
import { ActionTree, createStore, GetterTree, MutationTree, Store, StoreOptions, useStore } from 'vuex'
+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'
+
const initStateFunc = () => ({
Layers: [
{
@@ -23,12 +27,6 @@
type: 2
}
],
- GatewayInfo: { // remote controller, dock
-
- },
- DeviceInfo: { // drone
-
- },
layerBaseInfo: {} as {
[key:string]:string
},
@@ -40,6 +38,55 @@
mapElementCreat: {},
mapElementUpdate: {},
mapElementDelete: {}
+ },
+ deviceStatusEvent: {
+ deviceOnline: {} as DeviceStatus,
+ deviceOffline: {}
+ },
+ markerInfo: {
+ coverMap: {} as {
+ [sn: string]: any
+ },
+ pathMap: {} as {
+ [sn: string]: any[]
+ }
+ },
+ deviceState: {
+ // remote controller, dock
+ gatewayInfo: {} as {
+ [sn: string]: GatewayOsd
+ },
+ // drone
+ deviceInfo: {} as {
+ [sn: string]: DeviceOsd
+ },
+ dockInfo: {} as {
+ [sn: string]: DockOsd
+ },
+ currentSn: '',
+ currentType: ''
+ },
+ osdVisible: {
+ sn: '',
+ callsign: '',
+ model: '',
+ visible: false,
+ gateway_sn: '',
+ is_dock: false,
+ } as OSDVisible,
+ waylineInfo: {
+
+ } as WaylineFile,
+ dockInfo: {
+
+ } as Device,
+ taskProgressInfo: {
+
+ } as {
+ [bid: string]: TaskInfo
+ },
+ hmsInfo: {} as {
+ [sn: string]: DeviceHms[]
}
})
@@ -52,12 +99,29 @@
state.Layers = info
},
SET_DEVICE_INFO (state, info) {
- state.DeviceInfo = info
- // console.log(state.DeviceInfo)
+ state.deviceState.deviceInfo[info.sn] = info.host
+ state.deviceState.currentSn = info.sn
+ state.deviceState.currentType = EDeviceTypeName.Aircraft
},
SET_GATEWAY_INFO (state, info) {
- state.GatewayInfo = info
- // console.log(state.GatewayInfo)
+ state.deviceState.gatewayInfo[info.sn] = info.host
+ state.deviceState.currentSn = info.sn
+ state.deviceState.currentType = EDeviceTypeName.Gateway
+ },
+ SET_DOCK_INFO (state, info) {
+ 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
+ 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
},
SET_DRAW_VISIBLE_INFO (state, bool) {
state.drawVisible = bool
@@ -71,6 +135,43 @@
SET_MAP_ELEMENT_DELETE (state, info) {
state.wsEvent.mapElementDelete = info
},
+ SET_DEVICE_ONLINE (state, info) {
+ state.deviceStatusEvent.deviceOnline = info
+ },
+ SET_DEVICE_OFFLINE (state, info) {
+ state.deviceStatusEvent.deviceOffline = info
+ delete state.deviceState.gatewayInfo[info.sn]
+ delete state.deviceState.deviceInfo[info.sn]
+ delete state.deviceState.dockInfo[info.sn]
+ delete state.hmsInfo[info.sn]
+
+ // delete state.markerInfo.coverMap[info.sn]
+ // delete state.markerInfo.pathMap[info.sn]
+ },
+ SET_OSD_VISIBLE_INFO (state, info) {
+ state.osdVisible = info
+ },
+ SET_SELECT_WAYLINE_INFO (state, info) {
+ state.waylineInfo = info
+ },
+ 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 ?? [])
+ }
}
const actions: ActionTree<RootStateType, RootStateType> = {
diff --git a/src/styles/common.scss b/src/styles/common.scss
index eae2982..6d41b09 100644
--- a/src/styles/common.scss
+++ b/src/styles/common.scss
@@ -10,10 +10,22 @@
// Prevent font enlargement in horizontal screen
text-size-adjust: 100%;
- font-family: Roboto, sans-serif-medium, Arial, sans-serif;
+ font-family: sans-serif, Roboto, sans-serif-medium, Arial;
+ font-feature-settings: normal;
color: $main-text-color;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ border-radius: 4px;
+ border: none;
+ background: rgb(89, 89, 89);
+ }
}
\ No newline at end of file
diff --git a/src/styles/fonts.scss b/src/styles/fonts.scss
index 273bb1f..8b06c1e 100644
--- a/src/styles/fonts.scss
+++ b/src/styles/fonts.scss
@@ -32,7 +32,7 @@
}
.fz10 {
- font-size: 10;
+ font-size: 10px;
}
.fz12 {
font-size: 12px;
diff --git a/src/types/device.ts b/src/types/device.ts
new file mode 100644
index 0000000..8a77512
--- /dev/null
+++ b/src/types/device.ts
@@ -0,0 +1,198 @@
+import { EDeviceTypeName } from ".";
+
+export interface Device {
+ device_name: string,
+ device_sn: string,
+ nickname: string,
+ firmware_version: string,
+ status: string,
+ workspace_name: string,
+ bound_time: string,
+ login_time: string,
+ children?: Device[]
+ domain: string
+}
+
+export interface DeviceStatus {
+ sn: string,
+ online_status: boolean,
+ device_callsign: string,
+ user_id: string,
+ user_callsign: string
+ bound_status: boolean,
+ model: string,
+ gateway_sn: string,
+ domain: string
+}
+
+export interface OSDVisible {
+ sn: string,
+ model: string,
+ callsign: string,
+ visible: boolean,
+ is_dock: boolean,
+ gateway_sn: string,
+ gateway_callsign: string,
+}
+
+export interface GatewayOsd {
+ capacity_percent: string,
+ transmission_signal_quality: string,
+ longitude: number,
+ latitude: number,
+}
+export interface DeviceOsd {
+ longitude: number,
+ latitude: number,
+ gear: number,
+ mode_code: number,
+ height: string,
+ home_distance: string,
+ horizontal_speed: string,
+ vertical_speed: string,
+ wind_speed: string,
+ wind_direction: string,
+ elevation: string,
+ position_state: {
+ gps_number: string,
+ is_fixed: number,
+ rtk_number: string
+ },
+ battery: {
+ capacity_percent: string,
+ landing_power: string,
+ remain_flight_time: number,
+ return_home_power: string,
+ }
+}
+
+export interface DockOsd {
+ media_file_detail: {
+ remain_upload: number
+ },
+ sdr: {
+ up_quality: string,
+ down_quality: string,
+ frequency_band: number,
+ },
+ network_state: {
+ type: number,
+ quality: number,
+ rate: number,
+ },
+ drone_in_dock: number,
+ drone_charge_state: {
+ state: number,
+ capacity_percent: string,
+ },
+ rainfall: string,
+ wind_speed: string,
+ environment_temperature: string,
+ environment_humidity: string
+ temperature: string,
+ humidity: string,
+ latitude: number,
+ longitude: number,
+ height: number,
+ job_number: number,
+ acc_time: number,
+ first_power_on: number,
+ positionState: {
+ gps_number: string,
+ is_fixed: number,
+ rtk_number: string,
+ is_calibration: number,
+ quality: number,
+ },
+ 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,
+ putter_state: number,
+ sub_device: {
+ device_sn: string,
+ device_model_key: string,
+ device_online_status: number,
+ device_paired: number,
+ },
+}
+
+export enum EModeCode {
+ Standby,
+ Preparing,
+ Ready,
+ Manual,
+ Automatic,
+ Waypoint,
+ Panoramic,
+ Active_Track,
+ ADS_B,
+ Return_To_Home,
+ Landing,
+ Forced_Landing,
+ Three_Blades_Landing,
+ Upgrading,
+ Disconnected,
+}
+
+export enum EGear {
+ A,
+ P,
+ NAV,
+ FPV,
+ FARM,
+ S,
+ F,
+ M,
+ G,
+ 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,
+ Debugging,
+ Remote_Debugging,
+ Upgrading,
+ Working,
+}
+
+export interface DeviceHms {
+ hms_id: string,
+ tid: string,
+ bid: string,
+ sn: string,
+ level: number,
+ module: number,
+ key: string,
+ message_en: string,
+ message_zh: string,
+ create_time: string,
+ update_time: string,
+ domain: string
+}
\ No newline at end of file
diff --git a/src/types/enums.ts b/src/types/enums.ts
index 15119db..999f1e1 100644
--- a/src/types/enums.ts
+++ b/src/types/enums.ts
@@ -1,15 +1,25 @@
export enum ERouterName {
- Element = 'element',
- Project = 'project',
- Tsa = 'tsa',
- Layer = 'layer',
- Media = 'media',
- Wayline = 'wayline',
- Livestream = 'livestream',
- Pilot = 'pilot-login',
- PilotHome = 'pilot-home',
- PilotMedia = 'pilot-media',
- PilotLiveshare = 'pilot-liveshare'
+ ELEMENT = 'element',
+ PROJECT = 'project',
+ HOME = 'home',
+ TSA = 'tsa',
+ LAYER = 'layer',
+ MEDIA = 'media',
+ WAYLINE = 'wayline',
+ LIVESTREAM = 'livestream',
+ LIVING = 'living',
+ WORKSPACE = 'workspace',
+ MEMBERS = 'members',
+ DEVICES = 'devices',
+ TASK = 'task',
+ CREATE_PLAN = 'create-plan',
+ SELECT_PLAN = 'select-plan',
+
+ PILOT = 'pilot-login',
+ PILOT_HOME = 'pilot-home',
+ PILOT_MEDIA = 'pilot-media',
+ PILOT_LIVESHARE = 'pilot-liveshare',
+ PILOT_BIND = 'pilot-bind'
}
export enum EStorageKey {
@@ -17,3 +27,81 @@
TEST_TOOLS_POSITION_STORAGE_KEY = 'DJI_CREATE_VITE_H5_APP:test_tools_position',
SESSION_ID = 'DJI_CREATE_VITE_H5_APP:sess'
}
+
+export enum EStatusValue {
+ CONNECTED = 'Connected',
+ DISCONNECT = 'Disconnect',
+ LIVING = 'Living'
+}
+
+export enum ELiveStatusValue {
+ DISCONNECT,
+ CONNECTED,
+ LIVING
+}
+
+export enum EComponentName {
+ Thing = 'thing',
+ Liveshare = 'liveshare',
+ Api = 'api',
+ Ws = 'ws',
+ Map = 'map',
+ Tsa = 'tsa',
+ Media = 'media',
+ Mission = 'mission'
+}
+
+export enum ELocalStorageKey {
+ Username = 'username',
+ WorkspaceId = 'workspace_id',
+ Token = 'x-auth-token',
+ PlatformName = 'platform_name',
+ WorkspaceName = 'workspace_name',
+ WorkspaceDesc = 'workspace_desc',
+ Flag = 'flag',
+ UserId = 'user_id',
+ Device = 'device',
+ GatewayOnline = 'gateway_online',
+}
+
+export enum EPhotoType {
+ Original = 0,
+ Preview = 1,
+ Unknown = -1
+}
+
+export enum EDownloadOwner {
+ Mine = 0,
+ Others = 1,
+ Unknown = -1
+}
+
+export enum EUserType {
+ Web = 1,
+ Pilot = 2,
+}
+
+export enum EBizCode {
+ GatewayOsd = 'gateway_osd',
+ DeviceOsd = 'device_osd',
+ DockOsd = 'dock_osd',
+ MapElementCreate = 'map_element_create',
+ MapElementUpdate = 'map_element_update',
+ MapElementDelete = 'map_element_delete',
+ DeviceOnline = 'device_online',
+ DeviceOffline = 'device_offline',
+ FlightTaskProgress = 'flighttask_progress',
+ DeviceHms = 'device_hms',
+}
+
+export enum EDeviceTypeName {
+ Aircraft = 'sub-device',
+ Gateway = 'gateway',
+ Dock = 'dock',
+}
+
+export enum EHmsLevel {
+ NOTICE,
+ CAUTION,
+ WARN,
+}
\ No newline at end of file
diff --git a/src/types/live-stream.ts b/src/types/live-stream.ts
new file mode 100644
index 0000000..2103910
--- /dev/null
+++ b/src/types/live-stream.ts
@@ -0,0 +1,55 @@
+
+export interface LiveStreamStatus {
+ audioBitRate: number,
+ dropRate: number,
+ fps: number,
+ jitter: number,
+ quality: number,
+ rtt: number,
+ status: number,
+ type: number,
+ videoBitRate: number
+}
+
+export interface GB28181Param {
+ serverIp: string,
+ serverPort: string,
+ serverId: string,
+ agentId: string,
+ password: string,
+ agentPort: string,
+ agentChannel: string
+}
+
+export interface RTSPParam {
+ userName: string,
+ password: string,
+ port: string
+}
+
+export interface LiveConfigParam {
+ params: number,
+ type: any
+}
+
+export enum EVideoPublishType {
+ VideoOnDemand = 'video-on-demand',
+ VideoByManual = 'video-by-manual',
+ VideoDemandAuxManual = 'video-demand-aux-manual'
+}
+
+export enum ELiveTypeValue {
+ Unknown,
+ Agora,
+ RTMP,
+ RTSP,
+ GB28181
+}
+
+export enum ELiveTypeName {
+ Unknown = 'Unknown',
+ Agora = 'Agora',
+ RTMP = 'RTMP',
+ RTSP = 'RTSP',
+ GB28181 = 'GB28181'
+}
diff --git a/src/types/wayline.ts b/src/types/wayline.ts
new file mode 100644
index 0000000..69746e6
--- /dev/null
+++ b/src/types/wayline.ts
@@ -0,0 +1,30 @@
+export interface WaylineFile {
+ id: string,
+ name: string,
+ drone_model_key: any,
+ payload_model_keys: string[],
+ template_types: number[],
+ update_time: number,
+ user_name: string,
+}
+
+export interface TaskExt {
+ current_waypoint_index: number,
+ media_count: number,
+}
+
+export interface TaskProgress {
+ current_step: number,
+ percent: number,
+}
+
+export interface TaskInfo {
+ status: string,
+ progress: TaskProgress,
+ ext: TaskExt,
+}
+
+export enum ETaskStatus {
+ OK = 'ok',
+ FAILED = 'failed'
+}
diff --git a/src/utils/common.ts b/src/utils/common.ts
new file mode 100644
index 0000000..03c65f2
--- /dev/null
+++ b/src/utils/common.ts
@@ -0,0 +1,8 @@
+
+export function downloadFile (data: Blob, fileName: string) {
+ const lable = document.createElement('a')
+ lable.href = window.URL.createObjectURL(data)
+ lable.download = fileName
+ lable.click()
+ URL.revokeObjectURL(lable.href)
+}
diff --git a/tsconfig.json b/tsconfig.json
index 10e74cf..cffee3a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,6 +24,7 @@
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
- "src/**/*.vue"
-, "src/vendors/coordtransform.js" ]
+ "src/**/*.vue",
+ "src/vendors/coordtransform.js"
+ ]
}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index fd4a118..29c6933 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -28,7 +28,7 @@
}),
viteVConsole({
entry: path.resolve(__dirname, './src/main.ts'), // 入口文件
- // localEnabled: command === 'serve', // serve开发环境下
+ localEnabled: command === 'serve', // serve开发环境下
// enabled: command !== 'serve' || mode === 'test', // 打包环境下/发布测试包,
config: { // vconsole 配置项
maxLogNumber: 1000,
diff --git a/yarn.lock b/yarn.lock
index 084f608..475880b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -613,10 +613,17 @@
"resolved" "https://registry.npmmirror.com/acorn/download/acorn-7.4.1.tgz"
"version" "7.4.1"
-"agora-rtc-sdk-ng@latest":
- "integrity" "sha512-Jogn3TQC7VdA7uZjGYmaAs0XzgYBgGs6nGA67/dQVjqC7kiwAfkQsAuvbevE/qxrVJmLfqtDTNxP40IFvnTlgQ=="
- "resolved" "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.9.1.tgz"
- "version" "4.9.1"
+"agora-rtc-sdk-ng@^4.12.1":
+ "integrity" "sha512-kmc+ZyKDdnY/BN3iAwBs+MSgTX8Zkc6THFSIAXN9WebjZ/F+N/JXItoNEcgQe3MdTABUli6w3pZ+iObnDqVkBw=="
+ "resolved" "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.12.1.tgz"
+ "version" "4.12.1"
+ dependencies:
+ "agora-rte-extension" "^1.0.22"
+
+"agora-rte-extension@^1.0.22":
+ "integrity" "sha512-X2cGBg+L5ZJIFU91qvMASvRsBfg1HXTktVG3YROw9wxHsILSI7jgF9R9XraLc3fNX/UjovaYAlUW+hiJe0v6Xw=="
+ "resolved" "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.0.23.tgz"
+ "version" "1.0.23"
"ajv@^6.10.0", "ajv@^6.12.4":
"integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="
--
Gitblit v1.9.3