Browse Source

Update v1.2.0

v1.2.0
sean.zhou 2 years ago
parent
commit
fa611ff2ee
  1. 11
      package-lock.json
  2. 3
      package.json
  3. 20
      src/api/device-cmd/index.ts
  4. 172
      src/api/device-log/index.ts
  5. 47
      src/api/device-upgrade/index.ts
  6. 2
      src/api/http/type.ts
  7. 17
      src/api/manage.ts
  8. 17
      src/api/media.ts
  9. 13
      src/api/wayline.ts
  10. 33
      src/api/websocket.ts
  11. 52
      src/components/GMap.vue
  12. 10
      src/components/MediaPanel.vue
  13. 2
      src/components/TaskPanel.vue
  14. 6
      src/components/common/sidebar.vue
  15. 2
      src/components/common/topbar.vue
  16. 268
      src/components/devices/device-hms/DeviceHmsDrawer.vue
  17. 150
      src/components/devices/device-log/DeviceLogDetailModal.vue
  18. 210
      src/components/devices/device-log/DeviceLogUploadModal.vue
  19. 326
      src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue
  20. 23
      src/components/devices/device-log/use-device-log-upload-detail.ts
  21. 19
      src/components/devices/device-log/use-device-log-upload-progress-event.ts
  22. 64
      src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue
  23. 93
      src/components/devices/device-upgrade/DeviceFirmwareUpgradeModal.vue
  24. 19
      src/components/devices/device-upgrade/use-device-upgrade-event.ts
  25. 42
      src/components/devices/device-upgrade/use-device-upgrade.ts
  26. 131
      src/components/g-map/DockControlPanel.vue
  27. 48
      src/components/g-map/useDockControl.ts
  28. 10
      src/event-bus/index.ts
  29. 21
      src/hooks/use-connect-websocket.ts
  30. 18
      src/hooks/use-g-map-cover.ts
  31. 34
      src/hooks/use-g-map-tsa.ts
  32. 19
      src/hooks/use-g-map.ts
  33. 8
      src/hooks/use-mouse-tool.ts
  34. 21
      src/pages/elements/elements.vue
  35. 20
      src/pages/page-pilot/pilot-home.vue
  36. 32
      src/pages/page-web/home.vue
  37. 10
      src/pages/page-web/index.vue
  38. 0
      src/pages/page-web/projects/create-plan.vue
  39. 436
      src/pages/page-web/projects/devices.vue
  40. 0
      src/pages/page-web/projects/dock.vue
  41. 0
      src/pages/page-web/projects/layer.vue
  42. 0
      src/pages/page-web/projects/livestream.vue
  43. 0
      src/pages/page-web/projects/media.vue
  44. 15
      src/pages/page-web/projects/members.vue
  45. 0
      src/pages/page-web/projects/task.vue
  46. 4
      src/pages/page-web/projects/tsa.vue
  47. 10
      src/pages/page-web/projects/wayline.vue
  48. 45
      src/pages/page-web/projects/workspace.vue
  49. 518
      src/pages/project-app/projects/devices.vue
  50. 4
      src/root.ts
  51. 37
      src/router/index.ts
  52. 26
      src/store/index.ts
  53. 12
      src/styles/flex.style.scss
  54. 38
      src/types/airport-tsa.ts
  55. 210
      src/types/device-cmd.ts
  56. 65
      src/types/device-log.ts
  57. 186
      src/types/device.ts
  58. 19
      src/types/enums.ts
  59. 86
      src/utils/bytes.ts
  60. 8
      src/utils/color.ts
  61. 6
      src/utils/common.ts
  62. 15
      src/utils/constants.ts
  63. 350
      src/utils/device-cmd.ts
  64. 82
      src/utils/download.ts
  65. 15
      src/utils/time.ts
  66. 85
      src/websocket/index.ts
  67. 8
      src/websocket/util/config.ts
  68. 6982
      yarn.lock

11
package-lock.json generated

@ -15,6 +15,7 @@
"agora-rtc-sdk-ng": "^4.12.1", "agora-rtc-sdk-ng": "^4.12.1",
"ant-design-vue": "^2.2.8", "ant-design-vue": "^2.2.8",
"axios": "^0.21.1", "axios": "^0.21.1",
"mitt": "^3.0.0",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"vconsole": "^3.8.1", "vconsole": "^3.8.1",
@ -4901,6 +4902,11 @@
"integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=", "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=",
"license": "MIT" "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": { "node_modules/mixin-deep": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.2.tgz", "resolved": "https://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.2.tgz",
@ -11340,6 +11346,11 @@
"resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.5.tgz", "resolved": "https://registry.npm.taobao.org/minimist/download/minimist-1.2.5.tgz",
"integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=" "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": { "mixin-deep": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.2.tgz", "resolved": "https://registry.npm.taobao.org/mixin-deep/download/mixin-deep-1.3.2.tgz",

3
package.json

@ -15,6 +15,7 @@
"agora-rtc-sdk-ng": "^4.12.1", "agora-rtc-sdk-ng": "^4.12.1",
"ant-design-vue": "^2.2.8", "ant-design-vue": "^2.2.8",
"axios": "^0.21.1", "axios": "^0.21.1",
"mitt": "^3.0.0",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"vconsole": "^3.8.1", "vconsole": "^3.8.1",
@ -90,9 +91,11 @@
"ant-design-vue/es/spin/style/css", "ant-design-vue/es/spin/style/css",
"ant-design-vue/es/switch/style/css", "ant-design-vue/es/switch/style/css",
"ant-design-vue/es/table/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/tooltip/style/css",
"ant-design-vue/es/tree/style/css", "ant-design-vue/es/tree/style/css",
"axios", "axios",
"mitt",
"moment", "moment",
"reconnecting-websocket", "reconnecting-websocket",
"vconsole", "vconsole",

20
src/api/device-cmd/index.ts

@ -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
}

172
src/api/device-log/index.ts

@ -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
}

47
src/api/device-upgrade/index.ts

@ -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
}

2
src/api/http/type.ts

@ -19,11 +19,11 @@ export interface IListWorkspaceResponse<T> {
} }
// Workspace // Workspace
export interface IWorkspaceResponse<T> { export interface IWorkspaceResponse<T> {
[x: string]: number;
code: number; code: number;
data: T; data: T;
message: string; message: string;
} }
export type IStatus = 'WAITING' | 'DOING' | 'SUCCESS' | 'FAILED'; export type IStatus = 'WAITING' | 'DOING' | 'SUCCESS' | 'FAILED';
export interface CommonListResponse<T> extends IResult { export interface CommonListResponse<T> extends IResult {

17
src/api/manage.ts

@ -1,4 +1,6 @@
import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request' import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
import { Device } from '/@/types/device'
const HTTP_PREFIX = '/manage/api/v1' const HTTP_PREFIX = '/manage/api/v1'
// login // login
@ -116,7 +118,14 @@ export const getDeviceBySn = async function (workspace_id: string, device_sn: st
return result.data 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 url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}&domain=${domain}`
const result = await request.get(url) const result = await request.get(url)
return result.data return result.data
@ -141,11 +150,11 @@ export const updateDeviceHms = async function (workspace_id: string, device_sn:
} }
export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: string, pagination: IPage): Promise<IListWorkspaceResponse<any>> { 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}` + let url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms?page=${pagination.page}&page_size=${pagination.page_size}` +
`&level=${body.level ?? ''}&beginTime=${body.begin_time ?? ''}&endTime=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}` `&level=${body.level ?? ''}&begin_time=${body.begin_time ?? ''}&end_time=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}`
body.sns.forEach((sn: string) => { body.sns.forEach((sn: string) => {
if (sn !== '') { if (sn !== '') {
url = url.concat(`&deviceSn=${sn}`) url = url.concat(`&device_sn=${sn}`)
} }
}) })
const result = await request.get(url) const result = await request.get(url)

17
src/api/media.ts

@ -1,3 +1,4 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request' import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/media/api/v1' const HTTP_PREFIX = '/media/api/v1'
@ -8,11 +9,19 @@ export const getMediaFiles = async function (wid: string, pagination: IPage): Pr
return result.data return result.data
} }
// Download Media File // Download Media File
export const downloadMediaFile = async function (workspaceId: string, fingerprint: string): Promise<any> { export const downloadMediaFile = async function (workspaceId: string, fileId: string): Promise<any> {
const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fingerprint}/url` const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fileId}/url`
const result = await request.get(url, { responseType: 'blob' }) 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.data
} }
return result
} }

13
src/api/wayline.ts

@ -1,3 +1,4 @@
import { message } from 'ant-design-vue'
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request' import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1' const HTTP_PREFIX = '/wayline/api/v1'
@ -20,10 +21,18 @@ export const getWaylineFiles = async function (wid: string, body: {}): Promise<I
export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> { export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> {
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url` const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url`
const result = await request.get(url, { responseType: 'blob' }) 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.data
} }
return result
} }
// Delete Wayline File // Delete Wayline File

33
src/api/websocket.ts

@ -1,33 +0,0 @@
import ReconnectingWebSocket from 'reconnecting-websocket'
import { ELocalStorageKey } from '../types/enums'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
let socket: ReconnectingWebSocket
export default {
init (getMsgFunc: any) {
const token: string = localStorage.getItem(ELocalStorageKey.Token)!
const wspath = config.websocketURL + '?x-auth-token=' + encodeURI(token)
socket = new ReconnectingWebSocket(wspath, '', { maxRetries: 5 })
socket.onopen = this.onOpen
socket.onerror = this.onError
socket.onmessage = getMsgFunc
socket.onclose = this.onClose
return socket
},
onOpen () {
console.log('ws opened')
},
onError (err: any) {
console.error(err)
},
onClose () {
console.log('ws closed')
},
sendMsg (data: any) {
socket.send(data)
},
close () {
socket.close()
}
}

52
src/components/GMap.vue

@ -137,13 +137,15 @@
</div> </div>
</div> </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%;"> <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>{{ osdVisible.gateway_callsign }}</span>
<span><a style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span> <span><a style="color: white;" @click="() => osdVisible.visible = false"><CloseOutlined /></a></span>
</div> </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"> <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><RobotFilled style="font-size: 48px;"/></span>
@ -151,7 +153,7 @@
</div> </div>
</a-tooltip> </a-tooltip>
</div> </div>
<div class="osd"> <div class="osd flex-1" style="flex: 1">
<a-row> <a-row>
<a-col span="16" :style="deviceInfo.dock.mode_code === EDockModeCode.Disconnected ? 'color: red; font-weight: 700;': 'color: rgb(25,190,107)'"> <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> {{ EDockModeCode[deviceInfo.dock.mode_code] }}</a-col>
@ -260,10 +262,20 @@
</a-tooltip> </a-tooltip>
</a-col> </a-col>
</a-row> </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> </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"> <a-tooltip :title="osdVisible.model">
<div style="width: 90%;" class="flex-column flex-align-center flex-justify-center"> <div style="width: 90%;" class="flex-column flex-align-center flex-justify-center">
<span><a-image :src="M30" :preview="false"/></span> <span><a-image :src="M30" :preview="false"/></span>
@ -271,7 +283,7 @@
</div> </div>
</a-tooltip> </a-tooltip>
</div> </div>
<div class="osd"> <div class="osd flex-1">
<a-row> <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)'"> <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> {{ !deviceInfo.device ? EModeCode[EModeCode.Disconnected] : EModeCode[deviceInfo.device?.mode_code] }}</a-col>
@ -409,6 +421,8 @@ import {
FieldTimeOutlined, CloudOutlined, CloudFilled, FolderOpenOutlined, RobotFilled, ArrowUpOutlined FieldTimeOutlined, CloudOutlined, CloudFilled, FolderOpenOutlined, RobotFilled, ArrowUpOutlined
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { EDeviceTypeName } from '../types' import { EDeviceTypeName } from '../types'
import DockControlPanel from './g-map/DockControlPanel.vue'
import { useDockControl } from './g-map/useDockControl'
export default defineComponent({ export default defineComponent({
components: { components: {
@ -428,7 +442,8 @@ export default defineComponent({
FolderOpenOutlined, FolderOpenOutlined,
RobotFilled, RobotFilled,
ArrowUpOutlined, ArrowUpOutlined,
ArrowDownOutlined ArrowDownOutlined,
DockControlPanel
}, },
name: 'GMap', name: 'GMap',
props: {}, props: {},
@ -642,6 +657,15 @@ export default defineComponent({
useMouseToolHook.mouseTool(type, getDrawCallback) useMouseToolHook.mouseTool(type, getDrawCallback)
mouseMode.value = bool mouseMode.value = bool
} }
// dock
const {
controlPanelVisible,
setControlPanelVisible,
sendDockControlCmd,
dockDebugOnOff,
} = useDockControl()
onMounted(() => { onMounted(() => {
const app = getApp() const app = getApp()
useGMapManageHook.globalPropertiesConfig(app) useGMapManageHook.globalPropertiesConfig(app)
@ -813,15 +837,19 @@ export default defineComponent({
EModeCode, EModeCode,
str, str,
EDockModeCode, EDockModeCode,
controlPanelVisible,
dockDebugOnOff,
} }
} }
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.g-map-wrapper { .g-map-wrapper {
height: 100%; height: 100%;
width: 100%; width: 100%;
.g-action-panle { .g-action-panle {
position: absolute; position: absolute;
top: 16px; top: 16px;
@ -845,13 +873,19 @@ export default defineComponent({
border: 1px solid $primary; border: 1px solid $primary;
border-radius: 2px; border-radius: 2px;
} }
// antd button
&:deep(.ant-btn){
&::after {
display: none;
}
}
} }
.osd-panel { .osd-panel {
position: absolute; position: absolute;
left: 350px; left: 350px;
top: 10px; top: 10px;
width: 480px; width: 480px;
height: 160px;
background: black; background: black;
color: white; color: white;
border-radius: 2px; border-radius: 2px;

10
src/components/MediaPanel.vue

@ -32,7 +32,7 @@ import { ELocalStorageKey } from '../types/enums'
import { downloadFile } from '../utils/common' import { downloadFile } from '../utils/common'
import { downloadMediaFile, getMediaFiles } from '/@/api/media' import { downloadMediaFile, getMediaFiles } from '/@/api/media'
import { DownloadOutlined } from '@ant-design/icons-vue' 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' import { load } from '@amap/amap-jsapi-loader'
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
@ -101,6 +101,7 @@ interface MediaFile {
file_name: string, file_name: string,
file_path: string, file_path: string,
create_time: string, create_time: string,
file_id: string,
} }
const mediaData = reactive({ const mediaData = reactive({
@ -128,12 +129,11 @@ function refreshData (page: Pagination) {
function downloadMedia (media: MediaFile) { function downloadMedia (media: MediaFile) {
loading.value = true loading.value = true
downloadMediaFile(workspaceId, media.fingerprint).then(res => { downloadMediaFile(workspaceId, media.file_id).then(res => {
if (res.code && res.code !== 0) { if (!res) {
return return
} }
const suffix = media.file_name.substring(media.file_name.lastIndexOf('.')) const data = new Blob([res])
const data = new Blob([res.data], { type: suffix === '.mp4' ? 'video/mp4' : 'image/jpeg' })
downloadFile(data, media.file_name) downloadFile(data, media.file_name)
}).finally(() => { }).finally(() => {
loading.value = false loading.value = false

2
src/components/TaskPanel.vue

@ -9,7 +9,7 @@
<template #status="{ record }"> <template #status="{ record }">
<span v-if="taskProgressMap[record.bid]"> <span v-if="taskProgressMap[record.bid]">
<a-progress type="line" :percent="taskProgressMap[record.bid]?.progress?.percent" <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"> <template #format="percent">
<a-tooltip :title="taskProgressMap[record.bid]?.status"> <a-tooltip :title="taskProgressMap[record.bid]?.status">
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;"> <div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;">

6
src/pages/project-app/sidebar.vue → src/components/common/sidebar.vue

@ -28,7 +28,6 @@ import { createVNode, defineComponent } from 'vue'
import { getRoot } from '/@/root' import { getRoot } from '/@/root'
import * as icons from '@ant-design/icons-vue' import * as icons from '@ant-design/icons-vue'
import { ERouterName } from '/@/types' import { ERouterName } from '/@/types'
import websocket from '/@/api/websocket'
interface IOptions { interface IOptions {
key: number key: number
@ -58,7 +57,7 @@ export default defineComponent({
{ key: 1, label: 'Livestream', path: '/' + ERouterName.LIVESTREAM, icon: 'VideoCameraOutlined' }, { key: 1, label: 'Livestream', path: '/' + ERouterName.LIVESTREAM, icon: 'VideoCameraOutlined' },
{ key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' }, { key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
{ key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' }, { 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' } { key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' }
] ]
@ -66,10 +65,11 @@ export default defineComponent({
const path = typeof item.path === 'string' ? item.path : item.path.path const path = typeof item.path === 'string' ? item.path : item.path.path
return root.$route.path?.indexOf(path) === 0 return root.$route.path?.indexOf(path) === 0
} }
function goHome () { function goHome () {
root.$router.push('/' + ERouterName.MEMBERS) root.$router.push('/' + ERouterName.MEMBERS)
websocket.close()
} }
return { return {
options, options,
selectedRoute, selectedRoute,

2
src/pages/project-app/topbar.vue → src/components/common/topbar.vue

@ -44,8 +44,6 @@ import { getPlatformInfo } from '/@/api/manage'
import { ELocalStorageKey, ERouterName } from '/@/types' import { ELocalStorageKey, ERouterName } from '/@/types'
import { UserOutlined, ExportOutlined } from '@ant-design/icons-vue' import { UserOutlined, ExportOutlined } from '@ant-design/icons-vue'
import cloudapi from '/@/assets/icons/cloudapi.png' import cloudapi from '/@/assets/icons/cloudapi.png'
import ReconnectingWebSocket from 'reconnecting-websocket'
import websocket from '/@/api/websocket'
const root = getRoot() const root = getRoot()

268
src/components/devices/device-hms/DeviceHmsDrawer.vue

@ -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>

150
src/components/devices/device-log/DeviceLogDetailModal.vue

@ -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>
>

210
src/components/devices/device-log/DeviceLogUploadModal.vue

@ -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>

326
src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue

@ -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>

23
src/components/devices/device-log/use-device-log-upload-detail.ts

@ -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
}
}

19
src/components/devices/device-log/use-device-log-upload-progress-event.ts

@ -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)
})
}

64
src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue

@ -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>

93
src/components/devices/device-upgrade/DeviceFirmwareUpgradeModal.vue

@ -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>

19
src/components/devices/device-upgrade/use-device-upgrade-event.ts

@ -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)
})
}

42
src/components/devices/device-upgrade/use-device-upgrade.ts

@ -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,
}
}

131
src/components/g-map/DockControlPanel.vue

@ -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>

48
src/components/g-map/useDockControl.ts

@ -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,
}
}

10
src/event-bus/index.ts

@ -0,0 +1,10 @@
import mitt, { Emitter } from 'mitt'
type Events = {
deviceUpgrade: any;
deviceLogUploadProgress: any
};
const emitter: Emitter<Events> = mitt<Events>()
export default emitter

21
src/hooks/use-connect-websocket.ts

@ -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()
})
}

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

@ -10,16 +10,18 @@ import { GeojsonCoordinate } from '/@/types/map'
export function useGMapCover () { export function useGMapCover () {
const root = getRoot() const root = getRoot()
const AMap = root.$aMapObj const AMap = root.$aMap
const normalColor = '#2D8CF0' const normalColor = '#2D8CF0'
const store = rootStore const store = rootStore
const coverList = store.state.coverList const coverList = store.state.coverList
function AddCoverToMap (cover :any) { function AddCoverToMap (cover :any) {
root.$aMap.add(cover) root.$map.add(cover)
coverList.push(cover) coverList.push(cover)
// console.log('coverList:', store.state.coverList) // console.log('coverList:', store.state.coverList)
} }
function getPinIcon (color?:string) { function getPinIcon (color?:string) {
// console.log('color', color) // console.log('color', color)
const colorObj: { const colorObj: {
@ -31,7 +33,6 @@ export function useGMapCover () {
'b620e0': pinb620e0, 'b620e0': pinb620e0,
'e23c39': pine23c39, 'e23c39': pine23c39,
'ffbb00': pineffbb00, 'ffbb00': pineffbb00,
} }
const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase() const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
return new AMap.Icon({ return new AMap.Icon({
@ -41,6 +42,7 @@ export function useGMapCover () {
// imageSize: new AMap.Size(40, 50) // imageSize: new AMap.Size(40, 50)
}) })
} }
function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) { function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) {
console.log(name, coordinates[0], coordinates[1], color, data) console.log(name, coordinates[0], coordinates[1], color, data)
const pin = new AMap.Marker({ const pin = new AMap.Marker({
@ -54,8 +56,9 @@ export function useGMapCover () {
// console.log('coordinates pin', pin) // console.log('coordinates pin', pin)
AddCoverToMap(pin) AddCoverToMap(pin)
} }
function AddOverlayGroup (overlayGroup) { function AddOverlayGroup (overlayGroup) {
root.$aMap.add(overlayGroup) root.$map.add(overlayGroup)
coverList.push(overlayGroup) coverList.push(overlayGroup)
} }
function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) { function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
@ -74,6 +77,7 @@ export function useGMapCover () {
}) })
AddOverlayGroup(polyline) AddOverlayGroup(polyline)
} }
function initPolygon (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) { function initPolygon (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
const path = [] as GeojsonCoordinate[] const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => { coordinates.forEach(coordinate => {
@ -92,6 +96,7 @@ export function useGMapCover () {
}) })
AddOverlayGroup(Polygon) AddOverlayGroup(Polygon)
} }
function removeCoverFromMap (id:string) { function removeCoverFromMap (id:string) {
for (let i = 0; i < coverList.length; i++) { for (let i = 0; i < coverList.length; i++) {
const ele = coverList[i] const ele = coverList[i]
@ -99,12 +104,13 @@ export function useGMapCover () {
const extdata = ele?.getExtData() const extdata = ele?.getExtData()
if (extdata?.id === id) { if (extdata?.id === id) {
console.log(extdata) console.log(extdata)
root.$aMap.remove(ele) root.$map.remove(ele)
coverList.slice(i, 1) coverList.slice(i, 1)
break break
} }
} }
} }
function getElementFromMap (id:string) { function getElementFromMap (id:string) {
// console.log('start', new Date().getTime()) // console.log('start', new Date().getTime())
const ele = coverList.find(ele => ele?.getExtData().id === id) const ele = coverList.find(ele => ele?.getExtData().id === id)
@ -118,6 +124,7 @@ export function useGMapCover () {
// } // }
// }) // })
} }
function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) { function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) {
const element = getElementFromMap(id) as any const element = getElementFromMap(id) as any
if (element) { if (element) {
@ -133,6 +140,7 @@ export function useGMapCover () {
}) })
} }
} }
return { return {
init2DPin, init2DPin,
initPolyline, initPolyline,

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

@ -6,23 +6,30 @@ import { message } from 'ant-design-vue'
export function deviceTsaUpdate () { export function deviceTsaUpdate () {
const root = getRoot() const root = getRoot()
const AMap = root.$aMapObj const AMap = root.$aMap
const map = root.$aMap
const icons: { const icons: {
[key: string]: string [key: string]: string
} = { } = {
'sub-device' : '/@/assets/icons/drone.png', 'sub-device': '/@/assets/icons/drone.png',
'gateway': '/@/assets/icons/rc.png', 'gateway': '/@/assets/icons/rc.png',
'dock': '/@/assets/icons/dock.png' 'dock': '/@/assets/icons/dock.png'
} }
const markers = store.state.markerInfo.coverMap const markers = store.state.markerInfo.coverMap
const paths = store.state.markerInfo.pathMap const paths = store.state.markerInfo.pathMap
const passedPolyline = new AMap.Polyline({ // Fix: 航迹初始化报错
map: map, // TODO: 从时序上解决
let trackLine = null as any
function getTrackLineInstance () {
if (!trackLine) {
trackLine = new AMap.Polyline({
map: root.$map,
strokeColor: '#939393' // 线颜色 strokeColor: '#939393' // 线颜色
}) })
}
return trackLine
}
function initIcon (type: string) { function initIcon (type: string) {
return new AMap.Icon({ return new AMap.Icon({
@ -36,13 +43,13 @@ export function deviceTsaUpdate () {
return return
} }
markers[sn] = new AMap.Marker({ 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), icon: initIcon(type),
title: name, title: name,
anchor: 'top-center', anchor: 'top-center',
offset: [0, -20], offset: [0, -20],
}) })
root.$aMap.add(markers[sn]) root.$map.add(markers[sn])
// markers[sn].on('moving', function (e: any) { // markers[sn].on('moving', function (e: any) {
// let path = paths[sn] // let path = paths[sn]
@ -52,19 +59,21 @@ export function deviceTsaUpdate () {
// } // }
// path.push(e.passedPath[0]) // path.push(e.passedPath[0])
// path.push(e.passedPath[1]) // path.push(e.passedPath[1])
// passedPolyline.setPath(path) // getTrackLineInstance().setPath(path)
// }) // })
} }
function removeMarker (sn: string) { function removeMarker (sn: string) {
if (!markers[sn]) { if (!markers[sn]) {
return return
} }
root.$aMap.remove(markers[sn]) root.$map.remove(markers[sn])
passedPolyline.setPath([]) getTrackLineInstance().setPath([])
delete markers[sn] delete markers[sn]
delete paths[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) getDeviceBySn(localStorage.getItem(ELocalStorageKey.WorkspaceId)!, sn)
.then(data => { .then(data => {
if (data.code !== 0) { if (data.code !== 0) {
@ -74,7 +83,8 @@ export function deviceTsaUpdate () {
initMarker(data.data.domain, data.data.nickname, sn, lng, lat) 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] let marker = markers[sn]
if (!marker) { if (!marker) {
addMarker(sn, lng, lat) addMarker(sn, lng, lat)

19
src/hooks/use-g-map.ts

@ -4,30 +4,35 @@ import { AMapConfig } from '/@/constants/index'
export function useGMapManage () { export function useGMapManage () {
const state = reactive({ const state = reactive({
mapEntity: null, aMap: null, // Map类
mapObj: null, map: null, // 地图对象
mouseTool: null, mouseTool: null,
}) })
async function initMap (container: string, app:App) { async function initMap (container: string, app:App) {
AMapLoader.load({ AMapLoader.load({
...AMapConfig ...AMapConfig
}).then((AMap) => { }).then((AMap) => {
state.mapObj = AMap state.aMap = AMap
state.mapEntity = new AMap.Map(container, { state.map = new AMap.Map(container, {
center: [113.935913, 22.525335], center: [113.935913, 22.525335],
zoom: 15 zoom: 15
}) })
state.mouseTool = new AMap.MouseTool(state.mapEntity) state.mouseTool = new AMap.MouseTool(state.map)
app.config.globalProperties.$aMap = state.mapEntity
app.config.globalProperties.$aMapObj = state.mapObj // 挂在到全局
app.config.globalProperties.$aMap = state.aMap
app.config.globalProperties.$map = state.map
app.config.globalProperties.$mouseTool = state.mouseTool app.config.globalProperties.$mouseTool = state.mouseTool
}).catch(e => { }).catch(e => {
console.log(e) console.log(e)
}) })
} }
function globalPropertiesConfig (app:App) { function globalPropertiesConfig (app:App) {
initMap('g-container', app) initMap('g-container', app)
} }
return { return {
globalPropertiesConfig, globalPropertiesConfig,
} }

8
src/hooks/use-mouse-tool.ts

@ -6,13 +6,14 @@ import { MapDoodleEnum } from '/@/types/map-enum'
export function useMouseTool () { export function useMouseTool () {
const root = getRoot() const root = getRoot()
const AMap = root.$aMapObj
const state = reactive({ const state = reactive({
pinNum: 0, pinNum: 0,
polylineNum: 0, polylineNum: 0,
PolygonNum: 0, PolygonNum: 0,
currentType: '', currentType: '',
}) })
function drawPin (type:MapDoodleType, getDrawCallback:Function) { function drawPin (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.marker({ root?.$mouseTool.marker({
title: type + state.pinNum, title: type + state.pinNum,
@ -21,6 +22,7 @@ export function useMouseTool () {
state.pinNum++ state.pinNum++
root?.$mouseTool.on('draw', getDrawCallback) root?.$mouseTool.on('draw', getDrawCallback)
} }
function drawPolyline (type:MapDoodleType, getDrawCallback:Function) { function drawPolyline (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.polyline({ root?.$mouseTool.polyline({
strokeColor: '#2d8cf0', strokeColor: '#2d8cf0',
@ -32,6 +34,7 @@ export function useMouseTool () {
}) })
root?.$mouseTool.on('draw', getDrawCallback) root?.$mouseTool.on('draw', getDrawCallback)
} }
function drawPolygon (type:MapDoodleType, getDrawCallback:Function) { function drawPolygon (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.polygon({ root?.$mouseTool.polygon({
strokeColor: '#2d8cf0', strokeColor: '#2d8cf0',
@ -44,10 +47,12 @@ export function useMouseTool () {
}) })
root?.$mouseTool.on('draw', getDrawCallback) root?.$mouseTool.on('draw', getDrawCallback)
} }
function drawOff (type:MapDoodleType) { function drawOff (type:MapDoodleType) {
root?.$mouseTool.close() root?.$mouseTool.close()
root?.$mouseTool.off('draw') root?.$mouseTool.off('draw')
} }
function mouseTool (type: MapDoodleType, getDrawCallback: Function) { function mouseTool (type: MapDoodleType, getDrawCallback: Function) {
state.currentType = type state.currentType = type
switch (type) { switch (type) {
@ -65,6 +70,7 @@ export function useMouseTool () {
break break
} }
} }
return { return {
mouseTool mouseTool
} }

21
src/pages/elements/elements.vue

@ -1,21 +0,0 @@
<template>
<div class="element-map-wrapper">
<GMap/>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue'
import GMap from '/@/components/GMap.vue'
export default defineComponent({
name: 'Elements',
components: { GMap },
setup () {
return {
}
},
})
</script>
<style lang="scss" scoped>
</style>

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

@ -130,9 +130,8 @@ import { EBizCode, EComponentName, EDownloadOwner, ELocalStorageKey, ERouterName
import cloudapi from '/@/assets/icons/cloudapi.png' import cloudapi from '/@/assets/icons/cloudapi.png'
import { RightOutlined, CloudOutlined, CloudSyncOutlined, SyncOutlined } from '@ant-design/icons-vue' import { RightOutlined, CloudOutlined, CloudSyncOutlined, SyncOutlined } from '@ant-design/icons-vue'
import { useMyStore } from '/@/store' import { useMyStore } from '/@/store'
import ReconnectingWebSocket from 'reconnecting-websocket'
import websocket from '/@/api/websocket'
import { DeviceStatus } from '/@/types/device' import { DeviceStatus } from '/@/types/device'
import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
const root = getRoot() const root = getRoot()
const gatewayState = ref<boolean>(localStorage.getItem(ELocalStorageKey.GatewayOnline) === 'true') const gatewayState = ref<boolean>(localStorage.getItem(ELocalStorageKey.GatewayOnline) === 'true')
@ -211,8 +210,10 @@ const modules = [{
const store = useMyStore() const store = useMyStore()
const wsGetMsg = async (res: any) => { const messageHandler = async (payload: any) => {
const payload = JSON.parse(res.data) if (!payload) {
return
}
switch (payload.biz_code) { switch (payload.biz_code) {
case EBizCode.DeviceOnline: { case EBizCode.DeviceOnline: {
console.info('online: ', payload) console.info('online: ', payload)
@ -241,8 +242,11 @@ const wsGetMsg = async (res: any) => {
break break
} }
} }
// ws
useConnectWebSocket(messageHandler)
let bindNum: number let bindNum: number
let socket: ReconnectingWebSocket
onMounted(() => { onMounted(() => {
apiPilot.onBackClickReg() apiPilot.onBackClickReg()
@ -260,8 +264,6 @@ onMounted(() => {
device.data.sn = apiPilot.getAircraftSN() device.data.sn = apiPilot.getAircraftSN()
getDeviceInfo() getDeviceInfo()
socket = websocket.init(wsGetMsg)
const isLoaded = apiPilot.isComponentLoaded(EComponentName.Thing) const isLoaded = apiPilot.isComponentLoaded(EComponentName.Thing)
if (isLoaded) { if (isLoaded) {
username.value = '' + localStorage.getItem(ELocalStorageKey.Username) username.value = '' + localStorage.getItem(ELocalStorageKey.Username)
@ -302,10 +304,6 @@ onMounted(() => {
} }
}) })
onUnmounted(() => {
socket.close()
})
const connectCallback = async (arg: any) => { const connectCallback = async (arg: any) => {
if (arg) { if (arg) {
thingState.value = EStatusValue.CONNECTED thingState.value = EStatusValue.CONNECTED

32
src/pages/project-app/home.vue → src/pages/page-web/home.vue

@ -11,16 +11,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Topbar from './topbar.vue' import Topbar from '/@/components/common/topbar.vue'
import { message } from 'ant-design-vue'
import { onMounted, reactive, ref, UnwrapRef, watch } from 'vue' import { onMounted, reactive, ref, UnwrapRef, watch } from 'vue'
import { getPlatformInfo, getUserInfo } from '/@/api/manage'
import websocket from '/@/api/websocket'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { getRoot } from '/@/root' import { getRoot } from '/@/root'
import { useMyStore } from '/@/store' import { EBizCode, ELocalStorageKey, ERouterName } from '/@/types'
import { ELocalStorageKey, ERouterName } from '/@/types' import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
import ReconnectingWebSocket from 'reconnecting-websocket' import EventBus from '/@/event-bus'
interface FormState { interface FormState {
user: string user: string
@ -28,7 +24,25 @@ interface FormState {
} }
const root = getRoot() const root = getRoot()
const showLogin = ref(true)
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(() => { onMounted(() => {
const token = localStorage.getItem(ELocalStorageKey.Token) const token = localStorage.getItem(ELocalStorageKey.Token)

10
src/pages/project-app/index.vue → src/pages/page-web/index.vue

@ -34,7 +34,7 @@
class="m0" class="m0"
type="primary" type="primary"
html-type="submit" html-type="submit"
:disabled="formState.user === '' || formState.password === ''" :disabled="loginBtnDisabled"
@click="onSubmit" @click="onSubmit"
> >
Login Login
@ -49,23 +49,27 @@
import djiLogo from '/@/assets/icons/dji_logo.png' import djiLogo from '/@/assets/icons/dji_logo.png'
import { LockOutlined, UserOutlined } from '@ant-design/icons-vue' import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-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 { login, LoginBody } from '/@/api/manage'
import { getRoot } from '/@/root' import { getRoot } from '/@/root'
import { ELocalStorageKey, ERouterName, EUserType } from '/@/types' import { ELocalStorageKey, ERouterName, EUserType } from '/@/types'
import router from '/@/router' import router from '/@/router'
const root = getRoot() const root = getRoot()
const formState: UnwrapRef<LoginBody> = reactive({ const formState: UnwrapRef<LoginBody> = reactive({
username: 'adminPC', username: 'adminPC',
password: 'adminPC', password: 'adminPC',
flag: EUserType.Web, flag: EUserType.Web,
}) })
const loginBtnDisabled = computed(() => {
return !formState.username || !formState.password
})
const onSubmit = async (e: any) => { const onSubmit = async (e: any) => {
const result = await login(formState) const result = await login(formState)
if (result.code === 0) { if (result.code === 0) {
console.log(result)
localStorage.setItem(ELocalStorageKey.Token, result.data.access_token) localStorage.setItem(ELocalStorageKey.Token, result.data.access_token)
localStorage.setItem(ELocalStorageKey.WorkspaceId, result.data.workspace_id) localStorage.setItem(ELocalStorageKey.WorkspaceId, result.data.workspace_id)
localStorage.setItem(ELocalStorageKey.Username, result.data.username) localStorage.setItem(ELocalStorageKey.Username, result.data.username)

0
src/pages/project-app/projects/create-plan.vue → src/pages/page-web/projects/create-plan.vue

436
src/pages/page-web/projects/devices.vue

@ -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>

0
src/pages/project-app/projects/dock.vue → src/pages/page-web/projects/dock.vue

0
src/pages/project-app/projects/layer.vue → src/pages/page-web/projects/layer.vue

0
src/pages/project-app/projects/livestream.vue → src/pages/page-web/projects/livestream.vue

0
src/pages/project-app/projects/media.vue → src/pages/page-web/projects/media.vue

15
src/pages/project-app/projects/members.vue → src/pages/page-web/projects/members.vue

@ -57,13 +57,13 @@ interface MemberData {
member: Member[] member: Member[]
} }
const columns = [ const columns = [
{ title: 'Account', dataIndex: 'username', width: 250, sorter: (a: Member, b: Member) => a.username.localeCompare(b.username), className: 'titleStyle' }, { 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: 250, className: 'titleStyle' }, { title: 'User Type', dataIndex: 'user_type', width: 150, className: 'titleStyle' },
{ title: 'Workspace Name', dataIndex: 'workspace_name', width: 250, className: 'titleStyle' }, { title: 'Workspace Name', dataIndex: 'workspace_name', width: 150, className: 'titleStyle' },
{ title: 'Mqtt Username', dataIndex: 'mqtt_username', width: 250, className: 'titleStyle', slots: { customRender: 'mqtt_username' } }, { title: 'Mqtt Username', dataIndex: 'mqtt_username', width: 150, className: 'titleStyle', slots: { customRender: 'mqtt_username' } },
{ title: 'Mqtt Password', dataIndex: 'mqtt_password', width: 250, className: 'titleStyle', slots: { customRender: 'mqtt_password' } }, { title: 'Mqtt Password', dataIndex: 'mqtt_password', width: 150, 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: 'Joined', dataIndex: 'create_time', width: 150, sorter: (a: Member, b: Member) => a.create_time.localeCompare(b.create_time), className: 'titleStyle' },
{ title: 'Action', dataIndex: 'action', className: 'titleStyle', slots: { customRender: 'action' } }, { title: 'Action', dataIndex: 'action', width: 100, className: 'titleStyle', slots: { customRender: 'action' } },
] ]
const data = reactive<MemberData>({ const data = reactive<MemberData>({
@ -117,7 +117,6 @@ function getAllUsers (workspaceId: string, page: IPage) {
data.member = userList data.member = userList
paginationProp.total = res.data.pagination.total paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page paginationProp.current = res.data.pagination.page
}) })
} }

0
src/pages/project-app/projects/task.vue → src/pages/page-web/projects/task.vue

4
src/pages/project-app/projects/tsa.vue → src/pages/page-web/projects/tsa.vue

@ -24,9 +24,9 @@
</a-tooltip> </a-tooltip>
</div> </div>
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> <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="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] }} {{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].mode_code] : EDockModeCode[EDockModeCode.Disconnected] }}
</span> </span>
</div> </div>

10
src/pages/project-app/projects/wayline.vue → src/pages/page-web/projects/wayline.vue

@ -8,6 +8,7 @@
</a-row> </a-row>
</div> </div>
<div class="height-100"> <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 class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll">
<div v-for="wayline in waylinesData.data" :key="wayline.id"> <div v-for="wayline in waylinesData.data" :key="wayline.id">
<div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)"> <div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)">
@ -62,6 +63,7 @@
</div> </div>
</template> </template>
</a-modal> </a-modal>
</a-spin>
</div> </div>
</div> </div>
</template> </template>
@ -79,6 +81,7 @@ import { WaylineFile } from '/@/types/wayline'
import { downloadFile } from '/@/utils/common' import { downloadFile } from '/@/utils/common'
import { IPage } from '/@/api/http/type' import { IPage } from '/@/api/http/type'
const loading = ref(false)
const store = useMyStore() const store = useMyStore()
const pagination :IPage = { const pagination :IPage = {
page: 1, page: 1,
@ -154,12 +157,15 @@ function deleteWayline () {
} }
function downloadWayline (waylineId: string, fileName: string) { function downloadWayline (waylineId: string, fileName: string) {
loading.value = true
downloadWaylineFile(workspaceId, waylineId).then(res => { downloadWaylineFile(workspaceId, waylineId).then(res => {
if (res.code && res.code !== 0) { if (!res) {
return return
} }
const data = new Blob([res.data], { type: 'application/zip' }) const data = new Blob([res], { type: 'application/zip' })
downloadFile(data, fileName + '.kmz') downloadFile(data, fileName + '.kmz')
}).finally(() => {
loading.value = false
}) })
} }

45
src/pages/project-app/projects/workspace.vue → src/pages/page-web/projects/workspace.vue

@ -21,22 +21,23 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Sidebar from '../sidebar.vue' import Sidebar from '/@/components/common/sidebar.vue'
import MediaPanel from '/@/components/MediaPanel.vue' import MediaPanel from '/@/components/MediaPanel.vue'
import TaskPanel from '/@/components/TaskPanel.vue' import TaskPanel from '/@/components/TaskPanel.vue'
import GMap from '/@/components/GMap.vue' import GMap from '/@/components/GMap.vue'
import { EBizCode, ERouterName } from '/@/types' import { EBizCode, ERouterName } from '/@/types'
import { getRoot } from '/@/root' import { getRoot } from '/@/root'
import { onMounted, onUnmounted, watch } from 'vue'
import ReconnectingWebSocket from 'reconnecting-websocket'
import { useMyStore } from '/@/store' import { useMyStore } from '/@/store'
import websocket from '/@/api/websocket' import { useConnectWebSocket } from '/@/hooks/use-connect-websocket'
// import { enableAgoraLive, enableOthersLive } from '/@/pages/project-app/projects/livestream.vue'
const root = getRoot() const root = getRoot()
const store = useMyStore()
const messageHandler = async (payload: any) => {
if (!payload) {
return
}
const wsGetMsg = async (res: any) => {
const payload = JSON.parse(res.data)
switch (payload.biz_code) { switch (payload.biz_code) {
case EBizCode.GatewayOsd: { case EBizCode.GatewayOsd: {
store.commit('SET_GATEWAY_INFO', payload.data) store.commit('SET_GATEWAY_INFO', payload.data)
@ -78,21 +79,33 @@ const wsGetMsg = async (res: any) => {
store.commit('SET_DEVICE_HMS_INFO', payload.data) store.commit('SET_DEVICE_HMS_INFO', payload.data)
break 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: default:
break break
} }
} }
const store = useMyStore() // ws
useConnectWebSocket(messageHandler)
let socket: ReconnectingWebSocket
onMounted(() => {
socket = websocket.init(wsGetMsg)
})
onUnmounted(() => {
socket.close()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '/@/styles/index.scss'; @import '/@/styles/index.scss';

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

@ -1,518 +0,0 @@
<template>
<a-menu v-model:selectedKeys="current" mode="horizontal" @select="select">
<a-menu-item :key="EDeviceTypeName.Aircraft" class="ml20">
Aircraft
</a-menu-item>
<a-menu-item :key="EDeviceTypeName.Dock">
Dock
</a-menu-item>
</a-menu>
<div class="table flex-display flex-column">
<a-table :columns="columns" :data-source="data.device" :pagination="paginationProp" @change="refreshData" row-key="device_sn" :expandedRowKeys="expandRows"
:row-selection="rowSelection" :rowClassName="rowClassName" :scroll="{ x: '100%', y: 600 }"
:expandIcon="expandIcon" :loading="loading">
<template v-for="col in ['nickname']" #[col]="{ text, record }" :key="col">
<div>
<a-input
v-if="editableData[record.device_sn]"
v-model:value="editableData[record.device_sn][col]"
style="margin: -5px 0"
/>
<template v-else>
{{ text }}
</template>
</div>
</template>
<template v-for="col in ['sn', 'workspace']" #[col]="{ text }" :key="col">
<a-tooltip :title="text">
<span>{{ text }}</span>
</a-tooltip>
</template>
<template #status="{ text }">
<span v-if="text" class="flex-row flex-align-center">
<span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: green;" />
<span>Online</span>
</span>
<span class="flex-row flex-align-center" v-else>
<span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: red;" />
<span>Offline</span>
</span>
</template>
<template #action="{ record }">
<div class="editable-row-operations">
<span v-if="editableData[record.device_sn]">
<a-tooltip title="Confirm changes">
<span @click="save(record)" style="color: #28d445;"><CheckOutlined /></span>
</a-tooltip>
<a-tooltip title="Modification canceled">
<span @click="() => delete editableData[record.device_sn]" class="ml15" style="color: #e70102;"><CloseOutlined /></span>
</a-tooltip>
</span>
<span v-else class="flex-align-center flex-row" style="color: #2d8cf0">
<a-tooltip v-if="current.indexOf(EDeviceTypeName.Dock) !== -1" title="Hms Info">
<FileSearchOutlined @click="showHms(record)"/>
</a-tooltip>
<a-tooltip title="Edit">
<EditOutlined @click="edit(record)" class="ml10" />
</a-tooltip>
<a-tooltip title="Delete">
<DeleteOutlined @click="() => { deleteTip = true, deleteSn = record.device_sn }" class="ml15" />
</a-tooltip>
</span>
</div>
</template>
</a-table>
<a-modal v-model:visible="deleteTip" width="450px" :closable="false" centered :okButtonProps="{ danger: true }" @ok="unbind">
<p class="pt10 pl20" style="height: 50px;">Delete device from workspace?</p>
<template #title>
<div class="flex-row flex-justify-center">
<span>Delete devices</span>
</div>
</template>
</a-modal>
<a-drawer
title="Hms Info"
placement="right"
v-model:visible="hmsVisible"
:width="800">
<div class="flex-row flex-align-center">
<div style="width: 240px;">
<a-range-picker
v-model:value="time"
format="YYYY-MM-DD"
:placeholder="['Start Time', 'End Time']"
@change="onTimeChange"/>
</div>
<div class="ml5">
<a-select
style="width: 150px"
v-model:value="param.level"
@select="onLevelSelect">
<a-select-option
v-for="item in levels"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="ml5">
<a-select
v-model:value="param.domain"
:disabled="!param.children_sn || !param.device_sn"
style="width: 150px"
@select="onDeviceTypeSelect">
<a-select-option
v-for="item in deviceTypes"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="ml5">
<a-input-search
v-model:value="param.message"
placeholder="input search message"
style="width: 200px"
@search="getHms"/>
</div>
</div>
<div>
<a-table :columns="hmsColumns" :scroll="{ x: '100%', y: 600 }" :data-source="hmsData.data" :pagination="hmsPaginationProp" @change="refreshHmsData" row-key="hms_id"
:rowClassName="rowClassName" :loading="loading">
<template #time="{ record }">
<div>{{ record.create_time }}</div>
<div :style="record.update_time ? '' : record.level === EHmsLevel.CAUTION ? 'color: orange;' :
record.level === EHmsLevel.WARN ? 'color: red;' : 'color: #28d445;'">{{ record.update_time ?? 'It is happening...' }}</div>
</template>
<template #level="{ text }">
<div class="flex-row flex-align-center">
<div :class="text === EHmsLevel.CAUTION ? 'caution' : text === EHmsLevel.WARN ? 'warn' : 'notice'" style="width: 10px; height: 10px; border-radius: 50%;"></div>
<div style="margin-left: 3px;">{{ EHmsLevel[text] }}</div>
</div>
</template>
<template v-for="col in ['code', 'message']" #[col]="{ text }" :key="col">
<a-tooltip :title="text">
<span>{{ text }}</span>
</a-tooltip>
</template>
</a-table>
</div>
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
import { h, onMounted, reactive, ref, UnwrapRef } from 'vue'
import { IPage } from '/@/api/http/type'
import { BindBody, bindDevice, getBindingDevices, getDeviceHms, HmsQueryBody, unbindDevice, updateDevice } from '/@/api/manage'
import { EDeviceTypeName, EHmsLevel, ELocalStorageKey } from '/@/types'
import { EditOutlined, CheckOutlined, CloseOutlined, DeleteOutlined, FileSearchOutlined } from '@ant-design/icons-vue'
import { Device, DeviceHms } from '/@/types/device'
import moment, { Moment } from 'moment'
interface DeviceData {
device: Device[]
}
const loading = ref(true)
const deleteTip = ref<boolean>(false)
const deleteSn = ref<string>()
const hmsVisible = ref<boolean>(false)
const columns: ColumnProps[] = [
{ title: 'Model', dataIndex: 'device_name', width: '10%', className: 'titleStyle' },
{ title: 'SN', dataIndex: 'device_sn', width: '10%', className: 'titleStyle', ellipsis: true, slots: { customRender: 'sn' } },
{
title: 'Name',
dataIndex: 'nickname',
width: '15%',
sorter: (a: Device, b: Device) => a.nickname.localeCompare(b.nickname),
className: 'titleStyle',
ellipsis: true,
slots: { customRender: 'nickname' }
},
{ title: 'Firmware Version', dataIndex: 'firmware_version', width: '10%', className: 'titleStyle' },
{ title: 'Status', dataIndex: 'status', width: '100px', className: 'titleStyle', slots: { customRender: 'status' } },
{
title: 'Workspace',
dataIndex: 'workspace_name',
width: '10%',
className: 'titleStyle',
ellipsis: true,
slots: { customRender: 'workspace' },
customRender: ({ text, record, index }) => {
const obj = {
children: text,
props: {} as any,
}
if (current.value.indexOf(EDeviceTypeName.Aircraft) !== -1 || (!record.child_device_sn && record.domain === EDeviceTypeName.Dock)) {
return obj
}
obj.props.rowSpan = record.domain === EDeviceTypeName.Dock ? 2 : 0
return obj
}
},
{ title: 'Joined', dataIndex: 'bound_time', width: '15%', sorter: (a: Device, b: Device) => a.bound_time.localeCompare(b.bound_time), className: 'titleStyle' },
{ title: 'Last Online', dataIndex: 'login_time', width: '15%', sorter: (a: Device, b: Device) => a.login_time.localeCompare(b.login_time), className: 'titleStyle' },
{
title: 'Actions',
dataIndex: 'actions',
className: 'titleStyle',
slots: { customRender: 'action' }
},
]
const expandIcon = (props: any) => {
if (judgeCurrentType(EDeviceTypeName.Dock) && !props.expanded) {
return h('div',
{
style: 'border-left: 2px solid rgb(200,200,200); border-bottom: 2px solid rgb(200,200,200); height: 16px; width: 16px; float: left;',
class: 'mt-5 ml0',
})
}
}
const rowClassName = (record: any, index: number) => {
const className = []
if ((index & 1) === 0) {
className.push('table-striped')
}
if (record.domain !== EDeviceTypeName.Dock) {
className.push('child-row')
}
return className.toString().replaceAll(',', ' ')
}
const expandRows = ref<string[]>([])
const data = reactive<DeviceData>({
device: []
})
const paginationProp = reactive({
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
})
const rowSelection = {
onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
},
onSelect: (record: any, selected: boolean, selectedRows: []) => {
console.log(record, selected, selectedRows)
},
onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => {
console.log(selected, selectedRows, changeRows)
},
getCheckboxProps: (record: any) => ({
disabled: judgeCurrentType(EDeviceTypeName.Dock) && record.domain !== EDeviceTypeName.Dock,
style: judgeCurrentType(EDeviceTypeName.Dock) && record.domain !== EDeviceTypeName.Dock ? 'display: none' : ''
}),
}
type Pagination = TableState['pagination']
const body: IPage = {
page: 1,
total: 0,
page_size: 50
}
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const editableData: UnwrapRef<Record<string, Device>> = reactive({})
const current = ref([EDeviceTypeName.Aircraft])
onMounted(() => {
getDevices(workspaceId, body, current.value[0])
})
function judgeCurrentType (type: EDeviceTypeName): boolean {
return current.value.indexOf(type) !== -1
}
function getDevices (workspaceId: string, body: IPage, domain: string) {
loading.value = true
getBindingDevices(workspaceId, body, domain).then(res => {
if (res.code !== 0) {
return
}
const resData: Device[] = res.data.list
expandRows.value = []
resData.forEach((val: any) => {
if (val.children) {
val.children = [val.children]
}
if (judgeCurrentType(EDeviceTypeName.Dock)) {
expandRows.value.push(val.device_sn)
}
})
data.device = resData
paginationProp.total = res.data.pagination.total
paginationProp.current = res.data.pagination.page
loading.value = false
})
}
function refreshData (page: Pagination) {
body.page = page?.current!
body.page_size = page?.pageSize!
getDevices(workspaceId, body, current.value[0])
}
function edit (record: Device) {
editableData[record.device_sn] = record
}
function save (record: Device) {
delete editableData[record.device_sn]
updateDevice({ nickname: record.nickname }, workspaceId, record.device_sn)
}
function showDeleteTip (sn: any) {
deleteTip.value = true
}
function unbind () {
deleteTip.value = false
unbindDevice(deleteSn.value?.toString()!).then(res => {
if (res.code !== 0) {
return
}
getDevices(workspaceId, body, current.value[0])
})
}
function select (item: any) {
getDevices(workspaceId, body, item.key)
}
const hmsColumns: ColumnProps[] = [
{ title: 'Alarm Begin | End Time', dataIndex: 'create_time', width: '25%', className: 'titleStyle', slots: { customRender: 'time' } },
{ title: 'Level', dataIndex: 'level', width: '120px', className: 'titleStyle', slots: { customRender: 'level' } },
{ title: 'Device', dataIndex: 'domain', width: '12%', className: 'titleStyle' },
{ title: 'Error Code', dataIndex: 'key', width: '20%', className: 'titleStyle', slots: { customRender: 'code' } },
{ title: 'Hms Message', dataIndex: 'message_en', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } },
]
interface DeviceHmsData {
data: DeviceHms[]
}
const hmsData = reactive<DeviceHmsData>({
data: []
})
const hmsPaginationProp = reactive({
pageSizeOptions: ['20', '50', '100'],
showQuickJumper: true,
showSizeChanger: true,
pageSize: 50,
current: 1,
total: 0
})
const hmsPage: IPage = {
page: 1,
total: 0,
page_size: 50
}
function showHms (dock: Device) {
hmsVisible.value = true
if (dock.domain === EDeviceTypeName.Dock) {
param.domain = ''
getDeviceHmsBySn(dock.device_sn, dock.children?.[0].device_sn ?? '')
}
if (dock.domain === EDeviceTypeName.Aircraft) {
param.domain = EDeviceTypeName.Aircraft
getDeviceHmsBySn('', dock.device_sn)
}
}
function refreshHmsData (page: Pagination) {
hmsPage.page = page?.current!
hmsPage.page_size = page?.pageSize!
getHms()
}
const param = reactive<HmsQueryBody>({
sns: [],
device_sn: '',
children_sn: '',
language: 'en',
begin_time: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0),
end_time: new Date().setHours(23, 59, 59, 999),
domain: '',
level: '',
message: ''
})
const levels = [
{
label: 'All',
value: ''
}, {
label: EHmsLevel[0],
value: EHmsLevel.NOTICE
}, {
label: EHmsLevel[1],
value: EHmsLevel.CAUTION
}, {
label: EHmsLevel[2],
value: EHmsLevel.WARN
}
]
const deviceTypes = [
{
label: 'All',
value: ''
}, {
label: EDeviceTypeName.Aircraft,
value: EDeviceTypeName.Aircraft
}, {
label: EDeviceTypeName.Dock,
value: EDeviceTypeName.Dock
}
]
const time = ref([moment(param.begin_time), moment(param.end_time)])
function getHms () {
getDeviceHms(param, workspaceId, hmsPage)
.then(res => {
hmsPaginationProp.total = res.data.pagination.total
hmsPaginationProp.current = res.data.pagination.page
hmsData.data = res.data.list
hmsData.data.forEach(hms => {
hms.domain = hms.sn === param.children_sn ? EDeviceTypeName.Aircraft : EDeviceTypeName.Dock
})
})
}
function getDeviceHmsBySn (sn: string, childSn: string) {
param.device_sn = sn
param.children_sn = childSn
param.sns = [param.device_sn, param.children_sn]
getHms()
}
function onTimeChange (newTime: [Moment, Moment]) {
param.begin_time = newTime[0].valueOf()
param.end_time = newTime[1].valueOf()
getHms()
}
function onDeviceTypeSelect (val: string) {
param.sns = [param.device_sn, param.children_sn]
if (val === EDeviceTypeName.Dock) {
param.sns = [param.device_sn, '']
}
if (val === EDeviceTypeName.Aircraft) {
param.sns = ['', param.children_sn]
}
getHms()
}
function onLevelSelect (val: number) {
param.level = val
getHms()
}
</script>
<style lang="scss">
.table {
background-color: white;
margin: 20px;
padding: 20px;
height: 88vh;
}
.table-striped {
background-color: #f7f9fa;
}
.ant-table {
border-top: 1px solid rgb(0,0,0,0.06);
border-bottom: 1px solid rgb(0,0,0,0.06);
}
.ant-table-tbody tr td {
border: 0;
}
.ant-table td {
white-space: nowrap;
}
.ant-table-thead tr th {
background: white !important;
border: 0;
}
th.ant-table-selection-column {
background-color: white !important;
}
.ant-table-header {
background-color: white !important;
}
.child-row {
height: 70px;
}
.notice {
background: $success;
overflow: hidden;
cursor: pointer;
}
.caution {
background: orange;
cursor: pointer;
overflow: hidden;
}
.warn {
background: red;
cursor: pointer;
overflow: hidden;
}
</style>

4
src/root.ts

@ -1,8 +1,8 @@
import { createApp, ComponentCustomProperties, App as VueApp } from 'vue' import { createApp, ComponentCustomProperties, App as VueApp } from 'vue'
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$aMap: any $aMap: any // Map类
$aMapObj: any $map: any // 地图对象
$mouseTool: any $mouseTool: any
} }
} }

37
src/router/index.ts

@ -1,8 +1,8 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { ERouterName } from '/@/types/index' import { ERouterName } from '/@/types/index'
import CreatePlan from '../pages/project-app/projects/create-plan.vue' import CreatePlan from '../pages/page-web/projects/create-plan.vue'
import WaylinePanel from '/@/pages/project-app/projects/wayline.vue' import WaylinePanel from '/@/pages/page-web/projects/wayline.vue'
import DockPanel from '/@/pages/project-app/projects/dock.vue' import DockPanel from '/@/pages/page-web/projects/dock.vue'
import LiveAgora from '/@/components/livestream-agora.vue' import LiveAgora from '/@/components/livestream-agora.vue'
import LiveOthers from '/@/components/livestream-others.vue' import LiveOthers from '/@/components/livestream-others.vue'
@ -11,38 +11,41 @@ const routes: Array<RouteRecordRaw> = [
path: '/', path: '/',
redirect: '/' + ERouterName.PROJECT redirect: '/' + ERouterName.PROJECT
}, },
// 首页
{ {
path: '/' + ERouterName.PROJECT, path: '/' + ERouterName.PROJECT,
name: ERouterName.PROJECT, name: ERouterName.PROJECT,
component: () => import('/@/pages/project-app/index.vue') component: () => import('/@/pages/page-web/index.vue')
}, },
// members, devices
{ {
path: '/' + ERouterName.HOME, path: '/' + ERouterName.HOME,
name: ERouterName.HOME, name: ERouterName.HOME,
component: () => import('/@/pages/project-app/home.vue'), component: () => import('/@/pages/page-web/home.vue'),
children: [ children: [
{ {
path: '/' + ERouterName.MEMBERS, path: '/' + ERouterName.MEMBERS,
name: ERouterName.MEMBERS, name: ERouterName.MEMBERS,
component: () => import('/@/pages/project-app/projects/members.vue') component: () => import('/@/pages/page-web/projects/members.vue')
}, },
{ {
path: '/' + ERouterName.DEVICES, path: '/' + ERouterName.DEVICES,
name: 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, path: '/' + ERouterName.WORKSPACE,
name: ERouterName.WORKSPACE, name: ERouterName.WORKSPACE,
component: () => import('/@/pages/project-app/projects/workspace.vue'), component: () => import('/@/pages/page-web/projects/workspace.vue'),
redirect: '/' + ERouterName.TSA, redirect: '/' + ERouterName.TSA,
children: [ children: [
{ {
path: '/' + ERouterName.LIVESTREAM, path: '/' + ERouterName.LIVESTREAM,
name: ERouterName.LIVESTREAM, name: ERouterName.LIVESTREAM,
component: () => import('/@/pages/project-app/projects/livestream.vue'), component: () => import('/@/pages/page-web/projects/livestream.vue'),
children: [ children: [
{ {
path: ERouterName.LIVING, path: ERouterName.LIVING,
@ -56,27 +59,27 @@ const routes: Array<RouteRecordRaw> = [
}, },
{ {
path: '/' + ERouterName.TSA, path: '/' + ERouterName.TSA,
component: () => import('/@/pages/project-app/projects/tsa.vue') component: () => import('/@/pages/page-web/projects/tsa.vue')
}, },
{ {
path: '/' + ERouterName.LAYER, path: '/' + ERouterName.LAYER,
name: ERouterName.LAYER, name: ERouterName.LAYER,
component: () => import('/@/pages/project-app/projects/layer.vue') component: () => import('/@/pages/page-web/projects/layer.vue')
}, },
{ {
path: '/' + ERouterName.MEDIA, path: '/' + ERouterName.MEDIA,
name: ERouterName.MEDIA, name: ERouterName.MEDIA,
component: () => import('/@/pages/project-app/projects/media.vue') component: () => import('/@/pages/page-web/projects/media.vue')
}, },
{ {
path: '/' + ERouterName.WAYLINE, path: '/' + ERouterName.WAYLINE,
name: ERouterName.WAYLINE, name: ERouterName.WAYLINE,
component: () => import('/@/pages/project-app/projects/wayline.vue') component: () => import('/@/pages/page-web/projects/wayline.vue')
}, },
{ {
path: '/' + ERouterName.TASK, path: '/' + ERouterName.TASK,
name: ERouterName.TASK, name: ERouterName.TASK,
component: () => import('/@/pages/project-app/projects/task.vue'), component: () => import('/@/pages/page-web/projects/task.vue'),
children: [ children: [
{ {
path: ERouterName.CREATE_PLAN, path: ERouterName.CREATE_PLAN,
@ -98,6 +101,7 @@ const routes: Array<RouteRecordRaw> = [
} }
] ]
}, },
// pilot
{ {
path: '/' + ERouterName.PILOT, path: '/' + ERouterName.PILOT,
name: ERouterName.PILOT, name: ERouterName.PILOT,
@ -118,11 +122,6 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: '/' + ERouterName.PILOT_BIND, path: '/' + ERouterName.PILOT_BIND,
component: () => import('/@/pages/page-pilot/pilot-bind.vue') component: () => import('/@/pages/page-pilot/pilot-bind.vue')
},
{
path: '/' + ERouterName.ELEMENT,
name: ERouterName.ELEMENT,
component: () => import('/@/pages/elements/elements.vue')
} }
] ]

26
src/store/index.ts

@ -5,6 +5,7 @@ import { Device, DeviceHms, DeviceOsd, DeviceStatus, DockOsd, GatewayOsd, OSDVis
import { getLayers } from '/@/api/layer' import { getLayers } from '/@/api/layer'
import { LayerType } from '/@/types/mapLayer' import { LayerType } from '/@/types/mapLayer'
import { ETaskStatus, TaskInfo, WaylineFile } from '/@/types/wayline' import { ETaskStatus, TaskInfo, WaylineFile } from '/@/types/wayline'
import { DevicesCmdExecuteInfo } from '/@/types/device-cmd'
const initStateFunc = () => ({ const initStateFunc = () => ({
Layers: [ Layers: [
@ -87,7 +88,10 @@ const initStateFunc = () => ({
}, },
hmsInfo: {} as { hmsInfo: {} as {
[sn: string]: DeviceHms[] [sn: string]: DeviceHms[]
} },
// 机场指令执行状态信息
devicesCmdExecuteInfo: {
} as DevicesCmdExecuteInfo
}) })
export type RootStateType = ReturnType<typeof initStateFunc> export type RootStateType = ReturnType<typeof initStateFunc>
@ -144,7 +148,6 @@ const mutations: MutationTree<RootStateType> = {
delete state.deviceState.deviceInfo[info.sn] delete state.deviceState.deviceInfo[info.sn]
delete state.deviceState.dockInfo[info.sn] delete state.deviceState.dockInfo[info.sn]
delete state.hmsInfo[info.sn] delete state.hmsInfo[info.sn]
// delete state.markerInfo.coverMap[info.sn] // delete state.markerInfo.coverMap[info.sn]
// delete state.markerInfo.pathMap[info.sn] // delete state.markerInfo.pathMap[info.sn]
}, },
@ -171,6 +174,25 @@ const mutations: MutationTree<RootStateType> = {
SET_DEVICE_HMS_INFO (state, info) { SET_DEVICE_HMS_INFO (state, info) {
const hmsList: Array<DeviceHms> = state.hmsInfo[info.sn] const hmsList: Array<DeviceHms> = state.hmsInfo[info.sn]
state.hmsInfo[info.sn] = info.host.concat(hmsList ?? []) 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]
}
} }
} }

12
src/styles/flex.style.scss

@ -44,6 +44,18 @@
justify-content: space-around; justify-content: space-around;
} }
.flex-1 {
flex: 1;
}
.flex-shrink-0 {
flex-shrink: 0;
}
.flex-shrink-1 {
flex-shrink: 1;
}
//width //width
.width-100vw { .width-100vw {
width: 100vw; width: 100vw;

38
src/types/airport-tsa.ts

@ -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, // 打开
}

210
src/types/device-cmd.ts

@ -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
}

65
src/types/device-log.ts

@ -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,
}

186
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 { export interface Device {
device_name: string, device_name: string,
device_sn: string, device_sn: string,
nickname: string, nickname: string,
firmware_version: string, firmware_version: string,
firmware_status: DeviceFirmwareStatusEnum,
status: string, status: string,
workspace_name: string, workspace_name: string,
bound_time: string, bound_time: string,
login_time: string, login_time: string,
children?: Device[] children?: Device[],
domain: string domain: string,
firmware_progress?: number, // 升级进度
} }
export interface DeviceStatus { export interface DeviceStatus {
@ -196,3 +368,11 @@ export interface DeviceHms {
update_time: string, update_time: string,
domain: string domain: string
} }
// TODO: 设备拓扑管理优化
// 设备信息
export interface DeviceInfoType {
gateway: GatewayOsd, // 遥控器
dock: DockOsd, // 机场
device: DeviceOsd, // 飞机
}

19
src/types/enums.ts

@ -92,6 +92,25 @@ export enum EBizCode {
DeviceOffline = 'device_offline', DeviceOffline = 'device_offline',
FlightTaskProgress = 'flighttask_progress', FlightTaskProgress = 'flighttask_progress',
DeviceHms = 'device_hms', 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 { export enum EDeviceTypeName {

86
src/utils/bytes.ts

@ -0,0 +1,86 @@
import { DEFAULT_PLACEHOLDER, SIZES as byteSizes, BYTE_SIZES } from './constants'
/**
* BKBGB...
*
* @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))

8
src/utils/color.ts

@ -0,0 +1,8 @@
export const commonColor = {
WARN: '#FF9900', // 黄色
FAIL: '#E02020', // 红色
WHITE: '#FFFFFF', // 白色
NORMAL: '#19BE6B', // 绿色
BLUE: '#2B85E4', // 蓝色
PINK: '#F7C0BA', // 粉
}

6
src/utils/common.ts

@ -1,4 +1,8 @@
/**
*
* @param data
* @param fileName
*/
export function downloadFile (data: Blob, fileName: string) { export function downloadFile (data: Blob, fileName: string) {
const lable = document.createElement('a') const lable = document.createElement('a')
lable.href = window.URL.createObjectURL(data) lable.href = window.URL.createObjectURL(data)

15
src/utils/constants.ts

@ -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

350
src/utils/device-cmd.ts

@ -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
}
}
}
})
}

82
src/utils/download.ts

@ -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()
}

15
src/utils/time.ts

@ -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
}

85
src/websocket/index.ts

@ -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

8
src/websocket/util/config.ts

@ -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
}

6982
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save