sean.zhou
2022-09-23 fa611ff2eeaac289931eb0e21a962743fb271ba7
Update v1.2.0
23 files modified
13 files renamed
30 files added
4 files deleted
10209 ■■■■■ changed files
package-lock.json 11 ●●●●● patch | view | raw | blame | history
package.json 3 ●●●●● patch | view | raw | blame | history
src/api/device-cmd/index.ts 20 ●●●●● patch | view | raw | blame | history
src/api/device-log/index.ts 172 ●●●●● patch | view | raw | blame | history
src/api/device-upgrade/index.ts 47 ●●●●● patch | view | raw | blame | history
src/api/http/type.ts 2 ●●● patch | view | raw | blame | history
src/api/manage.ts 17 ●●●● patch | view | raw | blame | history
src/api/media.ts 17 ●●●● patch | view | raw | blame | history
src/api/wayline.ts 13 ●●●● patch | view | raw | blame | history
src/api/websocket.ts 33 ●●●●● patch | view | raw | blame | history
src/components/GMap.vue 54 ●●●● patch | view | raw | blame | history
src/components/MediaPanel.vue 10 ●●●● patch | view | raw | blame | history
src/components/TaskPanel.vue 2 ●●● patch | view | raw | blame | history
src/components/common/sidebar.vue 6 ●●●● patch | view | raw | blame | history
src/components/common/topbar.vue 2 ●●●●● patch | view | raw | blame | history
src/components/devices/device-hms/DeviceHmsDrawer.vue 268 ●●●●● patch | view | raw | blame | history
src/components/devices/device-log/DeviceLogDetailModal.vue 150 ●●●●● patch | view | raw | blame | history
src/components/devices/device-log/DeviceLogUploadModal.vue 210 ●●●●● patch | view | raw | blame | history
src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue 326 ●●●●● patch | view | raw | blame | history
src/components/devices/device-log/use-device-log-upload-detail.ts 23 ●●●●● patch | view | raw | blame | history
src/components/devices/device-log/use-device-log-upload-progress-event.ts 19 ●●●●● patch | view | raw | blame | history
src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue 64 ●●●●● patch | view | raw | blame | history
src/components/devices/device-upgrade/DeviceFirmwareUpgradeModal.vue 93 ●●●●● patch | view | raw | blame | history
src/components/devices/device-upgrade/use-device-upgrade-event.ts 19 ●●●●● patch | view | raw | blame | history
src/components/devices/device-upgrade/use-device-upgrade.ts 42 ●●●●● patch | view | raw | blame | history
src/components/g-map/DockControlPanel.vue 131 ●●●●● patch | view | raw | blame | history
src/components/g-map/useDockControl.ts 48 ●●●●● patch | view | raw | blame | history
src/event-bus/index.ts 10 ●●●●● patch | view | raw | blame | history
src/hooks/use-connect-websocket.ts 21 ●●●●● patch | view | raw | blame | history
src/hooks/use-g-map-cover.ts 18 ●●●● patch | view | raw | blame | history
src/hooks/use-g-map-tsa.ts 42 ●●●●● patch | view | raw | blame | history
src/hooks/use-g-map.ts 19 ●●●●● patch | view | raw | blame | history
src/hooks/use-mouse-tool.ts 8 ●●●● patch | view | raw | blame | history
src/pages/elements/elements.vue 21 ●●●●● patch | view | raw | blame | history
src/pages/page-pilot/pilot-home.vue 20 ●●●●● patch | view | raw | blame | history
src/pages/page-web/home.vue 71 ●●●●● patch | view | raw | blame | history
src/pages/page-web/index.vue 10 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/create-plan.vue patch | view | raw | blame | history
src/pages/page-web/projects/devices.vue 436 ●●●●● patch | view | raw | blame | history
src/pages/page-web/projects/dock.vue patch | view | raw | blame | history
src/pages/page-web/projects/layer.vue patch | view | raw | blame | history
src/pages/page-web/projects/livestream.vue patch | view | raw | blame | history
src/pages/page-web/projects/media.vue patch | view | raw | blame | history
src/pages/page-web/projects/members.vue 15 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/task.vue patch | view | raw | blame | history
src/pages/page-web/projects/tsa.vue 4 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/wayline.vue 10 ●●●● patch | view | raw | blame | history
src/pages/page-web/projects/workspace.vue 45 ●●●●● patch | view | raw | blame | history
src/pages/project-app/home.vue 57 ●●●●● patch | view | raw | blame | history
src/pages/project-app/projects/devices.vue 518 ●●●●● patch | view | raw | blame | history
src/root.ts 4 ●●●● patch | view | raw | blame | history
src/router/index.ts 37 ●●●● patch | view | raw | blame | history
src/store/index.ts 26 ●●●●● patch | view | raw | blame | history
src/styles/flex.style.scss 12 ●●●●● patch | view | raw | blame | history
src/types/airport-tsa.ts 38 ●●●●● patch | view | raw | blame | history
src/types/device-cmd.ts 210 ●●●●● patch | view | raw | blame | history
src/types/device-log.ts 65 ●●●●● patch | view | raw | blame | history
src/types/device.ts 190 ●●●●● patch | view | raw | blame | history
src/types/enums.ts 21 ●●●●● patch | view | raw | blame | history
src/utils/bytes.ts 86 ●●●●● patch | view | raw | blame | history
src/utils/color.ts 8 ●●●●● patch | view | raw | blame | history
src/utils/common.ts 6 ●●●● patch | view | raw | blame | history
src/utils/constants.ts 15 ●●●●● patch | view | raw | blame | history
src/utils/device-cmd.ts 350 ●●●●● patch | view | raw | blame | history
src/utils/download.ts 82 ●●●●● patch | view | raw | blame | history
src/utils/time.ts 15 ●●●●● patch | view | raw | blame | history
src/websocket/index.ts 85 ●●●●● patch | view | raw | blame | history
src/websocket/util/config.ts 8 ●●●●● patch | view | raw | blame | history
tsconfig.json 4 ●●●● patch | view | raw | blame | history
yarn.lock 5820 ●●●● patch | view | raw | blame | history
package-lock.json
@@ -15,6 +15,7 @@
        "agora-rtc-sdk-ng": "^4.12.1",
        "ant-design-vue": "^2.2.8",
        "axios": "^0.21.1",
        "mitt": "^3.0.0",
        "query-string": "^7.0.1",
        "reconnecting-websocket": "^4.4.0",
        "vconsole": "^3.8.1",
@@ -4900,6 +4901,11 @@
      "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.5.tgz",
      "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=",
      "license": "MIT"
    },
    "node_modules/mitt": {
      "version": "3.0.0",
      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.0.tgz",
      "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ=="
    },
    "node_modules/mixin-deep": {
      "version": "1.3.2",
@@ -11340,6 +11346,11 @@
      "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.5.tgz",
      "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI="
    },
    "mitt": {
      "version": "3.0.0",
      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.0.tgz",
      "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ=="
    },
    "mixin-deep": {
      "version": "1.3.2",
      "resolved": "https://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.2.tgz",
package.json
@@ -15,6 +15,7 @@
    "agora-rtc-sdk-ng": "^4.12.1",
    "ant-design-vue": "^2.2.8",
    "axios": "^0.21.1",
    "mitt": "^3.0.0",
    "query-string": "^7.0.1",
    "reconnecting-websocket": "^4.4.0",
    "vconsole": "^3.8.1",
@@ -90,9 +91,11 @@
        "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/tag/style/css",
        "ant-design-vue/es/tooltip/style/css",
        "ant-design-vue/es/tree/style/css",
        "axios",
        "mitt",
        "moment",
        "reconnecting-websocket",
        "vconsole",
src/api/device-cmd/index.ts
New file
@@ -0,0 +1,20 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { DeviceCmd } from '/@/types/device-cmd'
const CMD_API_PREFIX = '/control/api/v1'
export interface SendCmdParams {
  dock_sn: string, // 机场cn
  device_cmd: DeviceCmd // 指令
}
/**
 * 发送机场控制指令
 * @param params
 * @returns
 */
// /control/api/v1/devices/{dock_sn}/jobs/{service_identifier}
export async function postSendCmd (params: SendCmdParams): Promise<IWorkspaceResponse<{}>> {
  const resp = await request.post(`${CMD_API_PREFIX}/devices/${params.dock_sn}/jobs/${params.device_cmd}`)
  return resp.data
}
src/api/device-log/index.ts
New file
@@ -0,0 +1,172 @@
import request, { IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
import { DeviceValue, DOMAIN } from '/@/types/device'
import { DeviceLogUploadStatusEnum } from '/@/types/device-log'
import { ELocalStorageKey } from '/@/types'
import { CURRENT_CONFIG } from '/@/api/http/config'
const MNG_API_PREFIX = '/manage/api/v1'
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
export interface GetDeviceUploadLogListParams {
  device_sn: string,
  page: number,
  page_size: number,
  begin_time?: number, // 开始时间
  end_time?: number, // 结束时间
  status?: DeviceLogUploadStatusEnum, // 日志上传状态
  logs_information?: string // 搜索内容
}
export interface BriefDeviceInfo {
  sn: string,
  device_model: DeviceValue,
  device_callsign: string
}
export interface DeviceLogProgressInfo{
  device_sn: string,
  device_model_domain: DOMAIN,
  progress: number, // 进度
  result: number, // 上传结果
  upload_rate: number, // 上传速率
  status: DeviceLogUploadStatusEnum // 上传状态
}
export interface DeviceLogItem {
  boot_index: number, // 日志id
  start_time: number, // 日志开始时间
  end_time: number, // 日志结束时间
  size: number // 日志大小
}
export interface DeviceLogFileInfo {
  device_sn: string,
  module: DOMAIN,
  result: number,
  object_key: string,
  file_id: string,
  list: DeviceLogItem[]
}
export interface DeviceLogFileListInfo {
  files: DeviceLogFileInfo[]
}
export interface GetDeviceUploadLogListRsp {
  logs_id: string, // 记录id
  happen_time: string, // 发生时间
  user_name: string, // 用户
  logs_information: string, // 异常描述
  create_time: string, // 上传时间
  status:DeviceLogUploadStatusEnum, // 日志上传状态
  device_topo:{ // 设备topo
    hosts: BriefDeviceInfo[],
    parents: BriefDeviceInfo[]
  },
  logs_progress: DeviceLogProgressInfo[], // 日志上传进度
  device_logs: DeviceLogFileListInfo // 设备日志
}
/**
 * 获取设备上传日志列表信息
 * @param params
 * @returns
 */
export async function getDeviceUploadLogList (params: GetDeviceUploadLogListParams): Promise<IListWorkspaceResponse<GetDeviceUploadLogListRsp>> {
  const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${params.device_sn}/logs-uploaded`, {
    params: params
  })
  return resp.data
}
export interface GetDeviceLogListParams{
  device_sn: string,
  domain: DOMAIN[]
}
/**
 * 获取设备日志列表信息
 * @param params
 * @returns
 */
export async function getDeviceLogList (params: GetDeviceLogListParams): Promise<IWorkspaceResponse<DeviceLogFileListInfo>> {
  const domain = params.domain ? params.domain : []
  const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${params.device_sn}/logs`, {
    params: {
      domain_list: domain.join(',')
    }
  })
  return resp.data
}
export interface UploadDeviceLogBody {
  device_sn: string
  happen_time: string // 发生时间
  logs_information: string // 异常描述
  files:{
    list: DeviceLogItem[],
    device_sn: string,
    module: DOMAIN
  }[]
}
/**
 * 上传设备日志
 * @param body
 * @returns
 */
export async function postDeviceUpgrade (body: UploadDeviceLogBody): Promise<IWorkspaceResponse<{}>> {
  const resp = await request.post(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs`, body)
  return resp.data
}
export type DeviceLogUploadAction = 'cancel'
export interface CancelDeviceLogUploadBody {
  device_sn: string
  status: DeviceLogUploadAction
  module_list: DOMAIN[]
}
// 取消上传
export async function cancelDeviceLogUpload (body: CancelDeviceLogUploadBody): Promise<IWorkspaceResponse<{}>> {
  const url = `${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs`
  const result = await request.delete(url, {
    data: body
  })
  return result.data
}
export interface DeleteDeviceLogUploadBody {
  device_sn: string
  logs_id: string
}
// 取消上传
export async function deleteDeviceLogUpload (body: DeleteDeviceLogUploadBody): Promise<IWorkspaceResponse<{}>> {
  const url = `${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs/${body.logs_id}`
  const result = await request.delete(url, {
    data: body
  })
  return result.data
}
export interface GetUploadDeviceLogUrlParams{
  logs_id: string,
  file_id: string,
}
// export interface GetUploadDeviceLogRsp{
//   url: string
// }
/**
 * 获取设备上传日志url
 * @param params
 * @returns
 */
export async function getUploadDeviceLogUrl (params: GetUploadDeviceLogUrlParams): Promise<IWorkspaceResponse<string>> {
  const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/logs/${params.logs_id}/url/${params.file_id}`)
  return resp.data
}
src/api/device-upgrade/index.ts
New file
@@ -0,0 +1,47 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { DeviceFirmwareTypeEnum } from '/@/types/device'
const MNG_API_PREFIX = '/manage/api/v1'
export interface GetDeviceUpgradeInfoParams {
  device_name: string
}
export interface GetDeviceUpgradeInfoRsp {
  device_name: string
  product_version: string
  release_note: string
  released_time: string
}
/**
 * 获取设备升级信息
 * @param params
 * @returns
 */
export async function getDeviceUpgradeInfo (params: GetDeviceUpgradeInfoParams): Promise<IWorkspaceResponse<GetDeviceUpgradeInfoRsp[]>> {
  const resp = await request.get(`${MNG_API_PREFIX}/workspaces/firmware-release-notes/latest`, {
    params: params
  })
  return resp.data
}
export interface UpgradeDeviceInfo {
  device_name: string,
  sn: string,
  product_version: string,
  firmware_upgrade_type: DeviceFirmwareTypeEnum // 1-普通升级,2-一致性升级
}
export type DeviceUpgradeBody = UpgradeDeviceInfo[]
/**
 * 设备升级
 * @param workspace_id
 * @param body
 * @returns
 */
export async function postDeviceUpgrade (workspace_id: string, body: DeviceUpgradeBody): Promise<IWorkspaceResponse<{}>> {
  const resp = await request.post(`${MNG_API_PREFIX}/devices/${workspace_id}/devices/ota`, body)
  return resp.data
}
src/api/http/type.ts
@@ -19,11 +19,11 @@
}
// Workspace
export interface IWorkspaceResponse<T> {
[x: string]: number;
 code: number;
 data: T;
 message: string;
}
export type IStatus = 'WAITING' | 'DOING' | 'SUCCESS' | 'FAILED';
export interface CommonListResponse<T> extends IResult {
src/api/manage.ts
@@ -1,4 +1,6 @@
import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
import { Device } from '/@/types/device'
const HTTP_PREFIX = '/manage/api/v1'
// login
@@ -116,7 +118,14 @@
  return result.data
}
export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: string): Promise<IWorkspaceResponse<any>> {
/**
 * 获取绑定设备信息
 * @param workspace_id
 * @param body
 * @param domain
 * @returns
 */
export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: string): Promise<IListWorkspaceResponse<Device>> {
  const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}&domain=${domain}`
  const result = await request.get(url)
  return result.data
@@ -141,11 +150,11 @@
}
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}`
  let url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms?page=${pagination.page}&page_size=${pagination.page_size}` +
    `&level=${body.level ?? ''}&begin_time=${body.begin_time ?? ''}&end_time=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}`
  body.sns.forEach((sn: string) => {
    if (sn !== '') {
      url = url.concat(`&deviceSn=${sn}`)
      url = url.concat(`&device_sn=${sn}`)
    }
  })
  const result = await request.get(url)
src/api/media.ts
@@ -1,3 +1,4 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/media/api/v1'
@@ -8,11 +9,19 @@
  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`
export const downloadMediaFile = async function (workspaceId: string, fileId: string): Promise<any> {
  const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fileId}/url`
  const result = await request.get(url, { responseType: 'blob' })
  if (result.data.code) {
  if (result.data.type === 'application/json') {
    const reader = new FileReader()
    reader.onload = function (e) {
      let text = reader.result as string
      const result = JSON.parse(text)
      message.error(result.message)
    }
    reader.readAsText(result.data, 'utf-8')
    return
  } else {
    return result.data
  }
  return result
}
src/api/wayline.ts
@@ -1,3 +1,4 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1'
@@ -20,10 +21,18 @@
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) {
  if (result.data.type === 'application/json') {
    const reader = new FileReader()
    reader.onload = function (e) {
      let text = reader.result as string
      const result = JSON.parse(text)
      message.error(result.message)
    }
    reader.readAsText(result.data, 'utf-8')
    return
  } else {
    return result.data
  }
  return result
}
// Delete Wayline File
src/api/websocket.ts
File was deleted
src/components/GMap.vue
@@ -137,21 +137,23 @@
        </div>
      </div>
    </div>
    <div v-if="osdVisible.visible && osdVisible.is_dock" class="osd-panel fz12" style="height: 280px;">
    <!-- 机场OSD -->
    <div v-if="osdVisible.visible && osdVisible.is_dock" class="osd-panel fz12">
      <div class="fz16 pl5 pr5 flex-align-center flex-row flex-justify-between" style="border-bottom: 1px solid #515151; height: 10%;">
        <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;">
        <!-- 机场 -->
      <div class ="flex-display" style="border-bottom: 1px solid #515151;">
        <div class="flex-column flex-align-stretch flex-justify-center" style="width: 60px; background: #2d2d2d;">
          <a-tooltip :title="osdVisible.model">
            <div class="flex-column flex-align-center flex-justify-center" style="width: 90%;">
            <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">
        <div class="osd flex-1" style="flex: 1">
            <a-row>
              <a-col span="16" :style="deviceInfo.dock.mode_code === EDockModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'">
                {{ EDockModeCode[deviceInfo.dock.mode_code] }}</a-col>
@@ -260,10 +262,20 @@
                </a-tooltip>
              </a-col>
            </a-row>
            <a-row class="p5">
              <a-col span="24">
                <a-button type="primary" :disabled="controlPanelVisible" size="small" @click="dockDebugOnOff(osdVisible.gateway_sn, true)">
                  远程调试
                </a-button>
              </a-col>
            </a-row>
            <DockControlPanel v-if="controlPanelVisible" :sn="osdVisible.gateway_sn"  :deviceInfo="deviceInfo" @close-control-panel="dockDebugOnOff">
            </DockControlPanel>
        </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;">
      <!--  飞机-->
      <div class ="flex-display">
        <div class="flex-column flex-align-stretch flex-justify-center" style="width: 60px;  background: #2d2d2d;">
          <a-tooltip :title="osdVisible.model">
            <div style="width: 90%;" class="flex-column flex-align-center flex-justify-center">
              <span><a-image :src="M30" :preview="false"/></span>
@@ -271,7 +283,7 @@
            </div>
          </a-tooltip>
        </div>
        <div class="osd">
        <div class="osd flex-1">
            <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>
@@ -409,6 +421,8 @@
  FieldTimeOutlined, CloudOutlined, CloudFilled, FolderOpenOutlined, RobotFilled, ArrowUpOutlined
} from '@ant-design/icons-vue'
import { EDeviceTypeName } from '../types'
import DockControlPanel from './g-map/DockControlPanel.vue'
import { useDockControl } from './g-map/useDockControl'
export default defineComponent({
  components: {
@@ -428,7 +442,8 @@
    FolderOpenOutlined,
    RobotFilled,
    ArrowUpOutlined,
    ArrowDownOutlined
    ArrowDownOutlined,
    DockControlPanel
  },
  name: 'GMap',
  props: {},
@@ -642,6 +657,15 @@
      useMouseToolHook.mouseTool(type, getDrawCallback)
      mouseMode.value = bool
    }
    // dock 控制指令
    const {
      controlPanelVisible,
      setControlPanelVisible,
      sendDockControlCmd,
      dockDebugOnOff,
    } = useDockControl()
    onMounted(() => {
      const app = getApp()
      useGMapManageHook.globalPropertiesConfig(app)
@@ -813,15 +837,19 @@
      EModeCode,
      str,
      EDockModeCode,
      controlPanelVisible,
      dockDebugOnOff,
    }
  }
})
</script>
<style lang="scss" scoped>
.g-map-wrapper {
  height: 100%;
  width: 100%;
  .g-action-panle {
    position: absolute;
    top: 16px;
@@ -845,13 +873,19 @@
    border: 1px solid $primary;
    border-radius: 2px;
  }
  // antd button 光晕
  &:deep(.ant-btn){
    &::after {
      display: none;
    }
  }
}
.osd-panel {
  position: absolute;
  left: 350px;
  top: 10px;
  width: 480px;
  height: 160px;
  background: black;
  color: white;
  border-radius: 2px;
src/components/MediaPanel.vue
@@ -32,7 +32,7 @@
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 { message, Pagination } from 'ant-design-vue'
import { load } from '@amap/amap-jsapi-loader'
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
@@ -101,6 +101,7 @@
  file_name: string,
  file_path: string,
  create_time: string,
  file_id: string,
}
const mediaData = reactive({
@@ -128,12 +129,11 @@
function downloadMedia (media: MediaFile) {
  loading.value = true
  downloadMediaFile(workspaceId, media.fingerprint).then(res => {
    if (res.code && res.code !== 0) {
  downloadMediaFile(workspaceId, media.file_id).then(res => {
    if (!res) {
      return
    }
    const suffix = media.file_name.substring(media.file_name.lastIndexOf('.'))
    const data = new Blob([res.data], { type: suffix === '.mp4' ? 'video/mp4' : 'image/jpeg' })
    const data = new Blob([res])
    downloadFile(data, media.file_name)
  }).finally(() => {
    loading.value = false
src/components/TaskPanel.vue
@@ -9,7 +9,7 @@
      <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'">
            :status="taskProgressMap[record.bid]?.status.indexOf(ETaskStatus.FAILED) != -1 ? 'exception' : taskProgressMap[record.bid]?.status.indexOf(ETaskStatus.OK) != -1 ? 'success' : 'normal'">
            <template #format="percent">
              <a-tooltip :title="taskProgressMap[record.bid]?.status">
                <div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;">
src/components/common/sidebar.vue
File was renamed from src/pages/project-app/sidebar.vue
@@ -28,7 +28,6 @@
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
@@ -58,7 +57,7 @@
      { 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: 4, label: 'Flight Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
      { key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' }
    ]
@@ -66,10 +65,11 @@
      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,
src/components/common/topbar.vue
File was renamed from src/pages/project-app/topbar.vue
@@ -44,8 +44,6 @@
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()
src/components/devices/device-hms/DeviceHmsDrawer.vue
New file
@@ -0,0 +1,268 @@
<template>
  <a-drawer
    title="Hms Info"
    placement="right"
    v-model:visible="sVisible"
    @update:visible="onVisibleChange"
    :destroyOnClose="true"
    :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>
</template>
<!-- 暂时只抽取该组件 -->
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits } from 'vue'
import { getDeviceHms, HmsQueryBody } from '/@/api/manage'
import moment, { Moment } from 'moment'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { Device, DeviceHms } from '/@/types/device'
import { IPage } from '/@/api/http/type'
import { EDeviceTypeName, EHmsLevel, ELocalStorageKey } from '/@/types'
const props = defineProps<{
  visible: boolean,
  device: null | Device,
}>()
const emit = defineEmits(['update:visible', 'ok', 'cancel'])
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
// 健康状态
const sVisible = ref(false)
watchEffect(() => {
  sVisible.value = props.visible
  // 显示弹框时,获取设备hms信息
  if (props.visible) {
    showHms()
  }
})
function onVisibleChange (sVisible: boolean) {
  setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
  sVisible.value = v
  emit('update:visible', v, e)
}
const loading = ref(false)
const hmsColumns: ColumnProps[] = [
  { title: 'Alarm Begin | End Time', dataIndex: 'create_time', width: '25%', className: 'titleStyle', slots: { customRender: 'time' } },
  { title: 'Level', dataIndex: 'level', width: '120px', className: 'titleStyle', slots: { customRender: 'level' } },
  { title: 'Device', dataIndex: 'domain', width: '12%', className: 'titleStyle' },
  { title: 'Error Code', dataIndex: 'key', width: '20%', className: 'titleStyle', slots: { customRender: 'code' } },
  { title: 'Hms Message', dataIndex: 'message_en', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } },
]
interface DeviceHmsData {
  data: DeviceHms[]
}
const hmsData = reactive<DeviceHmsData>({
  data: []
})
type Pagination = TableState['pagination']
const hmsPaginationProp = reactive({
  pageSizeOptions: ['20', '50', '100'],
  showQuickJumper: true,
  showSizeChanger: true,
  pageSize: 50,
  current: 1,
  total: 0
})
// 获取分页信息
function getPaginationBody () {
  return {
    page: hmsPaginationProp.current,
    page_size: hmsPaginationProp.pageSize
  } as IPage
}
function showHms () {
  const dock = props.device
  if (!dock) return
  if (dock.domain === EDeviceTypeName.Dock) {
    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) {
  hmsPaginationProp.current = page?.current!
  hmsPaginationProp.pageSize = 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 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 time = ref([moment(param.begin_time), moment(param.end_time)])
function getHms () {
  loading.value = true
  getDeviceHms(param, workspaceId, getPaginationBody())
    .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
      })
      loading.value = false
    }).catch(_err => {
      loading.value = false
    })
}
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>
src/components/devices/device-log/DeviceLogDetailModal.vue
New file
@@ -0,0 +1,150 @@
<template>
  <a-modal
    title="日志上传详情"
    v-model:visible="sVisible"
    width="900px"
    :footer="null"
    @update:visible="onVisibleChange">
    <div class="device-log-detail-wrap">
      <div class="device-log-list">
        <div class="log-list-item">
          <a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id"  size="small" @click="onDownloadLog(airportTableLogState.logList.file_id)">
             下载机场日志
          </a-button>
          <a-table  :columns="airportLogColumns"
                    :scroll="{ x: '100%', y: 600 }"
                    :data-source="airportTableLogState.logList?.list"
                    rowKey="boot_index"
                    :pagination = "false"
                    >
            <template #log_time="{record}">
              <div>{{getLogTime(record)}}</div>
            </template>
            <template #size="{record}">
              <div>{{getLogSize(record.size)}}</div>
            </template>
          </a-table>
        </div>
        <div class="log-list-item">
          <a-button type="primary"  class="download-btn" :disabled="!droneTableLogState.logList?.file_id" size="small" @click="onDownloadLog(droneTableLogState.logList.file_id)">
             下载飞行器日志
          </a-button>
          <a-table  :columns="droneLogColumns"
                    :scroll="{ x: '100%', y: 600 }"
                    :data-source="droneTableLogState.logList?.list"
                    rowKey="boot_index"
                    :pagination = "false"
          >
            <template #log_time="{record}">
              <div>{{getLogTime(record)}}</div>
            </template>
            <template #size="{record}">
              <div>{{getLogSize(record.size)}}</div>
            </template>
          </a-table>
        </div>
      </div>
    </div>
  </a-modal>
</template>
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits } from 'vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { IPage } from '/@/api/http/type'
import { DOMAIN } from '/@/types/device'
import { DeviceLogFileInfo, GetDeviceUploadLogListRsp, getUploadDeviceLogUrl } from '/@/api/device-log'
import { useDeviceLogUploadDetail } from './use-device-log-upload-detail'
import { download } from '/@/utils/download'
const props = defineProps<{
  visible: boolean,
  deviceLog: null | GetDeviceUploadLogListRsp,
}>()
const emit = defineEmits(['update:visible'])
const sVisible = ref(false)
watchEffect(() => {
  sVisible.value = props.visible
  if (props.visible) {
    classifyDeviceLog()
  }
})
function onVisibleChange (sVisible: boolean) {
  setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
  sVisible.value = v
  emit('update:visible', v, e)
}
// 表格
const airportLogColumns: ColumnProps[] = [
  { title: '机场日志', dataIndex: 'time', width: '70%', slots: { customRender: 'log_time' } },
  { title: '文件大小', dataIndex: 'size', width: '30%', slots: { customRender: 'size' } },
]
const droneLogColumns: ColumnProps[] = [
  { title: '飞行器日志', dataIndex: 'time', width: '70%', slots: { customRender: 'log_time' } },
  { title: '文件大小', dataIndex: 'size', width: '30%', slots: { customRender: 'size' } },
]
const airportTableLogState = reactive({
  logList: {} as DeviceLogFileInfo,
})
const droneTableLogState = reactive({
  logList: {} as DeviceLogFileInfo,
})
function classifyDeviceLog () {
  if (!props.deviceLog) return
  const { device_logs } = props.deviceLog
  const { files } = device_logs || {}
  if (files && files.length > 0) {
    files.forEach(file => {
      if (file.module === DOMAIN.DOCK) {
        airportTableLogState.logList = file
      } else if (file.module === DOMAIN.DRONE) {
        droneTableLogState.logList = file
      }
    })
  }
}
const { getLogTime, getLogSize } = useDeviceLogUploadDetail()
async function onDownloadLog (fileId: string) {
  const { data } = await getUploadDeviceLogUrl({
    file_id: fileId,
    logs_id: props.deviceLog?.logs_id || ''
  })
  if (data) {
    download(data)
  // download('https:/github.com/dji-sdk/Mobile-SDK-Android-V5/archive/refs/heads/dev-sdk-main.zip')
  }
}
</script>
<style lang="scss" scoped>
.device-log-detail-wrap{
  .device-log-list{
    display: flex;
    justify-content: space-between;
    padding: 8px 0;
    .log-list-item{
      width: 420px;
      .download-btn{
        margin-bottom: 10px;
      }
    }
  }
}
</style>
>
src/components/devices/device-log/DeviceLogUploadModal.vue
New file
@@ -0,0 +1,210 @@
<template>
  <a-modal
    title="设备日志上传"
    v-model:visible="sVisible"
    width="900px"
    :footer="null"
    @update:visible="onVisibleChange">
    <div class="device-log-upload-wrap">
      <div class="page-action-row">
        <a-button type="primary" :disabled="deviceLogUploadBtnDisabled" @click="uploadDeviceLog">上传日志</a-button>
      </div>
      <div class="device-log-list">
        <div class="log-list-item">
          <a-table  :columns="airportLogColumns"
                    :scroll="{ x: '100%', y: 600 }"
                    :data-source="airportTableLogState.logList?.list"
                    :loading="airportTableLogState.tableLoading"
                    :row-selection="airportTableLogState.rowSelection"
                    rowKey="boot_index"
                    :pagination = "false">
            <template #log_time="{record}">
              <div>{{getLogTime(record)}}</div>
            </template>
            <template #size="{record}">
              <div>{{getLogSize(record.size)}}</div>
            </template>
          </a-table>
        </div>
        <div class="log-list-item">
          <a-table  :columns="droneLogColumns"
                    :scroll="{ x: '100%', y: 600 }"
                    :data-source="droneTableLogState.logList?.list"
                    :loading="droneTableLogState.tableLoading"
                    :row-selection="droneTableLogState.rowSelection"
                    rowKey="boot_index"
                    :pagination = "false">
            <template #log_time="{record}">
              <div>{{getLogTime(record)}}</div>
            </template>
            <template #size="{record}">
              <div>{{getLogSize(record.size)}}</div>
            </template>
          </a-table>
        </div>
      </div>
    </div>
  </a-modal>
</template>
<script lang="ts" setup>
import { watchEffect, reactive, ref, computed, defineProps, defineEmits } from 'vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { IPage } from '/@/api/http/type'
import { Device, DOMAIN } from '/@/types/device'
import { getDeviceLogList, postDeviceUpgrade, DeviceLogFileInfo, UploadDeviceLogBody, DeviceLogItem } from '/@/api/device-log'
import { message } from 'ant-design-vue'
import { useDeviceLogUploadDetail } from './use-device-log-upload-detail'
const props = defineProps<{
  visible: boolean,
  device: null | Device,
}>()
const emit = defineEmits(['update:visible', 'upload-log-ok'])
const sVisible = ref(false)
watchEffect(() => {
  sVisible.value = props.visible
  // 显示弹框时,获取设备日志信息
  if (props.visible) {
    getDeviceLogInfo()
  }
})
function onVisibleChange (sVisible: boolean) {
  setVisible(sVisible)
  if (!sVisible) {
    resetTableLogState()
  }
}
function setVisible (v: boolean, e?: Event) {
  sVisible.value = v
  emit('update:visible', v, e)
}
// 表格
const airportLogColumns: ColumnProps[] = [
  { title: '机场日志', dataIndex: 'time', width: 100, slots: { customRender: 'log_time' } },
  { title: '文件大小', dataIndex: 'size', width: 25, slots: { customRender: 'size' } },
]
const droneLogColumns: ColumnProps[] = [
  { title: '飞行器日志', dataIndex: 'time', width: 100, slots: { customRender: 'log_time' } },
  { title: '文件大小', dataIndex: 'size', width: 25, slots: { customRender: 'size' } },
]
const airportTableLogState = reactive({
  logList: {} as DeviceLogFileInfo,
  tableLoading: false,
  selectRow: [],
  rowSelection: {
    columnWidth: 15,
    selectedRowKeys: [] as number[],
    onChange: (selectedRowKeys:number[], selectedRows: []) => {
      airportTableLogState.rowSelection.selectedRowKeys = selectedRowKeys
      airportTableLogState.selectRow = selectedRows
      console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
    },
  }
})
function resetTableLogState () {
  airportTableLogState.logList = {} as DeviceLogFileInfo
  airportTableLogState.selectRow = []
  airportTableLogState.tableLoading = false
}
const droneTableLogState = reactive({
  logList: {} as DeviceLogFileInfo,
  tableLoading: false,
  selectRow: [],
  rowSelection: {
    columnWidth: 15,
    selectedRowKeys: [] as number[],
    onChange: (selectedRowKeys: number[], selectedRows: []) => {
      droneTableLogState.rowSelection.selectedRowKeys = selectedRowKeys
      droneTableLogState.selectRow = selectedRows
      console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
    },
  }
})
const deviceLogUploadBtnDisabled = computed(() => {
  return (airportTableLogState.rowSelection.selectedRowKeys && airportTableLogState.rowSelection.selectedRowKeys.length <= 0) &&
  (droneTableLogState.rowSelection.selectedRowKeys && droneTableLogState.rowSelection.selectedRowKeys.length <= 0)
})
// 获取设备内日志
async function getDeviceLogInfo () {
  airportTableLogState.tableLoading = true
  droneTableLogState.tableLoading = true
  try {
    const { code, data } = await getDeviceLogList({
      device_sn: props.device?.device_sn || '',
      domain: [DOMAIN.DOCK, DOMAIN.DRONE]
    })
    if (code === 0) {
      const { files } = data
      if (files && files.length > 0) {
        files.forEach(file => {
          if (file.module === DOMAIN.DOCK) {
            airportTableLogState.logList = file
          } else if (file.module === DOMAIN.DRONE) {
            droneTableLogState.logList = file
          }
        })
      }
    }
  } catch (err) {
  }
  airportTableLogState.tableLoading = false
  droneTableLogState.tableLoading = false
}
// 日志上传
async function uploadDeviceLog () {
  const body = {
    device_sn: props.device?.device_sn || '',
    files: [] as any
  } as UploadDeviceLogBody
  if (airportTableLogState.selectRow && airportTableLogState.selectRow.length > 0) {
    body.files.push({
      list: airportTableLogState.selectRow,
      device_sn: airportTableLogState.logList.device_sn,
      module: airportTableLogState.logList.module
    })
  }
  if (droneTableLogState.selectRow && droneTableLogState.selectRow.length > 0) {
    body.files.push({
      list: droneTableLogState.selectRow,
      device_sn: droneTableLogState.logList.device_sn,
      module: droneTableLogState.logList.module
    })
  }
  const { code } = await postDeviceUpgrade(body)
  if (code === 0) {
    message.success('日志上传任务执行成功')
    emit('upload-log-ok')
    setVisible(false)
  }
}
const { getLogTime, getLogSize } = useDeviceLogUploadDetail()
</script>
<style lang="scss" scoped>
.device-log-upload-wrap{
  .device-log-list{
    display: flex;
    justify-content: space-between;
    padding: 8px 0;
    .log-list-item{
      width: 420px;
    }
  }
}
</style>
src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue
New file
@@ -0,0 +1,326 @@
<template>
  <a-drawer
    title="设备日志上传记录"
    placement="right"
    v-model:visible="sVisible"
    @update:visible="onVisibleChange"
    :width="800">
    <!-- 设备日志上传记录 -->
    <div class="device-log-upload-record-wrap">
      <div class="page-action-row">
        <a-button type="primary" @click="onUploadDeviceLog">上传日志</a-button>
      </div>
      <div class="device-log-upload-list">
        <a-table :columns="deviceLogUploadListColumns"
                  :scroll="{ x: '100%', y: 600 }"
                  :data-source="deviceUploadLogState.uploadLogList"
                  :loading="deviceUploadLogState.loading"
                  :pagination="deviceUploadLogState.paginationProp"
                  @change="onDeviceUploadLogTableChange"
                  rowKey="logs_id">
         <!-- 设备类型 -->
          <template #device_type="{ record }">
            <div>
              <div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).parents[0].device_model.key]}}</div>
              <div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).hosts[0].device_model.key]}}</div>
            </div>
          </template>
          <!-- 设备sn -->
          <template #device_sn="{ record }">
            <div>
              <div v-if="getDeviceInfo(record).parents && getDeviceInfo(record).parents.length > 0">{{ getDeviceInfo(record).parents[0].sn }}</div>
              <div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ getDeviceInfo(record).hosts[0].sn }}</div>
            </div>
          </template>
          <!-- 上传状态 -->
          <template #status="{ record }">
            <div>
              <div>
                <span class="circle-icon" :style="{backgroundColor: getDeviceLogUploadStatus(record).color}"></span>
                {{ getDeviceLogUploadStatus(record).text }}
              </div>
              <div v-if="record.status === DeviceLogUploadStatusEnum.Uploading">
                <a-progress :percent="getLogProgress(record)" />
              </div>
            </div>
          </template>
          <!-- 操作 -->
          <template #action="{ record }">
            <div class="row-action">
              <a-tooltip title="查看详情">
                  <FileTextOutlined  @click="showDeviceLogDetail(record)"/>
              </a-tooltip>
              <span v-if="record.status === DeviceLogUploadStatusEnum.Uploading">
                <a-tooltip title="取消">
                  <StopOutlined @click="onCancelUploadDeviceLog(record)"/>
                </a-tooltip>
              </span>
              <span v-else>
                <a-tooltip title="删除">
                  <DeleteOutlined @click="onDeleteUploadDeviceLog(record)"/>
                </a-tooltip>
              </span>
            </div>
          </template>
        </a-table>
      </div>
    </div>
  </a-drawer>
  <!-- 设备日志上传弹框 -->
  <DeviceLogUploadModal
     v-model:visible="deviceLogUploadModalVisible"
     :device="props.device"
     @upload-log-ok="onUploadLogOk"
  ></DeviceLogUploadModal>
  <!-- 设备日志上传详情弹框 -->
  <DeviceLogDetailModal
     v-model:visible="deviceLogDetailModalVisible"
     :deviceLog="currentDeviceLog"
  ></DeviceLogDetailModal>
</template>
<script lang="ts" setup>
import { watchEffect, reactive, ref, defineProps, defineEmits } from 'vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { IPage } from '/@/api/http/type'
import { Device, DOMAIN, DEVICE_NAME } from '/@/types/device'
import DeviceLogUploadModal from './DeviceLogUploadModal.vue'
import DeviceLogDetailModal from './DeviceLogDetailModal.vue'
import { getDeviceUploadLogList, GetDeviceUploadLogListRsp, cancelDeviceLogUpload, deleteDeviceLogUpload } from '/@/api/device-log'
import { StopOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons-vue'
import { DeviceLogUploadStatusEnum, DeviceLogUploadStatusMap, DeviceLogUploadStatusColor, DeviceLogUploadInfo, DeviceLogUploadWsStatusMap, DeviceLogProgressInfo } from '/@/types/device-log'
import { useDeviceLogUploadProgressEvent } from './use-device-log-upload-progress-event'
import { Modal } from 'ant-design-vue'
const props = defineProps<{
  visible: boolean,
  device: null | Device,
}>()
const emit = defineEmits(['update:visible'])
const sVisible = ref(false)
watchEffect(() => {
  sVisible.value = props.visible
  // 显示弹框时,获取设备日志上传记录信息
  if (props.visible) {
    getDeviceUploadLogInfo()
  }
})
function onVisibleChange (sVisible: boolean) {
  setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
  sVisible.value = v
  emit('update:visible', v, e)
}
// 日志列表
const deviceLogUploadListColumns: ColumnProps[] = [
  { title: '上传时间', dataIndex: 'create_time', width: 100 },
  { title: '设备型号', dataIndex: 'device_type', width: 80, slots: { customRender: 'device_type' } },
  { title: '设备SN', dataIndex: 'device_sn', width: 120, slots: { customRender: 'device_sn' } },
  { title: '上传状态', dataIndex: 'status', width: 120, slots: { customRender: 'status' } },
  { title: '操作', dataIndex: 'actions', width: 80, slots: { customRender: 'action' } },
]
const deviceUploadLogState = reactive({
  uploadLogList: [] as GetDeviceUploadLogListRsp[],
  loading: false,
  paginationProp: {
    pageSizeOptions: ['20', '50', '100'],
    showQuickJumper: true,
    showSizeChanger: true,
    pageSize: 50,
    current: 1,
    total: 0
  }
})
// 获取上传的设备日志
async function getDeviceUploadLogInfo () {
  deviceUploadLogState.loading = true
  try {
    const { code, data } = await getDeviceUploadLogList({
      device_sn: props.device?.device_sn || '',
      page: deviceUploadLogState.paginationProp.current,
      page_size: deviceUploadLogState.paginationProp.pageSize
    })
    if (code === 0) {
      deviceUploadLogState.uploadLogList = data.list
      deviceUploadLogState.paginationProp.total = data.pagination.total
      deviceUploadLogState.paginationProp.current = data.pagination.page
      deviceUploadLogState.paginationProp.pageSize = data.pagination.page_size
    }
    deviceUploadLogState.loading = false
  } catch (error) {
    deviceUploadLogState.loading = false
  }
}
type Pagination = TableState['pagination']
// 获取设备信息
function getDeviceInfo (deviceLogItem: GetDeviceUploadLogListRsp) {
  const { device_topo: deviceTopo } = deviceLogItem
  return deviceTopo
}
// 获取上传状态
function getDeviceLogUploadStatus (deviceLogItem: GetDeviceUploadLogListRsp) {
  const statusObj = {
    color: '',
    text: ''
  }
  const { status } = deviceLogItem
  statusObj.color = DeviceLogUploadStatusColor[status]
  statusObj.text = DeviceLogUploadStatusMap[status]
  return statusObj
}
// 获取上传进度
function getLogProgress (deviceLogItem: GetDeviceUploadLogListRsp) {
  let percent = 0
  const { logs_progress } = deviceLogItem
  if (logs_progress && logs_progress.length > 0) {
    logs_progress.forEach(log => {
      percent += (log.progress || 0)
    })
    percent = percent / logs_progress.length
  }
  return Math.floor(percent)
}
// 设备日志上传进度更新
function onDeviceLogUploadWs (data: DeviceLogUploadInfo) {
  const { sn, output } = data
  if (output) {
    const { files, status, logs_id: logId } = output || {}
    const deviceLogItem = deviceUploadLogState.uploadLogList.find(log => log.logs_id === logId)
    if (!deviceLogItem) return
    if (status) {
      deviceLogItem.status = DeviceLogUploadWsStatusMap[status]
    }
    if (files && files.length > 0) {
      const logsProgress = [] as DeviceLogProgressInfo[]
      files.forEach(file => {
        logsProgress.push({
          ...file,
          status: DeviceLogUploadWsStatusMap[file.status]
        })
      })
      deviceLogItem.logs_progress = logsProgress
    }
  }
}
useDeviceLogUploadProgressEvent(onDeviceLogUploadWs)
// 搜索
async function onDeviceUploadLogTableChange (page: Pagination) {
  deviceUploadLogState.paginationProp.current = page?.current || 1
  deviceUploadLogState.paginationProp.pageSize = page?.pageSize || 20
  await getDeviceUploadLogInfo()
}
// 查看上传设备日志详情
const deviceLogDetailModalVisible = ref(false)
const currentDeviceLog = ref({} as GetDeviceUploadLogListRsp)
function showDeviceLogDetail (deviceLogItem: GetDeviceUploadLogListRsp) {
  if (!deviceLogItem) return
  currentDeviceLog.value = deviceLogItem
  deviceLogDetailModalVisible.value = true
}
// 取消上传设备日志
async function onCancelUploadDeviceLog (deviceLogItem: GetDeviceUploadLogListRsp) {
  Modal.confirm({
    title: '取消日志上传',
    content: '您确认取消设备日志上传吗?',
    okType: 'danger',
    onOk () {
      cancelDeviceLogUploadOk()
    },
  })
}
async function cancelDeviceLogUploadOk () {
  const { code } = await cancelDeviceLogUpload({
    device_sn: props.device?.device_sn || '',
    module_list: [DOMAIN.DOCK, DOMAIN.DRONE],
    status: 'cancel'
  })
  if (code === 0) {
    await getDeviceUploadLogInfo()
  }
}
// 删除上传的设备日志
function onDeleteUploadDeviceLog (deviceLogItem: GetDeviceUploadLogListRsp) {
  Modal.confirm({
    title: '删除上传日志',
    content: '您确认删除该条已上传设备日志吗?',
    okType: 'danger',
    onOk () {
      deleteUploadDeviceLogOk(deviceLogItem)
    },
  })
}
async function deleteUploadDeviceLogOk (deviceLogItem: GetDeviceUploadLogListRsp) {
  const { code } = await deleteDeviceLogUpload({
    device_sn: props.device?.device_sn || '',
    logs_id: deviceLogItem.logs_id
  })
  if (code === 0) {
    await getDeviceUploadLogInfo()
  }
}
// 上传日志
const deviceLogUploadModalVisible = ref(false)
function onUploadDeviceLog () {
  deviceLogUploadModalVisible.value = true
}
function onUploadLogOk () {
  // 刷新列表
  getDeviceUploadLogInfo()
}
</script>
<style lang="scss" scoped>
.device-log-upload-record-wrap{
  .page-action-row{
    display: flex;
    justify-content: space-between;
    width: 100%;
  }
  .device-log-upload-list{
    padding: 20px 0 10px;
  }
  .circle-icon {
    display: inline-block;
    width: 12px;
    height: 12px;
    margin-right: 3px;
    border-radius: 50%;
    vertical-align: middle;
    flex-shrink: 0;
  }
  .row-action{
    color: #2d8cf0;
    & > span{
      margin-right: 10px;
    }
  }
}
</style>
src/components/devices/device-log/use-device-log-upload-detail.ts
New file
@@ -0,0 +1,23 @@
import { DeviceLogItem } from '/@/api/device-log'
import { bytesToSize } from '/@/utils/bytes'
import { formatUnixTime } from '/@/utils/time'
import {
  DATE_FORMAT_MINUTE
} from '/@/utils/constants'
export function useDeviceLogUploadDetail () {
  function getLogTime (deviceLog: DeviceLogItem): string {
    const startTime = formatUnixTime(deviceLog.start_time, DATE_FORMAT_MINUTE)
    const endTime = formatUnixTime(deviceLog.end_time, DATE_FORMAT_MINUTE)
    return `${startTime} — ${endTime}`
  }
  function getLogSize (size: number) {
    return bytesToSize(size)
  }
  return {
    getLogTime,
    getLogSize
  }
}
src/components/devices/device-log/use-device-log-upload-progress-event.ts
New file
@@ -0,0 +1,19 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { DeviceLogUploadInfo } from '/@/types/device-log'
export function useDeviceLogUploadProgressEvent (onDeviceLogUploadWs: (data: DeviceLogUploadInfo) => void): void {
  function handleDeviceLogUploadProgress (payload: any) {
    onDeviceLogUploadWs(payload.data)
    // eslint-disable-next-line no-unused-expressions
    // console.log('payload', payload.data)
  }
  onMounted(() => {
    EventBus.on('deviceLogUploadProgress', handleDeviceLogUploadProgress)
  })
  onBeforeUnmount(() => {
    EventBus.off('deviceLogUploadProgress', handleDeviceLogUploadProgress)
  })
}
src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue
New file
@@ -0,0 +1,64 @@
<template>
<div class="firmware_upgrade_wrap">
  <!-- 版本 -->
  <span class="version"> {{ device.firmware_version }}</span>
  <!-- tag -->
  <span v-if="getTagStatus(device)"
        class="status-tag pointer">
    <a-tag class="pointer"
           :color="getFirmwareTag(device.firmware_status).color"
           @click="deviceUpgrade(device)">
      {{ getFirmwareTag(device.firmware_status).text }}
    </a-tag>
  </span>
  <!-- 进度 -->
  <span v-if="device.firmware_status === DeviceFirmwareStatusEnum.DuringUpgrade">
  {{ `${device.firmware_progress}%`}}
  </span>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
import { Device, DeviceFirmwareStatusEnum, DeviceFirmwareStatus, DeviceFirmwareStatusColor } from '/@/types/device'
const props = defineProps<{
  device: Device,
}>()
const emit = defineEmits(['device-upgrade'])
const needUpgrade = computed(() => {
  return props.device.firmware_status === DeviceFirmwareStatusEnum.ConsistencyUpgrade ||
         props.device.firmware_status === DeviceFirmwareStatusEnum.ToUpgraded
})
function getTagStatus (record: Device) {
  return record.firmware_status && record.firmware_status !== DeviceFirmwareStatusEnum.None
}
function getFirmwareTag (status: DeviceFirmwareStatusEnum) {
  return {
    text: DeviceFirmwareStatus[status] || '',
    color: DeviceFirmwareStatusColor[status] || ''
  }
}
function deviceUpgrade (record: Device) {
  if (!needUpgrade.value) return
  emit('device-upgrade', record)
}
</script>
<style lang="scss" scoped>
.firmware_upgrade_wrap{
  .status-tag{
    margin-left: 10px;
  }
  .pointer {
    cursor: pointer;
  }
}
</style>
src/components/devices/device-upgrade/DeviceFirmwareUpgradeModal.vue
New file
@@ -0,0 +1,93 @@
<template>
<a-modal :visible="sVisible"
         :title="title"
         :closable="false"
         centered
         @update:visible="onVisibleChange"
         @cancel="onCancel"
         @ok="onConfirm">
         <div>
          升级固件版本: {{ deviceUpgradeInfo?.product_version }}
         </div>
</a-modal>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, Ref, watchEffect } from 'vue'
import { Device, DeviceFirmwareStatusEnum, DeviceFirmwareStatus, DeviceFirmwareTypeEnum } from '/@/types/device'
import { getDeviceUpgradeInfo, GetDeviceUpgradeInfoRsp, DeviceUpgradeBody } from '/@/api/device-upgrade'
const props = defineProps<{
  visible: boolean,
  title: string,
  device: null | Device,
}>()
const emit = defineEmits(['update:visible', 'ok', 'cancel'])
const deviceUpgradeInfo:Ref<GetDeviceUpgradeInfoRsp> = ref({} as GetDeviceUpgradeInfoRsp)
const sVisible = ref(false)
watchEffect(() => {
  sVisible.value = props.visible
  // 显示弹框时,获取设备升级信息
  if (props.visible) {
    initDeviceUpgradeInfo()
  }
})
function onVisibleChange (sVisible: boolean) {
  setVisible(sVisible)
}
function setVisible (v: boolean, e?: Event) {
  sVisible.value = v
  emit('update:visible', v, e)
}
// 获取设备升级信息
async function initDeviceUpgradeInfo () {
  if (!props.device?.device_name) {
    return
  }
  const { code, data } = await getDeviceUpgradeInfo({ device_name: props.device?.device_name })
  if (code === 0) {
    deviceUpgradeInfo.value = data && data[0]
  }
}
// 提交
function checkConfirm () {
  if (!deviceUpgradeInfo.value.product_version) {
    return false
  }
  if (!props.device) {
    return false
  }
  if (props.device.firmware_status !== DeviceFirmwareStatusEnum.ToUpgraded && props.device.firmware_status !== DeviceFirmwareStatusEnum.ConsistencyUpgrade) {
    return false
  }
  return true
}
function onConfirm (e: Event) {
  if (!checkConfirm()) {
    return
  }
  setVisible(false, e)
  emit('ok', [{
    device_name: props.device?.device_name,
    sn: props.device?.device_sn,
    product_version: deviceUpgradeInfo.value.product_version,
    firmware_upgrade_type: props.device?.firmware_status === DeviceFirmwareStatusEnum.ToUpgraded ? DeviceFirmwareTypeEnum.ToUpgraded : DeviceFirmwareTypeEnum.ConsistencyUpgrade // 1-普通升级,2-一致性升级
  }] as DeviceUpgradeBody, e)
}
function onCancel (e: Event) {
  setVisible(false, e)
  emit('cancel', e)
}
</script>
<style lang="scss" scoped>
</style>
src/components/devices/device-upgrade/use-device-upgrade-event.ts
New file
@@ -0,0 +1,19 @@
import EventBus from '/@/event-bus/'
import { onMounted, onBeforeUnmount } from 'vue'
import { DeviceCmdExecuteInfo, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
export function useDeviceUpgradeEvent (onDeviceUpgradeWs: (payload: DeviceCmdExecuteInfo) => void): void {
  function handleDeviceUpgrade (payload: any) {
    onDeviceUpgradeWs(payload.data)
    // eslint-disable-next-line no-unused-expressions
    // console.log('payload', payload.data)
  }
  onMounted(() => {
    EventBus.on('deviceUpgrade', handleDeviceUpgrade)
  })
  onBeforeUnmount(() => {
    EventBus.off('deviceUpgrade', handleDeviceUpgrade)
  })
}
src/components/devices/device-upgrade/use-device-upgrade.ts
New file
@@ -0,0 +1,42 @@
import { Ref, ref } from 'vue'
import { Device } from '/@/types/device'
import { postDeviceUpgrade, DeviceUpgradeBody } from '/@/api/device-upgrade'
export function useDeviceFirmwareUpgrade (workspaceId: string) {
  const deviceFirmwareUpgradeModalVisible = ref(false)
  const selectedDevice: Ref<null | Device> = ref(null)
  function setDeviceFirmwareUpgradeModalVisible (visible: boolean) {
    deviceFirmwareUpgradeModalVisible.value = visible
  }
  function setSelectedDevice (device: null | Device) {
    selectedDevice.value = device
  }
  // 点击设备升级
  function onDeviceUpgrade (record: Device) {
    if (!record) {
      return
    }
    setSelectedDevice(record)
    setDeviceFirmwareUpgradeModalVisible(true)
  }
  // 确认设备升级
  async function onUpgradeDeviceOk (deviceUpgradeBody: DeviceUpgradeBody) {
    const { code } = await postDeviceUpgrade(workspaceId, deviceUpgradeBody)
    if (code === 0) {
      // setDeviceFirmwareUpgradeModalVisible(false)
    }
  }
  return {
    deviceFirmwareUpgradeModalVisible,
    setDeviceFirmwareUpgradeModalVisible,
    selectedDevice,
    setSelectedDevice,
    onDeviceUpgrade,
    onUpgradeDeviceOk,
  }
}
src/components/g-map/DockControlPanel.vue
New file
@@ -0,0 +1,131 @@
<template>
<div class="dock-control-panel">
  <!-- title -->
  <div class="dock-control-panel-header fz16 pl5 pr5 flex-align-center flex-row flex-justify-between">
    <span>远程调试 {{ props.sn}}</span>
    <span @click="closeControlPanel">
    <CloseOutlined />
    </span>
  </div>
  <!-- cmd -->
  <div class="control-cmd-wrapper">
    <div v-for="(cmdItem, index) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
     <div class="control-cmd-item-left">
        <div class="item-label">{{ cmdItem.label }}</div>
        <div class="item-status">{{ cmdItem.status }}</div>
     </div>
     <div class="control-cmd-item-right">
        <a-button :loading="cmdItem.loading" size="small" type="primary" @click="sendControlCmd(cmdItem, index)">
        {{ cmdItem.operateText }}
        </a-button>
     </div>
   </div>
 </div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, ref, watch } from 'vue'
import {
  CloseOutlined
} from '@ant-design/icons-vue'
import { useDockControl } from './useDockControl'
import { DeviceInfoType } from '/@/types/device'
import { cmdList as baseCmdList, DeviceCmdItem } from '/@/types/device-cmd'
import { useMyStore } from '/@/store'
import { updateDeviceCmdInfoByOsd, updateDeviceCmdInfoByExecuteInfo } from '/@/utils/device-cmd'
const props = defineProps<{
  sn: string,
  deviceInfo: DeviceInfoType,
}>()
const store = useMyStore()
const initCmdList = baseCmdList.map(cmdItem => Object.assign({}, cmdItem))
const cmdList = ref(initCmdList)
// 根据机场指令执行状态更新信息
watch(() => store.state.devicesCmdExecuteInfo, (devicesCmdExecuteInfo) => {
  if (props.sn && devicesCmdExecuteInfo[props.sn]) {
    updateDeviceCmdInfoByExecuteInfo(cmdList.value, devicesCmdExecuteInfo[props.sn])
  }
}, {
  immediate: true,
  deep: true,
})
// 根据设备osd信息更新信息
watch(() => props.deviceInfo, (value) => {
  updateDeviceCmdInfoByOsd(cmdList.value, value)
  // console.log('deviceInfo', value)
}, {
  immediate: true,
  deep: true
})
const emit = defineEmits(['close-control-panel'])
function closeControlPanel () {
  emit('close-control-panel', props.sn, false)
}
// dock 控制指令
const {
  sendDockControlCmd,
} = useDockControl()
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
  const success = await sendDockControlCmd({
    sn: props.sn,
    cmd: cmdItem.cmdKey
  }, true)
  if (success) {
    // updateDeviceSingleCmdInfo(cmdList.value[index])
  }
}
</script>
<style lang='scss' scoped>
.dock-control-panel{
  position: absolute;
  left: calc(100% + 10px);
  top: 0px;
  width: 480px;
  padding: 0 !important;
  background: #000;
  color: #fff;
  border-radius: 2px;
  .dock-control-panel-header{
    border-bottom: 1px solid #515151;
  }
  .control-cmd-wrapper{
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    padding: 4px 10px;
    .control-cmd-item{
      width: 220px;
      height: 58px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      border: 1px solid #666;
      margin: 4px 0;
      padding: 0 8px;
      .control-cmd-item-left{
        display: flex;
        flex-direction: column;
        .item-label{
          font-weight: 700;
        }
      }
    }
  }
}
</style>
src/components/g-map/useDockControl.ts
New file
@@ -0,0 +1,48 @@
import { message } from 'ant-design-vue'
import { ref } from 'vue'
import { postSendCmd } from '/@/api/device-cmd'
import { DeviceCmd } from '/@/types/device-cmd'
export function useDockControl () {
  const controlPanelVisible = ref(false)
  function setControlPanelVisible (visible: boolean) {
    controlPanelVisible.value = visible
  }
  // 远程调试开关
  async function dockDebugOnOff (sn: string, on: boolean) {
    const success = await sendDockControlCmd({
      sn: sn,
      cmd: on ? DeviceCmd.DebugModeOpen : DeviceCmd.DebugModeClose
    }, false)
    if (success) {
      setControlPanelVisible(on)
    }
  }
  // 发送指令
  async function sendDockControlCmd (params: {
    sn: string,
    cmd: DeviceCmd
  }, tip = true) {
    try {
      const { code, message: msg } = await postSendCmd({ dock_sn: params.sn, device_cmd: params.cmd })
      if (code === 0) {
        tip && message.success('指令发送成功')
        return true
      }
      throw (msg)
    } catch (e) {
      tip && message.error('指令发送失败')
      return false
    }
  }
  return {
    controlPanelVisible,
    setControlPanelVisible,
    sendDockControlCmd,
    dockDebugOnOff,
  }
}
src/event-bus/index.ts
New file
@@ -0,0 +1,10 @@
import mitt, { Emitter } from 'mitt'
type Events = {
  deviceUpgrade: any;
  deviceLogUploadProgress: any
};
const emitter: Emitter<Events> = mitt<Events>()
export default emitter
src/hooks/use-connect-websocket.ts
New file
@@ -0,0 +1,21 @@
import { onMounted, onUnmounted } from 'vue'
import ReconnectingWebSocket from 'reconnecting-websocket'
import ConnectWebSocket, { MessageHandler } from '/@/websocket'
import { getWebsocketUrl } from '/@/websocket/util/config'
/**
 * 接收一个message函数
 * @param messageHandler
 */
export function useConnectWebSocket (messageHandler: MessageHandler) {
  const webSocket = new ConnectWebSocket(getWebsocketUrl())
  onMounted(() => {
    webSocket?.registerMessageHandler(messageHandler)
    webSocket?.initSocket()
  })
  onUnmounted(() => {
    webSocket?.close()
  })
}
src/hooks/use-g-map-cover.ts
@@ -10,16 +10,18 @@
export function useGMapCover () {
  const root = getRoot()
  const AMap = root.$aMapObj
  const AMap = root.$aMap
  const normalColor = '#2D8CF0'
  const store = rootStore
  const coverList = store.state.coverList
  function AddCoverToMap (cover :any) {
    root.$aMap.add(cover)
    root.$map.add(cover)
    coverList.push(cover)
    // console.log('coverList:', store.state.coverList)
  }
  function getPinIcon (color?:string) {
    // console.log('color', color)
    const colorObj: {
@@ -31,7 +33,6 @@
      'b620e0': pinb620e0,
      'e23c39': pine23c39,
      'ffbb00': pineffbb00,
    }
    const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
    return new AMap.Icon({
@@ -41,6 +42,7 @@
      // imageSize: new AMap.Size(40, 50)
    })
  }
  function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) {
    console.log(name, coordinates[0], coordinates[1], color, data)
    const pin = new AMap.Marker({
@@ -54,8 +56,9 @@
    // console.log('coordinates pin', pin)
    AddCoverToMap(pin)
  }
  function AddOverlayGroup (overlayGroup) {
    root.$aMap.add(overlayGroup)
    root.$map.add(overlayGroup)
    coverList.push(overlayGroup)
  }
  function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
@@ -74,6 +77,7 @@
    })
    AddOverlayGroup(polyline)
  }
  function initPolygon (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
    const path = [] as GeojsonCoordinate[]
    coordinates.forEach(coordinate => {
@@ -92,6 +96,7 @@
    })
    AddOverlayGroup(Polygon)
  }
  function removeCoverFromMap (id:string) {
    for (let i = 0; i < coverList.length; i++) {
      const ele = coverList[i]
@@ -99,12 +104,13 @@
      const extdata = ele?.getExtData()
      if (extdata?.id === id) {
        console.log(extdata)
        root.$aMap.remove(ele)
        root.$map.remove(ele)
        coverList.slice(i, 1)
        break
      }
    }
  }
  function getElementFromMap (id:string) {
    // console.log('start', new Date().getTime())
    const ele = coverList.find(ele => ele?.getExtData().id === id)
@@ -118,6 +124,7 @@
    //   }
    // })
  }
  function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) {
    const element = getElementFromMap(id) as any
    if (element) {
@@ -133,6 +140,7 @@
      })
    }
  }
  return {
    init2DPin,
    initPolyline,
src/hooks/use-g-map-tsa.ts
@@ -6,23 +6,30 @@
export function deviceTsaUpdate () {
  const root = getRoot()
  const AMap = root.$aMapObj
  const AMap = root.$aMap
  const map = root.$aMap
  const icons: {
    [key: string]: string
  } = {
    'sub-device' : '/@/assets/icons/drone.png',
    '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' // 线颜色
  })
  // Fix: 航迹初始化报错
  // TODO: 从时序上解决
  let trackLine = null as any
  function getTrackLineInstance () {
    if (!trackLine) {
      trackLine = new AMap.Polyline({
        map: root.$map,
        strokeColor: '#939393' // 线颜色
      })
    }
    return trackLine
  }
  function initIcon (type: string) {
    return new AMap.Icon({
@@ -36,13 +43,13 @@
      return
    }
    markers[sn] = new AMap.Marker({
      position: new AMap.LngLat(lng ? lng : 113.935913, lat ? lat : 22.525335),
      position: new AMap.LngLat(lng || 113.935913, lat || 22.525335),
      icon: initIcon(type),
      title: name,
      anchor: 'top-center',
      offset: [0, -20],
    })
    root.$aMap.add(markers[sn])
    root.$map.add(markers[sn])
    // markers[sn].on('moving', function (e: any) {
    //   let path = paths[sn]
@@ -52,19 +59,21 @@
    //   }
    //   path.push(e.passedPath[0])
    //   path.push(e.passedPath[1])
    //   passedPolyline.setPath(path)
    //   getTrackLineInstance().setPath(path)
    // })
  }
  function removeMarker (sn: string) {
    if (!markers[sn]) {
      return
    }
    root.$aMap.remove(markers[sn])
    passedPolyline.setPath([])
    root.$map.remove(markers[sn])
    getTrackLineInstance().setPath([])
    delete markers[sn]
    delete paths[sn]
  }
  function addMarker(sn: string, lng?: number, lat?: number) {
  function addMarker (sn: string, lng?: number, lat?: number) {
    getDeviceBySn(localStorage.getItem(ELocalStorageKey.WorkspaceId)!, sn)
      .then(data => {
        if (data.code !== 0) {
@@ -74,7 +83,8 @@
        initMarker(data.data.domain, data.data.nickname, sn, lng, lat)
      })
  }
  function moveTo(sn: string, lng: number, lat: number) {
  function moveTo (sn: string, lng: number, lat: number) {
    let marker = markers[sn]
    if (!marker) {
      addMarker(sn, lng, lat)
@@ -86,7 +96,7 @@
      autoRotation: true
    })
  }
  return {
    marker: markers,
    initMarker,
src/hooks/use-g-map.ts
@@ -4,30 +4,35 @@
export function useGMapManage () {
  const state = reactive({
    mapEntity: null,
    mapObj: null,
    aMap: null, // Map类
    map: null, // 地图对象
    mouseTool: null,
  })
  async function initMap (container: string, app:App) {
    AMapLoader.load({
      ...AMapConfig
    }).then((AMap) => {
      state.mapObj = AMap
      state.mapEntity = new AMap.Map(container, {
      state.aMap = AMap
      state.map = new AMap.Map(container, {
        center: [113.935913, 22.525335],
        zoom: 15
      })
      state.mouseTool = new AMap.MouseTool(state.mapEntity)
      app.config.globalProperties.$aMap = state.mapEntity
      app.config.globalProperties.$aMapObj = state.mapObj
      state.mouseTool = new AMap.MouseTool(state.map)
      // 挂在到全局
      app.config.globalProperties.$aMap = state.aMap
      app.config.globalProperties.$map = state.map
      app.config.globalProperties.$mouseTool = state.mouseTool
    }).catch(e => {
      console.log(e)
    })
  }
  function globalPropertiesConfig (app:App) {
    initMap('g-container', app)
  }
  return {
    globalPropertiesConfig,
  }
src/hooks/use-mouse-tool.ts
@@ -6,13 +6,14 @@
export function useMouseTool () {
  const root = getRoot()
  const AMap = root.$aMapObj
  const state = reactive({
    pinNum: 0,
    polylineNum: 0,
    PolygonNum: 0,
    currentType: '',
  })
  function drawPin (type:MapDoodleType, getDrawCallback:Function) {
    root?.$mouseTool.marker({
      title: type + state.pinNum,
@@ -21,6 +22,7 @@
    state.pinNum++
    root?.$mouseTool.on('draw', getDrawCallback)
  }
  function drawPolyline (type:MapDoodleType, getDrawCallback:Function) {
    root?.$mouseTool.polyline({
      strokeColor: '#2d8cf0',
@@ -32,6 +34,7 @@
    })
    root?.$mouseTool.on('draw', getDrawCallback)
  }
  function drawPolygon (type:MapDoodleType, getDrawCallback:Function) {
    root?.$mouseTool.polygon({
      strokeColor: '#2d8cf0',
@@ -44,10 +47,12 @@
    })
    root?.$mouseTool.on('draw', getDrawCallback)
  }
  function drawOff (type:MapDoodleType) {
    root?.$mouseTool.close()
    root?.$mouseTool.off('draw')
  }
  function mouseTool (type: MapDoodleType, getDrawCallback: Function) {
    state.currentType = type
    switch (type) {
@@ -65,6 +70,7 @@
        break
    }
  }
  return {
    mouseTool
  }
src/pages/elements/elements.vue
File was deleted
src/pages/page-pilot/pilot-home.vue
@@ -130,9 +130,8 @@
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'
import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
const root = getRoot()
const gatewayState = ref<boolean>(localStorage.getItem(ELocalStorageKey.GatewayOnline) === 'true')
@@ -211,8 +210,10 @@
const store = useMyStore()
const wsGetMsg = async (res: any) => {
  const payload = JSON.parse(res.data)
const messageHandler = async (payload: any) => {
  if (!payload) {
    return
  }
  switch (payload.biz_code) {
    case EBizCode.DeviceOnline: {
      console.info('online: ', payload)
@@ -241,8 +242,11 @@
      break
  }
}
// 监听ws 消息
useConnectWebSocket(messageHandler)
let bindNum: number
let socket: ReconnectingWebSocket
onMounted(() => {
  apiPilot.onBackClickReg()
@@ -259,8 +263,6 @@
  }
  device.data.sn = apiPilot.getAircraftSN()
  getDeviceInfo()
  socket = websocket.init(wsGetMsg)
  const isLoaded = apiPilot.isComponentLoaded(EComponentName.Thing)
  if (isLoaded) {
@@ -300,10 +302,6 @@
  window.wsConnectCallback = arg => {
    wsConnectCallback(arg)
  }
})
onUnmounted(() => {
  socket.close()
})
const connectCallback = async (arg: any) => {
src/pages/page-web/home.vue
New file
@@ -0,0 +1,71 @@
<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 '/@/components/common/topbar.vue'
import { onMounted, reactive, ref, UnwrapRef, watch } from 'vue'
import { getRoot } from '/@/root'
import { EBizCode, ELocalStorageKey, ERouterName } from '/@/types'
import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
import EventBus from '/@/event-bus'
interface FormState {
  user: string
  password: string
}
const root = getRoot()
const messageHandler = async (payload: any) => {
  if (!payload) {
    return
  }
  switch (payload.biz_code) {
    case EBizCode.DeviceUpgrade: {
      EventBus.emit('deviceUpgrade', payload)
      break
    }
    case EBizCode.DeviceLogUploadProgress: {
      EventBus.emit('deviceLogUploadProgress', payload)
      break
    }
  }
}
// 监听ws 消息
useConnectWebSocket(messageHandler)
onMounted(() => {
  const token = localStorage.getItem(ELocalStorageKey.Token)
  if (!token) {
    root.$router.push(ERouterName.PROJECT)
  }
})
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.fontBold {
  font-weight: 500;
  font-size: 18px;
}
.header {
  background-color: black;
  color: white;
  height: 60px;
  font-size: 15px;
  padding: 0 20px;
}
</style>
src/pages/page-web/index.vue
File was renamed from src/pages/project-app/index.vue
@@ -34,7 +34,7 @@
          class="m0"
          type="primary"
          html-type="submit"
          :disabled="formState.user === '' || formState.password === ''"
          :disabled="loginBtnDisabled"
          @click="onSubmit"
        >
          Login
@@ -49,23 +49,27 @@
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 { reactive, computed, UnwrapRef } from 'vue'
import { login, LoginBody } from '/@/api/manage'
import { getRoot } from '/@/root'
import { ELocalStorageKey, ERouterName, EUserType } from '/@/types'
import router from '/@/router'
const root = getRoot()
const formState: UnwrapRef<LoginBody> = reactive({
  username: 'adminPC',
  password: 'adminPC',
  flag: EUserType.Web,
})
const loginBtnDisabled = computed(() => {
  return !formState.username || !formState.password
})
const onSubmit = async (e: any) => {
  const result = await login(formState)
  if (result.code === 0) {
    console.log(result)
    localStorage.setItem(ELocalStorageKey.Token, result.data.access_token)
    localStorage.setItem(ELocalStorageKey.WorkspaceId, result.data.workspace_id)
    localStorage.setItem(ELocalStorageKey.Username, result.data.username)
src/pages/page-web/projects/create-plan.vue
src/pages/page-web/projects/devices.vue
New file
@@ -0,0 +1,436 @@
<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="device-table-wrap 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 #firmware_version="{ record }">
        <span v-if="judgeCurrentType(EDeviceTypeName.Dock)">
          <DeviceFirmwareUpgrade :device="record"
                                  class="table-flex-col"
                                  @device-upgrade="onDeviceUpgrade"
                                 />
        </span>
        <span v-else>
          {{ record.firmware_version }}
        </span>
      </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">
          <!-- 编辑态操作 -->
          <div 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]" style="color: #e70102;"><CloseOutlined /></span>
            </a-tooltip>
          </div>
          <!-- 非编辑态操作 -->
          <div v-else class="flex-align-center flex-row" style="color: #2d8cf0">
            <a-tooltip v-if="current.indexOf(EDeviceTypeName.Dock) !== -1" title="设备日志">
              <CloudServerOutlined @click="showDeviceLogUploadRecord(record)"/>
            </a-tooltip>
            <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)"/>
            </a-tooltip>
            <a-tooltip title="Delete">
              <DeleteOutlined @click="() => { deleteTip = true, deleteSn = record.device_sn }"/>
            </a-tooltip>
          </div>
        </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>
    <!-- 设备升级 -->
    <DeviceFirmwareUpgradeModal title="设备升级"
      v-model:visible="deviceFirmwareUpgradeModalVisible"
      :device="selectedDevice"
      @ok="onUpgradeDeviceOk"
    ></DeviceFirmwareUpgradeModal>
    <!-- 设备日志上传记录 -->
    <DeviceLogUploadRecordDrawer
      v-model:visible="deviceLogUploadRecordVisible"
      :device="currentDevice"
    ></DeviceLogUploadRecordDrawer>
    <!-- hms 信息 -->
    <DeviceHmsDrawer
       v-model:visible="hmsVisible"
      :device="currentDevice">
    </DeviceHmsDrawer>
  </div>
</template>
<script lang="ts" setup>
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, unbindDevice, updateDevice } from '/@/api/manage'
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
import { EditOutlined, CheckOutlined, CloseOutlined, DeleteOutlined, FileSearchOutlined, CloudServerOutlined } from '@ant-design/icons-vue'
import { Device, DeviceFirmwareStatusEnum } from '/@/types/device'
import DeviceFirmwareUpgrade from '/@/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue'
import DeviceFirmwareUpgradeModal from '/@/components/devices/device-upgrade/DeviceFirmwareUpgradeModal.vue'
import { useDeviceFirmwareUpgrade } from '/@/components/devices/device-upgrade/use-device-upgrade'
import { useDeviceUpgradeEvent } from '/@/components/devices/device-upgrade/use-device-upgrade-event'
import { DeviceCmdExecuteInfo, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
import DeviceLogUploadRecordDrawer from '/@/components/devices/device-log/DeviceLogUploadRecordDrawer.vue'
import DeviceHmsDrawer from '/@/components/devices/device-hms/DeviceHmsDrawer.vue'
import { message } from 'ant-design-vue'
interface DeviceData {
  device: Device[]
}
const loading = ref(true)
const deleteTip = ref<boolean>(false)
const deleteSn = ref<string>()
const columns: ColumnProps[] = [
  { title: 'Model', dataIndex: 'device_name', width: 100, className: 'titleStyle' },
  { title: 'SN', dataIndex: 'device_sn', width: 100, className: 'titleStyle', ellipsis: true, slots: { customRender: 'sn' } },
  {
    title: 'Name',
    dataIndex: 'nickname',
    width: 100,
    sorter: (a: Device, b: Device) => a.nickname.localeCompare(b.nickname),
    className: 'titleStyle',
    ellipsis: true,
    slots: { customRender: 'nickname' }
  },
  { title: 'Firmware Version', dataIndex: 'firmware_version', width: 150, className: 'titleStyle', slots: { customRender: 'firmware_version' } },
  { title: 'Status', dataIndex: 'status', width: 100, className: 'titleStyle', slots: { customRender: 'status' } },
  {
    title: 'Workspace',
    dataIndex: 'workspace_name',
    width: 100,
    className: 'titleStyle',
    ellipsis: true,
    slots: { customRender: 'workspace' },
    customRender: ({ text, record, index }) => {
      const obj = {
        children: text,
        props: {} as any,
      }
      if (current.value.indexOf(EDeviceTypeName.Dock) !== -1) {
        if (record.domain === EDeviceTypeName.Aircraft) {
          obj.children = ''
        }
      }
      return obj
    }
  },
  { title: 'Joined', dataIndex: 'bound_time', width: 150, sorter: (a: Device, b: Device) => a.bound_time.localeCompare(b.bound_time), className: 'titleStyle' },
  { title: 'Last Online', dataIndex: 'login_time', width: 150, sorter: (a: Device, b: Device) => a.login_time.localeCompare(b.login_time), className: 'titleStyle' },
  {
    title: 'Actions',
    dataIndex: 'actions',
    width: 100,
    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
})
// 获取分页信息
function getPaginationBody () {
  return {
    page: paginationProp.current,
    page_size: paginationProp.pageSize
  } as IPage
}
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 workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
const editableData: UnwrapRef<Record<string, Device>> = reactive({})
const current = ref([EDeviceTypeName.Aircraft])
function judgeCurrentType (type: EDeviceTypeName): boolean {
  return current.value.indexOf(type) !== -1
}
// 设备升级
const {
  deviceFirmwareUpgradeModalVisible,
  selectedDevice,
  onDeviceUpgrade,
  onUpgradeDeviceOk
} = useDeviceFirmwareUpgrade(workspaceId)
function onDeviceUpgradeWs (payload: DeviceCmdExecuteInfo) {
  updateDevicesByWs(data.device, payload)
}
function updateDevicesByWs (devices: Device[], payload: DeviceCmdExecuteInfo) {
  if (!devices || devices.length <= 0) {
    return
  }
  for (let i = 0; i < devices.length; i++) {
    if (devices[i].device_sn === payload.sn) {
      if (!payload.output) return
      const { status, progress } = payload.output
      if (status === DeviceCmdExecuteStatus.Sent || status === DeviceCmdExecuteStatus.InProgress) { // 升级中
        devices[i].firmware_status = DeviceFirmwareStatusEnum.DuringUpgrade
        devices[i].firmware_progress = progress?.percent || 0
      } else { // 终态:成功,失败,超时
        if (status === DeviceCmdExecuteStatus.Failed || status === DeviceCmdExecuteStatus.Timeout) {
          message.error(`设备(${payload.sn}) 升级失败`)
        }
        // 拉取列表
        getDevices(current.value[0], true)
      }
      return
    }
    if (devices[i].children) {
      updateDevicesByWs(devices[i].children || [], payload)
    }
  }
}
useDeviceUpgradeEvent(onDeviceUpgradeWs)
// 获取设备列表信息
function getDevices (domain: string, closeLoading?: boolean) {
  if (!closeLoading) {
    loading.value = true
  }
  getBindingDevices(workspaceId, getPaginationBody(), 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
    paginationProp.pageSize = res.data.pagination.page_size
    loading.value = false
  })
}
function refreshData (page: Pagination) {
  paginationProp.current = page?.current!
  paginationProp.pageSize = page?.pageSize!
  getDevices(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(current.value[0])
  })
}
// 选择设备
function select (item: any) {
  getDevices(item.key)
}
const currentDevice = ref({} as Device)
// 设备日志
const deviceLogUploadRecordVisible = ref(false)
function showDeviceLogUploadRecord (dock: Device) {
  deviceLogUploadRecordVisible.value = true
  currentDevice.value = dock
}
// 健康状态
const hmsVisible = ref<boolean>(false)
function showHms (dock: Device) {
  hmsVisible.value = true
  currentDevice.value = dock
}
onMounted(() => {
  getDevices(current.value[0])
})
</script>
<style lang="scss" scoped>
.device-table-wrap{
  .editable-row-operations{
    div > span {
      margin-right: 10px;
    }
  }
}
</style>
<style lang="scss">
.table {
  background-color: white;
  margin: 20px;
  padding: 20px;
  height: 88vh;
}
.table-striped {
  background-color: #f7f9fa;
}
.ant-table {
  border-top: 1px solid rgb(0,0,0,0.06);
  border-bottom: 1px solid rgb(0,0,0,0.06);
}
.ant-table-tbody tr td {
  border: 0;
}
.ant-table td {
  white-space: nowrap;
}
.ant-table-thead tr th {
  background: white !important;
  border: 0;
}
th.ant-table-selection-column {
  background-color: white !important;
}
.ant-table-header {
  background-color: white !important;
}
.child-row {
  height: 70px;
}
.notice {
  background: $success;
  overflow: hidden;
  cursor: pointer;
}
.caution {
  background: orange;
  cursor: pointer;
  overflow: hidden;
}
.warn {
  background: red;
  cursor: pointer;
  overflow: hidden;
}
</style>
src/pages/page-web/projects/dock.vue
src/pages/page-web/projects/layer.vue
src/pages/page-web/projects/livestream.vue
src/pages/page-web/projects/media.vue
src/pages/page-web/projects/members.vue
File was renamed from src/pages/project-app/projects/members.vue
@@ -57,13 +57,13 @@
  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' } },
  { title: 'Account', dataIndex: 'username', width: 150, sorter: (a: Member, b: Member) => a.username.localeCompare(b.username), className: 'titleStyle' },
  { title: 'User Type', dataIndex: 'user_type', width: 150, className: 'titleStyle' },
  { title: 'Workspace Name', dataIndex: 'workspace_name', width: 150, className: 'titleStyle' },
  { title: 'Mqtt Username', dataIndex: 'mqtt_username', width: 150, className: 'titleStyle', slots: { customRender: 'mqtt_username' } },
  { title: 'Mqtt Password', dataIndex: 'mqtt_password', width: 150, className: 'titleStyle', slots: { customRender: 'mqtt_password' } },
  { title: 'Joined', dataIndex: 'create_time', width: 150, sorter: (a: Member, b: Member) => a.create_time.localeCompare(b.create_time), className: 'titleStyle' },
  { title: 'Action', dataIndex: 'action', width: 100, className: 'titleStyle', slots: { customRender: 'action' } },
]
const data = reactive<MemberData>({
@@ -117,7 +117,6 @@
    data.member = userList
    paginationProp.total = res.data.pagination.total
    paginationProp.current = res.data.pagination.page
  })
}
src/pages/page-web/projects/task.vue
src/pages/page-web/projects/tsa.vue
File was renamed from src/pages/project-app/projects/tsa.vue
@@ -24,9 +24,9 @@
                    </a-tooltip>
                  </div>
                  <div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
                    <div>
                    <div class="flex-align-center flex-row">
                      <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;'">
                      <span class="font-bold text-hidden" style="max-width: 80px;" :style="dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected ? 'color: #00ee8b' :  'color: red;'">
                        {{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].mode_code] : EDockModeCode[EDockModeCode.Disconnected] }}
                      </span>
                    </div>
src/pages/page-web/projects/wayline.vue
File was renamed from src/pages/project-app/projects/wayline.vue
@@ -8,6 +8,7 @@
      </a-row>
    </div>
    <div class="height-100">
    <a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
      <div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
        <div v-for="wayline in waylinesData.data" :key="wayline.id">
          <div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)">
@@ -62,6 +63,7 @@
              </div>
          </template>
      </a-modal>
    </a-spin>
    </div>
  </div>
</template>
@@ -79,6 +81,7 @@
import { downloadFile } from '/@/utils/common'
import { IPage } from '/@/api/http/type'
const loading = ref(false)
const store = useMyStore()
const pagination :IPage = {
  page: 1,
@@ -154,12 +157,15 @@
}
function downloadWayline (waylineId: string, fileName: string) {
  loading.value = true
  downloadWaylineFile(workspaceId, waylineId).then(res => {
    if (res.code && res.code !== 0) {
    if (!res) {
      return
    }
    const data = new Blob([res.data], { type: 'application/zip' })
    const data = new Blob([res], { type: 'application/zip' })
    downloadFile(data, fileName + '.kmz')
  }).finally(() => {
    loading.value = false
  })
}
src/pages/page-web/projects/workspace.vue
File was renamed from src/pages/project-app/projects/workspace.vue
@@ -21,22 +21,23 @@
</template>
<script lang="ts" setup>
import Sidebar from '../sidebar.vue'
import Sidebar from '/@/components/common/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'
import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
const root = getRoot()
const store = useMyStore()
const wsGetMsg = async (res: any) => {
  const payload = JSON.parse(res.data)
const messageHandler = async (payload: any) => {
  if (!payload) {
    return
  }
  switch (payload.biz_code) {
    case EBizCode.GatewayOsd: {
      store.commit('SET_GATEWAY_INFO', payload.data)
@@ -78,21 +79,33 @@
      store.commit('SET_DEVICE_HMS_INFO', payload.data)
      break
    }
    case EBizCode.DeviceReboot:
    case EBizCode.DroneOpen:
    case EBizCode.DroneClose:
    case EBizCode.CoverOpen:
    case EBizCode.CoverClose:
    case EBizCode.PutterOpen:
    case EBizCode.PutterClose:
    case EBizCode.ChargeOpen:
    case EBizCode.ChargeClose:
    case EBizCode.DeviceFormat:
    case EBizCode.DroneFormat:
    {
      store.commit('SET_DEVICES_CMD_EXECUTE_INFO', {
        biz_code: payload.biz_code,
        timestamp: payload.timestamp,
        ...payload.data,
      })
      break
    }
    default:
      break
  }
}
const store = useMyStore()
// 监听ws 消息
useConnectWebSocket(messageHandler)
let socket: ReconnectingWebSocket
onMounted(() => {
  socket = websocket.init(wsGetMsg)
})
onUnmounted(() => {
  socket.close()
})
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
src/pages/project-app/home.vue
File was deleted
src/pages/project-app/projects/devices.vue
File was deleted
src/root.ts
@@ -1,8 +1,8 @@
import { createApp, ComponentCustomProperties, App as VueApp } from 'vue'
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $aMap: any
    $aMapObj: any
    $aMap: any // Map类
    $map: any // 地图对象
    $mouseTool: any
  }
}
src/router/index.ts
@@ -1,8 +1,8 @@
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 CreatePlan from '../pages/page-web/projects/create-plan.vue'
import WaylinePanel from '/@/pages/page-web/projects/wayline.vue'
import DockPanel from '/@/pages/page-web/projects/dock.vue'
import LiveAgora from '/@/components/livestream-agora.vue'
import LiveOthers from '/@/components/livestream-others.vue'
@@ -11,38 +11,41 @@
    path: '/',
    redirect: '/' + ERouterName.PROJECT
  },
  // 首页
  {
    path: '/' + ERouterName.PROJECT,
    name: ERouterName.PROJECT,
    component: () => import('/@/pages/project-app/index.vue')
    component: () => import('/@/pages/page-web/index.vue')
  },
  // members, devices
  {
    path: '/' + ERouterName.HOME,
    name: ERouterName.HOME,
    component: () => import('/@/pages/project-app/home.vue'),
    component: () => import('/@/pages/page-web/home.vue'),
    children: [
      {
        path: '/' + ERouterName.MEMBERS,
        name: ERouterName.MEMBERS,
        component: () => import('/@/pages/project-app/projects/members.vue')
        component: () => import('/@/pages/page-web/projects/members.vue')
      },
      {
        path: '/' + ERouterName.DEVICES,
        name: ERouterName.DEVICES,
        component: () => import('/@/pages/project-app/projects/devices.vue')
        component: () => import('/@/pages/page-web/projects/devices.vue')
      }
    ]
  },
  // workspace
  {
    path: '/' + ERouterName.WORKSPACE,
    name: ERouterName.WORKSPACE,
    component: () => import('/@/pages/project-app/projects/workspace.vue'),
    component: () => import('/@/pages/page-web/projects/workspace.vue'),
    redirect: '/' + ERouterName.TSA,
    children: [
      {
        path: '/' + ERouterName.LIVESTREAM,
        name: ERouterName.LIVESTREAM,
        component: () => import('/@/pages/project-app/projects/livestream.vue'),
        component: () => import('/@/pages/page-web/projects/livestream.vue'),
        children: [
          {
            path: ERouterName.LIVING,
@@ -56,27 +59,27 @@
      },
      {
        path: '/' + ERouterName.TSA,
        component: () => import('/@/pages/project-app/projects/tsa.vue')
        component: () => import('/@/pages/page-web/projects/tsa.vue')
      },
      {
        path: '/' + ERouterName.LAYER,
        name: ERouterName.LAYER,
        component: () => import('/@/pages/project-app/projects/layer.vue')
        component: () => import('/@/pages/page-web/projects/layer.vue')
      },
      {
        path: '/' + ERouterName.MEDIA,
        name: ERouterName.MEDIA,
        component: () => import('/@/pages/project-app/projects/media.vue')
        component: () => import('/@/pages/page-web/projects/media.vue')
      },
      {
        path: '/' + ERouterName.WAYLINE,
        name: ERouterName.WAYLINE,
        component: () => import('/@/pages/project-app/projects/wayline.vue')
        component: () => import('/@/pages/page-web/projects/wayline.vue')
      },
      {
        path: '/' + ERouterName.TASK,
        name: ERouterName.TASK,
        component: () => import('/@/pages/project-app/projects/task.vue'),
        component: () => import('/@/pages/page-web/projects/task.vue'),
        children: [
          {
            path: ERouterName.CREATE_PLAN,
@@ -98,6 +101,7 @@
      }
    ]
  },
  // pilot
  {
    path: '/' + ERouterName.PILOT,
    name: ERouterName.PILOT,
@@ -118,11 +122,6 @@
  {
    path: '/' + ERouterName.PILOT_BIND,
    component: () => import('/@/pages/page-pilot/pilot-bind.vue')
  },
  {
    path: '/' + ERouterName.ELEMENT,
    name: ERouterName.ELEMENT,
    component: () => import('/@/pages/elements/elements.vue')
  }
]
src/store/index.ts
@@ -5,6 +5,7 @@
import { getLayers } from '/@/api/layer'
import { LayerType } from '/@/types/mapLayer'
import { ETaskStatus, TaskInfo, WaylineFile } from '/@/types/wayline'
import { DevicesCmdExecuteInfo } from '/@/types/device-cmd'
const initStateFunc = () => ({
  Layers: [
@@ -87,7 +88,10 @@
  },
  hmsInfo: {} as {
    [sn: string]: DeviceHms[]
  }
  },
  // 机场指令执行状态信息
  devicesCmdExecuteInfo: {
  } as DevicesCmdExecuteInfo
})
export type RootStateType = ReturnType<typeof initStateFunc>
@@ -144,7 +148,6 @@
    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]
  },
@@ -171,6 +174,25 @@
  SET_DEVICE_HMS_INFO (state, info) {
    const hmsList: Array<DeviceHms> = state.hmsInfo[info.sn]
    state.hmsInfo[info.sn] = info.host.concat(hmsList ?? [])
  },
  SET_DEVICES_CMD_EXECUTE_INFO (state, info) { // 保存设备指令ws消息推送
    if (!info.sn) {
      return
    }
    if (state.devicesCmdExecuteInfo[info.sn]) {
      const index = state.devicesCmdExecuteInfo[info.sn].findIndex(cmdExecuteInfo => cmdExecuteInfo.biz_code === info.biz_code)
      if (index >= 0) {
        // 丢弃前面的消息
        if (state.devicesCmdExecuteInfo[info.sn][index].timestamp > info.timestamp) {
          return
        }
        state.devicesCmdExecuteInfo[info.sn][index] = info
      } else {
        state.devicesCmdExecuteInfo[info.sn].push(info)
      }
    } else {
      state.devicesCmdExecuteInfo[info.sn] = [info]
    }
  }
}
src/styles/flex.style.scss
@@ -44,6 +44,18 @@
  justify-content: space-around;
}
.flex-1 {
  flex: 1;
}
.flex-shrink-0 {
  flex-shrink: 0;
}
.flex-shrink-1 {
  flex-shrink: 1;
}
//width
.width-100vw {
  width: 100vw;
src/types/airport-tsa.ts
New file
@@ -0,0 +1,38 @@
// 机场存储容量:总容量(单位:KB)、已使用(单位:KB)
export interface AirportStorage {
  total: number, // 单位:KB
  used: number
}
// 舱盖状态
export enum CoverStateEnum {
  Close = 0, // 关闭
  Open = 1, // 打开
  HalfOpen = 2, // 半打开
  Failed = 3 // 失败
}
// 推杆状态
export enum PutterStateEnum {
  Close = 0, // 关闭
  Open = 1, // 打开
  HalfOpen = 2, // 半打开
  Failed = 3 // 失败
}
// 充电状态
export enum ChargeStateEnum {
  NotCharge = 0, // 空闲
  Charge = 1, // 正在充电
}
export interface DroneChargeState {
  state: ChargeStateEnum,
  capacity_percent: string,
}
// 补光灯状态
export enum SupplementLightStateEnum {
  Close = 0, // 关闭
  Open = 1, // 打开
}
src/types/device-cmd.ts
New file
@@ -0,0 +1,210 @@
// 机场指令集
export enum DeviceCmd {
  // 简单指令
  DebugModeOpen = 'debug_mode_open', // 调试模式开启
  DebugModeClose = 'debug_mode_close', // 调试模式关闭
  SupplementLightOpen = 'supplement_light_open', // 打开补光灯
  SupplementLightClose = 'supplement_light_close', // 关闭补光灯
  ReturnHome = 'return_home', // 一键返航
  // 复杂指令
  DeviceReboot = 'device_reboot', // 机场重启
  DroneOpen = 'drone_open', // 飞行器开机
  DroneClose = 'drone_close', // 飞行器关机
  // DeviceCheck = 'device_check', // 一键排障(一键起飞自检)
  DeviceFormat = 'device_format', // 机场数据格式化
  DroneFormat = 'drone_format', // 飞行器数据格式化
  CoverOpen = 'cover_open', // 打开舱盖
  CoverClose = 'cover_close', // 关闭舱盖
  PutterOpen = 'putter_open', // 推杆展开
  PutterClose = 'putter_close', // 推杆闭合
  ChargeOpen = 'charge_open', // 打开充电
  ChargeClose = 'charge_close', // 关闭充电
}
export interface DeviceCmdItem{
  label: string, // 标题
  status: string, // 当前状态
  operateText: string, // 按钮文字
  cmdKey: DeviceCmd, // 请求指令
  oppositeCmdKey?: DeviceCmd, // 相反状态指令
  func: string, // 处理函数
  loading: boolean // 按钮loading
}
// 机场指令
export const cmdList: DeviceCmdItem[] = [
  {
    // iconName: ,
    label: '机场系统',
    status: '工作中',
    operateText: '重启',
    cmdKey: DeviceCmd.DeviceReboot,
    func: 'deviceReboot',
    loading: false,
    // btnAnimationIconName: '',
    // operateTips: '',
    // statusColor: '',
  },
  {
    label: '飞行器',
    status: '关机',
    operateText: '开机',
    cmdKey: DeviceCmd.DroneOpen,
    oppositeCmdKey: DeviceCmd.DroneClose,
    func: 'droneStatus',
    loading: false,
  },
  {
    label: '舱盖',
    status: '关',
    operateText: '开启',
    cmdKey: DeviceCmd.CoverOpen,
    oppositeCmdKey: DeviceCmd.CoverClose,
    func: 'coverStatus',
    loading: false,
  },
  {
    label: '推杆',
    status: '闭合',
    operateText: '展开',
    cmdKey: DeviceCmd.PutterOpen,
    oppositeCmdKey: DeviceCmd.PutterClose,
    func: 'putterStatus',
    loading: false,
  },
  {
    label: '充电状态',
    status: '未充电',
    operateText: '充电',
    cmdKey: DeviceCmd.ChargeOpen,
    oppositeCmdKey: DeviceCmd.ChargeClose,
    func: 'chargeStatus',
    loading: false,
  },
  {
    label: '一键返航',
    status: '--',
    operateText: '返航',
    cmdKey: DeviceCmd.ReturnHome,
    func: 'returnHome',
    loading: false,
  },
  {
    label: '机场存储',
    status: '--',
    operateText: '格式化',
    cmdKey: DeviceCmd.DeviceFormat,
    func: 'deviceFormat',
    loading: false,
  },
  {
    label: '飞行器存储',
    status: '--',
    operateText: '格式化',
    cmdKey: DeviceCmd.DroneFormat,
    func: 'droneFormat',
    loading: false,
  },
  {
    label: '补光灯',
    status: '关',
    operateText: '打开',
    cmdKey: DeviceCmd.SupplementLightOpen,
    oppositeCmdKey: DeviceCmd.SupplementLightClose,
    func: 'supplementLightStatus',
    loading: false,
  },
]
export enum DeviceCmdStatusText {
  DeviceRebootNormalText = '工作中',
  DeviceRebootInProgressText = '重启中...',
  DeviceRebootFailedText = '重启失败',
  DroneStatusOpenNormalText = '开',
  DroneStatusOpenInProgressText = '开机中...',
  DroneStatusOpenFailedText = '关',
  DroneStatusOpenBtnText = '关机',
  DroneStatusCloseNormalText = '关',
  DroneStatusCloseInProgressText = '关机中...',
  DroneStatusCloseFailedText = '开',
  DroneStatusCloseBtnText = '开机',
  DeviceCoverOpenNormalText = '开',
  DeviceCoverOpenInProgressText = '开启中...',
  DeviceCoverOpenFailedText = '关',
  DeviceCoverOpenBtnText = '关闭',
  DeviceCoverCloseNormalText = '关',
  DeviceCoverCloseInProgressText = '关闭中...',
  DeviceCoverCloseFailedText = '开',
  DeviceCoverCloseBtnText = '开启',
  DevicePutterOpenNormalText = '展开',
  DevicePutterOpenBtnText = '闭合',
  DevicePutterOpenInProgressText = '推杆展开中',
  DevicePutterOpenFailedText = '闭合',
  DevicePutterCloseNormalText = '闭合',
  DevicePutterCloseInProgressText = '推杆闭合中',
  DevicePutterCloseFailedText = '展开',
  DevicePutterCloseBtnText = '展开',
  DeviceChargeOpenNormalText = '充电',
  DeviceChargeOpenInProgressText = '充电中...',
  DeviceChargeOpenFailedText = '未充电',
  DeviceChargeOpenBtnText = '断电',
  DeviceChargeCloseNormalText = '断电',
  DeviceChargeCloseInProgressText = '断电中...',
  DeviceChargeCloseFailedText = '充电',
  DeviceChargeCloseBtnText = '充电',
  DeviceFormatInProgressText = '格式化...',
  DeviceFormatFailedText = '格式化失败',
  DroneFormatInProgressText = '格式化...',
  DroneFormatFailedText = '格式化失败',
  DeviceSupplementLightOpenNormalText = '开',
  DeviceSupplementLightOpenInProgressText = '开启中...',
  DeviceSupplementLightOpenFailedText = '关',
  DeviceSupplementLightOpenBtnText = '关闭',
  DeviceSupplementLightCloseNormalText = '关',
  DeviceSupplementLightCloseText = '关闭中...',
  DeviceSupplementLightCloseFailedText = '开',
  DeviceSupplementLightCloseBtnText = '打开',
}
// cmd ws 消息状态
export enum DeviceCmdExecuteStatus {
  Sent = 'sent', // 已下发
  InProgress = 'in_progress', // 执行中
  OK = 'ok', // 执行成功
  Failed = 'failed', // 失败
  Canceled = 'canceled', // 取消
  Timeout = 'timeout' // 超时
}
export interface DeviceCmdExecuteInfo {
  biz_code: string,
  timestamp: number,
  sn: string,
  bid: string,
  output:{
    status: DeviceCmdExecuteStatus,
    progress?: {
      percent: number,
      step_key: string,
      step_result: number
    }
  }
  result: number,
}
// 所有机场的指令执行状态
export interface DevicesCmdExecuteInfo {
  [key: string]: DeviceCmdExecuteInfo[], // sn --- DeviceCmdExecuteInfo
}
src/types/device-log.ts
New file
@@ -0,0 +1,65 @@
import { DOMAIN } from '/@/types/device'
import { commonColor } from '/@/utils/color'
// 日志上传状态
export enum DeviceLogUploadStatusEnum {
  Uploading = 1, //  上传中
  Done = 2, // 完成
  Canceled = 3, // 取消
  Failed = 4, // 失败
}
export const DeviceLogUploadStatusMap = {
  [DeviceLogUploadStatusEnum.Uploading]: '上传中',
  [DeviceLogUploadStatusEnum.Done]: '上传成功',
  [DeviceLogUploadStatusEnum.Canceled]: '取消上传',
  [DeviceLogUploadStatusEnum.Failed]: '上传失败',
}
export const DeviceLogUploadStatusColor = {
  [DeviceLogUploadStatusEnum.Uploading]: commonColor.BLUE,
  [DeviceLogUploadStatusEnum.Done]: commonColor.NORMAL,
  [DeviceLogUploadStatusEnum.Canceled]: commonColor.WARN,
  [DeviceLogUploadStatusEnum.Failed]: commonColor.FAIL,
}
// 设备日志上传 ws 消息状态
export enum DeviceLogUploadStatus {
  FilePull = 'file_pull', // 拉取日志 可以作为 正在处理中
  FileZip = 'file_zip', // 拉取日志,日志压缩可以作为 正在处理中
  FileUploading = 'file_uploading', // 正在上传
  Canceled = 'canceled', // 取消
  Timeout = 'timeout', // 超时
  Failed = 'failed', // 失败
  OK = 'ok', // 上传成功
  // Paused = 'paused' // 暂停
}
export interface DeviceLogUploadInfo {
  sn: string,
  bid: string,
  output:{
    logs_id: string
    status: DeviceLogUploadStatus,
    files: {
        device_sn: string,
        device_model_domain: DOMAIN,
        progress: number,
        result: number,
        upload_rate: number,
        status: DeviceLogUploadStatus
    }[]
  }
  result: number,
}
// ws status => log status
export const DeviceLogUploadWsStatusMap = {
  [DeviceLogUploadStatus.FilePull]: DeviceLogUploadStatusEnum.Uploading,
  [DeviceLogUploadStatus.FileZip]: DeviceLogUploadStatusEnum.Uploading,
  [DeviceLogUploadStatus.FileUploading]: DeviceLogUploadStatusEnum.Uploading,
  [DeviceLogUploadStatus.OK]: DeviceLogUploadStatusEnum.Done,
  [DeviceLogUploadStatus.Failed]: DeviceLogUploadStatusEnum.Failed,
  [DeviceLogUploadStatus.Canceled]: DeviceLogUploadStatusEnum.Canceled,
  [DeviceLogUploadStatus.Timeout]: DeviceLogUploadStatusEnum.Failed,
}
src/types/device.ts
@@ -1,16 +1,188 @@
import { EDeviceTypeName } from ".";
import { commonColor } from '/@/utils/color'
export interface DeviceValue {
  key: string; // 'domain-type-subtype'
  domain: string; // 表示一个领域,作为一个命名空间,暂时分 飞机类-0, 负载类-1,RC类-2,机场类-3 4种
  type: number; // 设备类型枚举
  sub_type: number; // 设备类型枚举 负载一般表示镜头
}
// domain
export enum DOMAIN {
  DRONE = '0', // 飞行器
  PAYLOAD = '1', // 负载
  RC = '2', // 遥控
  DOCK = '3', // 机场
}
// DJI飞机类型
export enum DRONE_TYPE {
  M30 = 67,
  M300 = 60,
  Phantom4 = 11,
  Phantom4Pro = 18,
  Phantom4RTK = 59,
  Phantom4Advanced = 27,
  Mavic3EnterpriseAdvanced= 77,
}
// DJI负载类型枚举值
export enum PAYLOAD_TYPE {
  FPV = 39,
  H20 = 42,
  H20T = 43,
  H20N = 61,
  EP600 = 50,
  EP800 = 90742,
  M30D = 52,
  M30T = 53,
  XT2 = 26,
  XTS = 41,
  Z30 = 20,
  DockTopCamera = 165,
  M3E = 66,
  M3T = 67,
  // UNKNOWN = 65535
}
// RC type
export enum RC_TYPE {
  RC = 56,
  RCPlus = 119,
  RC144 = 144,
}
// DOCK type
export enum DOCK_TYPE {
  Dock = 1,
}
// 设备sub_type 从0升序
export enum DEVICE_SUB_TYPE {
  ZERO,
  ONE,
  TWO,
  THREE,
  UNKNOWN = 65535,
}
export const DEVICE_MODEL_KEY = {
  M30: `${DOMAIN.DRONE}-${DRONE_TYPE.M30}-${DEVICE_SUB_TYPE.ZERO}`,
  M30T: `${DOMAIN.DRONE}-${DRONE_TYPE.M30}-${DEVICE_SUB_TYPE.ONE}`,
  M3E: `${DOMAIN.DRONE}-${DRONE_TYPE.Mavic3EnterpriseAdvanced}-${DEVICE_SUB_TYPE.ZERO}`,
  M3T: `${DOMAIN.DRONE}-${DRONE_TYPE.Mavic3EnterpriseAdvanced}-${DEVICE_SUB_TYPE.ONE}`,
  M300: `${DOMAIN.DRONE}-${DRONE_TYPE.M300}-${DEVICE_SUB_TYPE.ZERO}`,
  Phantom4: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4}-${DEVICE_SUB_TYPE.ZERO}`,
  Phantom4Pro: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4Pro}-${DEVICE_SUB_TYPE.ZERO}`,
  Phantom4RTK: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4RTK}-${DEVICE_SUB_TYPE.ZERO}`,
  Phantom4Advanced: `${DOMAIN.DRONE}-${DRONE_TYPE.Phantom4Advanced}-${DEVICE_SUB_TYPE.ZERO}`,
  FPV: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.FPV}-${DEVICE_SUB_TYPE.ZERO}`,
  H20: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20}-${DEVICE_SUB_TYPE.ZERO}`,
  H20T: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20T}-${DEVICE_SUB_TYPE.ZERO}`,
  H20N: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.H20N}-${DEVICE_SUB_TYPE.ZERO}`,
  EP600: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.EP600}-${DEVICE_SUB_TYPE.UNKNOWN}`,
  EP800: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.EP800}-${DEVICE_SUB_TYPE.ZERO}`,
  M30Camera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M30D}-${DEVICE_SUB_TYPE.ZERO}`,
  M30TCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M30T}-${DEVICE_SUB_TYPE.ZERO}`,
  M3ECamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3E}-${DEVICE_SUB_TYPE.ZERO}`,
  M3TCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3T}-${DEVICE_SUB_TYPE.ZERO}`,
  // M3MCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.M3M}-${DEVICE_SUB_TYPE.ZERO}`,
  XT2: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.XT2}-${DEVICE_SUB_TYPE.ZERO}`,
  XTS: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.XTS}-${DEVICE_SUB_TYPE.ZERO}`,
  Z30: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.Z30}-${DEVICE_SUB_TYPE.ZERO}`,
  DockTopCamera: `${DOMAIN.PAYLOAD}-${PAYLOAD_TYPE.DockTopCamera}-${DEVICE_SUB_TYPE.ZERO}`,
  RC: `${DOMAIN.RC}-${RC_TYPE.RC}-${DEVICE_SUB_TYPE.ZERO}`,
  RCPlus: `${DOMAIN.RC}-${RC_TYPE.RCPlus}-${DEVICE_SUB_TYPE.ZERO}`,
  Dock: `${DOMAIN.DOCK}-${DOCK_TYPE.Dock}-${DEVICE_SUB_TYPE.ZERO}`,
}
export const DEVICE_NAME = {
  // drone
  [DEVICE_MODEL_KEY.M30]: 'M30',
  [DEVICE_MODEL_KEY.M30T]: 'M30T',
  [DEVICE_MODEL_KEY.M3E]: 'Mavic 3E',
  [DEVICE_MODEL_KEY.M3T]: 'Mavic 3T',
  // [DEVICE_MODEL_KEY.M3M]: 'Mavic 3M',
  [DEVICE_MODEL_KEY.M300]: 'M300 RTK',
  [DEVICE_MODEL_KEY.Phantom4]: 'Phantom 4',
  [DEVICE_MODEL_KEY.Phantom4Pro]: 'Phantom 4 Pro',
  [DEVICE_MODEL_KEY.Phantom4RTK]: 'Phantom 4 RTK',
  [DEVICE_MODEL_KEY.Phantom4Advanced]: 'Phantom 4 Advanced',
  // payload
  [DEVICE_MODEL_KEY.FPV]: 'FPV',
  [DEVICE_MODEL_KEY.H20]: 'H20',
  [DEVICE_MODEL_KEY.H20T]: 'H20T',
  [DEVICE_MODEL_KEY.H20N]: 'H20N',
  [DEVICE_MODEL_KEY.EP600]: 'P1',
  [DEVICE_MODEL_KEY.EP800]: 'L1',
  [DEVICE_MODEL_KEY.M30Camera]: 'M30 Camera',
  [DEVICE_MODEL_KEY.M30TCamera]: 'M30T Camera',
  [DEVICE_MODEL_KEY.M3ECamera]: 'Mavic 3E',
  [DEVICE_MODEL_KEY.M3TCamera]: 'Mavic 3T',
  // [DEVICE_MODEL_KEY.M3MCamera]: 'Mavic 3M',
  [DEVICE_MODEL_KEY.XT2]: 'XT2',
  [DEVICE_MODEL_KEY.XTS]: 'XTS',
  [DEVICE_MODEL_KEY.Z30]: 'Z30',
  [DEVICE_MODEL_KEY.DockTopCamera]: 'Dock Camera',
  // rc
  [DEVICE_MODEL_KEY.RC]: 'RC',
  [DEVICE_MODEL_KEY.RCPlus]: 'RC Plus',
  // dock
  [DEVICE_MODEL_KEY.Dock]: 'Dock',
}
// 固件升级类型
export enum DeviceFirmwareTypeEnum {
  ToUpgraded = 3, // 普通升级
  ConsistencyUpgrade =2, // 一致性升级
}
// 固件升级状态
export enum DeviceFirmwareStatusEnum {
  None = 1, // 无需升级
  ToUpgraded = 2, // 待升级
  ConsistencyUpgrade = 3, // 一致性升级
  DuringUpgrade = 4, // 升级中
}
export const DeviceFirmwareStatus = {
  [DeviceFirmwareStatusEnum.None]: '',
  [DeviceFirmwareStatusEnum.ToUpgraded]: '待升级',
  [DeviceFirmwareStatusEnum.ConsistencyUpgrade]: '一致性升级',
  [DeviceFirmwareStatusEnum.DuringUpgrade]: '升级中',
}
export const DeviceFirmwareStatusColor = {
  [DeviceFirmwareStatusEnum.None]: commonColor.WHITE,
  [DeviceFirmwareStatusEnum.ToUpgraded]: commonColor.BLUE,
  [DeviceFirmwareStatusEnum.ConsistencyUpgrade]: commonColor.WARN,
  [DeviceFirmwareStatusEnum.DuringUpgrade]: commonColor.NORMAL,
}
export interface Device {
  device_name: string,
  device_sn: string,
  nickname: string,
  firmware_version: string,
  firmware_status: DeviceFirmwareStatusEnum,
  status: string,
  workspace_name: string,
  bound_time: string,
  login_time: string,
  children?: Device[]
  domain: string
  children?: Device[],
  domain: string,
  firmware_progress?: number, // 升级进度
}
export interface DeviceStatus {
@@ -78,7 +250,7 @@
  network_state: {
    type: number,
    quality: number,
    rate: number,
    rate: number,
  },
  drone_in_dock: number,
  drone_charge_state: {
@@ -195,4 +367,12 @@
  create_time: string,
  update_time: string,
  domain: string
}
}
// TODO: 设备拓扑管理优化
// 设备信息
export interface DeviceInfoType {
  gateway: GatewayOsd, // 遥控器
  dock: DockOsd, // 机场
  device: DeviceOsd, // 飞机
}
src/types/enums.ts
@@ -92,6 +92,25 @@
    DeviceOffline = 'device_offline',
    FlightTaskProgress = 'flighttask_progress',
    DeviceHms = 'device_hms',
    // 设备指令
    DeviceReboot = 'device_reboot', // 机场重启
    DroneOpen = 'drone_open', // 飞行器开机
    DroneClose = 'drone_close', // 飞行器关机
    DeviceFormat = 'device_format', // 机场数据格式化
    DroneFormat = 'drone_format', // 飞行器数据格式化
    CoverOpen = 'cover_open', // 打开舱盖
    CoverClose = 'cover_close', // 关闭舱盖
    PutterOpen = 'putter_open', // 推杆展开
    PutterClose = 'putter_close', // 推杆闭合
    ChargeOpen = 'charge_open', // 打开充电
    ChargeClose = 'charge_close', // 关闭充电
    // 设备升级
    DeviceUpgrade = 'ota_progress', // 设备升级
    // 设备日志
    DeviceLogUploadProgress = 'fileupload_progress' // 设备日志上传上传
}
export enum EDeviceTypeName {
@@ -104,4 +123,4 @@
    NOTICE,
    CAUTION,
    WARN,
}
}
src/utils/bytes.ts
New file
@@ -0,0 +1,86 @@
import { DEFAULT_PLACEHOLDER, SIZES as byteSizes, BYTE_SIZES } from './constants'
/**
 * 转换字节数为单位B,KB,GB...
 * 保留一位小数
 * @param bytes 字节数
 * @param holder 0字节占位符,默认 --
 * @returns
 */
export function bytesToSize (bytes: number, holder = DEFAULT_PLACEHOLDER, fix = 1, unit = false): string {
  if (isNaN(bytes) || bytes === 0) {
    return holder
  }
  // 兼容负数
  let prefix = ''
  if (bytes < 0) {
    bytes = 0 - bytes
    prefix = '-'
  }
  const k = 1024
  const sizes = unit ? BYTE_SIZES : byteSizes// ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return prefix + (bytes / Math.pow(k, i)).toFixed(fix) + '' + sizes[i]
}
//  获取转化后数据及单位
export function getBytesObject (bytes: number, holder = DEFAULT_PLACEHOLDER, fix = 1): {
  value: string,
  size: string
  index: number
} {
  if (isNaN(bytes) || bytes === 0) {
    return {
      value: holder,
      size: '',
      index: -1,
    }
  }
  // 兼容负数
  let prefix = ''
  if (bytes < 0) {
    bytes = 0 - bytes
    prefix = '-'
  }
  const k = 1024
  const sizes = byteSizes// ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return {
    value: prefix + (bytes / Math.pow(k, i)).toFixed(fix),
    size: sizes[i],
    index: i,
  }
}
/**
 * 根据最小单位返回文件大小
 * @param bytes
 * @param minUnit
 * @param fix
 * @returns
 */
export function bytesToSizeWithMinUnit (bytes: number, minUnit = 'B', fix = 1): string {
  const holder = `0${minUnit}`
  const sizes = byteSizes// ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
  const k = 1024
  const findIndex = sizes.findIndex(item => item === minUnit)
  const { value, size, index } = getBytesObject(bytes, holder, fix)
  // 0
  if (index === -1) {
    return holder
  }
  // 转换后单位小于传入的最小单位
  if (index < findIndex) {
    const sizeToMinUint = parseFloat(value) / (Math.pow(k, findIndex - index))
    return sizeToMinUint.toFixed(fix) + minUnit
  }
  // 其他
  return value + size
}
// console.log('size', bytesToSizeWithMinUnit(0))
// console.log('size', bytesToSizeWithMinUnit(1023))
// console.log('size', bytesToSizeWithMinUnit(1024))
// console.log('size', bytesToSizeWithMinUnit(1000 * 1024, 'MB', 2))
// console.log('size', bytesToSizeWithMinUnit(1024 * 1024, 'MB', 2))
src/utils/color.ts
New file
@@ -0,0 +1,8 @@
export const commonColor = {
  WARN: '#FF9900', // 黄色
  FAIL: '#E02020', // 红色
  WHITE: '#FFFFFF', // 白色
  NORMAL: '#19BE6B', // 绿色
  BLUE: '#2B85E4', // 蓝色
  PINK: '#F7C0BA', // 粉
}
src/utils/common.ts
@@ -1,4 +1,8 @@
/**
 * 下载文件
 * @param data
 * @param fileName
 */
export function downloadFile (data: Blob, fileName: string) {
  const lable = document.createElement('a')
  lable.href = window.URL.createObjectURL(data)
src/utils/constants.ts
New file
@@ -0,0 +1,15 @@
export const DEFAULT_PLACEHOLDER = '--' // 默认占位符
// 全局日期格式
export const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export const DATE_FORMAT_MINUTE = 'YYYY-MM-DD HH:mm'
export const DATE_FORMAT_DAY = 'YYYY-MM-DD'
export const TIME_FORMAT = 'HH:mm:ss'
export const TIME_FORMAT_MINUTE = 'HH:mm'
export const DATE_FORMAT_MM = 'MM-DD HH:mm'
export const SIZES = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
export const BYTE_SIZES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
export const PAGE_SIZE_OPTIONS = ['20', '50', '100']
export const PAGE_SIZE = 50
src/utils/device-cmd.ts
New file
@@ -0,0 +1,350 @@
import { DeviceInfoType } from '/@/types/device'
import { DeviceCmd, DeviceCmdItem, DeviceCmdExecuteInfo, DeviceCmdStatusText, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
import { AirportStorage, CoverStateEnum, PutterStateEnum, ChargeStateEnum, SupplementLightStateEnum } from '/@/types/airport-tsa'
import { getBytesObject } from './bytes'
import { DEFAULT_PLACEHOLDER } from './constants'
/**
 * 根据osd 更新信息
 * @param cmdList
 * @param deviceInfo
 * @returns
 */
export function updateDeviceCmdInfoByOsd (cmdList: DeviceCmdItem[], deviceInfo: DeviceInfoType) {
  const { device, dock, gateway } = deviceInfo || {}
  if (!cmdList || cmdList.length < 1) {
    return
  }
  cmdList.forEach(cmdItem => {
    if (cmdItem.loading) {
      return
    }
    if (cmdItem.cmdKey === DeviceCmd.DeviceReboot) { // 重启
      // console.log('DeviceReboot')
    } else if (cmdItem.cmdKey === DeviceCmd.DroneOpen || cmdItem.cmdKey === DeviceCmd.DroneClose) { // 飞行器开关机
      getDroneState(cmdItem, device)
    } else if (cmdItem.cmdKey === DeviceCmd.CoverOpen || cmdItem.cmdKey === DeviceCmd.CoverClose) { // 舱盖开关
      getCoverState(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.PutterOpen || cmdItem.cmdKey === DeviceCmd.PutterClose) { // 推杆闭合展开
      getPutterState(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.ChargeOpen || cmdItem.cmdKey === DeviceCmd.ChargeClose) { // 充电状态
      getChargeState(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.DeviceFormat) { // 机场存储
      deviceFormat(cmdItem, dock)
    } else if (cmdItem.cmdKey === DeviceCmd.DroneFormat) { // 飞行器存储
      droneFormat(cmdItem, device)
    } else if (cmdItem.cmdKey === DeviceCmd.SupplementLightOpen || cmdItem.cmdKey === DeviceCmd.SupplementLightClose) { // 补光灯开关
      getSupplementLightState(cmdItem, dock)
    }
  })
}
// 飞行器开关机
function getDroneState (cmdItem: DeviceCmdItem, droneProperties: any) {
  if (!droneProperties) {
    cmdItem.status = DeviceCmdStatusText.DroneStatusCloseNormalText
    cmdItem.operateText = DeviceCmdStatusText.DroneStatusCloseBtnText
    if (cmdItem.cmdKey !== DeviceCmd.DroneOpen) {
      exchangeDeviceCmd(cmdItem)
    }
  } else {
    cmdItem.status = DeviceCmdStatusText.DroneStatusOpenNormalText
    cmdItem.operateText = DeviceCmdStatusText.DroneStatusOpenBtnText
    if (cmdItem.cmdKey !== DeviceCmd.DroneClose) {
      exchangeDeviceCmd(cmdItem)
    }
  }
}
// 舱盖开关
function getCoverState (cmdItem: DeviceCmdItem, airportProperties: any) {
  const coverState = airportProperties?.cover_state as CoverStateEnum
  if (coverState === CoverStateEnum.Close || coverState === CoverStateEnum.Failed) {
    cmdItem.status = DeviceCmdStatusText.DeviceCoverCloseNormalText
    cmdItem.operateText = DeviceCmdStatusText.DeviceCoverCloseBtnText
    if (cmdItem.cmdKey !== DeviceCmd.CoverOpen) {
      exchangeDeviceCmd(cmdItem)
    }
  } else if (coverState === CoverStateEnum.Open || coverState === CoverStateEnum.HalfOpen) {
    cmdItem.status = DeviceCmdStatusText.DeviceCoverOpenNormalText
    cmdItem.operateText = DeviceCmdStatusText.DeviceCoverOpenBtnText
    if (cmdItem.cmdKey !== DeviceCmd.CoverClose) {
      exchangeDeviceCmd(cmdItem)
    }
  }
}
// 推杆状态
function getPutterState (cmdItem: DeviceCmdItem, airportProperties: any) {
  const putterState = airportProperties?.putter_state as PutterStateEnum
  if (putterState === PutterStateEnum.Close || putterState === PutterStateEnum.Failed) {
    cmdItem.status = DeviceCmdStatusText.DevicePutterCloseNormalText
    cmdItem.operateText = DeviceCmdStatusText.DevicePutterCloseBtnText
    if (cmdItem.cmdKey !== DeviceCmd.PutterOpen) {
      exchangeDeviceCmd(cmdItem)
    }
  } else if (putterState === PutterStateEnum.Open || putterState === PutterStateEnum.HalfOpen) {
    cmdItem.status = DeviceCmdStatusText.DevicePutterOpenNormalText
    cmdItem.operateText = DeviceCmdStatusText.DevicePutterOpenBtnText
    if (cmdItem.cmdKey !== DeviceCmd.PutterClose) {
      exchangeDeviceCmd(cmdItem)
    }
  }
}
// 充电状态
function getChargeState (cmdItem: DeviceCmdItem, airportProperties: any) {
  const chargeState = airportProperties?.drone_charge_state
  const state = chargeState?.state as ChargeStateEnum
  if (!state) return
  if (state === ChargeStateEnum.Charge) {
    cmdItem.status = DeviceCmdStatusText.DeviceChargeOpenNormalText
    cmdItem.operateText = DeviceCmdStatusText.DeviceChargeOpenBtnText
    if (cmdItem.cmdKey !== DeviceCmd.ChargeClose) {
      exchangeDeviceCmd(cmdItem)
    }
  } else if (state === ChargeStateEnum.NotCharge) {
    cmdItem.status = DeviceCmdStatusText.DeviceChargeCloseNormalText
    cmdItem.operateText = DeviceCmdStatusText.DeviceChargeCloseBtnText
    if (cmdItem.cmdKey !== DeviceCmd.ChargeOpen) {
      exchangeDeviceCmd(cmdItem)
    }
  }
}
// 机场存储格式化
function deviceFormat (cmdItem: DeviceCmdItem, airportProperties: any) {
  const airportStorage = airportProperties?.storage
  const value = getAirportStorage(airportStorage)
  cmdItem.status = value
}
// 机场存储格式化
function droneFormat (cmdItem: DeviceCmdItem, droneProperties: any) {
  const droneStorage = droneProperties?.storage
  const value = getAirportStorage(droneStorage)
  cmdItem.status = value
}
// 获取机场存储容量
// {
// "total": 10000, // 单位:KB
// "used": 500
// }
export function getAirportStorage (storage: AirportStorage) {
  if (!storage) {
    return DEFAULT_PLACEHOLDER
  }
  const total = storage.total
  const used = storage.used
  const byteObj = getBytesObject(total * 1024)
  const _total = byteObj.value
  const _used = getBytes(used * 1024, byteObj.index)
  return `${_used}/${_total} ${byteObj.size}`
}
function getBytes (bytes: number, index: number, fixed = 1) {
  return (bytes / Math.pow(1024, index)).toFixed(fixed)
}
// 补光灯状态
function getSupplementLightState (cmdItem: DeviceCmdItem, airportProperties: any) {
  const supplementLightState = airportProperties?.supplement_light_state
  if (supplementLightState === SupplementLightStateEnum.Close) {
    cmdItem.operateText = DeviceCmdStatusText.DeviceSupplementLightCloseBtnText
    cmdItem.status = DeviceCmdStatusText.DeviceSupplementLightCloseNormalText
    if (cmdItem.cmdKey !== DeviceCmd.SupplementLightOpen) {
      exchangeDeviceCmd(cmdItem)
    }
  } else if (supplementLightState === SupplementLightStateEnum.Open) {
    cmdItem.operateText = DeviceCmdStatusText.DeviceSupplementLightOpenBtnText
    cmdItem.status = DeviceCmdStatusText.DeviceSupplementLightOpenNormalText
    if (cmdItem.cmdKey !== DeviceCmd.SupplementLightClose) {
      exchangeDeviceCmd(cmdItem)
    }
  }
}
/**
 * 交换指令
 * @param cmd
 */
function exchangeDeviceCmd (cmdItem: DeviceCmdItem) {
  if (cmdItem.oppositeCmdKey) {
    const oppositeCmdKey = cmdItem.oppositeCmdKey
    cmdItem.oppositeCmdKey = cmdItem.cmdKey
    cmdItem.cmdKey = oppositeCmdKey
  }
}
// /**
//  * 更新简单指令发送情况更新信息
//  * @param cmd
//  */
// export function updateDeviceSingleCmdInfo (cmdItem: DeviceCmdItem) {
//   // 补光灯
//   if (cmdItem.cmdKey === DeviceCmd.SupplementLightOpen) {
//     cmdItem.status = DeviceCmdStatusText.DeviceSupplementLightOpenNormalText
//     cmdItem.operateText = DeviceCmdStatusText.DeviceSupplementLightOpenBtnText
//     exchangeDeviceCmd(cmdItem)
//   } else if (cmdItem.cmdKey === DeviceCmd.SupplementLightClose) {
//     cmdItem.status = DeviceCmdStatusText.DeviceSupplementLightCloseNormalText
//     cmdItem.operateText = DeviceCmdStatusText.DeviceSupplementLightCloseBtnText
//     exchangeDeviceCmd(cmdItem)
//   }
// }
/**
 * 根据指令执行消息更新信息
 * @param cmd
 * @param deviceCmdExecuteInfo
 * @returns
 */
export function updateDeviceCmdInfoByExecuteInfo (cmdList: DeviceCmdItem[], deviceCmdExecuteInfos?: DeviceCmdExecuteInfo[]) {
  if (!deviceCmdExecuteInfos || !cmdList) {
    return
  }
  cmdList.forEach(cmdItem => {
    // 获取当前设备相应指令信息
    const deviceCmdExecuteInfo = deviceCmdExecuteInfos.find(cmdExecuteInfo => cmdExecuteInfo.biz_code === cmdItem.cmdKey)
    if (deviceCmdExecuteInfo) {
      if (cmdItem.cmdKey === DeviceCmd.DeviceReboot) { // 重启
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DeviceRebootInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DeviceRebootFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DeviceRebootNormalText
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.DroneOpen) { // 飞行器开关机
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DroneStatusOpenInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DroneStatusOpenFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DroneStatusOpenNormalText
          cmdItem.operateText = DeviceCmdStatusText.DroneStatusOpenBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.DroneClose) {
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DroneStatusCloseInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DroneStatusCloseFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DroneStatusCloseNormalText
          cmdItem.operateText = DeviceCmdStatusText.DroneStatusCloseBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.CoverOpen) { // 舱盖开关
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DeviceCoverOpenInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DeviceCoverOpenFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DeviceCoverOpenNormalText
          cmdItem.operateText = DeviceCmdStatusText.DeviceCoverOpenBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.CoverClose) {
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DeviceCoverCloseInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DeviceCoverCloseFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DeviceCoverCloseNormalText
          cmdItem.operateText = DeviceCmdStatusText.DeviceCoverCloseBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.PutterOpen) { // 推杆闭合展开
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DevicePutterOpenInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DevicePutterOpenFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DevicePutterOpenNormalText
          cmdItem.operateText = DeviceCmdStatusText.DevicePutterOpenBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.PutterClose) {
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DevicePutterCloseInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DevicePutterCloseFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DevicePutterCloseNormalText
          cmdItem.operateText = DeviceCmdStatusText.DevicePutterCloseBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.ChargeOpen) { // 充电状态
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DeviceChargeOpenInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DeviceChargeOpenFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DeviceChargeOpenNormalText
          cmdItem.operateText = DeviceCmdStatusText.DeviceChargeOpenBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.ChargeClose) {
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DeviceChargeCloseInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DeviceChargeCloseFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.status = DeviceCmdStatusText.DeviceChargeCloseNormalText
          cmdItem.operateText = DeviceCmdStatusText.DeviceChargeCloseBtnText
          exchangeDeviceCmd(cmdItem)
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.DeviceFormat) { // 机场存储
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DeviceFormatInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DeviceFormatFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.loading = false
        }
      } else if (cmdItem.cmdKey === DeviceCmd.DroneFormat) { // 飞行器存储
        if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.InProgress) {
          cmdItem.status = DeviceCmdStatusText.DroneFormatInProgressText
          cmdItem.loading = true
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.Failed) {
          cmdItem.status = DeviceCmdStatusText.DroneFormatFailedText
          cmdItem.loading = false
        } else if (deviceCmdExecuteInfo.output.status === DeviceCmdExecuteStatus.OK) {
          cmdItem.loading = false
        }
      }
    }
  })
}
src/utils/download.ts
New file
@@ -0,0 +1,82 @@
/**
 * 加载图片
 * @param url
 * @returns
 */
export function urlToImage (url: string) {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image()
    image.src = url
    image.onload = () => { resolve(image) }
    image.onerror = () => { reject(new Error('image load error')) }
  })
}
export interface CompressImageData {
  blob: Blob | null;
  imageData: ImageData;
}
export function compressImage (imgToCompress: HTMLImageElement, targetWidth: number, targetHeight: number): Promise<CompressImageData> | undefined {
  // resizing the image
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  if (context) {
    const iWidth = imgToCompress.width
    const iHeight = imgToCompress.height
    const iRatio = iWidth / iHeight // 图像宽高比
    const tRatio = targetWidth / targetHeight // 目标宽高比
    let dw = targetWidth
    let dh = targetHeight
    let dx = 0
    let dy = 0
    if (iRatio > tRatio) {
      // 如果图像宽高比比目标宽高比要大,说明图像比目标尺寸更宽,这时候我们应该按照高度缩放比来进行缩放宽度
      dw = (targetHeight / iHeight) * iWidth
      // 宽度溢出,应该放在中间
      dx = -(dw - targetWidth) / 2
    } else {
      // 否则说明图像比目标尺寸更高,按照宽度缩放比来缩放高度
      dh = (targetWidth / iWidth) * iHeight
      // 高度溢出,应该放在中间
      dy = -(dh - targetHeight) / 2
    }
    canvas.width = targetWidth
    canvas.height = targetHeight
    context.drawImage(
      imgToCompress,
      dx,
      dy,
      dw,
      dh,
    )
    return new Promise<CompressImageData>((resolve) => {
      const imageData = context.getImageData(0, 0, canvas.width, canvas.height)
      canvas.toBlob(blob => resolve({
        blob,
        imageData,
      }))
    })
  }
}
/**
 * 根据资源url下载文件
 * @param url
 * @param fileName
 */
export function download (url: string, fileName = ''): void {
  const aLink = document.createElement('a')
  aLink.style.display = 'none'
  aLink.download = fileName
  aLink.href = url
  document.body.appendChild(aLink)
  // 避免新开页面,闪烁
  // aLink.target = '_blank'
  aLink.click()
  document.body.removeChild(aLink)
  // aLink.remove()
}
src/utils/time.ts
New file
@@ -0,0 +1,15 @@
import {
  DATE_FORMAT,
  DEFAULT_PLACEHOLDER
} from '/@/utils/constants'
import moment, { Moment } from 'moment'
// 时间字符串 或者 Unix 时间戳(毫秒数)
export function formatDateTime (time: string | number, format = DATE_FORMAT) {
  return time ? moment(time, format) : DEFAULT_PLACEHOLDER
}
// Unix 时间戳 (秒)
export function formatUnixTime (time: number, format = DATE_FORMAT): string {
  return time ? moment.unix(time).format(format) : DEFAULT_PLACEHOLDER
}
src/websocket/index.ts
New file
@@ -0,0 +1,85 @@
import { message } from 'ant-design-vue'
import ReconnectingWebSocket from 'reconnecting-websocket'
interface WebSocketOptions {
  data: any
  cache?: boolean | string
  destroyCache?: string
}
export interface MessageHandler {
  (data : {[key: string]: any}): void
}
/**
 * ConnectWebSocket 类
 * TODO: 优化messageHandler: EventEmitter。暂时传入回调函数
 */
class ConnectWebSocket {
  _url: string
  _socket: ReconnectingWebSocket | null
  _hasInit: boolean
  _messageHandler: MessageHandler | null
  constructor (url: string) {
    this._url = url
    this._socket = null
    this._hasInit = false
    this._messageHandler = null
  }
  initSocket () {
    if (this._hasInit) {
      return
    }
    if (!this._url) {
      return
    }
    // 会自动重连,无需处理重连逻辑
    this._socket = new ReconnectingWebSocket(this._url, [], {
      maxReconnectionDelay: 20000, // 断开后最大的重连时间: 20s,每多一次重连,会增加 1.3 倍,5 * 1.3 * 1.3 * 1.3...
      minReconnectionDelay: 5000, // 断开后最短的重连时间: 5s
      maxRetries: 5
    })
    this._hasInit = true
    this._socket.addEventListener('open', this._onOpen.bind(this))
    this._socket.addEventListener('close', this._onClose.bind(this))
    this._socket.addEventListener('error', this._onError.bind(this))
    this._socket.addEventListener('message', this._onMessage.bind(this))
  }
  _onOpen () {
    console.log('连接成功')
  }
  _onClose () {
    console.log('连接已断开')
  }
  _onError () {
    console.log('连接 error')
  }
  registerMessageHandler (messageHandler: MessageHandler) {
    this._messageHandler = messageHandler
  }
  _onMessage (msg: MessageEvent) {
    const data = JSON.parse(msg.data)
    this._messageHandler && this._messageHandler(data)
    // console.log('接受消息', message)
  }
  sendMessage = (message: WebSocketOptions): void => {
    this._socket?.send(JSON.stringify(message.data))
  }
  close () {
    this._socket?.close()
  }
}
export default ConnectWebSocket
src/websocket/util/config.ts
New file
@@ -0,0 +1,8 @@
import { ELocalStorageKey } from '/@/types/enums'
import { CURRENT_CONFIG } from '/@/api/http/config'
export function getWebsocketUrl () {
  const token: string = localStorage.getItem(ELocalStorageKey.Token) || '' as string
  const url = CURRENT_CONFIG.websocketURL + '?x-auth-token=' + encodeURI(token)
  return url
}
tsconfig.json
@@ -25,6 +25,6 @@
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue", 
    "src/vendors/coordtransform.js"
  ]
    "src/vendors/coordtransform.js"
    ]
}
yarn.lock
Diff too large