Browse Source

V1.1.0 for dock

v1.1.0
sean.zhou 3 years ago
parent
commit
4dccbb6839
  1. 6
      README.md
  2. 30
      package-lock.json
  3. 20
      package.json
  4. 36
      src/api/http/config.ts
  5. 40
      src/api/http/request.ts
  6. 3
      src/api/layer.ts
  7. 113
      src/api/manage.ts
  8. 17
      src/api/media.ts
  9. 266
      src/api/pilot-bridge.ts
  10. 52
      src/api/wayline.ts
  11. 21
      src/api/websocket.ts
  12. BIN
      src/assets/icons/cloudapi.png
  13. BIN
      src/assets/icons/dji_logo.png
  14. BIN
      src/assets/icons/dock.png
  15. BIN
      src/assets/icons/drone.png
  16. BIN
      src/assets/icons/m30.png
  17. BIN
      src/assets/icons/no-data.png
  18. BIN
      src/assets/icons/rc.png
  19. 624
      src/components/GMap.vue
  20. 168
      src/components/MediaPanel.vue
  21. 182
      src/components/TaskPanel.vue
  22. 350
      src/components/livestream-agora.vue
  23. 378
      src/components/livestream-others.vue
  24. 5
      src/components/wayline-panel.vue
  25. 7
      src/constants/index.ts
  26. 10
      src/hooks/use-g-map-cover.ts
  27. 96
      src/hooks/use-g-map-tsa.ts
  28. 54
      src/pages/page-pilot/pilot-bind.vue
  29. 579
      src/pages/page-pilot/pilot-home.vue
  30. 96
      src/pages/page-pilot/pilot-index.vue
  31. 301
      src/pages/page-pilot/pilot-liveshare.vue
  32. 96
      src/pages/page-pilot/pilot-media.vue
  33. 57
      src/pages/project-app/home.vue
  34. 164
      src/pages/project-app/index.vue
  35. 251
      src/pages/project-app/projects/create-plan.vue
  36. 518
      src/pages/project-app/projects/devices.vue
  37. 100
      src/pages/project-app/projects/dock.vue
  38. 10
      src/pages/project-app/projects/layer.vue
  39. 96
      src/pages/project-app/projects/livestream.vue
  40. 1
      src/pages/project-app/projects/media.vue
  41. 169
      src/pages/project-app/projects/members.vue
  42. 38
      src/pages/project-app/projects/task.vue
  43. 456
      src/pages/project-app/projects/tsa.vue
  44. 204
      src/pages/project-app/projects/wayline.vue
  45. 140
      src/pages/project-app/projects/workspace.vue
  46. 50
      src/pages/project-app/sidebar.vue
  47. 97
      src/pages/project-app/topbar.vue
  48. 114
      src/router/index.ts
  49. 121
      src/store/index.ts
  50. 14
      src/styles/common.scss
  51. 2
      src/styles/fonts.scss
  52. 198
      src/types/device.ts
  53. 110
      src/types/enums.ts
  54. 55
      src/types/live-stream.ts
  55. 30
      src/types/wayline.ts
  56. 8
      src/utils/common.ts
  57. 5
      tsconfig.json
  58. 2
      vite.config.ts
  59. 15
      yarn.lock

6
README.md

@ -6,15 +6,15 @@ The launch of the Cloud API mainly solves the problem of developers reinventing @@ -6,15 +6,15 @@ The launch of the Cloud API mainly solves the problem of developers reinventing
## 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

30
package-lock.json generated

@ -12,7 +12,7 @@ @@ -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 @@ @@ -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 @@ @@ -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",

20
package.json

@ -12,7 +12,7 @@ @@ -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 @@ @@ -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",

36
src/api/http/config.ts

@ -1,20 +1,36 @@ @@ -1,20 +1,36 @@
export const CURRENT_CONFIG = {
// license
appId: 'Please enter the app id.', // You need to go to the development website to apply.
appKey: 'Please enter the app key.', // You need to go to the development website to apply.
appLicense: 'Please enter the app license.' // You need to go to the development website to apply.
// 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/'
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.',
// 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 token.',
agoraToken: 'Please enter the agora temporary token.',
agoraChannel: 'Please enter the agora channel.',
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.
// map
// You can apply on the AMap website.
amapKey: 'Please enter the amap key.',
}

40
src/api/http/request.ts

@ -1,10 +1,14 @@ @@ -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 @@ const instance = axios.create({ @@ -17,7 +21,7 @@ const instance = axios.create({
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.request.use( @@ -28,10 +32,15 @@ instance.interceptors.request.use(
)
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 @@ instance.interceptors.response.use( @@ -46,15 +55,26 @@ instance.interceptors.response.use(
}
// @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)

3
src/api/layer.ts

@ -1,8 +1,9 @@ @@ -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 => {

113
src/api/manage.ts

@ -1,12 +1,31 @@ @@ -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 @@ export const refreshToken = async function (body: {}): Promise<IWorkspaceRespons @@ -20,23 +39,23 @@ export const refreshToken = async function (body: {}): Promise<IWorkspaceRespons
}
// 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 @@ export const stopLivestream = async function (body: {}): Promise<IWorkspaceRespo @@ -60,3 +79,75 @@ export const stopLivestream = async function (body: {}): Promise<IWorkspaceRespo
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
}

17
src/api/media.ts

@ -1,9 +1,18 @@ @@ -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
}

266
src/api/pilot-bridge.ts

@ -1,95 +1,151 @@ @@ -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', {
})
},
getComponentParam (key:string) {
}
components.set(EComponentName.Api, apiParam)
components.set(EComponentName.Tsa, {})
const mediaParam: MediaParam = {
autoUploadPhoto: true,
autoUploadPhotoType: EPhotoType.Preview,
autoUploadVideo: true
}
components.set(EComponentName.Media, mediaParam)
components.set(EComponentName.Mission, {})
return components
},
getComponentParam (key:EComponentName): any {
return components.get(key)
},
setComponentParam (key: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 @@ export default { @@ -98,14 +154,42 @@ export default {
return window.djiBridge.platformClearLogEncryptKey()
},
getLogPath ():string {
return window.djiBridge.platformGetLogPath()
return returnString(window.djiBridge.platformGetLogPath())
},
platformVerifyLicense (appId:string, appKey:string, appLicense:string): boolean {
return returnBool(window.djiBridge.platformVerifyLicense(appId, appKey, appLicense))
},
isPlatformVerifySuccess (): boolean {
return returnBool(window.djiBridge.platformIsVerified())
},
isAppInstalled (pkgName: string): boolean {
return returnBool(window.djiBridge.platformIsAppInstalled(pkgName))
},
getVersion (): string {
return window.djiBridge.platformGetVersion()
},
platformVerifyLicense (appId:string, appKey:string, appLicense:string):string {
return window.djiBridge.platformVerifyLicense(appId, appKey, appLicense)
// 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))
},
isPlatformVerifySuccess ():string {
return window.djiBridge.platformIsVerified()
getHost (): string {
return returnString(window.djiBridge.apiGetHost())
},
// liveshare
/**
*
@ -114,8 +198,8 @@ export default { @@ -114,8 +198,8 @@ export default {
* 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 @@ export default { @@ -123,8 +207,8 @@ export default {
* @returns
* type: liveshare type 0unknown, 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 @@ export default { @@ -134,50 +218,66 @@ export default {
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 (): boolean {
return returnBool(window.djiBridge.liveshareStartLive())
},
stopLiveshare (): boolean {
return returnBool(window.djiBridge.liveshareStopLive())
},
// WebSocket
wsGetConnectState (): boolean {
return returnBool(window.djiBridge.wsGetConnectState())
},
startLiveshare ():string {
return window.djiBridge.liveshareStartLive()
wsConnect (host: string, token: string, callback: string): string {
return window.djiBridge.wsConnect(host, token, callback)
},
stopLiveshare ():string {
return window.djiBridge.liveshareStopLive()
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()
}
}
}

52
src/api/wayline.ts

@ -1,9 +1,55 @@ @@ -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
}

21
src/api/websocket.ts

@ -1,14 +1,14 @@ @@ -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 @@ export default { @@ -18,13 +18,16 @@ export default {
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()
}
}

BIN
src/assets/icons/cloudapi.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/icons/dji_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
src/assets/icons/dock.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
src/assets/icons/drone.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
src/assets/icons/m30.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
src/assets/icons/no-data.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
src/assets/icons/rc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

624
src/components/GMap.vue

@ -5,19 +5,378 @@ @@ -5,19 +5,378 @@
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,13 +399,43 @@ import { MapDoodleEnum } from '/@/types/map-enum' @@ -40,13 +399,43 @@ import { MapDoodleEnum } from '/@/types/map-enum'
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()
@ -54,6 +443,86 @@ export default defineComponent({ @@ -54,6 +443,86 @@ export default defineComponent({
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 @@ export default defineComponent({ @@ -63,6 +532,57 @@ export default defineComponent({
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 @@ export default defineComponent({ @@ -144,7 +664,9 @@ export default defineComponent({
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 @@ export default defineComponent({ @@ -281,7 +803,16 @@ export default defineComponent({
return {
draw,
mouseMode,
drawVisible
drawVisible,
osdVisible,
pin,
state,
M30,
deviceInfo,
EGear,
EModeCode,
str,
EDockModeCode,
}
}
})
@ -296,16 +827,77 @@ export default defineComponent({ @@ -296,16 +827,77 @@ export default defineComponent({
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;
}
}
</style>
<style lang="scss">
.amap-logo {
opacity: 0;
.selection {
border: 1px solid $primary;
border-radius: 2px;
}
}
.osd-panel {
position: absolute;
left: 350px;
top: 10px;
width: 480px;
height: 160px;
background: black;
color: white;
border-radius: 2px;
opacity: 0.7;
}
.osd > div {
padding-top: 5px;
padding-left: 5px;
}
.amap-copyright {
opacity: 0;
.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>

168
src/components/MediaPanel.vue

@ -1,92 +1,168 @@ @@ -1,92 +1,168 @@
<template>
<div class="header">Media Files</div>
<a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
<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>
<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 #action>
<span class="action-area">
action
</span>
<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: 'File Name',
dataIndex: 'name',
key: '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: 'Payload Type',
dataIndex: 'payload_type',
key: 'payload_type',
ellipsis: true
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 body: IPage = {
page: 1,
total: 0,
page_size: 50
}
])
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
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])
})
console.log(res)
}
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;
margin-top: 10px;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
.header {
width: 100%;
height: 60px;
background: #fff;
padding: 16px 24px;
padding: 16px;
font-size: 20px;
font-weight: bold;
text-align: start;
color: #000;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
</style>

182
src/components/TaskPanel.vue

@ -0,0 +1,182 @@ @@ -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>

350
src/components/livestream-agora.vue

@ -0,0 +1,350 @@ @@ -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>

378
src/components/livestream-others.vue

@ -0,0 +1,378 @@ @@ -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>

5
src/components/wayline-panel.vue

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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 @@ onMounted(() => { @@ -74,7 +75,7 @@ onMounted(() => {
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, {

7
src/constants/index.ts

@ -1,5 +1,7 @@ @@ -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 @@ export const AMapConfig = { @@ -14,6 +16,7 @@ export const AMapConfig = {
'AMap.PolyEditor',
'AMap.RangingTool',
'AMap.Weather',
'AMap.MouseTool'
'AMap.MouseTool',
'AMap.MoveAnimation'
]
}

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

@ -11,10 +11,10 @@ import { GeojsonCoordinate } from '/@/types/map' @@ -11,10 +11,10 @@ import { GeojsonCoordinate } from '/@/types/map'
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 @@ export function useGMapCover () { @@ -27,10 +27,10 @@ export function useGMapCover () {
} = {
'2d8cf0': pin2d8cf0,
'19be6b': pin19be6b,
212121: pin212121,
b620e0: pinb620e0,
e23c39: pine23c39,
ffbb00: pineffbb00,
'212121': pin212121,
'b620e0': pinb620e0,
'e23c39': pine23c39,
'ffbb00': pineffbb00,
}
const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()

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

@ -0,0 +1,96 @@ @@ -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
}
}

54
src/pages/page-pilot/pilot-bind.vue

@ -0,0 +1,54 @@ @@ -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>

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

@ -1,132 +1,501 @@ @@ -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
>
<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 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 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 }}&nbsp;</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.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
}
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(
platformName.value,
'' + localStorage.getItem(ELocalStorageKey.PlatformName),
workspaceName.value,
workspaceDesc.value
'' + localStorage.getItem(ELocalStorageKey.WorkspaceDesc)
)
apiPilot.setWorkspaceId(wsId.value)
})
return
}
if (JSON.parse(apiPilot.isComponentLoaded('thing')).data === false || token) {
getUserInfo({}).then(res => {
const param = {
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: res.data.mqtt_username,
username: username.value,
password: res.data.mqtt_password,
connectCallback: 'connectCallback'
}
apiPilot.setComponentParam('thing', param)
apiPilot.loadComponent('thing', apiPilot.getComponentParam('thing'))
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
})
} else {
const connectState = JSON.parse(window.djiBridge.thingGetConnectState())
if (connectState.code === 0 && connectState.data) {
connect.value = 'Connected'
} else {
connect.value = 'Disconnect'
window.connectCallback = arg => {
connectCallback(arg)
}
window.wsConnectCallback = arg => {
wsConnectCallback(arg)
}
apiPilot.loadComponent('liveshare', apiPilot.getComponentParam('liveshare'))
console.log('ws token:', token)
apiPilot.setComponentParam('ws', {
host: CURRENT_CONFIG.websocketURL,
token: token
})
apiPilot.loadComponent('ws', apiPilot.getComponentParam('ws'))
apiPilot.setComponentParam('map', {
userName: 'pilot1',
elementPreName: 'PILOT'
onUnmounted(() => {
socket.close()
})
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)
const connectCallback = async (arg: any) => {
if (arg) {
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)
}
apiPilot.onBackClickReg()
})
const connectCallback = (arg: any) => {
console.info('into callback', arg)
}, 2000)
setTimeout(getDeviceInfo, 3000)
} else {
thingState.value = EStatusValue.DISCONNECT
}
refreshStatus()
}
const wsConnectCallback = async (arg: any) => {
if (arg) {
connect.value = 'Connected'
window.djiBridge.mediaSetDownloadOwner(0)
window.djiBridge.mediaSetUploadPhotoType(1)
wsState.value = EStatusValue.CONNECTED
} else {
connect.value = 'Disconnect'
wsState.value = EStatusValue.DISCONNECT
}
}
const confirmAgain = () => {
exitVisible.value = true
}
const onExit = async (e: any) => {
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 @@ const onOpen3rdApp = () => { @@ -139,12 +508,32 @@ const onOpen3rdApp = () => {
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>

96
src/pages/page-pilot/pilot-index.vue

@ -2,7 +2,7 @@ @@ -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 @@ @@ -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 @@ @@ -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>

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

@ -1,16 +1,17 @@ @@ -1,16 +1,17 @@
<template>
<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="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
>
<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 @@ @@ -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 @@ @@ -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 showLivingStatus = () => {
drawerVisible.value = !drawerVisible.value
}
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
}
const onGetStatus = () => {
const status = apiPilot.getLiveshareStatus()
message.info(status, 5)
}, 2000)
}
const onGetConfig = () => {
const status = apiPilot.getLiveshareConfig()
message.info(status, 5)
}
</script>

96
src/pages/page-pilot/pilot-media.vue

@ -1,18 +1,16 @@ @@ -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 @@ @@ -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 @@ @@ -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 { message } from 'ant-design-vue' @@ -71,58 +72,31 @@ import { message } from 'ant-design-vue'
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)
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()
const enablePhotoUpload = ref<boolean>(apiPilot.getAutoUploadPhoto())
const enableVideoUpload = ref<boolean>(apiPilot.getAutoUploadVideo())
const photoType = ref<number>(apiPilot.getUploadPhotoType())
const uploadPath = ref<number>(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>

57
src/pages/project-app/home.vue

@ -0,0 +1,57 @@ @@ -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>

164
src/pages/project-app/index.vue

@ -1,20 +1,18 @@ @@ -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 @@ @@ -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)
}
}
// 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
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)
}
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>
@ -161,48 +85,4 @@ function getWaylineRoute () { @@ -161,48 +85,4 @@ function getWaylineRoute () {
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>

251
src/pages/project-app/projects/create-plan.vue

@ -0,0 +1,251 @@ @@ -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>

518
src/pages/project-app/projects/devices.vue

@ -0,0 +1,518 @@ @@ -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>

100
src/pages/project-app/projects/dock.vue

@ -0,0 +1,100 @@ @@ -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>

10
src/pages/project-app/projects/layer.vue

@ -1,5 +1,12 @@ @@ -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 @@ function updateCoordinates (transformType: string, element: LayerResource) { @@ -389,9 +396,6 @@ function updateCoordinates (transformType: string, element: LayerResource) {
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.project-layer-wrapper {
padding-top: 16px;
}
</style>
<style lang="scss">
.drawer-element-wrapper {

96
src/pages/project-app/projects/livestream.vue

@ -1,65 +1,56 @@ @@ -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
>
</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"
@click="selectLivestream(item.routeName)"
>{{ item.label }}</a-button
>
<LiveAgora />
</a-modal>
</router-link>
</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 onOthersLive = () => {
console.log('liveview')
enableOthersLive.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
}
onMounted(() => {
watch(() => root.$route.name, data => {
showLive.value = data === ERouterName.LIVING
},
{
deep: true
})
})
</script>
<style lang="scss">
@ -79,4 +70,17 @@ const onOthersLive = () => { @@ -79,4 +70,17 @@ const onOthersLive = () => {
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>

1
src/pages/project-app/projects/media.vue

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
<template>
<div class="project-media-wrapper">
Media
</div>
</template>

169
src/pages/project-app/projects/members.vue

@ -0,0 +1,169 @@ @@ -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>

38
src/pages/project-app/projects/task.vue

@ -0,0 +1,38 @@ @@ -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>

456
src/pages/project-app/projects/tsa.vue

@ -1,11 +1,463 @@ @@ -1,11 +1,463 @@
<template>
<div class="project-tsa-wrapper ">
TSA
<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>

204
src/pages/project-app/projects/wayline.vue

@ -1,9 +1,205 @@ @@ -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>
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'
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 lang="ts" setup></script>
</script>
<style lang="scss" scoped></style>
<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>

140
src/pages/project-app/projects/workspace.vue

@ -0,0 +1,140 @@ @@ -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>

50
src/pages/project-app/sidebar.vue

@ -1,5 +1,6 @@ @@ -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 @@ @@ -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 @@ interface IOptions { @@ -32,26 +42,38 @@ interface IOptions {
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 @@ export default defineComponent({ @@ -62,7 +84,7 @@ export default defineComponent({
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 @@ export default defineComponent({ @@ -76,7 +98,7 @@ export default defineComponent({
color: $text-white-basic;
cursor: pointer;
&.selected {
background-color: $dark-highlight;
background-color: #101010;
color: $primary;
}
&.disabled {
@ -95,8 +117,6 @@ export default defineComponent({ @@ -95,8 +117,6 @@ export default defineComponent({
}
}
</style>
<style>
.ant-tooltip-open {
border: 0;
}

97
src/pages/project-app/topbar.vue

@ -0,0 +1,97 @@ @@ -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>

114
src/router/index.ts

@ -1,63 +1,127 @@ @@ -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.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,
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'),
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.PILOT,
name: ERouterName.PILOT,
component: () => import('/@/pages/page-pilot/pilot-index.vue'),
},
{
path: '/' + ERouterName.PilotHome,
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')
}
]

121
src/store/index.ts

@ -1,7 +1,11 @@ @@ -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 @@ const initStateFunc = () => ({ @@ -23,12 +27,6 @@ const initStateFunc = () => ({
type: 2
}
],
GatewayInfo: { // remote controller, dock
},
DeviceInfo: { // drone
},
layerBaseInfo: {} as {
[key:string]:string
},
@ -40,6 +38,55 @@ const initStateFunc = () => ({ @@ -40,6 +38,55 @@ const initStateFunc = () => ({
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 @@ const mutations: MutationTree<RootStateType> = { @@ -52,12 +99,29 @@ const mutations: MutationTree<RootStateType> = {
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 @@ const mutations: MutationTree<RootStateType> = { @@ -71,6 +135,43 @@ const mutations: MutationTree<RootStateType> = {
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> = {

14
src/styles/common.scss

@ -10,10 +10,22 @@ body { @@ -10,10 +10,22 @@ body {
// 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);
}
}

2
src/styles/fonts.scss

@ -32,7 +32,7 @@ $line-heights: ( @@ -32,7 +32,7 @@ $line-heights: (
}
.fz10 {
font-size: 10;
font-size: 10px;
}
.fz12 {
font-size: 12px;

198
src/types/device.ts

@ -0,0 +1,198 @@ @@ -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
}

110
src/types/enums.ts

@ -1,15 +1,25 @@ @@ -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 @@ export enum EStorageKey { @@ -17,3 +27,81 @@ export enum EStorageKey {
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,
}

55
src/types/live-stream.ts

@ -0,0 +1,55 @@ @@ -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'
}

30
src/types/wayline.ts

@ -0,0 +1,30 @@ @@ -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'
}

8
src/utils/common.ts

@ -0,0 +1,8 @@ @@ -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)
}

5
tsconfig.json

@ -24,6 +24,7 @@ @@ -24,6 +24,7 @@
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
, "src/vendors/coordtransform.js" ]
"src/**/*.vue",
"src/vendors/coordtransform.js"
]
}

2
vite.config.ts

@ -28,7 +28,7 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => defineConfig( @@ -28,7 +28,7 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => defineConfig(
}),
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,

15
yarn.lock

@ -613,10 +613,17 @@ @@ -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=="

Loading…
Cancel
Save