From 4dccbb68399da2d46943b8d613585afb49aa5455 Mon Sep 17 00:00:00 2001
From: sean.zhou <sean.zhou@dji.com>
Date: Fri, 22 Jul 2022 20:48:54 +0800
Subject: [PATCH] V1.1.0 for dock

---
 src/api/websocket.ts                           |   21 
 src/pages/project-app/home.vue                 |   57 
 src/api/media.ts                               |   17 
 src/types/wayline.ts                           |   30 
 src/pages/project-app/topbar.vue               |   97 
 src/types/device.ts                            |  198 +
 src/components/wayline-panel.vue               |    5 
 src/api/wayline.ts                             |   52 
 src/components/livestream-others.vue           |  378 +++
 src/router/index.ts                            |  118 
 src/pages/project-app/projects/devices.vue     |  518 ++++
 src/pages/project-app/sidebar.vue              |   50 
 src/styles/fonts.scss                          |    2 
 src/pages/project-app/projects/layer.vue       |   10 
 src/assets/icons/rc.png                        |    0 
 src/styles/common.scss                         |   14 
 README.md                                      |    6 
 src/pages/project-app/index.vue                |  164 -
 src/types/live-stream.ts                       |   55 
 src/assets/icons/cloudapi.png                  |    0 
 src/pages/page-pilot/pilot-index.vue           |   96 
 src/api/pilot-bridge.ts                        |  264 +
 src/assets/icons/no-data.png                   |    0 
 tsconfig.json                                  |    5 
 yarn.lock                                      |   15 
 src/api/manage.ts                              |  113 
 src/pages/project-app/projects/workspace.vue   |  140 +
 src/assets/icons/drone.png                     |    0 
 src/assets/icons/dock.png                      |    0 
 src/components/GMap.vue                        |  624 +++++
 package-lock.json                              |   30 
 src/api/layer.ts                               |    3 
 src/utils/common.ts                            |    8 
 src/pages/project-app/projects/livestream.vue  |   96 
 src/pages/page-pilot/pilot-home.vue            |  601 ++++
 src/types/enums.ts                             |  110 
 vite.config.ts                                 |    2 
 src/pages/project-app/projects/task.vue        |   38 
 src/api/http/config.ts                         |   44 
 src/components/livestream-agora.vue            |  350 +++
 src/assets/icons/dji_logo.png                  |    0 
 src/components/MediaPanel.vue                  |  194 +
 src/hooks/use-g-map-cover.ts                   |   10 
 src/pages/project-app/projects/wayline.vue     |  204 +
 src/constants/index.ts                         |    7 
 src/hooks/use-g-map-tsa.ts                     |   96 
 src/pages/project-app/projects/tsa.vue         |  458 ++++
 src/assets/icons/m30.png                       |    0 
 src/pages/project-app/projects/create-plan.vue |  251 ++
 src/pages/project-app/projects/members.vue     |  169 +
 src/store/index.ts                             |  121 +
 src/api/http/request.ts                        |   40 
 src/pages/page-pilot/pilot-liveshare.vue       |  301 +
 src/pages/page-pilot/pilot-media.vue           |   96 
 src/pages/project-app/projects/media.vue       |    1 
 src/pages/project-app/projects/dock.vue        |  100 
 package.json                                   |   20 
 src/components/TaskPanel.vue                   |  182 +
 src/pages/page-pilot/pilot-bind.vue            |   54 
 59 files changed, 5,835 insertions(+), 800 deletions(-)

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

--
Gitblit v1.9.3