sean.zhou
2022-07-22 4dccbb68399da2d46943b8d613585afb49aa5455
V1.1.0 for dock
35 files modified
24 files added
6635 ■■■■ changed files
README.md 6 ●●●● patch | view | raw | blame | history
package-lock.json 30 ●●●● patch | view | raw | blame | history
package.json 20 ●●●●● patch | view | raw | blame | history
src/api/http/config.ts 44 ●●●●● patch | view | raw | blame | history
src/api/http/request.ts 40 ●●●● patch | view | raw | blame | history
src/api/layer.ts 3 ●●●● patch | view | raw | blame | history
src/api/manage.ts 113 ●●●●● patch | view | raw | blame | history
src/api/media.ts 17 ●●●● patch | view | raw | blame | history
src/api/pilot-bridge.ts 264 ●●●●● patch | view | raw | blame | history
src/api/wayline.ts 52 ●●●●● patch | view | raw | blame | history
src/api/websocket.ts 21 ●●●●● patch | view | raw | blame | history
src/assets/icons/cloudapi.png patch | view | raw | blame | history
src/assets/icons/dji_logo.png patch | view | raw | blame | history
src/assets/icons/dock.png patch | view | raw | blame | history
src/assets/icons/drone.png patch | view | raw | blame | history
src/assets/icons/m30.png patch | view | raw | blame | history
src/assets/icons/no-data.png patch | view | raw | blame | history
src/assets/icons/rc.png patch | view | raw | blame | history
src/components/GMap.vue 624 ●●●●● patch | view | raw | blame | history
src/components/MediaPanel.vue 194 ●●●●● patch | view | raw | blame | history
src/components/TaskPanel.vue 182 ●●●●● patch | view | raw | blame | history
src/components/livestream-agora.vue 350 ●●●●● patch | view | raw | blame | history
src/components/livestream-others.vue 378 ●●●●● patch | view | raw | blame | history
src/components/wayline-panel.vue 5 ●●●●● patch | view | raw | blame | history
src/constants/index.ts 7 ●●●● patch | view | raw | blame | history
src/hooks/use-g-map-cover.ts 10 ●●●● patch | view | raw | blame | history
src/hooks/use-g-map-tsa.ts 96 ●●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-bind.vue 54 ●●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-home.vue 601 ●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-index.vue 96 ●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-liveshare.vue 301 ●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-media.vue 96 ●●●●● patch | view | raw | blame | history
src/pages/project-app/home.vue 57 ●●●●● patch | view | raw | blame | history
src/pages/project-app/index.vue 164 ●●●● patch | view | raw | blame | history
src/pages/project-app/projects/create-plan.vue 251 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/devices.vue 518 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/dock.vue 100 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/layer.vue 10 ●●●● patch | view | raw | blame | history
src/pages/project-app/projects/livestream.vue 96 ●●●● patch | view | raw | blame | history
src/pages/project-app/projects/media.vue 1 ●●●● patch | view | raw | blame | history
src/pages/project-app/projects/members.vue 169 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/task.vue 38 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/tsa.vue 458 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/wayline.vue 204 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/workspace.vue 140 ●●●●● patch | view | raw | blame | history
src/pages/project-app/sidebar.vue 50 ●●●● patch | view | raw | blame | history
src/pages/project-app/topbar.vue 97 ●●●●● patch | view | raw | blame | history
src/router/index.ts 118 ●●●● patch | view | raw | blame | history
src/store/index.ts 121 ●●●●● patch | view | raw | blame | history
src/styles/common.scss 14 ●●●●● patch | view | raw | blame | history
src/styles/fonts.scss 2 ●●● patch | view | raw | blame | history
src/types/device.ts 198 ●●●●● patch | view | raw | blame | history
src/types/enums.ts 110 ●●●●● patch | view | raw | blame | history
src/types/live-stream.ts 55 ●●●●● patch | view | raw | blame | history
src/types/wayline.ts 30 ●●●●● patch | view | raw | blame | history
src/utils/common.ts 8 ●●●●● patch | view | raw | blame | history
tsconfig.json 5 ●●●●● patch | view | raw | blame | history
vite.config.ts 2 ●●● patch | view | raw | blame | history
yarn.lock 15 ●●●● patch | view | raw | blame | history
README.md
@@ -6,15 +6,15 @@
## Docker
If you don't want to install the development environment, you can try deploying with docker. [Click the link to download.](https://terra-sz-hc1pro-cloudapi.oss-cn-shenzhen.aliyuncs.com/c0af9fe0d7eb4f35a8fe5b695e4d0b96/docker/cloud_api_sample_docker_1.0.0.zip)
If you don't want to install the development environment, you can try deploying with docker. [Click the link to download.](https://terra-sz-hc1pro-cloudapi.oss-cn-shenzhen.aliyuncs.com/c0af9fe0d7eb4f35a8fe5b695e4d0b96/docker/cloud_api_sample_docker.zip)
## Usage
For more documentation, please visit the [DJI Developer Documentation](https://developer.dji.com/cn/document/209883f1-f2ad-406e-b99c-be7498df7f10).
For more documentation, please visit the [DJI Developer Documentation](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
## Latest Release
Cloud API 1.0.0 was released on 21 March 2022. For more information, please visit the [Release Note](https://developer.dji.com/cn/document/87026f9b-e906-4809-9aba-870f569061b5).
Cloud API 1.1.0 was released on 22 July 2022. For more information, please visit the [Release Note](https://developer.dji.com/doc/cloud-api-tutorial/cn/).
## License
package-lock.json
@@ -12,7 +12,7 @@
        "@amap/amap-jsapi-loader": "^1.0.1",
        "@ant-design/icons-vue": "^6.0.1",
        "@vitejs/plugin-legacy": "^1.6.2",
        "agora-rtc-sdk-ng": "latest",
        "agora-rtc-sdk-ng": "^4.12.1",
        "ant-design-vue": "^2.2.8",
        "axios": "^0.21.1",
        "query-string": "^7.0.1",
@@ -1226,9 +1226,17 @@
      }
    },
    "node_modules/agora-rtc-sdk-ng": {
      "version": "4.9.1",
      "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.9.1.tgz",
      "integrity": "sha512-Jogn3TQC7VdA7uZjGYmaAs0XzgYBgGs6nGA67/dQVjqC7kiwAfkQsAuvbevE/qxrVJmLfqtDTNxP40IFvnTlgQ=="
      "version": "4.12.1",
      "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.12.1.tgz",
      "integrity": "sha512-kmc+ZyKDdnY/BN3iAwBs+MSgTX8Zkc6THFSIAXN9WebjZ/F+N/JXItoNEcgQe3MdTABUli6w3pZ+iObnDqVkBw==",
      "dependencies": {
        "agora-rte-extension": "^1.0.22"
      }
    },
    "node_modules/agora-rte-extension": {
      "version": "1.0.23",
      "resolved": "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.0.23.tgz",
      "integrity": "sha512-X2cGBg+L5ZJIFU91qvMASvRsBfg1HXTktVG3YROw9wxHsILSI7jgF9R9XraLc3fNX/UjovaYAlUW+hiJe0v6Xw=="
    },
    "node_modules/ajv": {
      "version": "6.12.6",
@@ -8774,9 +8782,17 @@
      "requires": {}
    },
    "agora-rtc-sdk-ng": {
      "version": "4.9.1",
      "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.9.1.tgz",
      "integrity": "sha512-Jogn3TQC7VdA7uZjGYmaAs0XzgYBgGs6nGA67/dQVjqC7kiwAfkQsAuvbevE/qxrVJmLfqtDTNxP40IFvnTlgQ=="
      "version": "4.12.1",
      "resolved": "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.12.1.tgz",
      "integrity": "sha512-kmc+ZyKDdnY/BN3iAwBs+MSgTX8Zkc6THFSIAXN9WebjZ/F+N/JXItoNEcgQe3MdTABUli6w3pZ+iObnDqVkBw==",
      "requires": {
        "agora-rte-extension": "^1.0.22"
      }
    },
    "agora-rte-extension": {
      "version": "1.0.23",
      "resolved": "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.0.23.tgz",
      "integrity": "sha512-X2cGBg+L5ZJIFU91qvMASvRsBfg1HXTktVG3YROw9wxHsILSI7jgF9R9XraLc3fNX/UjovaYAlUW+hiJe0v6Xw=="
    },
    "ajv": {
      "version": "6.12.6",
package.json
@@ -12,7 +12,7 @@
    "@amap/amap-jsapi-loader": "^1.0.1",
    "@ant-design/icons-vue": "^6.0.1",
    "@vitejs/plugin-legacy": "^1.6.2",
    "agora-rtc-sdk-ng": "latest",
    "agora-rtc-sdk-ng": "^4.12.1",
    "ant-design-vue": "^2.2.8",
    "axios": "^0.21.1",
    "query-string": "^7.0.1",
@@ -61,21 +61,39 @@
        "agora-rtc-sdk-ng",
        "ant-design-vue",
        "ant-design-vue/es",
        "ant-design-vue/es/avatar/style/css",
        "ant-design-vue/es/breadcrumb/style/css",
        "ant-design-vue/es/button/style/css",
        "ant-design-vue/es/checkbox/style/css",
        "ant-design-vue/es/col/style/css",
        "ant-design-vue/es/collapse/style/css",
        "ant-design-vue/es/date-picker/style/css",
        "ant-design-vue/es/divider/style/css",
        "ant-design-vue/es/drawer/style/css",
        "ant-design-vue/es/dropdown/style/css",
        "ant-design-vue/es/empty/style/css",
        "ant-design-vue/es/form/style/css",
        "ant-design-vue/es/image/style/css",
        "ant-design-vue/es/input/style/css",
        "ant-design-vue/es/layout/style/css",
        "ant-design-vue/es/menu/style/css",
        "ant-design-vue/es/message/style/css",
        "ant-design-vue/es/modal/style/css",
        "ant-design-vue/es/pagination/style/css",
        "ant-design-vue/es/popconfirm/style/css",
        "ant-design-vue/es/popover/style/css",
        "ant-design-vue/es/progress/style/css",
        "ant-design-vue/es/radio/style/css",
        "ant-design-vue/es/row/style/css",
        "ant-design-vue/es/select/style/css",
        "ant-design-vue/es/space/style/css",
        "ant-design-vue/es/spin/style/css",
        "ant-design-vue/es/switch/style/css",
        "ant-design-vue/es/table/style/css",
        "ant-design-vue/es/tooltip/style/css",
        "ant-design-vue/es/tree/style/css",
        "axios",
        "moment",
        "reconnecting-websocket",
        "vconsole",
        "vue",
src/api/http/config.ts
@@ -1,20 +1,36 @@
export const CURRENT_CONFIG = {
  baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
  websocketURL: 'Please enter the WebSocket access address.',  // Example: 'ws://192.168.1.1:6789/api/v1/ws'
  rtmpURL: 'Please enter the rtmp access address.',  // Example: 'rtmp://192.168.1.1/live/'
  gb28181Para:
    'serverIP=Please enter the server ip.&serverPort=Please enter the server port.&serverID=Please enter the server id.' +
    '&agentID=Please enter the agent id.&agentPassword=Please enter the agent password' +
    '&localPort=Please enter the local port.&channel=Please enter the channel.',
  rtspPara: 'userName=Please enter the username.&password=Please enter the password&port=Please enter the port.',
  amapKey: 'Please enter the amap key.',
  agoraAPPID: 'Please enter the agora app id.',
  agoraToken: 'Please enter the agora token.',
  agoraChannel: 'Please enter the agora channel.',
  // license
  appId: 'Please enter the app id.',  // You need to go to the development website to apply.
  appKey: 'Please enter the app key.',  // You need to go to the development website to apply.
  appLicense: 'Please enter the app license.' // You need to go to the development website to apply.
  // http
  baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
  websocketURL: 'Please enter the WebSocket access address.',  // Example: 'ws://192.168.1.1:6789/api/v1/ws'
  // livestreaming
  // RTMP  Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
  rtmpURL: 'Please enter the rtmp access address.',  // Example: 'rtmp://192.168.1.1/live/'
  // GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
  gbServerIp: 'Please enter the server ip.',
  gbServerPort: 'Please enter the server port.',
  gbServerId: 'Please enter the server id.',
  gbAgentId: 'Please enter the agent id',
  gbPassword: 'Please enter the agent password',
  gbAgentPort: 'Please enter the local port.',
  gbAgentChannel: 'Please enter the channel.',
  // RTSP
  rtspUserName: 'Please enter the username.',
  rtspPassword: 'Please enter the password.',
  rtspPort: '8554',
  // Agora
  agoraAPPID: 'Please enter the agora app id.',
  agoraToken: 'Please enter the agora temporary token.',
  agoraChannel: 'Please enter the agora channel.',
  // map
  // You can apply on the AMap website.
  amapKey: 'Please enter the amap key.',
}
src/api/http/request.ts
@@ -1,10 +1,14 @@
import axios from 'axios'
import { uuidv4 } from '/@/utils/uuid'
import { CURRENT_CONFIG } from './config'
import { message } from 'ant-design-vue'
import router from '/@/router'
import { ELocalStorageKey, ERouterName, EUserType } from '/@/types/enums'
export * from './type'
const REQUEST_ID = 'X-Request-Id'
function getAuthToken () {
  return localStorage.getItem('x-auth-token')
  return localStorage.getItem(ELocalStorageKey.Token)
}
const instance = axios.create({
@@ -17,7 +21,7 @@
instance.interceptors.request.use(
  config => {
    config.headers['X-Auth-Token'] = getAuthToken()
    config.headers[ELocalStorageKey.Token] = getAuthToken()
    // config.headers[REQUEST_ID] = uuidv4()
    config.baseURL = CURRENT_CONFIG.baseURL
    return config
@@ -28,10 +32,15 @@
)
instance.interceptors.response.use(
  response => response,
  response => {
    console.info('URL: ' + response.config.baseURL + response.config.url, '\nData: ', response.data, '\nResponse:', response)
    if (response.data.code && response.data.code !== 0) {
      message.error(response.data.message)
    }
    return response
  },
  err => {
    const requestId = err?.config?.headers && err?.config?.headers[REQUEST_ID]
    console.info('')
    if (requestId) {
      console.info(REQUEST_ID, ':', requestId)
    }
@@ -46,15 +55,26 @@
    }
    // @See: https://github.com/axios/axios/issues/383
    if (!err.response || !err.response.status) {
      console.log('The network is abnormal, please check the network and try again')
    } else if (err.response?.status !== 200) {
      console.log(`ERROR_CODE: ${err.response?.status}`)
      message.error('The network is abnormal, please check the backend service and try again')
      return
    }
    if (err.response?.status === 403) {
      // window.location.href = '/'
    if (err.response?.status !== 200) {
      message.error(`ERROR_CODE: ${err.response?.status}`)
    }
    // if (err.response?.status === 403) {
    //   // window.location.href = '/'
    // }
    if (err.response?.status === 401) {
      console.log(err.response)
      console.error(err.response)
      const flag: number = Number(localStorage.getItem(ELocalStorageKey.Flag))
      switch (flag) {
        case EUserType.Web:
          router.push(ERouterName.PROJECT)
          break
        case EUserType.Pilot:
          router.push(ERouterName.PILOT)
          break
      }
    }
    return Promise.reject(err)
src/api/layer.ts
@@ -1,8 +1,9 @@
import { ELocalStorageKey } from '../types/enums'
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { mapLayers } from '/@/constants/mock-layers'
import { elementGroupsReq, PostElementsBody, PutElementsBody } from '/@/types/mapLayer'
const PREFIX = '/map/api/v1'
const workspace_id = localStorage.getItem('workspace-id')
const workspace_id = localStorage.getItem(ELocalStorageKey.WorkspaceId)
type UnknownResponse = Promise<IWorkspaceResponse<unknown>>
// get elements group
// export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
src/api/manage.ts
@@ -1,12 +1,31 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/manage/api/v1'
// login
interface loginBody {
export interface LoginBody {
 username: string,
 password: string
 password: string,
 flag: number,
}
export const login = async function (body: loginBody): Promise<IWorkspaceResponse<any>> {
export interface BindBody {
  device_sn: string,
  user_id: string,
  workspace_id: string,
  domain?: string
}
export interface HmsQueryBody {
  sns: string[],
  children_sn: string,
  device_sn: string,
  language: string,
  level: number | string,
  begin_time: number,
  end_time: number,
  message: string,
  domain: string,
}
export const login = async function (body: LoginBody): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/login`
  const result = await request.post(url, body)
  return result.data
@@ -20,23 +39,23 @@
}
// Get Platform Info
export const getPlatformInfo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
export const getPlatformInfo = async function (): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/current`
  const result = await request.get(url, body)
  const result = await request.get(url)
  return result.data
}
// Get User Info
export const getUserInfo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
export const getUserInfo = async function (): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/users/current`
  const result = await request.get(url, body)
  const result = await request.get(url)
  return result.data
}
// Get Device Topo
export const getDeviceTopo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/devices`
  const result = await request.get(url, body)
export const getDeviceTopo = async function (workspace_id: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices`
  const result = await request.get(url)
  return result.data
}
@@ -60,3 +79,75 @@
  const result = await request.post(url, body)
  return result.data
}
// Update Quality
export const setLivestreamQuality = async function (body: {}): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/live/streams/update`
  const result = await request.post(url, body)
  return result.data
}
export const getAllUsersInfo = async function (wid: string, body: IPage): Promise<CommonListResponse<any>> {
  const url = `${HTTP_PREFIX}/users/${wid}/users?&page=${body.page}&page_size=${body.page_size}`
  const result = await request.get(url)
  return result.data
}
export const updateUserInfo = async function (wid: string, user_id: string, body: {}): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/users/${wid}/users/${user_id}`
  const result = await request.put(url, body)
  return result.data
}
export const bindDevice = async function (body: BindBody): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${body.device_sn}/binding`
  const result = await request.post(url, body)
  return result.data
}
export const unbindDevice = async function (device_sn: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${device_sn}/unbinding`
  const result = await request.delete(url)
  return result.data
}
export const getDeviceBySn = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
  const result = await request.get(url)
  return result.data
}
export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}&domain=${domain}`
  const result = await request.get(url)
  return result.data
}
export const updateDevice = async function (body: {}, workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
  const result = await request.put(url, body)
  return result.data
}
export const getUnreadDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
  const result = await request.get(url)
  return result.data
}
export const updateDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
  const result = await request.put(url)
  return result.data
}
export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: string, pagination: IPage): Promise<IListWorkspaceResponse<any>> {
  let url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms?page=${pagination.page}&pageSize=${pagination.page_size}` +
    `&level=${body.level ?? ''}&beginTime=${body.begin_time ?? ''}&endTime=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}`
  body.sns.forEach((sn: string) => {
    if (sn !== '') {
      url = url.concat(`&deviceSn=${sn}`)
    }
  })
  const result = await request.get(url)
  return result.data
}
src/api/media.ts
@@ -1,9 +1,18 @@
import request from '/@/api/http/request'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/media/api/v1'
// Get Media Files
export const getMediaFiles = async function (wid: string, body: {}): Promise<any> {
  const url = `${HTTP_PREFIX}/files/${wid}/files`
  const result = await request.get(url, body)
export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}`
  const result = await request.get(url)
  return result.data
}
// Download Media File
export const downloadMediaFile = async function (workspaceId: string, fingerprint: string): Promise<any> {
  const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fingerprint}/url`
  const result = await request.get(url, { responseType: 'blob' })
  if (result.data.code) {
    return result.data
  }
  return result
}
src/api/pilot-bridge.ts
@@ -1,95 +1,151 @@
import { message } from 'ant-design-vue'
import { EComponentName, EPhotoType, ERouterName } from '../types'
import { CURRENT_CONFIG } from './http/config'
import { EVideoPublishType, LiveStreamStatus } from '../types/live-stream'
import { getRoot } from '/@/root'
const root = getRoot()
const components = new Map()
export const components = new Map()
declare let window:any
interface JsResponse{
  code:number,
  message:string,
  data:{}
  data:any
}
export interface ThingParam {
  host: string,
  username: string,
  password: string,
  connectCallback: string
}
export interface LiveshareParam {
  videoPublishType: string, // video-on-demand、video-by-manual、video-demand-aux-manual
  statusCallback: string
}
export interface MapParam {
  userName: string,
  elementPreName: string
}
export interface WsParam {
  host: string,
  token: string,
  connectCallback: string
}
export interface ApiParam {
  host: string,
  token: string
}
export interface MediaParam {
  autoUploadPhoto: boolean, // 是否自动上传图片, 非必需
  autoUploadPhotoType: number, // 自动上传的照片类型,0:原图, 1:缩略图, 非必需
  autoUploadVideo: boolean // 是否自动上传视频, 非必需
}
function returnBool (response: string): boolean {
  const res: JsResponse = JSON.parse(response)
  const isError = errorHint(res)
  if (JSON.stringify(res.data) !== '{}') {
    return isError && res.data
  }
  return isError
}
function returnString (response: string): string {
  const res: JsResponse = JSON.parse(response)
  return errorHint(res) ? res.data : ''
}
function returnNumber (response: string): number {
  const res: JsResponse = JSON.parse(response)
  return errorHint(res) ? res.data : -1
}
function errorHint (response: JsResponse): boolean {
  if (response.code !== 0) {
    message.error(response.message)
    console.error(response.message)
    return false
  }
  return true
}
export default {
  init () {
    components.set('thing', {
  init (): Map<EComponentName, any> {
    const thingParam: ThingParam = {
      host: '',
      connectCallback: '',
      username: '',
      password: ''
    })
    components.set('liveshare', {
      videoPublishType: 'video-demand-aux-manual', // video-on-demand、video-by-manual、video-demand-aux-manual
      statusCallback: ''
    })
    components.set('map', {
    }
    components.set(EComponentName.Thing, thingParam)
    const liveshareParam: LiveshareParam = {
      videoPublishType: EVideoPublishType.VideoDemandAuxManual,
      statusCallback: 'liveStatusCallback'
    }
    components.set(EComponentName.Liveshare, liveshareParam)
    const mapParam: MapParam = {
      userName: '',
      elementPreName: ''
    })
    components.set('ws', {
      host: '',
      elementPreName: 'PILOT'
    }
    components.set(EComponentName.Map, mapParam)
    const wsParam: WsParam = {
      host: CURRENT_CONFIG.websocketURL,
      token: '',
      connectCallback: ''
    })
    components.set('api', {
      connectCallback: 'wsConnectCallback'
    }
    components.set(EComponentName.Ws, wsParam)
    const apiParam: ApiParam = {
      host: '',
      token: ''
    })
    components.set('tsa', {
    })
    components.set('media', {
      autoUploadPhoto: true, // 是否自动上传图片, 非必需
      autoUploadPhotoType: 1, // 自动上传的照片类型,0:原图, 1:缩略图, 非必需
      autoUploadVideo: true // 是否自动上传视频, 非必需
    })
    components.set('mission', {
    })
    }
    components.set(EComponentName.Api, apiParam)
    components.set(EComponentName.Tsa, {})
    const mediaParam: MediaParam = {
      autoUploadPhoto: true,
      autoUploadPhotoType: EPhotoType.Preview,
      autoUploadVideo: true
    }
    components.set(EComponentName.Media, mediaParam)
    components.set(EComponentName.Mission, {})
    return components
  },
  getComponentParam (key:string) {
  getComponentParam (key:EComponentName): any {
    return components.get(key)
  },
  setComponentParam (key:string, value:any) {
  setComponentParam (key:EComponentName, value:any) {
    components.set(key, value)
  },
  loadComponent (name:string, param:any):string {
    return window.djiBridge.platformLoadComponent(name, JSON.stringify(param))
    return returnString(window.djiBridge.platformLoadComponent(name, JSON.stringify(param)))
  },
  unloadComponent (name:string) :string {
    return window.djiBridge.platformUnloadComponent(name)
    return returnString(window.djiBridge.platformUnloadComponent(name))
  },
  isComponentLoaded (module:string):string {
    return window.djiBridge.platformIsComponentLoaded(module)
  isComponentLoaded (module:string): boolean {
    return returnBool(window.djiBridge.platformIsComponentLoaded(module))
  },
  setWorkspaceId (uuid:string):string {
    return window.djiBridge.platformSetWorkspaceId(uuid)
    return returnString(window.djiBridge.platformSetWorkspaceId(uuid))
  },
  setPlatformMessage (platformName:string, title:string, desc:string):string {
    return window.djiBridge.platformSetInformation(platformName, title, desc)
  setPlatformMessage (platformName:string, title:string, desc:string): boolean {
    return returnBool(window.djiBridge.platformSetInformation(platformName, title, desc))
  },
  getRemoteControllerSN () :string {
    return window.djiBridge.platformGetRemoteControllerSN()
    return returnString(window.djiBridge.platformGetRemoteControllerSN())
  },
  getAircraftSN ():string {
    return window.djiBridge.platformGetAircraftSN()
    return returnString(window.djiBridge.platformGetAircraftSN())
  },
  stopwebview ():string {
    return window.djiBridge.platformStopSelf()
  },
  getToken () :string {
    const res:string = this.isComponentLoaded('api')
    const resObj = JSON.parse(res)
    console.log('api load status:', resObj)
    if (resObj.data === true) {
      const tokenRes = JSON.parse(window.djiBridge.apiGetToken())
      return tokenRes.data
    } else {
      console.warn('warning: not api component loaded!!')
      return ''
    }
  },
  setToken (token:string):string {
    return window.djiBridge.apiSetToken(token)
    return returnString(window.djiBridge.platformStopSelf())
  },
  setLogEncryptKey (key:string):string {
    return window.djiBridge.platformSetLogEncryptKey(key)
@@ -98,14 +154,42 @@
    return window.djiBridge.platformClearLogEncryptKey()
  },
  getLogPath ():string {
    return window.djiBridge.platformGetLogPath()
    return returnString(window.djiBridge.platformGetLogPath())
  },
  platformVerifyLicense (appId:string, appKey:string, appLicense:string):string {
    return window.djiBridge.platformVerifyLicense(appId, appKey, appLicense)
  platformVerifyLicense (appId:string, appKey:string, appLicense:string): boolean {
    return returnBool(window.djiBridge.platformVerifyLicense(appId, appKey, appLicense))
  },
  isPlatformVerifySuccess ():string {
    return window.djiBridge.platformIsVerified()
  isPlatformVerifySuccess (): boolean {
    return returnBool(window.djiBridge.platformIsVerified())
  },
  isAppInstalled (pkgName: string): boolean {
    return returnBool(window.djiBridge.platformIsAppInstalled(pkgName))
  },
  getVersion (): string {
    return window.djiBridge.platformGetVersion()
  },
  // thing
  thingGetConnectState (): boolean {
    return returnBool(window.djiBridge.thingGetConnectState())
  },
  thingGetConfigs (): ThingParam {
    const thingParam = JSON.parse(window.djiBridge.thingGetConfigs())
    return thingParam.code === 0 ? JSON.parse(thingParam.data) : {}
  },
  // api
  getToken () : string {
    return returnString(window.djiBridge.apiGetToken())
  },
  setToken (token:string):string {
    return returnString(window.djiBridge.apiSetToken(token))
  },
  getHost (): string {
    return returnString(window.djiBridge.apiGetHost())
  },
  // liveshare
  /**
   *
@@ -114,8 +198,8 @@
   * video-by-manual:手动点播,配置好直播类型参数之后,在图传页面可修改直播参数,停止直播
   * video-demand-aux-manual: 混合模式,支持服务器点播,以及图传页面修改直播参数,停止直播
   */
  setVideoPublishType (type:string):string {
    return window.djiBridge.liveshareSetVideoPublishType(type)
  setVideoPublishType (type:string): boolean {
    return returnBool(window.djiBridge.liveshareSetVideoPublishType(type))
  },
  /**
@@ -123,8 +207,8 @@
   * @returns
   * type: liveshare type, 0:unknown, 1:agora, 2:rtmp, 3:rtsp, 4:gb28181
   */
  getLiveshareConfig () {
    return window.djiBridge.liveshareGetConfig()
  getLiveshareConfig (): string {
    return returnString(window.djiBridge.liveshareGetConfig())
  },
  setLiveshareConfig (type:number, params:string):string {
@@ -134,50 +218,66 @@
  setLiveshareStatusCallback (callbackFunc:string) :string {
    return window.djiBridge.liveshareSetStatusCallback(callbackFunc)
  },
  getLiveshareStatus () {
    return window.djiBridge.liveshareGetStatus()
  getLiveshareStatus (): LiveStreamStatus {
    return JSON.parse(JSON.parse(window.djiBridge.liveshareGetStatus()).data)
  },
  startLiveshare ():string {
    return window.djiBridge.liveshareStartLive()
  startLiveshare (): boolean {
    return returnBool(window.djiBridge.liveshareStartLive())
  },
  stopLiveshare ():string {
    return window.djiBridge.liveshareStopLive()
  stopLiveshare (): boolean {
    return returnBool(window.djiBridge.liveshareStopLive())
  },
  // WebSocket
  wsGetConnectState (): boolean {
    return returnBool(window.djiBridge.wsGetConnectState())
  },
  wsConnect (host: string, token: string, callback: string): string {
    return window.djiBridge.wsConnect(host, token, callback)
  },
  wsDisconnect (): string {
    return window.djiBridge.wsConnect()
  },
  wsSend (message: string): string {
    return window.djiBridge.wsSend(message)
  },
  // media
  setAutoUploadPhoto (auto:boolean):string {
    return window.djiBridge.mediaSetAutoUploadPhoto(auto)
  },
  getAutoUploadPhoto () {
    return window.djiBridge.mediaGetAutoUploadPhoto()
  getAutoUploadPhoto (): boolean {
    return returnBool(window.djiBridge.mediaGetAutoUploadPhoto())
  },
  setUploadPhotoType (type:number):string {
    return window.djiBridge.mediaSetUploadPhotoType(type)
  },
  getUploadPhotoType () {
    return window.djiBridge.mediaGetUploadPhotoType()
  getUploadPhotoType (): number {
    return returnNumber(window.djiBridge.mediaGetUploadPhotoType())
  },
  setAutoUploadVideo (auto:boolean):string {
    return window.djiBridge.mediaSetAutoUploadVideo(auto)
  },
  getAutoUploadVideo () {
    return window.djiBridge.mediaGetAutoUploadVideo()
  getAutoUploadVideo (): boolean {
    return returnBool(window.djiBridge.mediaGetAutoUploadVideo())
  },
  setDownloadOwner (rcIndex:number):string {
    return window.djiBridge.mediaSetDownloadOwner(rcIndex)
  },
  getDownloadOwner () {
    return window.djiBridge.mediaGetDownloadOwner()
  getDownloadOwner (): number {
    return returnNumber(window.djiBridge.mediaGetDownloadOwner())
  },
  onBackClickReg () {
    window.djiBridge.onBackClick = () => {
      if (root.$router.currentRoute.value.path === '/pilot-home') {
        console.log(root.$router.currentRoute.value.path)
      if (root.$router.currentRoute.value.path === '/' + ERouterName.PILOT_HOME) {
        return false
      } else {
        console.log(root.$router.currentRoute.value.path)
        history.go(-1)
        return true
      }
    }
  },
  onStopPlatform () {
    window.djiBridge.onStopPlatform = () => {
      localStorage.clear()
    }
  }
}
src/api/wayline.ts
@@ -1,9 +1,55 @@
import request from '/@/api/http/request'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1'
export interface CreatePlan {
  name: string,
  file_id: string,
  dock_sn: string,
  immediate: boolean,
  type: string,
}
// Get Wayline Files
export const getWaylineFiles = async function (wid: string, body: {}): Promise<any> {
  const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?` + 'order_by=' + body.order_by + '&page=' + body.page + '&page_size=' + body.page_size
export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?order_by=${body.order_by}&page=${body.page}&page_size=${body.page_size}`
  const result = await request.get(url)
  return result.data
}
// Download Wayline File
export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url`
  const result = await request.get(url, { responseType: 'blob' })
  if (result.data.code) {
    return result.data
  }
  return result
}
// Delete Wayline File
export const deleteWaylineFile = async function (workspaceId: string, waylineId: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}`
  const result = await request.delete(url)
  return result.data
}
// Create Wayline Job
export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks`
  const result = await request.post(url, plan)
  return result.data
}
// Get Wayline Jobs
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}`
  const result = await request.get(url)
  return result.data
}
// Execute Wayline Job
export const executeWaylineJobs = async function (workspaceId: string, plan_id: string): Promise<IWorkspaceResponse<any>> {
  const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${plan_id}`
  const result = await request.post(url)
  return result.data
}
src/api/websocket.ts
@@ -1,14 +1,14 @@
import ReconnectingWebSocket from 'reconnecting-websocket'
import { ELocalStorageKey } from '../types/enums'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
let socket = {}
let socket: ReconnectingWebSocket
export default {
  init (getMsgFunc) {
    const token = localStorage.getItem('x-auth-token')
    const wspath =
      config.websocketURL + '?x-auth-token=' + escape(token)
    socket = new ReconnectingWebSocket(wspath)
  init (getMsgFunc: any) {
    const token: string = localStorage.getItem(ELocalStorageKey.Token)!
    const wspath = config.websocketURL + '?x-auth-token=' + encodeURI(token)
    socket = new ReconnectingWebSocket(wspath, '', { maxRetries: 5 })
    socket.onopen = this.onOpen
    socket.onerror = this.onError
    socket.onmessage = getMsgFunc
@@ -18,13 +18,16 @@
  onOpen () {
    console.log('ws opened')
  },
  onError (err) {
  onError (err: any) {
    console.error(err)
  },
  onClose () {
    console.log('ws closed')
  },
  sendMsg (data) {
    this.socket.send(data)
  sendMsg (data: any) {
    socket.send(data)
  },
  close () {
    socket.close()
  }
}
src/assets/icons/cloudapi.png
src/assets/icons/dji_logo.png
src/assets/icons/dock.png
src/assets/icons/drone.png
src/assets/icons/m30.png
src/assets/icons/no-data.png
src/assets/icons/rc.png
src/components/GMap.vue
@@ -5,18 +5,377 @@
      class="g-action-panle"
      :style="{ right: drawVisible ? '316px' : '16px' }"
    >
      <div class="g-action-item" @click="draw('pin', true)">
        <a-button type="primary">PIN</a-button>
      <div :class="state.currentType === 'pin' ? 'g-action-item selection' : 'g-action-item'" @click="draw('pin', true)">
        <a><a-image :src="pin" :preview="false" /></a>
      </div>
      <div class="g-action-item" @click="draw('polyline', true)">
        <a-button type="primary">Line</a-button>
      <div :class="state.currentType === 'polyline' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polyline', true)">
        <a><LineOutlined :rotate="135" class="fz20"/></a>
      </div>
      <div class="g-action-item" @click="draw('polygon', true)">
        <a-button type="primary">Poly</a-button>
      <div :class="state.currentType === 'polygon' ? 'g-action-item selection' : 'g-action-item'" @click="draw('polygon', true)">
        <a><BorderOutlined class="fz18" /></a>
      </div>
      <div v-if="mouseMode" class="g-action-item" @click="draw('off', false)">
        <a-button type="primary" danger>X</a-button>
        <a style="color: red;"><CloseOutlined /></a>
      </div>
    </div>
    <div v-if="osdVisible.visible && !osdVisible.is_dock" class="osd-panel fz12">
      <div class="pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 18%;">
        <span>{{ osdVisible.callsign }}</span>
        <span><a class="fz16" style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
      </div>
      <div style="height: 82%;">
        <div class="flex-column flex-align-center flex-justify-center" style="float: left; width: 60px; height: 100%; background: #2d2d2d;">
          <a-tooltip :title="osdVisible.model">
            <div style="width: 90%;" class="flex-column flex-align-center flex-justify-center">
              <span><a-image :src="M30" :preview="false"/></span>
              <span>{{ osdVisible.model }}</span>
            </div>
          </a-tooltip>
        </div>
        <div class="osd">
            <a-row>
              <a-col span="16" :style="deviceInfo.device.mode_code === EModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">{{ EModeCode[deviceInfo.device.mode_code] }}</a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Signal strength">
                  <span>HD</span>
                  <span class="ml10">{{ deviceInfo.gateway?.transmission_signal_quality }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="RC Battery Level">
                  <span><ThunderboltOutlined class="fz14"/></span>
                  <span class="ml10">{{ deviceInfo.gateway && deviceInfo.gateway.capacity_percent !== str ? deviceInfo.gateway.capacity_percent + ' %' : deviceInfo.gateway.capacity_percent }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Drone Battery Level">
                  <span><ThunderboltOutlined class="fz14"/></span>
                  <span class="ml10">{{ deviceInfo.device.battery.capacity_percent !== str ? deviceInfo.device.battery.capacity_percent + ' %' : deviceInfo.device.battery.capacity_percent }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-tooltip title="RTK Fixed">
                <a-col span="6" class="flex-row flex-align-center flex-justify-start">
                  <span>Fixed</span>
                  <span class="ml10 circle" :style="deviceInfo.device.position_state.is_fixed === 1 ? 'backgroud: rgb(25,190,107);' : ' background: red;'"></span>
                </a-col>
              </a-tooltip>
              <a-col span="6">
                <a-tooltip title="GPS">
                  <span>GPS</span>
                  <span class="ml10">{{ deviceInfo.device.position_state.gps_number }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="RTK">
                  <span><TrademarkOutlined class="fz14"/></span>
                  <span class="ml10">{{ deviceInfo.device.position_state.rtk_number }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Flight Mode">
                  <span><ControlOutlined class="fz16" /></span>
                  <span class="ml10">{{ EGear[deviceInfo.device.gear] }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Altitude above sea level">
                  <span>ASL</span>
                  <span class="ml10">{{ deviceInfo.device.height === str ? str : deviceInfo.device.height.toFixed(2) + ' m'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Altitude above takeoff level">
                  <span>ALT</span>
                  <span class="ml10">{{ deviceInfo.device.elevation === str ? str : deviceInfo.device.elevation.toFixed(2) + ' m' }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Distance to Home Point">
                  <span>H</span>
                  <span class="ml10">{{ deviceInfo.device.home_distance === str ? str : deviceInfo.device.home_distance.toFixed(2) + ' m' }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Horizontal Speed">
                  <span>H.S</span>
                  <span class="ml10">{{ deviceInfo.device.horizontal_speed === str ? str : deviceInfo.device.horizontal_speed.toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Vertical Speed">
                  <span>V.S</span>
                  <span class="ml10">{{ deviceInfo.device.vertical_speed === str ? str : deviceInfo.device.vertical_speed.toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Wind Speed">
                  <span>W.S</span>
                  <span class="ml10">{{ deviceInfo.device.wind_speed === str ? str : (deviceInfo.device.wind_speed / 10).toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
            </a-row>
        </div>
      </div>
      <div class="battery-slide" v-if="deviceInfo.device.battery.remain_flight_time !== 0">
        <div style="background: #535759;" class="width-100"></div>
        <div class="capacity-percent" :style="{ width: deviceInfo.device.battery.capacity_percent + '%'}"></div>
        <div class="return-home" :style="{ width: deviceInfo.device.battery.return_home_power + '%'}"></div>
        <div class="landing" :style="{ width: deviceInfo.device.battery.landing_power + '%'}"></div>
        <div class="white-point" :style="{ left: deviceInfo.device.battery.landing_power + '%'}"></div>
        <div class="battery" :style="{ left: deviceInfo.device.battery.capacity_percent + '%' }">
          {{ Math.floor(deviceInfo.device.battery.remain_flight_time / 60) }}:
          {{ 10 > (deviceInfo.device.battery.remain_flight_time % 60) ? '0' : ''}}{{deviceInfo.device.battery.remain_flight_time % 60 }}
        </div>
      </div>
    </div>
    <div v-if="osdVisible.visible && osdVisible.is_dock" class="osd-panel fz12" style="height: 280px;">
      <div class="fz16 pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 10%;">
        <span>{{ osdVisible.gateway_callsign }}</span>
        <span><a style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
      </div>
      <div style="height: 45%; border-bottom: 1px solid #515151;">
        <div class="flex-column flex-align-center flex-justify-center" style="float: left; width: 60px; height: 100%; background: #2d2d2d;">
          <a-tooltip :title="osdVisible.model">
            <div class="flex-column flex-align-center flex-justify-center" style="width: 90%;">
              <span><RobotFilled style="font-size: 48px;"/></span>
              <span class="mt10">Dock</span>
            </div>
          </a-tooltip>
        </div>
        <div class="osd">
            <a-row>
              <a-col span="16" :style="deviceInfo.dock.mode_code === EDockModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">
                {{ EDockModeCode[deviceInfo.dock.mode_code] }}</a-col>
            </a-row>
            <a-row>
              <a-col span="12">
                <a-tooltip title="Accumulated Running Time">
                  <span><HistoryOutlined /></span>
                  <span class="ml10">
                    <span v-if="deviceInfo.dock.acc_time >= 2592000"> {{ Math.floor(deviceInfo.dock.acc_time / 2592000) }}m </span>
                    <span v-if="(deviceInfo.dock.acc_time % 2592000) >= 86400"> {{ Math.floor((deviceInfo.dock.acc_time % 2592000) / 86400) }}d </span>
                    <span v-if="(deviceInfo.dock.acc_time % 2592000 % 86400) >= 3600"> {{ Math.floor((deviceInfo.dock.acc_time % 2592000 % 86400) / 3600) }}h </span>
                    <span v-if="(deviceInfo.dock.acc_time % 2592000 % 86400 % 3600) >= 60"> {{ Math.floor((deviceInfo.dock.acc_time % 2592000 % 86400 % 3600) / 60) }}min </span>
                    <span>{{ Math.floor(deviceInfo.dock.acc_time % 2592000 % 86400 % 3600 % 60) }} s</span>
                  </span>
                </a-tooltip>
              </a-col>
              <a-col span="12">
                <a-tooltip title="Last login">
                  <span><FieldTimeOutlined /></span>
                  <span class="ml10">{{ new Date(deviceInfo.dock.first_power_on).toLocaleString() }}
                  </span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-col span="12">
                <a-tooltip title="Network State">
                  <span :style="deviceInfo.dock.network_state.quality === 2 ? 'color: #00ee8b' :
                    deviceInfo.dock.network_state.quality === 1 ? 'color: yellow' : 'color: red'">
                    <span v-if="deviceInfo.dock.network_state.type === 1"><SignalFilled /></span>
                    <span v-else><GlobalOutlined /></span>
                  </span>
                  <span class="ml10" >{{ deviceInfo.dock.network_state.rate }} KB/S</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Media File Remain Upload">
                  <span><CloudUploadOutlined class="fz14"/></span>
                  <span class="ml10">{{ deviceInfo.dock.media_file_detail?.remain_upload }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip>
                  <template #title>
                    <p>total: {{ deviceInfo.dock.storage.total }}</p>
                    <p>used: {{ deviceInfo.dock.storage.used  }}</p>
                  </template>
                  <span><FolderOpenOutlined /></span>
                  <span class="ml10" v-if="deviceInfo.dock.storage.total > 0">
                    <a-progress type="circle" :width="20" :percent="deviceInfo.dock.storage.used * 100/ deviceInfo.dock.storage.total"
                      :strokeWidth="20" :showInfo="false" :strokeColor="deviceInfo.dock.storage.used * 100 / deviceInfo.dock.storage.total > 80 ? 'red' : '#00ee8b' "/>
                  </span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Wind Speed">
                  <span>W.S</span>
                  <span class="ml10">{{ deviceInfo.dock.wind_speed === str ? str : (deviceInfo.dock.wind_speed / 10).toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Rainfall">
                  <span>🌧</span>
                  <span class="ml10">{{ deviceInfo.dock.rainfall === str ? str : deviceInfo.dock.rainfall + ' mm/h' }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Environment Temperature">
                  <span>°C</span>
                  <span class="ml10">{{ deviceInfo.dock.environment_temperature }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Environment Humidity">
                  <span>💦</span>
                  <span class="ml10">{{ deviceInfo.dock.environment_humidity === str ? str : deviceInfo.dock.environment_humidity }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
               <a-col span="6">
                <a-tooltip title="Dock Temperature">
                  <span>°C</span>
                  <span class="ml10">{{ deviceInfo.dock.temperature }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Dock Humidity">
                  <span>💦</span>
                  <span class="ml10">{{ deviceInfo.dock.humidity === str ? str : deviceInfo.dock.humidity }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Working Voltage">
                  <span style="border: 1px solid; border-radius: 50%; width: 18px; height: 18px; line-height: 16px; text-align: center; float: left;">V</span>
                  <span class="ml10">{{ deviceInfo.dock.working_voltage === str ? str : deviceInfo.dock.working_voltage + ' mV' }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Working Current">
                  <span style="border: 1px solid; border-radius: 50%; width: 18px; height: 18px; line-height: 15px; text-align: center; float: left;" >A</span>
                  <span class="ml10">{{ deviceInfo.dock.working_current === str ? str : deviceInfo.dock.working_current + ' mA' }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
        </div>
      </div>
      <div style="height: 45%;">
        <div class="flex-column flex-align-center flex-justify-center" style="float: left; width: 60px; height: 100%; background: #2d2d2d;">
          <a-tooltip :title="osdVisible.model">
            <div style="width: 90%;" class="flex-column flex-align-center flex-justify-center">
              <span><a-image :src="M30" :preview="false"/></span>
              <span>M30</span>
            </div>
          </a-tooltip>
        </div>
        <div class="osd">
            <a-row>
              <a-col span="16" :style="!deviceInfo.device || deviceInfo.device?.mode_code === EModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">
                {{ !deviceInfo.device ? EModeCode[EModeCode.Disconnected] : EModeCode[deviceInfo.device?.mode_code] }}</a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Upward Quality">
                  <span><SignalFilled /><ArrowUpOutlined style="font-size: 9px; vertical-align: top;" /></span>
                  <span class="ml10">{{ deviceInfo.dock.sdr?.up_quality }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Downward Quality">
                  <span><SignalFilled /><ArrowDownOutlined style="font-size: 9px; vertical-align: top;" /></span>
                  <span class="ml10">{{ deviceInfo.dock.sdr?.down_quality }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Drone Battery Level">
                  <span><ThunderboltOutlined class="fz14"/></span>
                  <span class="ml10">{{ deviceInfo.device && deviceInfo.device.battery.capacity_percent !== str ? deviceInfo.device?.battery.capacity_percent + ' %' : str }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-tooltip title="RTK Fixed">
                <a-col span="6" class="flex-row flex-align-center flex-justify-start">
                  <span>Fixed</span>
                  <span class="ml10 circle" :style="deviceInfo.device?.position_state.is_fixed === 1 ? 'backgroud: rgb(25,190,107);' : ' background: red;'"></span>
                </a-col>
              </a-tooltip>
              <a-col span="6">
                <a-tooltip title="GPS">
                  <span>GPS</span>
                  <span class="ml10">{{ deviceInfo.device ? deviceInfo.device.position_state.gps_number : str }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="RTK">
                  <span><TrademarkOutlined class="fz14"/></span>
                  <span class="ml10">{{ deviceInfo.device ? deviceInfo.device.position_state.rtk_number : str }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Flight Mode">
                  <span><ControlOutlined class="fz16" /></span>
                  <span class="ml10">{{ deviceInfo.device ? EGear[deviceInfo.device?.gear] : str }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Altitude above sea level">
                  <span>ASL</span>
                  <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.height === str ? str : deviceInfo.device?.height.toFixed(2) + ' m'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Altitude above takeoff level">
                  <span>ALT</span>
                  <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.elevation === str ? str : deviceInfo.device?.elevation.toFixed(2) + ' m' }}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Distance to Home Point">
                  <span style="border: 1px solid; border-radius: 50%; width: 18px; height: 18px; line-height: 15px; text-align: center;  display: block; float: left;" >H</span>
                  <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.home_distance === str ? str : deviceInfo.device?.home_distance.toFixed(2) + ' m' }}</span>
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row>
              <a-col span="6">
                <a-tooltip title="Horizontal Speed">
                  <span>H.S</span>
                  <span class="ml10">{{ !deviceInfo.device || deviceInfo.device?.horizontal_speed === str ? str : deviceInfo.device?.horizontal_speed.toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Vertical Speed">
                  <span>V.S</span>
                  <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.vertical_speed === str ? str : deviceInfo.device?.vertical_speed.toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
              <a-col span="6">
                <a-tooltip title="Wind Speed">
                  <span>W.S</span>
                  <span class="ml10">{{ !deviceInfo.device || deviceInfo.device.wind_speed === str ? str : (deviceInfo.device?.wind_speed / 10).toFixed(2) + ' m/s'}}</span>
                </a-tooltip>
              </a-col>
            </a-row>
        </div>
      </div>
      <div class="battery-slide" v-if="deviceInfo.device && deviceInfo.device.battery.remain_flight_time !== 0" style="border: 1px solid red">
        <div style="background: #535759;" class="width-100"></div>
        <div class="capacity-percent" :style="{ width: deviceInfo.device.battery.capacity_percent + '%'}"></div>
        <div class="return-home" :style="{ width: deviceInfo.device.battery.return_home_power + '%'}"></div>
        <div class="landing" :style="{ width: deviceInfo.device.battery.landing_power + '%'}"></div>
        <div class="white-point" :style="{ left: deviceInfo.device.battery.landing_power + '%'}"></div>
        <div class="battery" :style="{ left: deviceInfo.device.battery.capacity_percent + '%' }">
          {{ Math.floor(deviceInfo.device.battery.remain_flight_time / 60) }}:
          {{ 10 > (deviceInfo.device.battery.remain_flight_time % 60) ? '0' : ''}}{{deviceInfo.device.battery.remain_flight_time % 60 }}
        </div>
      </div>
    </div>
  </div>
</template>
@@ -40,19 +399,129 @@
import { PostElementsBody } from '/@/types/mapLayer'
import { uuidv4 } from '/@/utils/uuid'
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform'
import { deviceTsaUpdate } from '/@/hooks/use-g-map-tsa'
import { DeviceOsd, DeviceStatus, DockOsd, EGear, EModeCode, GatewayOsd, EDockModeCode } from '/@/types/device'
import pin from '/@/assets/icons/pin-2d8cf0.svg'
import M30 from '/@/assets/icons/m30.png'
import {
  BorderOutlined, LineOutlined, CloseOutlined, ControlOutlined, TrademarkOutlined, ArrowDownOutlined,
  ThunderboltOutlined, SignalFilled, GlobalOutlined, HistoryOutlined, CloudUploadOutlined,
  FieldTimeOutlined, CloudOutlined, CloudFilled, FolderOpenOutlined, RobotFilled, ArrowUpOutlined
} from '@ant-design/icons-vue'
import { EDeviceTypeName } from '../types'
export default defineComponent({
  components: {
    BorderOutlined,
    LineOutlined,
    CloseOutlined,
    ControlOutlined,
    TrademarkOutlined,
    ThunderboltOutlined,
    SignalFilled,
    GlobalOutlined,
    HistoryOutlined,
    CloudUploadOutlined,
    FieldTimeOutlined,
    CloudOutlined,
    CloudFilled,
    FolderOpenOutlined,
    RobotFilled,
    ArrowUpOutlined,
    ArrowDownOutlined
  },
  name: 'GMap',
  props: {},
  setup () {
    const useMouseToolHook = useMouseTool()
    const useGMapManageHook = useGMapManage()
    const deviceTsaUpdateHook = ref()
    const mouseMode = ref(false)
    const store = useMyStore()
    const state = reactive({
      currentType: '',
      coverIndex: 0
    })
    const str: string = '--'
    const deviceInfo = reactive({
      gateway: {
        capacity_percent: str,
        transmission_signal_quality: str,
      } as GatewayOsd,
      dock: {
        media_file_detail: {
          remain_upload: 0
        },
        sdr: {
          up_quality: str,
          down_quality: str,
          frequency_band: -1,
        },
        network_state: {
          type: 0,
          quality: 0,
          rate: 0,
        },
        drone_in_dock: 0,
        drone_charge_state: {
          state: 0,
          capacity_percent: str,
        },
        rainfall: str,
        wind_speed: str,
        environment_temperature: str,
        environment_humidity: str,
        temperature: str,
        humidity: str,
        job_number: 0,
        acc_time: 0,
        first_power_on: 0,
        positionState: {
          gps_number: str,
          is_fixed: 0,
          rtk_number: str,
          is_calibration: 0,
          quality: 0,
        },
        storage: {
          total: 0,
          used: 0,
        },
        electric_supply_voltage: 0,
        working_voltage: str,
        working_current: str,
        backup_battery_voltage: 0,
        mode_code: -1,
        cover_state: -1,
        supplement_light_state: -1,
        putter_state: -1,
      } as DockOsd,
      device: {
        gear: -1,
        mode_code: EModeCode.Disconnected,
        height: str,
        home_distance: str,
        horizontal_speed: str,
        vertical_speed: str,
        wind_speed: str,
        wind_direction: str,
        elevation: str,
        position_state: {
          gps_number: str,
          is_fixed: 0,
          rtk_number: str
        },
        battery: {
          capacity_percent: str,
          landing_power: str,
          remain_flight_time: 0,
          return_home_power: str,
        },
        latitude: 0,
        longitude: 0,
      } as DeviceOsd
    })
    const shareId = computed(() => {
      return store.state.layerBaseInfo.share
@@ -63,6 +532,57 @@
    const drawVisible = computed(() => {
      return store.state.drawVisible
    })
    const osdVisible = computed(() => {
      return store.state.osdVisible
    })
    watch(() => store.state.deviceStatusEvent,
      data => {
        deviceTsaUpdateHook.value = deviceTsaUpdate()
        if (Object.keys(data.deviceOnline).length !== 0) {
          deviceTsaUpdateHook.value.initMarker(data.deviceOnline.domain, data.deviceOnline.device_callsign, data.deviceOnline.sn)
          store.state.deviceStatusEvent.deviceOnline = {} as DeviceStatus
        }
        if (Object.keys(data.deviceOffline).length !== 0) {
          deviceTsaUpdateHook.value.removeMarker(data.deviceOffline.sn)
          if ((data.deviceOffline.sn === osdVisible.value.sn) || (osdVisible.value.is_dock && data.deviceOffline.sn === osdVisible.value.gateway_sn)) {
            osdVisible.value.visible = false
            store.commit('SET_OSD_VISIBLE_INFO', osdVisible)
          }
          store.state.deviceStatusEvent.deviceOffline = {}
        }
      },
      {
        deep: true
      }
    )
    watch(() => store.state.deviceState, data => {
      if (!deviceTsaUpdateHook.value) {
        deviceTsaUpdateHook.value = deviceTsaUpdate()
      }
      if (data.currentType === EDeviceTypeName.Gateway && data.gatewayInfo[data.currentSn]) {
        deviceTsaUpdateHook.value.moveTo(data.currentSn, data.gatewayInfo[data.currentSn].longitude, data.gatewayInfo[data.currentSn].latitude)
        if (osdVisible.value.visible && osdVisible.value.gateway_sn !== '') {
          deviceInfo.gateway = data.gatewayInfo[osdVisible.value.gateway_sn]
        }
      }
      if (data.currentType === EDeviceTypeName.Aircraft && data.deviceInfo[data.currentSn]) {
        deviceTsaUpdateHook.value.moveTo(data.currentSn, data.deviceInfo[data.currentSn].longitude, data.deviceInfo[data.currentSn].latitude)
        if (osdVisible.value.visible && osdVisible.value.sn !== '') {
          deviceInfo.device = data.deviceInfo[osdVisible.value.sn]
        }
      }
      if (data.currentType === EDeviceTypeName.Dock && data.dockInfo[data.currentSn]) {
        if (osdVisible.value.visible && osdVisible.value.is_dock && osdVisible.value.gateway_sn !== '') {
          deviceInfo.dock = data.dockInfo[osdVisible.value.gateway_sn]
          deviceInfo.device = data.deviceInfo[deviceInfo.dock.sub_device?.device_sn]
        }
      }
    }, {
      deep: true
    })
    watch(
      () => store.state.wsEvent,
      newData => {
@@ -144,7 +664,9 @@
    async function postPinPositionResource (obj) {
      const req = getPinPositionResource(obj)
      setLayers(req)
      updateCoordinates('gcj02-wgs84', req)
      const coordinates = req.resource.content.geometry.coordinates
      updateCoordinates('gcj02-wgs84', req);
      (req.resource.content.geometry.coordinates as GeojsonCoordinate).push((coordinates as GeojsonCoordinate)[2])
      const result = await postElementsReq(shareId.value, req)
      obj.setExtData({ id: req.id, name: req.name })
      store.state.coverList.push(obj)
@@ -281,7 +803,16 @@
    return {
      draw,
      mouseMode,
      drawVisible
      drawVisible,
      osdVisible,
      pin,
      state,
      M30,
      deviceInfo,
      EGear,
      EModeCode,
      str,
      EDockModeCode,
    }
  }
})
@@ -296,16 +827,77 @@
    top: 16px;
    right: 16px;
    .g-action-item {
      padding-top: 8px;
      width: 28px;
      height: 28px;
      background: white;
      color: $primary;
      border-radius: 2px;
      line-height: 28px;
      text-align: center;
      margin-bottom: 2px;
    }
    .g-action-item:hover {
      border: 1px solid $primary;
      border-radius: 2px;
    }
  }
  .selection {
    border: 1px solid $primary;
    border-radius: 2px;
  }
}
</style>
<style lang="scss">
.amap-logo {
  opacity: 0;
.osd-panel {
  position: absolute;
  left: 350px;
  top: 10px;
  width: 480px;
  height: 160px;
  background: black;
  color: white;
  border-radius: 2px;
  opacity: 0.7;
}
.amap-copyright {
  opacity: 0;
.osd > div {
  padding-top: 5px;
  padding-left: 5px;
}
.circle {
  border-radius: 50%;
  width: 10px;
  height: 10px;
}
.battery-slide {
  .capacity-percent {
    background: #00ee8b;
  }
  .return-home {
    background: #ff9f0a;
  }
  .landing {
    background: #f5222d;
  }
  .white-point {
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: white;
    bottom: -0.5px;
  }
  .battery {
    background: #141414;
    color: #00ee8b;
    margin-top: -10px;
    height: 20px;
    width: auto;
    border-left: 1px solid #00ee8b;
    padding: 0 5px;
  }
}
.battery-slide > div {
  position: absolute;
  min-height: 2px;
  border-radius: 2px;
}
</style>
src/components/MediaPanel.vue
@@ -1,92 +1,168 @@
<template>
  <div class="media-panel-wrapper">
    <div class="header">Media</div>
    <a-button type="primary" style="margin-top:20px" @click="onRefresh"
      >Refresh</a-button
    >
    <a-table class="media-table" :columns="columns" :data-source="data">
      <template #name="{ text, record }">
        <a :href="record.preview_url">{{ text }}</a>
      </template>
      <template #action>
        <span class="action-area">
          action
        </span>
      </template>
    </a-table>
  </div>
  <div class="header">Media Files</div>
  <a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
    <div class="media-panel-wrapper">
      <a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint"
        :pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
        <template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col">
          <a-tooltip :title="text">
              <a v-if="col === 'name'">{{ text }}</a>
              <span v-else>{{ text }}</span>
          </a-tooltip>
        </template>
        <template #original="{ text }">
          {{ text }}
        </template>
        <template #action="{ record }">
          <a-tooltip title="download">
            <a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a>
          </a-tooltip>
        </template>
      </a-table>
    </div>
  </a-spin>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { getMediaFiles } from '/@/api/media'
import { TableState } from 'ant-design-vue/lib/table/interface'
import { onMounted, reactive } from 'vue'
import { IPage } from '../api/http/type'
import { ELocalStorageKey } from '../types/enums'
import { downloadFile } from '../utils/common'
import { downloadMediaFile, getMediaFiles } from '/@/api/media'
import { DownloadOutlined } from '@ant-design/icons-vue'
import { Pagination } from 'ant-design-vue'
import { load } from '@amap/amap-jsapi-loader'
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const loading = ref(false)
const columns = [
  {
    title: 'FileName',
    dataIndex: 'name',
    key: 'name',
    title: 'File Name',
    dataIndex: 'file_name',
    ellipsis: true,
    slots: { customRender: 'name' }
  },
  {
    title: 'FileSize',
    dataIndex: 'size',
    key: 'size'
    title: 'File Path',
    dataIndex: 'file_path',
    ellipsis: true,
    slots: { customRender: 'path' }
  },
  // {
  //   title: 'FileSize',
  //   dataIndex: 'size',
  // },
  {
    title: 'Drone',
    dataIndex: 'drone'
  },
  {
    title: 'PayloadType',
    dataIndex: 'payload_type',
    key: 'payload_type',
    ellipsis: true
    title: 'Payload Type',
    dataIndex: 'payload'
  },
  {
    title: 'Original',
    dataIndex: 'is_original',
    slots: { customRender: 'original' }
  },
  {
    title: 'Created',
    dataIndex: 'create_time'
  },
  {
    title: 'Action',
    key: 'action',
    slots: { customRender: 'action' }
  }
]
const data = ref([
  {
    key: '1',
    name: 'name1',
    size: 32,
    payload_type: 'PM320_DUAL',
    preview_url: ''
  }
])
const onRefresh = async () => {
  const wid = localStorage.getItem('workspace-id')
  data.value = []
  const index = 1
  const res = await getMediaFiles(wid, {})
  res.data.forEach(ele => {
    data.value.push({
      key: index.toString(),
      name: ele.file_name
    })
  })
  console.log(res)
const body: IPage = {
  page: 1,
  total: 0,
  page_size: 50
}
const paginationProp = reactive({
  pageSizeOptions: ['20', '50', '100'],
  showQuickJumper: true,
  showSizeChanger: true,
  pageSize: 50,
  current: 1,
  total: 0
})
type Pagination = TableState['pagination']
interface MediaFile {
  fingerprint: string,
  drone: string,
  payload: string,
  is_original: string,
  file_name: string,
  file_path: string,
  create_time: string,
}
const mediaData = reactive({
  data: [] as MediaFile[]
})
onMounted(() => {
  getFiles()
})
function getFiles () {
  getMediaFiles(workspaceId, body).then(res => {
    mediaData.data = res.data.list
    paginationProp.total = res.data.pagination.total
    paginationProp.current = res.data.pagination.page
    console.info(mediaData.data[0])
  })
}
function refreshData (page: Pagination) {
  body.page = page?.current!
  body.page_size = page?.pageSize!
  getFiles()
}
function downloadMedia (media: MediaFile) {
  loading.value = true
  downloadMediaFile(workspaceId, media.fingerprint).then(res => {
    if (res.code && res.code !== 0) {
      return
    }
    const suffix = media.file_name.substring(media.file_name.lastIndexOf('.'))
    const data = new Blob([res.data], { type: suffix === '.mp4' ? 'video/mp4' : 'image/jpeg' })
    downloadFile(data, media.file_name)
  }).finally(() => {
    loading.value = false
  })
}
</script>
<style lang="scss" scoped>
.media-panel-wrapper {
  width: 100%;
  padding: 16px;
  .media-table {
    background: #fff;
    margin-top: 32px;
  }
  .header {
    width: 100%;
    height: 60px;
    background: #fff;
    padding: 16px 24px;
    font-size: 20px;
    text-align: start;
    color: #000;
    margin-top: 10px;
  }
  .action-area {
    color: $primary;
    cursor: pointer;
  }
}
.header {
  width: 100%;
  height: 60px;
  background: #fff;
  padding: 16px;
  font-size: 20px;
  font-weight: bold;
  text-align: start;
  color: #000;
}
</style>
src/components/TaskPanel.vue
New file
@@ -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>
src/components/livestream-agora.vue
New file
@@ -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>
src/components/livestream-others.vue
New file
@@ -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>
src/components/wayline-panel.vue
@@ -1,5 +1,5 @@
<template>
  <div class="panel-wrapper">
  <div class="panel-wrapper" draggable="true">
    <div class="header">Wayline Library</div>
    <a-button type="primary" style="margin-top:20px" @click="onRefresh"
      >Refresh</a-button
@@ -20,6 +20,7 @@
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { onMounted } from 'vue'
import { ELocalStorageKey } from '../types/enums'
import { getWaylineFiles } from '/@/api/wayline'
const columns = [
  {
@@ -74,7 +75,7 @@
  onRefresh()
})
const onRefresh = async () => {
  const wid: string = localStorage.getItem('workspace-id')
  const wid: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)
  data.value = []
  const index = 1
  const res = await getWaylineFiles(wid, {
src/constants/index.ts
@@ -1,5 +1,7 @@
import { CURRENT_CONFIG } from '/@/api/http/config'
export const AMapConfig = {
  key: '26d54da6733de88435c68d1a2e88b682',
  key: CURRENT_CONFIG.amapKey,
  version: '2.0',
  plugins: [
    'AMap.Scale',
@@ -14,6 +16,7 @@
    'AMap.PolyEditor',
    'AMap.RangingTool',
    'AMap.Weather',
    'AMap.MouseTool'
    'AMap.MouseTool',
    'AMap.MoveAnimation'
  ]
}
src/hooks/use-g-map-cover.ts
@@ -11,10 +11,10 @@
export function useGMapCover () {
  const root = getRoot()
  const AMap = root.$aMapObj
  const normalColor = '#2D8CF0'
  const store = rootStore
  const coverList = store.state.coverList
  function AddCoverToMap (cover :any) {
    root.$aMap.add(cover)
    coverList.push(cover)
@@ -27,10 +27,10 @@
    } = {
      '2d8cf0': pin2d8cf0,
      '19be6b': pin19be6b,
      212121: pin212121,
      b620e0: pinb620e0,
      e23c39: pine23c39,
      ffbb00: pineffbb00,
      '212121': pin212121,
      'b620e0': pinb620e0,
      'e23c39': pine23c39,
      'ffbb00': pineffbb00,
    }
    const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
src/hooks/use-g-map-tsa.ts
New file
@@ -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
  }
}
src/pages/page-pilot/pilot-bind.vue
New file
@@ -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>
src/pages/page-pilot/pilot-home.vue
@@ -1,132 +1,501 @@
<template>
  <div class="page">
    <div class="left flex-column flex-justify-start flex-align-center">
      <p class="fz26 mb0 mt10" style="color: #727272">
        {{ platformName }}
      </p>
      <p class="fz16 ml10 mb0 mt10" style="color: #2d8cf0">
        status:{{ connect }}
      </p>
      <p class="fz32 mb0 mt50" style="color: #000000">{{ workspaceName }}</p>
      <a-button
        class="fz20 mt20 flex-column flex-justify-center flex-align-center"
        style="width: 30vw; height: 12vh;"
        type="default"
        @click="onOpen3rdApp"
        >Open 3rd Party APP</a-button
      >
      <a-button
        class="fz20"
        style="width: 15vw; height: 12vh; position: fixed; bottom: 7vh"
        type="primary"
        @click="onExit"
        >Quit</a-button
      >
    </div>
    <div class="right flex-column flex-justify-start flex-align-center">
      <p class="fz24 mb0 mt10 ">Setting</p>
      <a-button class="mt10 fz16" style="width:90%" @click="onMediaSetting"
        >Media File Upload Setting</a-button
      >
      <a-button class="mt10 fz16" style="width:90%" @click="onLiveshareSetting"
        >Manual Live Share Setting</a-button
      >
    </div>
  </div>
  <a-layout class="page">
    <a-layout-sider class="left" width="40%" style="border-radius: 4px;">
      <div style="width:90%; height: 90%; margin: 4vh">
        <a-layout style="height: 20%; margin-top: 3vh; background-color: white; ">
          <a-layout-sider width="25%" theme="light" align="center">
            <a-avatar :size="60" :src="cloudapi">
            </a-avatar>
          </a-layout-sider>
          <a-layout-content style="margin-left: 1vw;" @click="showStatus">
            <div style="height: 50%;">
              <span style="font-size: 16px; font-weight: bolder">{{ workspaceName }}</span>
              <RightOutlined style="float: right; margin-top: 5px; color: #8894a0" />
            </div>
            <div style="height: 50%;">
              <CloudSyncOutlined v-if="state === EStatusValue.CONNECTED" style="color: #75c5f6" />
              <SyncOutlined spin v-else/>
              <span style="color: #737373; margin-left: 3px;">{{ state }}</span>
            </div>
            <a-drawer  placement="right" v-model:visible="drawerVisible" width="340px">
              <div class="mb10 flex-row flex-justify-center flex-align-center">
                <p class="fz14" style="font-weight: 100;">Module State</p>
              </div>
              <div class= "width-100 mb10 flex-align-start" v-for="m in modules" :key="m.name" style="height: 30px;">
                <div class="ml5" style="float: left; color: #000000;">{{m.name}}:</div>
                <div class="ml10" style="float: right; margin-bottom: 8px;">
                  <span :key="m.state" :class="m.state.value === EStatusValue.CONNECTED ? 'green' : 'red'">{{ m.state.value }}&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.setPlatformMessage(
        platformName.value,
        workspaceName.value,
        workspaceDesc.value
      )
      apiPilot.setWorkspaceId(wsId.value)
    })
  apiPilot.onBackClickReg()
  apiPilot.onStopPlatform()
  device.data.gateway_sn = apiPilot.getRemoteControllerSN()
  if (device.data.gateway_sn === EStatusValue.DISCONNECT.toString()) {
    message.warn('Data is not available, please restart the remote control.')
    return
  }
  if (JSON.parse(apiPilot.isComponentLoaded('thing')).data === false || token) {
    getUserInfo({}).then(res => {
      const param = {
        host: res.data.mqtt_addr,
        username: res.data.mqtt_username,
        password: res.data.mqtt_password,
        connectCallback: 'connectCallback'
      }
      apiPilot.setComponentParam('thing', param)
      apiPilot.loadComponent('thing', apiPilot.getComponentParam('thing'))
    })
  } else {
    const connectState = JSON.parse(window.djiBridge.thingGetConnectState())
    if (connectState.code === 0 && connectState.data) {
      connect.value = 'Connected'
    } else {
      connect.value = 'Disconnect'
  const oldDevice = localStorage.getItem(ELocalStorageKey.Device)
  if (oldDevice) {
    device.data = JSON.parse(oldDevice)
  }
  device.data.sn = apiPilot.getAircraftSN()
  getDeviceInfo()
  socket = websocket.init(wsGetMsg)
  const isLoaded = apiPilot.isComponentLoaded(EComponentName.Thing)
  if (isLoaded) {
    username.value = '' + localStorage.getItem(ELocalStorageKey.Username)
    workspaceName.value = '' + localStorage.getItem(ELocalStorageKey.WorkspaceName)
    refreshStatus()
    apiPilot.setPlatformMessage(
      '' + localStorage.getItem(ELocalStorageKey.PlatformName),
      workspaceName.value,
      '' + localStorage.getItem(ELocalStorageKey.WorkspaceDesc)
    )
    return
  }
  setWorkspaceInfo()
  getUserInfo().then(res => {
    username.value = res.data.username
    localStorage.setItem(ELocalStorageKey.Username, username.value)
    // thing
    const param: ThingParam = {
      host: res.data.mqtt_addr,
      username: username.value,
      password: res.data.mqtt_password,
      connectCallback: 'connectCallback'
    }
  }
  apiPilot.loadComponent('liveshare', apiPilot.getComponentParam('liveshare'))
  console.log('ws token:', token)
  apiPilot.setComponentParam('ws', {
    host: CURRENT_CONFIG.websocketURL,
    token: token
    components.set(EComponentName.Thing, param)
    apiPilot.loadComponent(EComponentName.Thing, components.get(EComponentName.Thing))
    bindParam.device_sn = device.data.gateway_sn
    bindParam.user_id = res.data.user_id
    bindParam.workspace_id = res.data.workspace_id
  })
  apiPilot.loadComponent('ws', apiPilot.getComponentParam('ws'))
  apiPilot.setComponentParam('map', {
    userName: 'pilot1',
    elementPreName: 'PILOT'
  })
  apiPilot.loadComponent('map', apiPilot.getComponentParam('map'))
  apiPilot.loadComponent('tsa', apiPilot.getComponentParam('tsa'))
  apiPilot.loadComponent('media', apiPilot.getComponentParam('media'))
  apiPilot.loadComponent('mission', {})
  window.connectCallback = arg => {
    connectCallback(arg)
  }
  apiPilot.onBackClickReg()
  window.wsConnectCallback = arg => {
    wsConnectCallback(arg)
  }
})
const connectCallback = (arg: any) => {
  console.info('into callback', arg)
onUnmounted(() => {
  socket.close()
})
const connectCallback = async (arg: any) => {
  if (arg) {
    connect.value = 'Connected'
    window.djiBridge.mediaSetDownloadOwner(0)
    window.djiBridge.mediaSetUploadPhotoType(1)
    thingState.value = EStatusValue.CONNECTED
    // liveshare
    apiPilot.loadComponent(EComponentName.Liveshare, components.get(EComponentName.Liveshare))
    // ws
    const wsParam: WsParam = components.get(EComponentName.Ws)
    wsParam.token = apiPilot.getToken()
    apiPilot.loadComponent(EComponentName.Ws, components.get(EComponentName.Ws))
    // map
    const mapParam: MapParam = components.get(EComponentName.Map)
    mapParam.userName = username.value
    apiPilot.loadComponent(EComponentName.Map, components.get(EComponentName.Map))
    // tsa
    apiPilot.loadComponent(EComponentName.Tsa, components.get(EComponentName.Tsa))
    // media
    apiPilot.loadComponent(EComponentName.Media, components.get(EComponentName.Media))
    apiPilot.setDownloadOwner(EDownloadOwner.Mine.valueOf())
    // mission
    apiPilot.loadComponent(EComponentName.Mission, {})
    bindNum = setInterval(() => {
      bindDevice(bindParam).then(bindRes => {
        if (bindRes.code !== 0) {
          message.error(bindRes.message)
          console.error(bindRes.message)
        } else {
          clearInterval(bindNum)
        }
      })
    }, 2000)
    setTimeout(getDeviceInfo, 3000)
  } else {
    connect.value = 'Disconnect'
    thingState.value = EStatusValue.DISCONNECT
  }
  refreshStatus()
}
const wsConnectCallback = async (arg: any) => {
  if (arg) {
    wsState.value = EStatusValue.CONNECTED
  } else {
    wsState.value = EStatusValue.DISCONNECT
  }
}
const onExit = async (e: any) => {
const confirmAgain = () => {
  exitVisible.value = true
}
const onBack = () => {
  exitVisible.value = false
}
const onExit = () => {
  localStorage.clear()
  apiPilot.stopwebview()
}
const bindingDevice = async () => {
  root.$router.push(ERouterName.PILOT_BIND)
}
const onMediaSetting = async (e: any) => {
  root.$router.push('/pilot-media')
  root.$router.push(ERouterName.PILOT_MEDIA)
}
const onLiveshareSetting = async (e: any) => {
  root.$router.push('/pilot-liveshare')
  root.$router.push(ERouterName.PILOT_LIVESHARE)
}
const onOpen3rdApp = () => {
  window.open('mydjischeme://www.dji.com')
  const packageName = 'com.dji.sample'
  const isInstalled = apiPilot.isAppInstalled(packageName)
  if (isInstalled) {
    window.open('https://www.dji.com')
  } else {
    message.error(packageName + ' is not installed.')
  }
}
const showStatus = async () => {
  minitor = setInterval(() => {
    refreshStatus()
    if (!drawerVisible.value) {
      clearInterval(minitor)
    }
  }, 2000)
  drawerVisible.value = true
}
function setWorkspaceInfo () {
  if (localStorage.getItem(ELocalStorageKey.WorkspaceName)) {
    apiPilot.setPlatformMessage(
      '' + localStorage.getItem(ELocalStorageKey.PlatformName),
      workspaceName.value,
      '' + localStorage.getItem(ELocalStorageKey.WorkspaceDesc)
    )
    apiPilot.setWorkspaceId(wsId.value)
    return
  }
  getPlatformInfo().then(res => {
    console.log(res)
    workspaceName.value = res.data.workspace_name
    wsId.value = res.data.workspace_id
    localStorage.setItem(ELocalStorageKey.PlatformName, res.data.platform_name)
    localStorage.setItem(ELocalStorageKey.WorkspaceName, workspaceName.value)
    localStorage.setItem(ELocalStorageKey.WorkspaceDesc, res.data.workspace_desc)
    apiPilot.setPlatformMessage(
      res.data.platform_name,
      workspaceName.value,
      res.data.workspace_desc
    )
    apiPilot.setWorkspaceId(wsId.value)
  })
}
function refreshStatus () {
  thingState.value = apiPilot.thingGetConnectState() ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  apiState.value = apiPilot.isComponentLoaded(EComponentName.Api) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  liveState.value = apiPilot.isComponentLoaded(EComponentName.Liveshare) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  wsState.value = apiPilot.isComponentLoaded(EComponentName.Ws) && apiPilot.wsGetConnectState()
    ? EStatusValue.CONNECTED
    : EStatusValue.DISCONNECT
  mapState.value = apiPilot.isComponentLoaded(EComponentName.Map) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  tsaState.value = apiPilot.isComponentLoaded(EComponentName.Tsa) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  mediaState.value = apiPilot.isComponentLoaded(EComponentName.Media) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  waylineState.value = apiPilot.isComponentLoaded(EComponentName.Mission) ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
  state.value = thingState.value === EStatusValue.CONNECTED && gatewayState.value ? EStatusValue.CONNECTED : EStatusValue.DISCONNECT
}
function moduleInstall (m: any) {
  let param
  switch (m.module) {
    case EComponentName.Thing:
      param = apiPilot.thingGetConfigs()
      break
    case EComponentName.Api: {
      const apiParam: ApiParam = {
        host: apiPilot.getHost(),
        token: apiPilot.getToken()
      }
      param = apiParam
      break
    }
    case EComponentName.Map: {
      const mapParam: MapParam = components.get(EComponentName.Map)
      mapParam.userName = '' + localStorage.getItem(ELocalStorageKey.Username)
      param = mapParam
      break
    }
    case EComponentName.Ws: {
      const wsParam: WsParam = components.get(EComponentName.Ws)
      wsParam.token = '' + localStorage.getItem(ELocalStorageKey.Token)
      param = wsParam
      break
    }
    default:
      param = components.get(m.module)
  }
  components.set(m.module, param)
  console.info(components.get(m.module))
  apiPilot.loadComponent(m.module, components.get(m.module))
  refreshStatus()
}
function moduleUninstall (m: any) {
  message.info('uninstall ' + m.module)
  apiPilot.unloadComponent(m.module)
  refreshStatus()
}
function getDeviceInfo () {
  if (device.data.sn === EStatusValue.DISCONNECT) {
    return
  }
  getDeviceBySn(bindParam.workspace_id, device.data.sn).then(res => {
    if (res.code !== 0) {
      return
    }
    device.data.online_status = res.data.status
    device.data.bound_status = res.data.bound_status
    device.data.device_callsign = res.data.nickname
    device.data.model = res.data.device_name
    localStorage.setItem(ELocalStorageKey.Device, JSON.stringify(device.data))
  })
}
</script>
@@ -139,12 +508,32 @@
  height: 100%;
  width: 100%;
  .left {
    width: 50%;
    border-right: red solid 2px;
    height: 90%;
    background-color: white;
    margin-top: 6vh;
    margin-left: 2vh;
  }
  .right {
    width: 100%;
    height: 100%;
    height: 90%;
    margin-top: 6vh;
    margin-left: 5vh;
    margin-right: 5vh;
  }
}
.green {
  color: green
}
.red {
  color: red;
}
#exitBtn:hover :active {
  background-color: rgb(77, 75, 75);
  width: 10vw;
  height: 10vh;
  position: fixed;
  bottom: 13vh;
  left: 15vw;
  line-height: 10vh;
}
</style>
src/pages/page-pilot/pilot-index.vue
@@ -2,7 +2,7 @@
  <div class="login flex-column flex-justify-center flex-align-center m0 b0">
    <a-image
      style="width: 17vw; height: 10vw; margin-bottom: 50px"
      src="http://lofrev.net/wp-content/photos/2016/09/dji_logo_png.png"
      :src="djiLogo"
    />
    <p class="logo fz35 pb50">Pilot Cloud API Demo</p>
    <a-form
@@ -11,7 +11,7 @@
      class="flex-row flex-justify-center flex-align-center"
    >
      <a-form-item>
        <a-input v-model:value="formState.user" placeholder="Username">
        <a-input v-model:value="formState.username" placeholder="Username">
          <template #prefix
            ><UserOutlined style="color: rgba(0, 0, 0, 0.25)"
          /></template>
@@ -44,93 +44,97 @@
</template>
<script lang="ts" setup>
import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { onMounted, reactive, UnwrapRef } from 'vue'
import { onMounted, reactive, ref, UnwrapRef } from 'vue'
import { CURRENT_CONFIG } from '/@/api/http/config'
import { login, refreshToken } from '/@/api/manage'
import { login, LoginBody, refreshToken } from '/@/api/manage'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
import router from '/@/router'
import { EComponentName, ELocalStorageKey, ERouterName, EUserType } from '/@/types'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import djiLogo from '/@/assets/icons/dji_logo.png'
interface FormState {
  user: string
  password: string
}
const root = getRoot()
const formState: UnwrapRef<FormState> = reactive({
  user: 'pilot',
  password: 'pilot123'
const formState: UnwrapRef<LoginBody> = reactive({
  username: 'pilot',
  password: 'pilot123',
  flag: EUserType.Pilot,
})
let isVerified:any
const isVerified = ref<boolean>(false)
onMounted(async () => {
  const verifyLicense = JSON.parse(apiPilot.platformVerifyLicense(CURRENT_CONFIG.appId,
    CURRENT_CONFIG.appKey, CURRENT_CONFIG.appLicense))
  const platformVerify = JSON.parse(apiPilot.isPlatformVerifySuccess())
  isVerified = platformVerify.data
  if (platformVerify.data === true) {
    message.success('The license verification is successful.')
  } else {
    message.error('Filed to verify the license. message is ' + verifyLicense.data)
  verifyLicense()
  if (!isVerified.value) {
    return
  }
  const token = apiPilot.getToken()
  console.log('api token:', token)
  apiPilot.setPlatformMessage('Cloud Api Platform', '', '')
  if (token && token !== undefined) {
  const token = localStorage.getItem(ELocalStorageKey.Token)
  if (token) {
    await refreshToken({})
      .then(res => {
        apiPilot.setComponentParam('api', {
        apiPilot.setComponentParam(EComponentName.Api, {
          host: CURRENT_CONFIG.baseURL,
          token: res.data.access_token
        })
        const jsres = JSON.parse(
          apiPilot.loadComponent('api', apiPilot.getComponentParam('api'))
        )
        console.log('load api module status:', jsres)
        const jsres = apiPilot.loadComponent(EComponentName.Api, apiPilot.getComponentParam(EComponentName.Api))
        if (!jsres) {
          message.error('Failed to load api module.')
          return
        }
        apiPilot.setToken(res.data.access_token)
        localStorage.setItem('x-auth-token', res.data.access_token)
        message.success('Login Success')
        root.$router.push('/pilot-home')
        localStorage.setItem(ELocalStorageKey.Token, res.data.access_token)
        root.$router.push(ERouterName.PILOT_HOME)
      })
      .catch(err => {
        console.error(err)
        message.error(err)
      })
  }
})
const onSubmit = async (e: any) => {
  await login({
    username: formState.user,
    password: formState.password
  })
  await login(formState)
    .then(res => {
      if (!isVerified) {
      if (!isVerified.value) {
        message.error('Please verify the license firstly.')
        return
      }
      console.log('login res:', res)
      if (res.code === 0) {
        apiPilot.setComponentParam('api', {
        apiPilot.setComponentParam(EComponentName.Api, {
          host: CURRENT_CONFIG.baseURL,
          token: res.data.access_token
        })
        const jsres = apiPilot.loadComponent(
          'api',
          apiPilot.getComponentParam('api')
          EComponentName.Api,
          apiPilot.getComponentParam(EComponentName.Api)
        )
        console.log('load api module res:', jsres)
        apiPilot.setToken(res.data.access_token)
        localStorage.setItem('x-auth-token', res.data.access_token)
        localStorage.setItem('workspace-id', res.data.workspace_id)
        localStorage.setItem('username', res.data.username)
        localStorage.setItem(ELocalStorageKey.Token, res.data.access_token)
        localStorage.setItem(ELocalStorageKey.WorkspaceId, res.data.workspace_id)
        localStorage.setItem(ELocalStorageKey.UserId, res.data.user_id)
        localStorage.setItem(ELocalStorageKey.Username, res.data.username)
        localStorage.setItem(ELocalStorageKey.Flag, EUserType.Pilot.toString())
        message.success('Login Success')
        root.$router.push('/pilot-home')
        root.$router.push(ERouterName.PILOT_HOME)
      }
    })
    .catch(err => {
      console.error(err)
      message.error(err)
    })
}
function verifyLicense () {
  isVerified.value = apiPilot.platformVerifyLicense(CURRENT_CONFIG.appId, CURRENT_CONFIG.appKey, CURRENT_CONFIG.appLicense) &&
    apiPilot.isPlatformVerifySuccess()
  if (isVerified.value) {
    message.success('The license verification is successful.')
  } else {
    message.error('Filed to verify the license. Please check license whether the license is correct, or apply again.')
  }
}
</script>
<style lang="scss" scoped>
src/pages/page-pilot/pilot-liveshare.vue
@@ -1,16 +1,17 @@
<template>
  <div
    class="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
  >
    <div class="width100 flex-column flex-justify-start flex-align-start" style="background-color: white;">
      <p class="fz16 ml10 mt15 mb10 color-text-title color-font-bold" style="color: #939393">
        Before starting manually, please select the publish mode and livestream type
      </p>
    <div
      class="mt20 flex-row flex-align-center flex-justify-between"
      style="width: 100%"
    >
      class="mt15 flex-row flex-align-center flex-justify-between"
      style="width: 100%;">
      <p class="ml10 mb0 fz16" style="color: black">
        Select Video Publish Mode:
      </p>
      <a-select
        style="width: 200px"
        style="width: 200px; margin-right: 20px;"
        placeholder="Select Mode"
        @select="onPublishModeSelect"
      >
@@ -24,16 +25,18 @@
      </a-select>
    </div>
    <a-divider dashed class="mt10 mb0"></a-divider>
    <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
      <a-divider />
    </div>
    <div
      class="flex-row flex-align-center flex-justify-between mt10"
      style="width: 100%"
      class="flex-row flex-align-center flex-justify-between"
      style="width: 100%; margin-top: -10px;"
    >
      <p class="ml10 mb0 fz16" style="color: black">Select Live Share Type:</p>
      <p class="ml10 mb0 fz16">Select Livestream Type:</p>
      <a-select
        style="width: 200px"
        style="width: 200px; margin-right: 20px;"
        placeholder="Select Live Type"
        :value="liveStreamStatus.type"
        @select="onLiveTypeSelect"
      >
        <a-select-option
@@ -45,141 +48,265 @@
        </a-select-option>
      </a-select>
    </div>
    <a-divider dashed class="mt10 mb0"></a-divider>
    <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
      <a-divider />
    </div>
    <div class="width-100" style="margin-top: -10px;">
      <div class="ml10" style="width: 97%;">
        <span class="fz16">Param: </span>
        <span v-if="liveStreamStatus.type === ELiveTypeValue.Agora" style="word-break: break-all; color: #75c5f6;">{{ agoraParam }}</span>
        <span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTMP" style="word-break: break-all; color: #75c5f6;">{{ rtmpParam }}</span>
        <span v-else-if="liveStreamStatus.type === ELiveTypeValue.RTSP" style="word-break: break-all; color: #75c5f6;">{{ rtspParam }}</span>
        <span v-else-if="liveStreamStatus.type === ELiveTypeValue.GB28181" style="word-break: break-all; color: #75c5f6;">{{ gb28181Param }}</span>
        <span v-else></span>
      </div>
    <div
      class="flex-row flex-align-center flex-justify-center mt20"
      style="width: 100%"
    >
      <p>Live Share State: {{ liveState }}</p>
    </div>
    <div
      class="flex-row flex-align-center flex-justify-center mt20"
      style="width: 100%"
    >
      <a-button type="primary" @click="onPlay">Play</a-button>
      <a-button class="ml20" type="primary" @click="onGetConfig"
        >Get Config</a-button
      >
      <a-button class="ml20" type="primary" @click="onGetStatus"
        >Get Status</a-button
      >
    <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
      <a-divider />
    </div>
    <div class="mb20 flex-row flex-align-center flex-justify-center"
      style="width: 100%; ">
      <a-button class="flex-column fz20 flex-align-center flex-justify-center" style="width: 100px;" type="ghost" @click="onPlay">Play</a-button>
      <a-button class="flex-column fz20 flex-align-center flex-justify-center ml40" style="width: 100px;" type="ghost" @click="onStop">Stop</a-button>
    </div>
    <a-button v-if="playVisiable" class="flex-column flex-align-center" shape="circle" @click="showLivingStatus"
      style="position: fixed; top: 13vh; left: 5vw; opacity: 0.8; background-color: rgb(0,0,0,0)">
      <template #icon><CaretRightFilled style="font-size: 26px; color: " /></template>
    </a-button>
    <a-drawer  placement="right" v-model:visible="drawerVisible" width="280px" :mask="false" @close="closeDrawer">
      <div class="fz16 width-100">
        <div class="mt20" style=" margin-bottom: -10px;">
          <span class="fz20 flex-row flex-align-center flex-justify-center">
            <font :color="liveState === EStatusValue.LIVING ? 'green' : liveState === EStatusValue.CONNECTED ? 'blue' : 'red'">{{ liveState }}</font></span>
        </div>
        <a-divider />
        <div style=" margin-top: -10px; margin-bottom: -15px;">
          <span>Frame Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.fps }}<span v-if="liveStreamStatus.fps != -1"> fps</span></span><br/>
        </div>
        <a-divider />
        <div style=" margin-top: -10px; margin-bottom: -10px;">
          <span>Video Bit Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.videoBitRate }}<span v-if="liveStreamStatus.videoBitRate != -1"> kbps</span></span><br/>
        </div>
        <a-divider />
        <div style=" margin-top: -10px; margin-bottom: -10px;">
          <span>Audio Bit Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.audioBitRate }}<span v-if="liveStreamStatus.audioBitRate != -1"> kbps</span></span><br/>
        </div>
        <a-divider />
        <div style=" margin-top: -10px; margin-bottom: -10px;">
          <span>Packet Loss Rate:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.dropRate }}<span v-if="liveStreamStatus.dropRate != -1"> %</span></span><br/>
        </div>
        <a-divider />
        <div style=" margin-top: -10px; margin-bottom: -10px;">
          <span>RTT:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.rtt }}<span v-if="liveStreamStatus.rtt != -1"> ms</span></span><br/>
        </div>
        <a-divider />
        <div style=" margin-top: -10px;">
          <span >Jitter:</span><span style="float: right; color: #75c5f6;">{{ liveStreamStatus.jitter }}</span><br/>
        </div>
      </div>
    </a-drawer>
  </div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { onMounted, reactive, ref, UnwrapRef } from 'vue'
import { CURRENT_CONFIG as config, CURRENT_CONFIG } from '/@/api/http/config'
import { ELiveTypeName, ELiveTypeValue, GB28181Param, LiveConfigParam, LiveStreamStatus, RTSPParam, EVideoPublishType } from '/@/api/live-stream'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
import { ELiveStatusValue, EStatusValue } from '/@/types'
import { CaretRightFilled } from '@ant-design/icons-vue'
const root = getRoot()
const publishModeList = [
  {
    value: 'video-on-demand',
    label: 'video-on-demand'
    value: EVideoPublishType.VideoOnDemand,
    label: EVideoPublishType.VideoOnDemand
  },
  {
    value: 'video-by-manual',
    label: 'video-by-manual'
    value: EVideoPublishType.VideoByManual,
    label: EVideoPublishType.VideoByManual
  },
  {
    value: 'video-demand-aux-manual',
    label: 'video-demand-aux-manual'
    value: EVideoPublishType.VideoDemandAuxManual,
    label: EVideoPublishType.VideoDemandAuxManual
  }
]
const liveTypeList = [
  {
    value: 1,
    label: 'AGORA'
    value: ELiveTypeValue.Agora,
    label: ELiveTypeName.Agora
  },
  {
    value: 2,
    label: 'RTMP'
    value: ELiveTypeValue.RTMP,
    label: ELiveTypeName.RTMP
  },
  {
    value: 3,
    label: 'RTSP'
    value: ELiveTypeValue.RTSP,
    label: ELiveTypeName.RTSP
  },
  {
    value: 4,
    label: 'GB28181'
    value: ELiveTypeValue.GB28181,
    label: ELiveTypeName.GB28181
  }
]
const agoraParam = {
  uid: config.agoraAPPID,
  uid: '2892130292',
  token: config.agoraToken,
  channelId: config.agoraChannel
}
const rtmpParam = {
  url: config.rtmpURL + '12345'
  url: config.rtmpURL + new Date().getTime()
}
const liveState = ref<string>('STOP')
const livetypeSelected = ref<number>(1)
const publishModeSelected = ref<string>('video-demand-aux-manual')
const rtspParam: RTSPParam = {
  userName: CURRENT_CONFIG.rtspUserName,
  password: CURRENT_CONFIG.rtspPassword,
  port: CURRENT_CONFIG.rtspPort
}
const gb28181Param: GB28181Param = {
  serverIp: CURRENT_CONFIG.gbServerIp,
  serverPort: CURRENT_CONFIG.gbServerPort,
  serverId: CURRENT_CONFIG.gbServerId,
  agentId: CURRENT_CONFIG.gbAgentId,
  password: CURRENT_CONFIG.gbPassword,
  agentPort: CURRENT_CONFIG.gbAgentPort,
  agentChannel: CURRENT_CONFIG.gbAgentChannel
}
const playVisiable = ref(false)
const drawerVisible = ref(false)
const liveState = ref(EStatusValue.DISCONNECT)
const liveTypeSelected = ref<string>()
const publishModeSelected = ref<string>()
const liveStreamStatus: LiveStreamStatus = reactive({
  audioBitRate: -1,
  dropRate: -1,
  fps: -1,
  jitter: -1,
  quality: -1,
  rtt: -1,
  status: -1,
  type: -1,
  videoBitRate: -1
})
onMounted(() => {
  const status: any = apiPilot.getLiveshareStatus()
  console.log(status)
  // liveState.value =
  //   status.status === 0
  //     ? 'Cannot connect to server'
  //     : status.status === 1
  //       ? 'Connect to server'
  //       : 'Playing'
  const config: LiveConfigParam = JSON.parse(apiPilot.getLiveshareConfig())
  liveStreamStatus.type = config.type
  refreshLiveType()
  // console.log(liveState.value)
  window.liveStatusCallback = arg => {
    liveStatusCallback(arg)
  }
})
const onLiveTypeSelect = (val: any) => {
  livetypeSelected.value = val
  message.info('set livetype:' + livetypeSelected.value, 5)
const liveStatusCallback = async (arg: LiveStreamStatus) => {
  liveStreamStatus.fps = arg.fps
  liveStreamStatus.audioBitRate = arg.audioBitRate
  liveStreamStatus.dropRate = arg.dropRate
  liveStreamStatus.jitter = arg.jitter
  liveStreamStatus.rtt = arg.rtt
  liveStreamStatus.videoBitRate = arg.videoBitRate
  liveStreamStatus.quality = arg.quality
  liveStreamStatus.type = arg.type
  liveStreamStatus.status = arg.status
  switch (liveStreamStatus.status) {
    case ELiveStatusValue.LIVING:
      liveState.value = EStatusValue.LIVING
      break
    case ELiveStatusValue.CONNECTED:
      liveState.value = EStatusValue.CONNECTED
      break
    default:
      liveState.value = EStatusValue.DISCONNECT
  }
}
function refreshLiveType () {
  switch (liveStreamStatus.type) {
    case ELiveTypeValue.Agora:
      liveTypeSelected.value = ELiveTypeName.Agora
      break
    case ELiveTypeValue.RTMP:
      liveTypeSelected.value = ELiveTypeName.RTMP
      break
    case ELiveTypeValue.RTSP:
      liveTypeSelected.value = ELiveTypeName.RTSP
      break
    case ELiveTypeValue.GB28181:
      liveTypeSelected.value = ELiveTypeName.GB28181
      break
    default:
      liveTypeSelected.value = ELiveTypeName.Unknown
  }
}
const onLiveTypeSelect = (val: number) => {
  liveStreamStatus.type = val
  refreshLiveType()
}
const onPublishModeSelect = (val: string) => {
  publishModeSelected.value = val
  message.info(
    'set publish mode res:' +
      apiPilot.setVideoPublishType(publishModeSelected.value),
    5
  )
  apiPilot.setVideoPublishType(publishModeSelected.value)
}
const onPlay = () => {
  switch (livetypeSelected.value) {
  if (!publishModeSelected.value) {
    message.warn('Please select publish mode!')
    return
  }
  if (liveTypeSelected.value === ELiveTypeName.Unknown) {
    message.warn('Please select livestream type!')
    return
  }
  switch (liveStreamStatus.type) {
    case 1: {
      message.info('agoraParam:' + JSON.stringify(agoraParam))
      apiPilot.setLiveshareConfig(1, JSON.stringify(agoraParam))
      apiPilot.startLiveshare()
      apiPilot.setLiveshareConfig(ELiveTypeValue.Agora, JSON.stringify(agoraParam))
      break
    }
    case 2: {
      message.info('rtmpParam:' + JSON.stringify(rtmpParam))
      apiPilot.setLiveshareConfig(2, JSON.stringify(rtmpParam))
      message.info(apiPilot.startLiveshare())
      apiPilot.setLiveshareConfig(ELiveTypeValue.RTMP, JSON.stringify(rtmpParam))
      break
    }
    case 3: {
      message.info('rtspParam:' + config.rtspPara)
      apiPilot.setLiveshareConfig(3, config.rtspPara)
      apiPilot.startLiveshare()
      apiPilot.setLiveshareConfig(ELiveTypeValue.RTSP, JSON.stringify(rtspParam))
      break
    }
    case 4: {
      message.info('gb28181Param:' + config.gb28181Para)
      apiPilot.setLiveshareConfig(4, config.gb28181Para)
      apiPilot.startLiveshare()
      apiPilot.setLiveshareConfig(ELiveTypeValue.GB28181, JSON.stringify(gb28181Param))
      break
    }
  }
  const status = apiPilot.startLiveshare()
  if (status) {
    playVisiable.value = true
    drawerVisible.value = true
    message.success('success')
  }
}
const onGetStatus = () => {
  const status = apiPilot.getLiveshareStatus()
  message.info(status, 5)
const showLivingStatus = () => {
  drawerVisible.value = !drawerVisible.value
}
const onGetConfig = () => {
  const status = apiPilot.getLiveshareConfig()
  message.info(status, 5)
const onStop = () => {
  const status = apiPilot.stopLiveshare()
  if (status) {
    message.success('success')
    playVisiable.value = false
    drawerVisible.value = false
    setTimeout(() => {
      let key: (keyof LiveStreamStatus)
      for (key in liveStreamStatus) {
        if (key === 'type') {
          continue
        }
        liveStreamStatus[key] = -1
      }
    }, 2000)
  }
}
</script>
src/pages/page-pilot/pilot-media.vue
@@ -1,18 +1,16 @@
<template>
  <div
    class="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
  >
    <p class="fz16 ml10 mt10 mb10 color-text-title color-font-bold">
      If Enabled, Pilot will upload photos or videos to the server
      automatically.
  <a-layout>
  <div class="width100 flex-column flex-justify-start flex-align-start" style="background-color: white;">
    <p class="fz16 ml10 mt15 mb10 color-text-title color-font-bold" style="color: #939393">
      When enabled, photos and videos will be automatically uploaded to this server
    </p>
    <div
      class="flex-row flex-align-center flex-justify-between"
      style="width: 100%"
      class="flex-row flex-align-center mt20"
      style="width: 100%;"
    >
      <p class="ml10 mb0 fz16" style="color: black">Auto Upload Photos</p>
      <p class="ml10 mb0 fz16" style="margin-right: 73vw;">Auto Photo Upload</p>
      <a-switch
        class="mt0 mb0"
        v-model:checked="enablePhotoUpload"
        @change="onPhotoUpload"
      ></a-switch>
@@ -23,34 +21,36 @@
    >
      <a-radio-group
        class="mt10 ml20"
        v-if="enablePhotoUpload == true"
        v-if="enablePhotoUpload === true"
        v-model:value="photoType"
        defaultChecked="0"
        @change="onPhototype"
      >
        <a-radio :value="0">Original Photo</a-radio>
        <a-radio class="ml20" :value="1">Preview Photo</a-radio>
        <a-radio :value="EPhotoType.Original">Original Photo</a-radio>
        <a-radio class="ml20" :value="EPhotoType.Preview">Preview Photo</a-radio>
      </a-radio-group>
    </div>
    <a-divider dashed class="mt10 mb0"></a-divider>
    <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
      <a-divider />
    </div>
    <div
      class="flex-row flex-align-center flex-justify-between mt10"
      style="width: 100%"
      class="flex-row flex-align-center"
      style="width: 100%; margin-top: -10px;"
    >
      <p class="ml10 mb0 fz16" style="color: black">Auto Upload Video</p>
      <p class="ml10 mb0 fz16" style="margin-right: 73vw;">Auto Video Upload</p>
      <a-switch
        @change="onVideoUpload"
        v-model:checked="enableVideoUpload"
      ></a-switch>
    </div>
    <a-divider dashed class="mt10 mb0"></a-divider>
    <div class="ml10 mr10" style="width: 96%; margin-top: -10px;">
      <a-divider />
    </div>
    <div
      class="flex-row flex-align-center flex-justify-between mt20"
      style="width: 100%"
      class="flex-row flex-align-center flex-justify-between mb15"
      style="width: 100%; margin-top: -10px;"
    >
      <p class="ml10 mb0 fz16 color-font-bold" style="color: black">
      <p class="ml10 mb0 fz16 color-font-bold">
        Path for uploading media resources in dual-controller mode
      </p>
      <a-radio-group
@@ -59,11 +59,12 @@
        button-style="solid"
        @change="onUploadPath"
      >
        <a-radio-button :value="0">Mine</a-radio-button>
        <a-radio-button :value="1">Another</a-radio-button>
        <a-radio-button :value="EDownloadOwner.Mine">Mine</a-radio-button>
        <a-radio-button :value="EDownloadOwner.Others">Another</a-radio-button>
      </a-radio-group>
    </div>
  </div>
  </a-layout>
</template>
<script lang="ts" setup>
@@ -71,58 +72,31 @@
import { onMounted, ref } from 'vue'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
import { EComponentName, EPhotoType, EDownloadOwner } from '/@/types'
const root = getRoot()
const enablePhotoUpload = ref<boolean>(true)
const enableVideoUpload = ref<boolean>(false)
const photoType = ref<number>(1)
const uploadPath = ref<number>(0)
const enablePhotoUpload = ref<boolean>(apiPilot.getAutoUploadPhoto())
const enableVideoUpload = ref<boolean>(apiPilot.getAutoUploadVideo())
const photoType = ref<number>(apiPilot.getUploadPhotoType())
const uploadPath = ref<number>(apiPilot.getDownloadOwner())
onMounted(() => {
  message.info('After setting, please use the physical button of the remote control to return, otherwise the setting is invalid.')
  enablePhotoUpload.value =
    apiPilot.getAutoUploadPhoto() === undefined
      ? true
      : apiPilot.getAutoUploadPhoto()
  enableVideoUpload.value =
    apiPilot.getAutoUploadVideo() === undefined
      ? false
      : apiPilot.getAutoUploadVideo()
  photoType.value =
    apiPilot.getUploadPhotoType() === undefined
      ? 1
      : apiPilot.getUploadPhotoType()
  uploadPath.value =
    apiPilot.getDownloadOwner() === undefined ? 0 : apiPilot.getDownloadOwner()
  console.log(
    enablePhotoUpload.value,
    enableVideoUpload.value,
    photoType.value,
    uploadPath.value
  )
  apiPilot.setComponentParam('media', {
    autoUploadPhoto: enablePhotoUpload.value,
    autoUploadPhotoType: photoType.value,
    autoUploadVideo: enableVideoUpload.value
  })
})
const onPhotoUpload = () => {
  apiPilot.setAutoUploadPhoto(enablePhotoUpload.value)
}
const onVideoUpload = () => {
  console.log(enableVideoUpload.value)
  apiPilot.setAutoUploadVideo(enableVideoUpload.value)
}
const onPhototype = () => {
  console.log(photoType.value)
  apiPilot.setUploadPhotoType(photoType.value)
}
const onUploadPath = (e: any) => {
  apiPilot.setDownloadOwner(uploadPath.value)
}
onMounted(() => {
  console.error(apiPilot.getUploadPhotoType())
  console.error(apiPilot.getAutoUploadVideo())
})
</script>
<style lang="scss" scoped>
src/pages/project-app/home.vue
New file
@@ -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>
src/pages/project-app/index.vue
@@ -1,20 +1,18 @@
<template>
  <div
    v-if="showLogin"
    class="login flex-column flex-justify-center flex-align-center m0 b0"
  >
    class="login flex-column flex-justify-center flex-align-center m0 b0">
    <a-image
      style="width: 17vw; height: 10vw; margin-bottom: 50px"
      src="http://lofrev.net/wp-content/photos/2016/09/dji_logo_png.png"
      :src="djiLogo"
    />
    <p class="logo fz35 pb50">Cloud API Demo</p>
    <p class="fz35 pb50" style="color: #2d8cf0">Cloud API Demo</p>
    <a-form
      layout="inline"
      :model="formState"
      class="flex-row flex-justify-center flex-align-center"
    >
      <a-form-item>
        <a-input v-model:value="formState.user" placeholder="Username">
        <a-input v-model:value="formState.username" placeholder="Username">
          <template #prefix
            ><UserOutlined style="color: rgba(0, 0, 0, 0.25)"
          /></template>
@@ -44,115 +42,41 @@
      </a-form-item>
    </a-form>
  </div>
  <div v-else class="project-app-wrapper">
    <div class="left">
      <Sidebar />
      <div class="main-content uranus-scrollbar dark">
        <router-view />
      </div>
    </div>
    <div class="right">
      <div class="map-wrapper">
        <GMap />
      </div>
      <div class="media-wrapper" v-if="getMediaRoute()">
        <MediaPanel />
      </div>
      <div class="wayline-wrapper" v-if="getWaylineRoute()">
        <WaylinePanel />
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import djiLogo from '/@/assets/icons/dji_logo.png'
import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { reactive, ref, UnwrapRef } from 'vue'
import Sidebar from './sidebar.vue'
import { login } from '/@/api/manage'
import websocket from '/@/api/websocket'
import GMap from '/@/components/GMap.vue'
import MediaPanel from '/@/components/MediaPanel.vue'
import WaylinePanel from '/@/components/wayline-panel.vue'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { login, LoginBody } from '/@/api/manage'
import { getRoot } from '/@/root'
import { useMyStore } from '/@/store'
interface FormState {
  user: string
  password: string
}
import { ELocalStorageKey, ERouterName, EUserType } from '/@/types'
import router from '/@/router'
const root = getRoot()
const showLogin = ref(true)
const store = useMyStore()
const formState: UnwrapRef<FormState> = reactive({
  user: 'adminPC',
  password: 'adminPC'
const formState: UnwrapRef<LoginBody> = reactive({
  username: 'adminPC',
  password: 'adminPC',
  flag: EUserType.Web,
})
let socket = {} as any
const gMapCoverHook = useGMapCover()
const onSubmit = async (e: any) => {
  const result = await login({
    username: formState.user,
    password: formState.password
  })
  const result = await login(formState)
  if (result.code === 0) {
    console.log(result)
    localStorage.setItem('x-auth-token', result.data.access_token)
    localStorage.setItem('workspace-id', result.data.workspace_id)
    localStorage.setItem('username', result.data.username)
    showLogin.value = false
    message.info('login success')
    socket = websocket.init(wsGetMsg)
    localStorage.setItem(ELocalStorageKey.Token, result.data.access_token)
    localStorage.setItem(ELocalStorageKey.WorkspaceId, result.data.workspace_id)
    localStorage.setItem(ELocalStorageKey.Username, result.data.username)
    localStorage.setItem(ELocalStorageKey.UserId, result.data.user_id)
    localStorage.setItem(ELocalStorageKey.Flag, EUserType.Web.toString())
    root.$router.push(ERouterName.MEMBERS)
  } else {
    message.error(result.message)
  }
}
// function wsInfo (data) {
//   store.commit('SET_DEVICE_INFO', data)
// }
// function getDeviceInfo () {
//   const info = store.state.DeviceInfo
//   console.log(info)
const wsGetMsg = async (res: any) => {
  const payload = JSON.parse(res.data)
  // console.log(payload)
  switch (payload.biz_code) {
    case 'gateway_osd': {
      store.commit('SET_GATEWAY_INFO', payload.data)
      break
    }
    case 'device_osd': {
      store.commit('SET_DEVICE_INFO', payload.data)
      break
    }
    case 'map_element_create': {
      store.commit('SET_MAP_ELEMENT_CREATE', payload.data)
      break
    }
    case 'map_element_update': {
      store.commit('SET_MAP_ELEMENT_UPDATE', payload.data)
      break
    }
    case 'map_element_delete': {
      store.commit('SET_MAP_ELEMENT_DELETE', payload.data)
      break
    }
    default:
      break
  }
}
function getMediaRoute () {
  return root.$route.name === 'media'
}
function getWaylineRoute () {
  return root.$route.name === 'wayline'
}
</script>
<style lang="scss" scoped>
@@ -160,49 +84,5 @@
.login {
  background-color: $dark-highlight;
  height: 100vh;
}
.logo {
  color: $primary;
}
.project-app-wrapper {
  display: flex;
  position: absolute;
  transition: width 0.2s ease;
  height: 100%;
  width: 100%;
  .left {
    width: 450px;
    display: flex;
    background-color: #232323;
    float: left;
  }
  .right {
    width: 100%;
    height: 100%;
    .map-wrapper {
      width: 100%;
      height: 100%;
    }
  }
  .main-content {
    flex: 1;
    color: $text-white-basic;
  }
  .media-wrapper {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: 100;
    background: #f6f8fa;
    padding: 16px;
  }
  .wayline-wrapper {
    position: absolute;
    top: 0;
    bottom: 0;
    z-index: 100;
    background: #f6f8fa;
    padding: 16px;
  }
}
</style>
src/pages/project-app/projects/create-plan.vue
New file
@@ -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>
src/pages/project-app/projects/devices.vue
New file
@@ -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>
src/pages/project-app/projects/dock.vue
New file
@@ -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>
src/pages/project-app/projects/layer.vue
@@ -1,5 +1,12 @@
<template>
  <div class="project-layer-wrapper">
    <div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;">
      <a-row>
        <a-col :span="1"></a-col>
        <a-col :span="22">Annotations</a-col>
        <a-col :span="1"></a-col>
      </a-row>
    </div>
    <LayersTree
      :layer-data="mapLayers"
      class="project-layer-content"
@@ -389,9 +396,6 @@
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.project-layer-wrapper {
  padding-top: 16px;
}
</style>
<style lang="scss">
.drawer-element-wrapper {
src/pages/project-app/projects/livestream.vue
@@ -1,65 +1,56 @@
<template>
  <div class="flex-column flex-justify-start flex-align-center">
    <a-button
      class="mt10  "
      style="width:90%"
      type="primary"
      @click="onAgoraLiveStream"
      >Agora Live</a-button
    <router-link
      style="width: 90%; margin: auto;"
      v-for="item in options"
      :key="item.key"
      :to="item.path"
      :class="{
        'menu-item': true,
      }"
    >
    <a-button
      class="mt10"
      style="width:90%"
      style="width:100%;"
      type="primary"
      @click="onOthersLive"
      >RTMP/GB28181 Live</a-button
      @click="selectLivestream(item.routeName)"
      >{{ item.label }}</a-button
    >
    </router-link>
  </div>
  <div v-if="enableAgoraLive">
    <a-modal
      style="top:0"
      v-model:visible="enableAgoraLive"
      title="Agora Live"
      width="100%"
      :maskClosable="false"
      wrapClassName="full-modal"
      :footer="null"
    >
      <LiveAgora />
    </a-modal>
  </div>
  <div v-if="enableOthersLive">
    <a-modal
      style="top:0"
      v-model:visible="enableOthersLive"
      title="RTMP/GB28181/RTSP Live"
      width="100%"
      :maskClosable="false"
      wrapClassName="full-modal"
      :footer="null"
    >
      <LiveOthers />
    </a-modal>
  <div class="live" v-if="showLive">
    <a style="position: absolute; right: 10px; top: 10px; font-size: 16px; color: white;" @click="() => root.$router.push('/' + ERouterName.LIVESTREAM)"><CloseOutlined /></a>
    <router-view :name="routeName" />
  </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import LiveAgora from './livestream-agora.vue'
import LiveOthers from './livestream-others.vue'
import { message } from 'ant-design-vue'
import { onMounted, ref, watch } from 'vue'
import { CloseOutlined } from '@ant-design/icons-vue'
import { getRoot } from '/@/root'
import { ERouterName } from '/@/types'
const root = getRoot()
const routeName = ref<string>()
const showLive = ref<boolean>(false)
const enableAgoraLive = ref(false)
const enableOthersLive = ref(false)
const onAgoraLiveStream = () => {
  console.log('agora')
  enableAgoraLive.value = true
const options = [
  { key: 0, label: 'Agora Live', path: '/' + ERouterName.LIVESTREAM + '/' + ERouterName.LIVING, routeName: 'LiveAgora' },
  { key: 1, label: 'RTMP/GB28181 Live', path: '/' + ERouterName.LIVESTREAM + '/' + ERouterName.LIVING, routeName: 'LiveOthers' }
]
const selectLivestream = (route: string) => {
  routeName.value = route
}
const onOthersLive = () => {
  console.log('liveview')
  enableOthersLive.value = true
}
onMounted(() => {
  watch(() => root.$route.name, data => {
    showLive.value = data === ERouterName.LIVING
  },
  {
    deep: true
  })
})
</script>
<style lang="scss">
@@ -79,4 +70,17 @@
    flex: 1;
  }
}
.live {
  position: absolute;
  z-index: 1;
  right: 50%;
  left: 50%;
  top: 50%;
  margin: auto;
  transform: translate(-50%, -50%);
  text-align: center;
  width: 800px;
  height: 700px;
  background: #232323;
}
</style>
src/pages/project-app/projects/media.vue
@@ -1,6 +1,5 @@
<template>
  <div class="project-media-wrapper">
      Media
  </div>
</template>
src/pages/project-app/projects/members.vue
New file
@@ -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>
src/pages/project-app/projects/task.vue
New file
@@ -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>
src/pages/project-app/projects/tsa.vue
@@ -1,11 +1,463 @@
<template>
  <div class="project-tsa-wrapper">
      TSA
  <div class="project-tsa-wrapper ">
    <div>
      <a-row>
        <a-col :span="1"></a-col>
        <a-col :span="11">My Username</a-col>
        <a-col :span="11" align="right" style="font-weight: 700">{{ username }}</a-col>
        <a-col :span="1"></a-col>
      </a-row>
    </div>
    <div>
      <a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;">
        <a-collapse-panel :key="EDeviceTypeName.Dock" header="Dock" style="border-bottom: 1px solid #4f4f4f;">
          <div v-if="onlineDocks.data.length === 0" style="height: 150px; color: white;">
            <a-empty :image="noData" :image-style="{ height: '60px' }" />
          </div>
          <div v-else class="fz12" style="color: white;">
            <div v-for="dock in onlineDocks.data" :key="dock.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;">
              <div style="border-radius: 2px; height: 100%; width: 100%;" class="flex-row flex-justify-between flex-align-center">
                <div style="float: left; padding: 0px 5px 8px 8px; width: 88%">
                  <div style="width: 80%; height: 30px; line-height: 30px; font-size: 16px;">
                    <a-tooltip :title="dock.gateway.callsign">
                      <span class="text-hidden" style="max-width: 200px;">{{ dock.gateway.callsign }}</span>
                    </a-tooltip>
                  </div>
                  <div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
                    <div>
                      <span class="ml5 mr5"><RobotOutlined /></span>
                      <span class="font-bold" :style="dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected ? 'color: #00ee8b' :  'color: red;'">
                        {{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].mode_code] : EDockModeCode[EDockModeCode.Disconnected] }}
                      </span>
                    </div>
                    <div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;">
                      <div v-if="hmsInfo[dock.gateway.sn]" class="flex-align-center flex-row">
                          <div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' :
                            hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;">
                            <span :style="hmsInfo[dock.gateway.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.gateway.sn].length }}</span>
                            <span class="fz10">{{ hmsInfo[dock.gateway.sn].length > 99 ? '+' : ''}}</span>
                          </div>
                        <a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.gateway.sn]"
                          @visibleChange="readHms(hmsVisible[dock.gateway.sn], dock.gateway.sn)"
                          :overlayStyle="{width: '200px', height: '300px'}">
                          <div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution' :
                            hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;">
                            <span class="word-loop">{{ hmsInfo[dock.gateway.sn][0].message_en }}</span>
                          </div>
                          <template #content>
                            <a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true">
                              <a-collapse-panel v-for="hms in hmsInfo[dock.gateway.sn]" :key="hms.hms_id" :showArrow="false"
                                style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px"
                                :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
                                >
                                <template #header="{ isActive }">
                                  <div class="flex-row flex-align-center" style="width: 130px;">
                                    <div style="width: 110px;">
                                      <span class="word-loop">{{ hms.message_en }}</span>
                                    </div>
                                    <div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center"
                                      :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
                                    >
                                      <DoubleRightOutlined :rotate="isActive ? 90 : 0" />
                                    </div>
                                  </div>
                                </template>
                                <a-tooltip :title="hms.create_time">
                                  <div style="color: white;" class="text-hidden">{{ hms.create_time }}</div>
                                </a-tooltip>
                              </a-collapse-panel>
                            </a-collapse>
                          </template>
                        </a-popover>
                      </div>
                      <div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div>
                    </div>
                  </div>
                  <div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
                    <div>
                      <span class="ml5 mr5"><RocketOutlined /></span>
                      <span class="font-bold" :style="deviceInfo[dock.sn] && deviceInfo[dock.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' :  'color: red;'">
                        {{ deviceInfo[dock.sn] ? EModeCode[deviceInfo[dock.sn].mode_code] : EModeCode[EModeCode.Disconnected] }}
                      </span>
                    </div>
                    <div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;">
                      <div v-if="hmsInfo[dock.sn]" class="flex-align-center flex-row">
                        <div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' :
                          hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;">
                          <span :style="hmsInfo[dock.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.sn].length }}</span>
                          <span class="fz10">{{ hmsInfo[dock.sn].length > 99 ? '+' : ''}}</span>
                        </div>
                        <a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.sn]" @visibleChange="readHms(hmsVisible[dock.sn], dock.sn)"
                          :overlayStyle="{width: '200px', height: '300px'}">
                          <div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution' :
                            hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;">
                            <span class="word-loop">{{ hmsInfo[dock.sn][0].message_en }}</span>
                          </div>
                          <template #content>
                            <a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true">
                              <a-collapse-panel v-for="hms in hmsInfo[dock.sn]" :key="hms.hms_id" :showArrow="false"
                                style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px"
                                :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
                                >
                                <template #header="{ isActive }">
                                  <div class="flex-row flex-align-center" style="width: 130px;">
                                    <div style="width: 110px;">
                                      <span class="word-loop">{{ hms.message_en }}</span>
                                    </div>
                                    <div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center"
                                      :class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'"
                                    >
                                      <DoubleRightOutlined :rotate="isActive ? 90 : 0" />
                                    </div>
                                  </div>
                                </template>
                                <a-tooltip :title="hms.create_time">
                                  <div style="color: white;" class="text-hidden">{{ hms.create_time }}</div>
                                </a-tooltip>
                              </a-collapse-panel>
                            </a-collapse>
                          </template>
                        </a-popover>
                      </div>
                      <div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div>
                    </div>
                  </div>
                </div>
                <div style="float: right; background: #595959; height: 100%; width: 40px;" class="flex-row flex-justify-center flex-align-center">
                  <div class="fz16" @click="switchVisible($event, dock, true, dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected)">
                    <a v-if="osdVisible.gateway_sn === dock.gateway.sn && osdVisible.visible"><EyeOutlined /></a>
                    <a v-else><EyeInvisibleOutlined /></a>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </a-collapse-panel>
      </a-collapse>
      <a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;">
        <a-collapse-panel :key="EDeviceTypeName.Aircraft" header="Online Devices" style="border-bottom: 1px solid #4f4f4f;">
          <div v-if="onlineDevices.data.length === 0" style="height: 150px; color: white;">
            <a-empty :image="noData" :image-style="{ height: '60px' }" />
          </div>
          <div v-else class="fz12" style="color: white;">
            <div v-for="device in onlineDevices.data" :key="device.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;">
              <div class="battery-slide" v-if="deviceInfo[device.sn]">
                <div style="background: #535759; width: 100%;"></div>
                <div class="capacity-percent" :style="{ width: deviceInfo[device.sn].battery.capacity_percent + '%'}"></div>
                <div class="return-home" :style="{ width: deviceInfo[device.sn].battery.return_home_power + '%'}"></div>
                <div class="landing" :style="{ width: deviceInfo[device.sn].battery.landing_power + '%'}"></div>
                <div class="battery" :style="{ left: deviceInfo[device.sn].battery.capacity_percent + '%' }"></div>
              </div>
              <div style="border-bottom: 1px solid #515151; border-radius: 2px; height: 50px; width: 100%;" class="flex-row flex-justify-between flex-align-center">
                <div style="float: left; padding: 5px 5px 8px 8px; width: 88%">
                  <div style="width: 100%; height: 100%;">
                    <a-tooltip>
                      <template #title>{{ device.model }} - {{ device.callsign }}</template>
                      <span class="text-hidden" style="max-width: 200px; display: block; height: 20px;">{{ device.model }} - {{ device.callsign }}</span>
                    </a-tooltip>
                  </div>
                  <div class="mt5" style="background: #595959;">
                    <span class="ml5 mr5"><RocketOutlined /></span>
                    <span class="font-bold" :style="deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' :  'color: red;'">
                      {{ deviceInfo[device.sn] ? EModeCode[deviceInfo[device.sn].mode_code] : EModeCode[EModeCode.Disconnected] }}
                    </span>
                  </div>
                </div>
                <div style="float: right; background: #595959; height: 50px; width: 40px;" class="flex-row flex-justify-center flex-align-center">
                  <div class="fz16" @click="switchVisible($event, device, false, deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected)">
                    <a v-if="osdVisible.sn === device.sn && osdVisible.visible"><EyeOutlined /></a>
                    <a v-else><EyeInvisibleOutlined /></a>
                  </div>
                </div>
              </div>
              <div class="flex-row flex-justify-center flex-align-center" style="height: 40px;">
                <div style="height: 20px; background: #595959; width: 94%;" >
                  <span class="mr5"><a-image style="margin-left: 2px; margin-top: -2px; height: 20px; width: 20px;" :src="rc" /></span>
                  <a-tooltip>
                    <template #title>{{ device.gateway.callsign }} </template>
                    <span>{{ device.gateway.callsign }}</span>
                  </a-tooltip>
                </div>
              </div>
            </div>
          </div>
        </a-collapse-panel>
      </a-collapse>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch, WritableComputedRef } from 'vue'
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
import noData from '/@/assets/icons/no-data.png'
import rc from '/@/assets/icons/rc.png'
import { DeviceStatus, EModeCode, OSDVisible, EDockModeCode, DeviceOsd } from '/@/types/device'
import { useMyStore } from '/@/store'
import { getDeviceTopo, getUnreadDeviceHms, updateDeviceHms } from '/@/api/manage'
import { message } from 'ant-design-vue'
import { RocketOutlined, EyeInvisibleOutlined, EyeOutlined, RobotOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'
import { EHmsLevel } from '/@/types/enums'
const store = useMyStore()
const username = ref(localStorage.getItem(ELocalStorageKey.Username))
const workspaceId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!)
const osdVisible = ref({} as OSDVisible)
const hmsVisible = new Map<string, boolean>()
interface OnlineDevice {
  model: string,
  callsign: string,
  sn: string,
  mode: number,
  gateway: {
    model: string,
    callsign: string,
    sn: string,
    domain: string,
  },
  payload: {
    model: string
  }[]
}
const onlineDevices = reactive({
  data: [] as OnlineDevice[]
})
const onlineDocks = reactive({
  data: [] as OnlineDevice[]
})
const deviceInfo = computed(() => store.state.deviceState.deviceInfo)
const dockInfo = computed(() => store.state.deviceState.dockInfo)
const hmsInfo = computed({
  get: () => store.state.hmsInfo,
  set: (val) => {
    return val
  }
})
onMounted(() => {
  getOnlineTopo()
  setTimeout(() => {
    watch(() => store.state.deviceStatusEvent,
      data => {
        getOnlineTopo()
        if (data.deviceOnline.sn) {
          getUnreadHms(data.deviceOnline.sn)
        }
      },
      {
        deep: true
      }
    )
    getOnlineDeviceHms()
  }, 3000)
})
function getOnlineTopo () {
  getDeviceTopo(workspaceId.value).then((res) => {
    if (res.code !== 0) {
      return
    }
    onlineDevices.data = []
    onlineDocks.data = []
    res.data.forEach((val: any) => {
      const gateway = val.gateways_list.pop()
      const device: OnlineDevice = {
        model: val.device_name,
        callsign: val.nickname,
        sn: val.device_sn,
        mode: EModeCode.Disconnected,
        gateway: {
          model: gateway?.device_name,
          callsign: gateway?.nickname,
          sn: gateway?.device_sn,
          domain: gateway?.domain
        },
        payload: []
      }
      val.payloads_list.forEach((payload: any) => {
        device.payload.push({
          model: payload.payload_name
        })
      })
      if (gateway && EDeviceTypeName.Dock === gateway.domain) {
        hmsVisible.set(device.sn, false)
        hmsVisible.set(device.gateway.sn, false)
        onlineDocks.data.push(device)
      }
      if (val.status && EDeviceTypeName.Gateway === gateway.domain) {
        onlineDevices.data.push(device)
      }
    })
  })
}
function switchVisible (e: any, device: OnlineDevice, isDock: boolean, isClick: boolean) {
  if (!isClick) {
    e.target.style.cursor = 'not-allowed'
    return
  }
  if (device.sn === osdVisible.value.sn) {
    osdVisible.value.visible = !osdVisible.value.visible
  } else {
    osdVisible.value.sn = device.sn
    osdVisible.value.callsign = device.callsign
    osdVisible.value.model = device.model
    osdVisible.value.visible = true
    osdVisible.value.gateway_sn = device.gateway.sn
    osdVisible.value.is_dock = isDock
    osdVisible.value.gateway_callsign = device.gateway.callsign
  }
  store.commit('SET_OSD_VISIBLE_INFO', osdVisible)
}
function getUnreadHms (sn: string) {
  getUnreadDeviceHms(workspaceId.value, sn).then(res => {
    if (res.data.length !== 0) {
      hmsInfo.value[sn] = res.data
    }
  })
  console.info(hmsInfo.value)
}
function getOnlineDeviceHms () {
  const snList = Object.keys(dockInfo.value)
  if (snList.length === 0) {
    return
  }
  snList.forEach(sn => {
    getUnreadHms(sn)
  })
  const deviceSnList = Object.keys(deviceInfo.value)
  if (deviceSnList.length === 0) {
    return
  }
  deviceSnList.forEach(sn => {
    getUnreadHms(sn)
  })
}
function readHms (visiable: boolean, sn: string) {
  if (!visiable) {
    updateDeviceHms(workspaceId.value, sn).then(res => {
      if (res.code === 0) {
        delete hmsInfo.value[sn]
      }
    })
  }
}
</script>
<style lang="scss" scoped>
<style lang="scss">
.project-tsa-wrapper > :first-child {
  height: 50px;
  line-height: 50px;
  align-items: center;
  border-bottom: 1px solid #4f4f4f;
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
  color: white;
  border: 0;
  padding-left: 14px;
}
.text-hidden {
  overflow: hidden !important;
  text-overflow: ellipsis !important;
  white-space: nowrap;
  -o-text-overflow: ellipsis;
}
.font-bold {
  font-weight: 700;
}
.battery-slide {
  width: 100%;
  .capacity-percent {
    background: #00ee8b;
  }
  .return-home {
    background: #ff9f0a;
  }
  .landing {
    background: #f5222d;
  }
  .battery {
    background: white;
    border-radius: 1px;
    width: 8px;
    height: 4px;
    margin-top: -3px;
  }
}
.battery-slide > div {
  position: relative;
  margin-top: -2px;
  min-height: 2px;
  border-radius: 2px;
  white-space: nowrap;
}
.disable {
  cursor: not-allowed;
}
.notice-blink {
  background: $success;
  animation: blink 500ms infinite;
}
.caution-blink {
  background: orange;
  animation: blink 500ms infinite;
}
.warn-blink {
  background: red;
  animation: blink 500ms infinite;
}
.notice {
  background: $success;
  overflow: hidden;
  cursor: pointer;
}
.caution {
  background: orange;
  cursor: pointer;
  overflow: hidden;
}
.warn {
  background: red;
  cursor: pointer;
  overflow: hidden;
}
.word-loop {
  white-space: nowrap;
  display: inline-block;
  animation: 10s loop linear infinite normal;
}
@keyframes blink {
  from {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
  to {
    opacity: 1;
  }
}
@keyframes loop {
  0% {
    transform: translateX(20px);
    -webkit-transform: translateX(20px);
  }
  100% {
    transform: translateX(-100%);
    -webkit-transform: translateX(-100%);
  }
}
</style>
src/pages/project-app/projects/wayline.vue
@@ -1,9 +1,205 @@
<template>
  <div class="project-wayline-wrapper">
    wayline
  <div class="project-wayline-wrapper height-100">
    <div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;">
      <a-row>
        <a-col :span="1"></a-col>
        <a-col :span="22">Flight Route Library</a-col>
        <a-col :span="1"></a-col>
      </a-row>
    </div>
    <div class="height-100">
      <div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
        <div v-for="wayline in waylinesData.data" :key="wayline.id">
          <div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)">
            <div class="title">
              <a-tooltip :title="wayline.name">
                <div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div>
              </a-tooltip>
              <div class="ml10"><UserOutlined /></div>
              <a-tooltip :title="wayline.user_name">
                <div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div>
              </a-tooltip>
              <div class="fz20">
                <a-dropdown>
                  <a style="color: white;">
                    <EllipsisOutlined />
                  </a>
                  <template #overlay>
                    <a-menu theme="dark" class="more" style="background: #3c3c3c;">
                      <a-menu-item @click="downloadWayline(wayline.id, wayline.name)">
                        <span>Download</span>
                      </a-menu-item>
                      <a-menu-item @click="showWaylineTip(wayline.id)">
                        <span>Delete</span>
                      </a-menu-item>
                    </a-menu>
                  </template>
                </a-dropdown>
              </div>
            </div>
            <div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
              <span><RocketOutlined /></span>
              <span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span>
              <span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span>
              <span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
                {{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }}
              </span>
            </div>
            <div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
              <span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span>
            </div>
          </div>
        </div>
      </div>
      <div v-else>
        <a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
      </div>
      <a-modal v-model:visible="deleteTip" width="450px" :closable="false" :maskClosable="false" centered :okButtonProps="{ danger: true }" @ok="deleteWayline">
          <p class="pt10 pl20" style="height: 50px;">Wayline file is unrecoverable once deleted. Continue?</p>
          <template #title>
              <div class="flex-row flex-justify-center">
                  <span>Delete</span>
              </div>
          </template>
      </a-modal>
    </div>
  </div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import { reactive } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { onMounted, onUpdated, ref } from 'vue'
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline'
import { ELocalStorageKey } from '/@/types'
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue'
import { EDeviceType } from '/@/types/device'
import { useMyStore } from '/@/store'
import { WaylineFile } from '/@/types/wayline'
import { downloadFile } from '/@/utils/common'
import { IPage } from '/@/api/http/type'
<style lang="scss" scoped></style>
const store = useMyStore()
const pagination :IPage = {
  page: 1,
  total: 0,
  page_size: 10
}
const waylinesData = reactive({
  data: [] as WaylineFile[]
})
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const deleteTip = ref(false)
const deleteWaylineId = ref<string>('')
const canRefresh = ref(true)
onMounted(() => {
  getWaylines()
})
onUpdated(() => {
  const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement
  const parent = element?.parentNode as HTMLDivElement
  setTimeout(() => {
    if (element?.scrollHeight < parent?.clientHeight && pagination.total > waylinesData.data.length) {
      if (canRefresh.value) {
        pagination.page++
        getWaylines()
      }
    } else if (element && element.className.indexOf('height-100') === -1) {
      element.className = element.className + ' height-100'
    }
  }, 300)
})
function getWaylines () {
  if (!canRefresh.value) {
    return
  }
  canRefresh.value = false
  getWaylineFiles(workspaceId, {
    page: pagination.page,
    page_size: pagination.page_size,
    order_by: 'update_time desc'
  }).then(res => {
    if (res.code !== 0) {
      return
    }
    res.data.list.forEach((wayline: WaylineFile) => waylinesData.data.push(wayline))
    pagination.total = res.data.pagination.total
    pagination.page = res.data.pagination.page
  }).finally(() => {
    canRefresh.value = true
  })
}
function showWaylineTip (waylineId: string) {
  deleteWaylineId.value = waylineId
  deleteTip.value = true
}
function deleteWayline () {
  deleteWaylineFile(workspaceId, deleteWaylineId.value).then(res => {
    if (res.code === 0) {
      message.success('Wayline file deleted')
    }
    deleteWaylineId.value = ''
    deleteTip.value = false
    pagination.total--
    waylinesData.data = []
    setTimeout(getWaylines, 500)
  })
}
function downloadWayline (waylineId: string, fileName: string) {
  downloadWaylineFile(workspaceId, waylineId).then(res => {
    if (res.code && res.code !== 0) {
      return
    }
    const data = new Blob([res.data], { type: 'application/zip' })
    downloadFile(data, fileName + '.kmz')
  })
}
function selectRoute (wayline: WaylineFile) {
  store.commit('SET_SELECT_WAYLINE_INFO', wayline)
}
function onScroll (e: any) {
  const element = e.srcElement
  if (element.scrollTop + element.clientHeight === element.scrollHeight && Math.ceil(pagination.total / pagination.page_size) > pagination.page && canRefresh.value) {
    pagination.page++
    getWaylines()
  }
}
</script>
<style lang="scss" scoped>
.wayline-panel {
  background: #3c3c3c;
  margin-left: auto;
  margin-right: auto;
  margin-top: 10px;
  height: 90px;
  width: 95%;
  font-size: 13px;
  border-radius: 2px;
  cursor: pointer;
  .title {
    display: flex;
    flex-direction: row;
    align-items: center;
    height: 30px;
    font-weight: bold;
    margin: 0px 10px 0 10px;
  }
}
.uranus-scrollbar {
  overflow: auto;
  scrollbar-width: thin;
  scrollbar-color: #c5c8cc transparent;
}
</style>
src/pages/project-app/projects/workspace.vue
New file
@@ -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>
src/pages/project-app/sidebar.vue
@@ -1,5 +1,6 @@
<template>
  <div class="demo-project-sidebar-wrapper">
  <div class="demo-project-sidebar-wrapper flex-justify-between">
    <div>
    <router-link
      v-for="item in options"
      :key="item.key"
@@ -7,19 +8,28 @@
      :class="{
        'menu-item': true,
        selected: selectedRoute(item),
        disabled: item.key > 6
      }"
    >
      <a-tooltip :title="item.label" placement="right">
        <span>{{ item.label }}</span>
        <Icon class="fz20" style="width: 50px;" :icon="item.icon"/>
      </a-tooltip>
    </router-link>
    </div>
    <div class="mb20 flex-display flex-column flex-align-center flex-justify-between">
      <a-tooltip title="Back to home" placement="right">
        <a @click="goHome"> <Icon icon="ImportOutlined" style="font-size: 22px; color: white"/></a>
      </a-tooltip>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { createVNode, defineComponent } from 'vue'
import { getRoot } from '/@/root'
import * as icons from '@ant-design/icons-vue'
import { ERouterName } from '/@/types'
import websocket from '/@/api/websocket'
interface IOptions {
  key: number
  label: string
@@ -32,26 +42,38 @@
  icon: string
}
const Icon = (props: {icon: string}) => {
  return createVNode((icons as any)[props.icon])
}
export default defineComponent({
  components: {
    Icon,
  },
  name: 'Sidebar',
  setup () {
    const root = getRoot()
    const options = [
      { key: 0, label: 'livestream', path: '/livestream', icon: 'livestream' },
      { key: 1, label: 'tsa', path: '/tsa', icon: 'tsa' },
      { key: 2, label: 'layer', path: '/layer', icon: 'layer' },
      { key: 3, label: 'media', path: '/media', icon: 'media' },
      { key: 4, label: 'wayline', path: '/wayline', icon: 'wayline' }
      { key: 0, label: 'Tsa', path: '/' + ERouterName.TSA, icon: 'TeamOutlined' },
      { key: 1, label: 'Livestream', path: '/' + ERouterName.LIVESTREAM, icon: 'VideoCameraOutlined' },
      { key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
      { key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' },
      { key: 4, label: 'Fligth Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
      { key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' }
    ]
    function selectedRoute (item: IOptions) {
      const path = typeof item.path === 'string' ? item.path : item.path.path
      return root.$route.path?.indexOf(path) === 0
    }
    function goHome () {
      root.$router.push('/' + ERouterName.MEMBERS)
      websocket.close()
    }
    return {
      options,
      selectedRoute
      selectedRoute,
      goHome,
    }
  }
})
@@ -62,7 +84,7 @@
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 80px;
  width: 50px;
  border-right: 1px solid #4f4f4f;
  color: $text-white-basic;
  // flex: 1;
@@ -76,7 +98,7 @@
    color: $text-white-basic;
    cursor: pointer;
    &.selected {
      background-color: $dark-highlight;
      background-color: #101010;
      color: $primary;
    }
    &.disabled {
@@ -95,8 +117,6 @@
  }
}
</style>
<style>
.ant-tooltip-open {
  border: 0;
}
src/pages/project-app/topbar.vue
New file
@@ -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>
src/router/index.ts
@@ -1,63 +1,127 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { ERouterName } from '/@/types/index'
import CreatePlan from '../pages/project-app/projects/create-plan.vue'
import WaylinePanel from '/@/pages/project-app/projects/wayline.vue'
import DockPanel from '/@/pages/project-app/projects/dock.vue'
import LiveAgora from '/@/components/livestream-agora.vue'
import LiveOthers from '/@/components/livestream-others.vue'
const routes: Array<RouteRecordRaw> = [
  {
    path: '/' + ERouterName.Project,
    name: ERouterName.Project,
    // redirect: {
    //   name: ERouterName.Project
    // },
    component: () => import('/@/pages/project-app/index.vue'),
    path: '/',
    redirect: '/' + ERouterName.PROJECT
  },
  {
    path: '/' + ERouterName.PROJECT,
    name: ERouterName.PROJECT,
    component: () => import('/@/pages/project-app/index.vue')
  },
  {
    path: '/' + ERouterName.HOME,
    name: ERouterName.HOME,
    component: () => import('/@/pages/project-app/home.vue'),
    children: [
      {
        path: '/' + ERouterName.Livestream,
        component: () => import('/@/pages/project-app/projects/livestream.vue')
        path: '/' + ERouterName.MEMBERS,
        name: ERouterName.MEMBERS,
        component: () => import('/@/pages/project-app/projects/members.vue')
      },
      {
        path: '/' + ERouterName.Tsa,
        path: '/' + ERouterName.DEVICES,
        name: ERouterName.DEVICES,
        component: () => import('/@/pages/project-app/projects/devices.vue')
      }
    ]
  },
  {
    path: '/' + ERouterName.WORKSPACE,
    name: ERouterName.WORKSPACE,
    component: () => import('/@/pages/project-app/projects/workspace.vue'),
    redirect: '/' + ERouterName.TSA,
    children: [
      {
        path: '/' + ERouterName.LIVESTREAM,
        name: ERouterName.LIVESTREAM,
        component: () => import('/@/pages/project-app/projects/livestream.vue'),
        children: [
          {
            path: ERouterName.LIVING,
            name: ERouterName.LIVING,
            components: {
              LiveAgora,
              LiveOthers
            }
          }
        ]
      },
      {
        path: '/' + ERouterName.TSA,
        component: () => import('/@/pages/project-app/projects/tsa.vue')
      },
      {
        path: '/' + ERouterName.Layer,
        name: ERouterName.Layer,
        path: '/' + ERouterName.LAYER,
        name: ERouterName.LAYER,
        component: () => import('/@/pages/project-app/projects/layer.vue')
      },
      {
        path: '/' + ERouterName.Media,
        name: ERouterName.Media,
        path: '/' + ERouterName.MEDIA,
        name: ERouterName.MEDIA,
        component: () => import('/@/pages/project-app/projects/media.vue')
      },
      {
        path: '/' + ERouterName.Wayline,
        name: ERouterName.Wayline,
        path: '/' + ERouterName.WAYLINE,
        name: ERouterName.WAYLINE,
        component: () => import('/@/pages/project-app/projects/wayline.vue')
      },
    ]
  },
  {
    path: '/' + ERouterName.Pilot,
    name: ERouterName.Pilot,
    component: () => import('/@/pages/page-pilot/pilot-index.vue'),
    children: [
      {
        path: '/' + ERouterName.TASK,
        name: ERouterName.TASK,
        component: () => import('/@/pages/project-app/projects/task.vue'),
        children: [
          {
            path: ERouterName.CREATE_PLAN,
            name: ERouterName.CREATE_PLAN,
            component: CreatePlan,
            children: [
              {
                path: ERouterName.SELECT_PLAN,
                name: ERouterName.SELECT_PLAN,
                components: {
                  WaylinePanel,
                  DockPanel
                }
              }
            ]
          }
        ]
      }
    ]
  },
  {
    path: '/' + ERouterName.PilotHome,
    path: '/' + ERouterName.PILOT,
    name: ERouterName.PILOT,
    component: () => import('/@/pages/page-pilot/pilot-index.vue'),
  },
  {
    path: '/' + ERouterName.PILOT_HOME,
    component: () => import('/@/pages/page-pilot/pilot-home.vue')
  },
  {
    path: '/' + ERouterName.PilotMedia,
    path: '/' + ERouterName.PILOT_MEDIA,
    component: () => import('/@/pages/page-pilot/pilot-media.vue')
  },
  {
    path: '/' + ERouterName.PilotLiveshare,
    path: '/' + ERouterName.PILOT_LIVESHARE,
    component: () => import('/@/pages/page-pilot/pilot-liveshare.vue')
  },
  {
    path: '/' + ERouterName.Element,
    name: ERouterName.Element,
    path: '/' + ERouterName.PILOT_BIND,
    component: () => import('/@/pages/page-pilot/pilot-bind.vue')
  },
  {
    path: '/' + ERouterName.ELEMENT,
    name: ERouterName.ELEMENT,
    component: () => import('/@/pages/elements/elements.vue')
  }
]
src/store/index.ts
@@ -1,7 +1,11 @@
import { InjectionKey } from 'vue'
import { ActionTree, createStore, GetterTree, MutationTree, Store, StoreOptions, useStore } from 'vuex'
import { EDeviceTypeName } from '../types'
import { Device, DeviceHms, DeviceOsd, DeviceStatus, DockOsd, GatewayOsd, OSDVisible } from '../types/device'
import { getLayers } from '/@/api/layer'
import { LayerType } from '/@/types/mapLayer'
import { ETaskStatus, TaskInfo, WaylineFile } from '/@/types/wayline'
const initStateFunc = () => ({
  Layers: [
    {
@@ -23,12 +27,6 @@
      type: 2
    }
  ],
  GatewayInfo: { // remote controller, dock
  },
  DeviceInfo: { // drone
  },
  layerBaseInfo: {} as {
    [key:string]:string
  },
@@ -40,6 +38,55 @@
    mapElementCreat: {},
    mapElementUpdate: {},
    mapElementDelete: {}
  },
  deviceStatusEvent: {
    deviceOnline: {} as DeviceStatus,
    deviceOffline: {}
  },
  markerInfo: {
    coverMap: {} as {
      [sn: string]: any
    },
    pathMap: {} as {
      [sn: string]: any[]
    }
  },
  deviceState: {
    // remote controller, dock
    gatewayInfo: {} as {
      [sn: string]: GatewayOsd
    },
    // drone
    deviceInfo: {} as {
      [sn: string]: DeviceOsd
    },
    dockInfo: {} as {
      [sn: string]: DockOsd
    },
    currentSn: '',
    currentType: ''
  },
  osdVisible: {
    sn: '',
    callsign: '',
    model: '',
    visible: false,
    gateway_sn: '',
    is_dock: false,
  } as OSDVisible,
  waylineInfo: {
  } as WaylineFile,
  dockInfo: {
  } as Device,
  taskProgressInfo: {
  } as {
    [bid: string]: TaskInfo
  },
  hmsInfo: {} as {
    [sn: string]: DeviceHms[]
  }
})
@@ -52,12 +99,29 @@
    state.Layers = info
  },
  SET_DEVICE_INFO (state, info) {
    state.DeviceInfo = info
    // console.log(state.DeviceInfo)
    state.deviceState.deviceInfo[info.sn] = info.host
    state.deviceState.currentSn = info.sn
    state.deviceState.currentType = EDeviceTypeName.Aircraft
  },
  SET_GATEWAY_INFO (state, info) {
    state.GatewayInfo = info
    // console.log(state.GatewayInfo)
    state.deviceState.gatewayInfo[info.sn] = info.host
    state.deviceState.currentSn = info.sn
    state.deviceState.currentType = EDeviceTypeName.Gateway
  },
  SET_DOCK_INFO (state, info) {
    state.deviceState.currentSn = info.sn
    state.deviceState.currentType = EDeviceTypeName.Dock
    const dock = state.deviceState.dockInfo[info.sn]
    if (info.host.sdr && state.deviceState.dockInfo[info.sn]) {
      dock.sdr = info.host.sdr
      dock.media_file_detail = info.host.media_file_detail
      return
    }
    const sdr = dock?.sdr
    const mediaFileDetail = dock?.media_file_detail
    state.deviceState.dockInfo[info.sn] = info.host
    state.deviceState.dockInfo[info.sn].sdr = sdr
    state.deviceState.dockInfo[info.sn].media_file_detail = mediaFileDetail
  },
  SET_DRAW_VISIBLE_INFO (state, bool) {
    state.drawVisible = bool
@@ -71,6 +135,43 @@
  SET_MAP_ELEMENT_DELETE (state, info) {
    state.wsEvent.mapElementDelete = info
  },
  SET_DEVICE_ONLINE (state, info) {
    state.deviceStatusEvent.deviceOnline = info
  },
  SET_DEVICE_OFFLINE (state, info) {
    state.deviceStatusEvent.deviceOffline = info
    delete state.deviceState.gatewayInfo[info.sn]
    delete state.deviceState.deviceInfo[info.sn]
    delete state.deviceState.dockInfo[info.sn]
    delete state.hmsInfo[info.sn]
    // delete state.markerInfo.coverMap[info.sn]
    // delete state.markerInfo.pathMap[info.sn]
  },
  SET_OSD_VISIBLE_INFO (state, info) {
    state.osdVisible = info
  },
  SET_SELECT_WAYLINE_INFO (state, info) {
    state.waylineInfo = info
  },
  SET_SELECT_DOCK_INFO (state, info) {
    state.dockInfo = info
  },
  SET_FLIGHT_TASK_PROGRESS (state, info) {
    const taskInfo: TaskInfo = info.output
    if (taskInfo.status === ETaskStatus.OK || taskInfo.status === ETaskStatus.FAILED) {
      taskInfo.status = taskInfo.status.concat('(Code:').concat(info.result).concat(')')
      setTimeout(() => {
        delete state.taskProgressInfo[info.bid]
      }, 60000)
    }
    state.taskProgressInfo[info.bid] = info.output
  },
  SET_DEVICE_HMS_INFO (state, info) {
    const hmsList: Array<DeviceHms> = state.hmsInfo[info.sn]
    state.hmsInfo[info.sn] = info.host.concat(hmsList ?? [])
  }
}
const actions: ActionTree<RootStateType, RootStateType> = {
src/styles/common.scss
@@ -10,10 +10,22 @@
  // Prevent font enlargement in horizontal screen
  text-size-adjust: 100%;
  font-family: Roboto, sans-serif-medium, Arial, sans-serif;
  font-family: sans-serif, Roboto, sans-serif-medium, Arial;
  font-feature-settings: normal;
  color: $main-text-color;
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  ::-webkit-scrollbar {
    width: 8px;
    height: 8px;
    background: transparent;
  }
  ::-webkit-scrollbar-thumb {
    border-radius: 4px;
    border: none;
    background: rgb(89, 89, 89);
  }
}
src/styles/fonts.scss
@@ -32,7 +32,7 @@
}
.fz10 {
  font-size: 10;
  font-size: 10px;
}
.fz12 {
  font-size: 12px;
src/types/device.ts
New file
@@ -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
}
src/types/enums.ts
@@ -1,15 +1,25 @@
export enum ERouterName {
    Element = 'element',
    Project = 'project',
    Tsa = 'tsa',
    Layer = 'layer',
    Media = 'media',
    Wayline = 'wayline',
    Livestream = 'livestream',
    Pilot = 'pilot-login',
    PilotHome = 'pilot-home',
    PilotMedia = 'pilot-media',
    PilotLiveshare = 'pilot-liveshare'
    ELEMENT = 'element',
    PROJECT = 'project',
    HOME = 'home',
    TSA = 'tsa',
    LAYER = 'layer',
    MEDIA = 'media',
    WAYLINE = 'wayline',
    LIVESTREAM = 'livestream',
    LIVING = 'living',
    WORKSPACE = 'workspace',
    MEMBERS = 'members',
    DEVICES = 'devices',
    TASK = 'task',
    CREATE_PLAN = 'create-plan',
    SELECT_PLAN = 'select-plan',
    PILOT = 'pilot-login',
    PILOT_HOME = 'pilot-home',
    PILOT_MEDIA = 'pilot-media',
    PILOT_LIVESHARE = 'pilot-liveshare',
    PILOT_BIND = 'pilot-bind'
}
export enum EStorageKey {
@@ -17,3 +27,81 @@
    TEST_TOOLS_POSITION_STORAGE_KEY = 'DJI_CREATE_VITE_H5_APP:test_tools_position',
    SESSION_ID = 'DJI_CREATE_VITE_H5_APP:sess'
}
export enum EStatusValue {
    CONNECTED = 'Connected',
    DISCONNECT = 'Disconnect',
    LIVING = 'Living'
}
export enum ELiveStatusValue {
    DISCONNECT,
    CONNECTED,
    LIVING
}
export enum EComponentName {
    Thing = 'thing',
    Liveshare = 'liveshare',
    Api = 'api',
    Ws = 'ws',
    Map = 'map',
    Tsa = 'tsa',
    Media = 'media',
    Mission = 'mission'
}
export enum ELocalStorageKey {
    Username = 'username',
    WorkspaceId = 'workspace_id',
    Token = 'x-auth-token',
    PlatformName = 'platform_name',
    WorkspaceName = 'workspace_name',
    WorkspaceDesc = 'workspace_desc',
    Flag = 'flag',
    UserId = 'user_id',
    Device = 'device',
    GatewayOnline = 'gateway_online',
}
export enum EPhotoType {
    Original = 0,
    Preview = 1,
    Unknown = -1
}
export enum EDownloadOwner {
    Mine = 0,
    Others = 1,
    Unknown = -1
}
export enum EUserType {
    Web = 1,
    Pilot = 2,
}
export enum EBizCode {
    GatewayOsd = 'gateway_osd',
    DeviceOsd = 'device_osd',
    DockOsd = 'dock_osd',
    MapElementCreate = 'map_element_create',
    MapElementUpdate = 'map_element_update',
    MapElementDelete = 'map_element_delete',
    DeviceOnline = 'device_online',
    DeviceOffline = 'device_offline',
    FlightTaskProgress = 'flighttask_progress',
    DeviceHms = 'device_hms',
}
export enum EDeviceTypeName {
    Aircraft = 'sub-device',
    Gateway = 'gateway',
    Dock = 'dock',
}
export enum EHmsLevel {
    NOTICE,
    CAUTION,
    WARN,
}
src/types/live-stream.ts
New file
@@ -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'
}
src/types/wayline.ts
New file
@@ -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'
}
src/utils/common.ts
New file
@@ -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)
}
tsconfig.json
@@ -24,6 +24,7 @@
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue"
, "src/vendors/coordtransform.js"  ]
    "src/**/*.vue",
    "src/vendors/coordtransform.js"
  ]
}
vite.config.ts
@@ -28,7 +28,7 @@
    }),
    viteVConsole({
      entry: path.resolve(__dirname, './src/main.ts'), // 入口文件
      // localEnabled: command === 'serve', // serve开发环境下
      localEnabled: command === 'serve', // serve开发环境下
      // enabled: command !== 'serve' || mode === 'test', // 打包环境下/发布测试包,
      config: { // vconsole 配置项
        maxLogNumber: 1000,
yarn.lock
@@ -613,10 +613,17 @@
  "resolved" "https://registry.npmmirror.com/acorn/download/acorn-7.4.1.tgz"
  "version" "7.4.1"
"agora-rtc-sdk-ng@latest":
  "integrity" "sha512-Jogn3TQC7VdA7uZjGYmaAs0XzgYBgGs6nGA67/dQVjqC7kiwAfkQsAuvbevE/qxrVJmLfqtDTNxP40IFvnTlgQ=="
  "resolved" "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.9.1.tgz"
  "version" "4.9.1"
"agora-rtc-sdk-ng@^4.12.1":
  "integrity" "sha512-kmc+ZyKDdnY/BN3iAwBs+MSgTX8Zkc6THFSIAXN9WebjZ/F+N/JXItoNEcgQe3MdTABUli6w3pZ+iObnDqVkBw=="
  "resolved" "https://registry.npmmirror.com/agora-rtc-sdk-ng/-/agora-rtc-sdk-ng-4.12.1.tgz"
  "version" "4.12.1"
  dependencies:
    "agora-rte-extension" "^1.0.22"
"agora-rte-extension@^1.0.22":
  "integrity" "sha512-X2cGBg+L5ZJIFU91qvMASvRsBfg1HXTktVG3YROw9wxHsILSI7jgF9R9XraLc3fNX/UjovaYAlUW+hiJe0v6Xw=="
  "resolved" "https://registry.npmmirror.com/agora-rte-extension/-/agora-rte-extension-1.0.23.tgz"
  "version" "1.0.23"
"ajv@^6.10.0", "ajv@^6.12.4":
  "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="