init
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
/src/vendors/**
|
||||||
22
.eslintrc.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
commonjs: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
extends: ['standard', 'plugin:vue/vue3-essential'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 12,
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
},
|
||||||
|
plugins: ['vue', '@typescript-eslint'],
|
||||||
|
rules: {
|
||||||
|
'comma-dangle': 'off',
|
||||||
|
'import/no-absolute-path': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
camelcase: 'off',
|
||||||
|
'no-redeclare': 'off',
|
||||||
|
'vue/no-unused-components': 'off'
|
||||||
|
}
|
||||||
|
}
|
||||||
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
# .vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.history
|
||||||
|
/coverage
|
||||||
|
/backup
|
||||||
|
node_modules
|
||||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 DJI-SDK
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
2
env/.env
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_APP_ENVIRONMENT=DEV
|
||||||
|
VITE_APP_APIGATEWAY_BACKEND_HOST=''
|
||||||
2
env/.env.production
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_APP_ENVIRONMENT=production
|
||||||
|
VITE_APP_APIGATEWAY_BACKEND_HOST=''
|
||||||
2
env/.env.stag
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_APP_ENVIRONMENT=STAG
|
||||||
|
VITE_APP_APIGATEWAY_BACKEND_HOST=''
|
||||||
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="./favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>demo-web</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="demo-app"></div>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
9857
package-lock.json
generated
Normal file
117
package.json
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"name": "demo-web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vite",
|
||||||
|
"build:test": "vite build --mode stag",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||||
|
"@ant-design/icons-vue": "^6.0.1",
|
||||||
|
"@vitejs/plugin-legacy": "^1.6.2",
|
||||||
|
"agora-rtc-sdk-ng": "^4.12.1",
|
||||||
|
"ant-design-vue": "^2.2.8",
|
||||||
|
"axios": "^0.21.4",
|
||||||
|
"eventemitter3": "^5.0.0",
|
||||||
|
"mitt": "^3.0.0",
|
||||||
|
"mqtt": "^4.3.7",
|
||||||
|
"query-string": "^7.0.1",
|
||||||
|
"reconnecting-websocket": "^4.4.0",
|
||||||
|
"vconsole": "^3.8.1",
|
||||||
|
"vite-plugin-components": "^0.13.3",
|
||||||
|
"vite-plugin-importer": "^0.2.5",
|
||||||
|
"vite-plugin-optimize-persist": "^0.1.2",
|
||||||
|
"vite-plugin-package-config": "^0.1.1",
|
||||||
|
"vue": "^3.2.26",
|
||||||
|
"vue-cookies": "^1.7.4",
|
||||||
|
"vue-i18n": "^9.1.6",
|
||||||
|
"vue-router": "4",
|
||||||
|
"vuex": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.3.2",
|
||||||
|
"@types/urlencode": "^1.1.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.8.1",
|
||||||
|
"@typescript-eslint/parser": "^5.8.1",
|
||||||
|
"@vitejs/plugin-vue": "^1.2.4",
|
||||||
|
"@vue/compiler-sfc": "^3.0.5",
|
||||||
|
"eslint": "^7.30.0",
|
||||||
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
"eslint-plugin-import": "^2.23.4",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^5.1.0",
|
||||||
|
"eslint-plugin-vue": "^7.13.0",
|
||||||
|
"rollup-plugin-external-globals": "^0.6.1",
|
||||||
|
"sass": "^1.35.1",
|
||||||
|
"typescript": "^4.5.4",
|
||||||
|
"vite": "^2.4.0",
|
||||||
|
"vite-plugin-eslint": "^1.3.0",
|
||||||
|
"vite-plugin-style-import": "^1.0.1",
|
||||||
|
"vite-plugin-svg-icons": "^1.0.5",
|
||||||
|
"vite-plugin-vconsole": "^1.1.0",
|
||||||
|
"vue-tsc": "^0.0.24"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"vite": {
|
||||||
|
"optimizeDeps": {
|
||||||
|
"include": [
|
||||||
|
"@amap/amap-jsapi-loader",
|
||||||
|
"@ant-design/icons-vue",
|
||||||
|
"@vue/reactivity",
|
||||||
|
"agora-rtc-sdk-ng",
|
||||||
|
"ant-design-vue",
|
||||||
|
"ant-design-vue/es",
|
||||||
|
"ant-design-vue/es/avatar/style/css",
|
||||||
|
"ant-design-vue/es/breadcrumb/style/css",
|
||||||
|
"ant-design-vue/es/button/style/css",
|
||||||
|
"ant-design-vue/es/checkbox/style/css",
|
||||||
|
"ant-design-vue/es/col/style/css",
|
||||||
|
"ant-design-vue/es/collapse/style/css",
|
||||||
|
"ant-design-vue/es/date-picker/style/css",
|
||||||
|
"ant-design-vue/es/divider/style/css",
|
||||||
|
"ant-design-vue/es/drawer/style/css",
|
||||||
|
"ant-design-vue/es/dropdown/style/css",
|
||||||
|
"ant-design-vue/es/empty/style/css",
|
||||||
|
"ant-design-vue/es/form/style/css",
|
||||||
|
"ant-design-vue/es/image/style/css",
|
||||||
|
"ant-design-vue/es/input-number/style/css",
|
||||||
|
"ant-design-vue/es/input/style/css",
|
||||||
|
"ant-design-vue/es/layout/style/css",
|
||||||
|
"ant-design-vue/es/menu/style/css",
|
||||||
|
"ant-design-vue/es/message/style/css",
|
||||||
|
"ant-design-vue/es/modal/style/css",
|
||||||
|
"ant-design-vue/es/pagination/style/css",
|
||||||
|
"ant-design-vue/es/popconfirm/style/css",
|
||||||
|
"ant-design-vue/es/popover/style/css",
|
||||||
|
"ant-design-vue/es/progress/style/css",
|
||||||
|
"ant-design-vue/es/radio/style/css",
|
||||||
|
"ant-design-vue/es/row/style/css",
|
||||||
|
"ant-design-vue/es/select/style/css",
|
||||||
|
"ant-design-vue/es/space/style/css",
|
||||||
|
"ant-design-vue/es/spin/style/css",
|
||||||
|
"ant-design-vue/es/switch/style/css",
|
||||||
|
"ant-design-vue/es/table/style/css",
|
||||||
|
"ant-design-vue/es/tag/style/css",
|
||||||
|
"ant-design-vue/es/time-picker/style/css",
|
||||||
|
"ant-design-vue/es/tooltip/style/css",
|
||||||
|
"ant-design-vue/es/tree/style/css",
|
||||||
|
"ant-design-vue/es/upload/style/css",
|
||||||
|
"axios",
|
||||||
|
"eventemitter3",
|
||||||
|
"lodash",
|
||||||
|
"mitt",
|
||||||
|
"moment",
|
||||||
|
"mqtt",
|
||||||
|
"mqtt/dist/mqtt.min",
|
||||||
|
"reconnecting-websocket",
|
||||||
|
"vconsole",
|
||||||
|
"vue",
|
||||||
|
"vue-router",
|
||||||
|
"vuex"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
42
src/App.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="demo-app">
|
||||||
|
<router-view />
|
||||||
|
<!-- <div class="map-wrapper">
|
||||||
|
<GMap/>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from 'vue'
|
||||||
|
import { useMyStore } from './store'
|
||||||
|
import GMap from '/@/components/GMap.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'App',
|
||||||
|
components: { GMap },
|
||||||
|
|
||||||
|
setup () {
|
||||||
|
const store = useMyStore()
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.demo-app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.map-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#demo-app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
src/antd.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// import Icon from '@ant-design/icons-vue'
|
||||||
|
import * as antDesign from 'ant-design-vue'
|
||||||
|
import 'ant-design-vue/dist/antd.css'
|
||||||
|
import { App } from 'vue'
|
||||||
|
import svgIcon from '/@/components/svgIcon.vue'
|
||||||
|
|
||||||
|
export const antComponents = {
|
||||||
|
install (app: App): void {
|
||||||
|
app.use(antDesign)
|
||||||
|
// app.component('Icon', Icon)
|
||||||
|
app.component('svg-icon', svgIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/api/device-cmd/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { DeviceCmd, DeviceCmdItemAction } from '/@/types/device-cmd'
|
||||||
|
|
||||||
|
const CMD_API_PREFIX = '/control/api/v1'
|
||||||
|
|
||||||
|
export interface SendCmdParams {
|
||||||
|
dock_sn: string, // 机场cn
|
||||||
|
device_cmd: DeviceCmd // 指令
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostSendCmdBody {
|
||||||
|
action: DeviceCmdItemAction
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 发送机场控制指令
|
||||||
|
* @param params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
// /control/api/v1/devices/{dock_sn}/jobs/{service_identifier}
|
||||||
|
export async function postSendCmd (params: SendCmdParams, body?: PostSendCmdBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const resp = await request.post(`${CMD_API_PREFIX}/devices/${params.dock_sn}/jobs/${params.device_cmd}`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
172
src/api/device-log/index.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import request, { IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { DeviceValue, DOMAIN } from '/@/types/device'
|
||||||
|
import { DeviceLogUploadStatusEnum } from '/@/types/device-log'
|
||||||
|
import { ELocalStorageKey } from '/@/types'
|
||||||
|
import { CURRENT_CONFIG } from '/@/api/http/config'
|
||||||
|
|
||||||
|
const MNG_API_PREFIX = '/manage/api/v1'
|
||||||
|
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
|
||||||
|
export interface GetDeviceUploadLogListParams {
|
||||||
|
device_sn: string,
|
||||||
|
page: number,
|
||||||
|
page_size: number,
|
||||||
|
begin_time?: number, // 开始时间
|
||||||
|
end_time?: number, // 结束时间
|
||||||
|
status?: DeviceLogUploadStatusEnum, // 日志上传状态
|
||||||
|
logs_information?: string // 搜索内容
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BriefDeviceInfo {
|
||||||
|
sn: string,
|
||||||
|
device_model: DeviceValue,
|
||||||
|
device_callsign: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceLogProgressInfo{
|
||||||
|
device_sn: string,
|
||||||
|
device_model_domain: DOMAIN,
|
||||||
|
progress: number, // 进度
|
||||||
|
result: number, // 上传结果
|
||||||
|
upload_rate: number, // 上传速率
|
||||||
|
status: DeviceLogUploadStatusEnum // 上传状态
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceLogItem {
|
||||||
|
boot_index: number, // 日志id
|
||||||
|
start_time: number, // 日志开始时间
|
||||||
|
end_time: number, // 日志结束时间
|
||||||
|
size: number // 日志大小
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceLogFileInfo {
|
||||||
|
device_sn: string,
|
||||||
|
module: DOMAIN,
|
||||||
|
result: number,
|
||||||
|
object_key: string,
|
||||||
|
file_id: string,
|
||||||
|
list: DeviceLogItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceLogFileListInfo {
|
||||||
|
files: DeviceLogFileInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDeviceUploadLogListRsp {
|
||||||
|
logs_id: string, // 记录id
|
||||||
|
happen_time: string, // 发生时间
|
||||||
|
user_name: string, // 用户
|
||||||
|
logs_information: string, // 异常描述
|
||||||
|
create_time: string, // 上传时间
|
||||||
|
status:DeviceLogUploadStatusEnum, // 日志上传状态
|
||||||
|
device_topo:{ // 设备topo
|
||||||
|
hosts: BriefDeviceInfo[],
|
||||||
|
parents: BriefDeviceInfo[]
|
||||||
|
},
|
||||||
|
logs_progress: DeviceLogProgressInfo[], // 日志上传进度
|
||||||
|
device_logs: DeviceLogFileListInfo // 设备日志
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备上传日志列表信息
|
||||||
|
* @param params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getDeviceUploadLogList (params: GetDeviceUploadLogListParams): Promise<IListWorkspaceResponse<GetDeviceUploadLogListRsp>> {
|
||||||
|
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${params.device_sn}/logs-uploaded`, {
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDeviceLogListParams{
|
||||||
|
device_sn: string,
|
||||||
|
domain: DOMAIN[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备日志列表信息
|
||||||
|
* @param params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getDeviceLogList (params: GetDeviceLogListParams): Promise<IWorkspaceResponse<DeviceLogFileListInfo>> {
|
||||||
|
const domain = params.domain ? params.domain : []
|
||||||
|
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${params.device_sn}/logs`, {
|
||||||
|
params: {
|
||||||
|
domain_list: domain.join(',')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadDeviceLogBody {
|
||||||
|
device_sn: string
|
||||||
|
happen_time: string // 发生时间
|
||||||
|
logs_information: string // 异常描述
|
||||||
|
files:{
|
||||||
|
list: DeviceLogItem[],
|
||||||
|
device_sn: string,
|
||||||
|
module: DOMAIN
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传设备日志
|
||||||
|
* @param body
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function postDeviceUpgrade (body: UploadDeviceLogBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const resp = await request.post(`${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeviceLogUploadAction = 'cancel'
|
||||||
|
|
||||||
|
export interface CancelDeviceLogUploadBody {
|
||||||
|
device_sn: string
|
||||||
|
status: DeviceLogUploadAction
|
||||||
|
module_list: DOMAIN[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消上传
|
||||||
|
export async function cancelDeviceLogUpload (body: CancelDeviceLogUploadBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const url = `${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs`
|
||||||
|
const result = await request.delete(url, {
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDeviceLogUploadBody {
|
||||||
|
device_sn: string
|
||||||
|
logs_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消上传
|
||||||
|
export async function deleteDeviceLogUpload (body: DeleteDeviceLogUploadBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const url = `${MNG_API_PREFIX}/workspaces/${workspaceId}/devices/${body.device_sn}/logs/${body.logs_id}`
|
||||||
|
const result = await request.delete(url, {
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetUploadDeviceLogUrlParams{
|
||||||
|
logs_id: string,
|
||||||
|
file_id: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface GetUploadDeviceLogRsp{
|
||||||
|
// url: string
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备上传日志url
|
||||||
|
* @param params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getUploadDeviceLogUrl (params: GetUploadDeviceLogUrlParams): Promise<IWorkspaceResponse<string>> {
|
||||||
|
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/${workspaceId}/logs/${params.logs_id}/url/${params.file_id}`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
24
src/api/device-setting/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { ELocalStorageKey } from '/@/types'
|
||||||
|
import { NightLightsStateEnum, DistanceLimitStatus, ObstacleAvoidance } from '/@/types/device-setting'
|
||||||
|
|
||||||
|
const MNG_API_PREFIX = '/manage/api/v1'
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
|
||||||
|
export interface PutDevicePropsBody {
|
||||||
|
night_lights_state?: NightLightsStateEnum;// 夜航灯开关
|
||||||
|
height_limit?: number;// 限高设置
|
||||||
|
distance_limit_status?: DistanceLimitStatus;// 限远开关
|
||||||
|
obstacle_avoidance?: ObstacleAvoidance;// 飞行器避障开关设置
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置设备属性
|
||||||
|
* @param params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
// /manage/api/v1/devices/{{workspace_id}}/devices/{{device_sn}}/property
|
||||||
|
export async function putDeviceProps (deviceSn: string, body: PutDevicePropsBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const resp = await request.put(`${MNG_API_PREFIX}/devices/${workspaceId}/devices/${deviceSn}/property`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
47
src/api/device-upgrade/index.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { DeviceFirmwareTypeEnum } from '/@/types/device'
|
||||||
|
|
||||||
|
const MNG_API_PREFIX = '/manage/api/v1'
|
||||||
|
|
||||||
|
export interface GetDeviceUpgradeInfoParams {
|
||||||
|
device_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDeviceUpgradeInfoRsp {
|
||||||
|
device_name: string
|
||||||
|
product_version: string
|
||||||
|
release_note: string
|
||||||
|
released_time: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备升级信息
|
||||||
|
* @param params
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getDeviceUpgradeInfo (params: GetDeviceUpgradeInfoParams): Promise<IWorkspaceResponse<GetDeviceUpgradeInfoRsp[]>> {
|
||||||
|
const resp = await request.get(`${MNG_API_PREFIX}/workspaces/firmware-release-notes/latest`, {
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpgradeDeviceInfo {
|
||||||
|
device_name: string,
|
||||||
|
sn: string,
|
||||||
|
product_version: string,
|
||||||
|
firmware_upgrade_type: DeviceFirmwareTypeEnum // 1-普通升级,2-一致性升级
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeviceUpgradeBody = UpgradeDeviceInfo[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备升级
|
||||||
|
* @param workspace_id
|
||||||
|
* @param body
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function postDeviceUpgrade (workspace_id: string, body: DeviceUpgradeBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const resp = await request.post(`${MNG_API_PREFIX}/devices/${workspace_id}/devices/ota`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
58
src/api/drc.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { ELocalStorageKey } from '/@/types'
|
||||||
|
|
||||||
|
// DRC 链路
|
||||||
|
const DRC_API_PREFIX = '/control/api/v1'
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
|
||||||
|
export interface PostDrcBody {
|
||||||
|
client_id?: string // token过期时,用于续期则必填
|
||||||
|
expire_sec?: number // 过期时间,单位秒,默认3600
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrcParams {
|
||||||
|
address: string
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
client_id: string
|
||||||
|
expire_time: number // 过期时间
|
||||||
|
enable_tls: boolean // 是否开启tls
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 mqtt 连接认证
|
||||||
|
export async function postDrc (body: PostDrcBody): Promise<IWorkspaceResponse<DrcParams>> {
|
||||||
|
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/connect`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrcEnterBody {
|
||||||
|
client_id: string
|
||||||
|
dock_sn: string
|
||||||
|
expire_sec?: number // 过期时间,单位秒,默认3600
|
||||||
|
device_info?: {
|
||||||
|
osd_frequency?: number
|
||||||
|
hsi_frequency?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrcEnterResp {
|
||||||
|
sub: string[] // 需要订阅接收的topic
|
||||||
|
pub: string[] // 推送的topic地址
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入飞行控制 (建立drc连接&获取云控控制权)
|
||||||
|
export async function postDrcEnter (body: DrcEnterBody): Promise<IWorkspaceResponse<DrcEnterResp>> {
|
||||||
|
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrcExitBody {
|
||||||
|
client_id: string
|
||||||
|
dock_sn: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出飞行控制 (退出drc连接&退出云控控制权)
|
||||||
|
export async function postDrcExit (body: DrcExitBody): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/exit`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
74
src/api/drone-control/drone.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
// import { ELocalStorageKey } from '/@/types'
|
||||||
|
|
||||||
|
const API_PREFIX = '/control/api/v1'
|
||||||
|
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
|
||||||
|
|
||||||
|
// 获取飞行控制权
|
||||||
|
export async function postFlightAuth (sn: string): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/flight`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
export enum WaylineLostControlActionInCommandFlight {
|
||||||
|
CONTINUE = 0,
|
||||||
|
EXEC_LOST_ACTION = 1
|
||||||
|
}
|
||||||
|
export enum LostControlActionInCommandFLight {
|
||||||
|
HOVER = 0, // 悬停
|
||||||
|
Land = 1, // 着陆
|
||||||
|
RETURN_HOME = 2, // 返航
|
||||||
|
}
|
||||||
|
export enum ERthMode {
|
||||||
|
SMART = 0,
|
||||||
|
SETTING = 1
|
||||||
|
}
|
||||||
|
export enum ECommanderModeLostAction {
|
||||||
|
CONTINUE = 0,
|
||||||
|
EXEC_LOST_ACTION = 1
|
||||||
|
}
|
||||||
|
export enum ECommanderFlightMode {
|
||||||
|
SMART = 0,
|
||||||
|
SETTING = 1
|
||||||
|
}
|
||||||
|
export interface PointBody {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
export interface PostFlyToPointBody {
|
||||||
|
max_speed: number,
|
||||||
|
points: PointBody[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 飞向目标点
|
||||||
|
export async function postFlyToPoint (sn: string, body: PostFlyToPointBody): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止飞向目标点
|
||||||
|
export async function deleteFlyToPoint (sn: string): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.delete(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostTakeoffToPointBody{
|
||||||
|
target_height: number;
|
||||||
|
target_latitude: number;
|
||||||
|
target_longitude: number;
|
||||||
|
security_takeoff_height: number; // 安全起飞高
|
||||||
|
max_speed: number; // flyto过程中能达到的最大速度, 单位m/s 跟飞机档位有关
|
||||||
|
rc_lost_action: LostControlActionInCommandFLight; // 失控行为
|
||||||
|
rth_altitude: number; // 返航高度
|
||||||
|
exit_wayline_when_rc_lost: WaylineLostControlActionInCommandFlight;
|
||||||
|
rth_mode: ERthMode;
|
||||||
|
commander_mode_lost_action: ECommanderModeLostAction;
|
||||||
|
commander_flight_mode: ECommanderFlightMode;
|
||||||
|
commander_flight_height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一键起飞
|
||||||
|
export async function postTakeoffToPoint (sn: string, body: PostTakeoffToPointBody): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/takeoff-to-point`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
93
src/api/drone-control/payload.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { CameraType, CameraMode } from '/@/types/live-stream'
|
||||||
|
import { GimbalResetMode } from '/@/types/drone-control'
|
||||||
|
// import { ELocalStorageKey } from '/@/types'
|
||||||
|
|
||||||
|
const API_PREFIX = '/control/api/v1'
|
||||||
|
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
|
||||||
|
|
||||||
|
export interface PostPayloadAuthBody {
|
||||||
|
payload_index: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取负载控制权
|
||||||
|
export async function postPayloadAuth (sn: string, body: PostPayloadAuthBody): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/payload`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 画面拖动控制
|
||||||
|
export enum PayloadCommandsEnum {
|
||||||
|
CameraModeSwitch = 'camera_mode_switch',
|
||||||
|
CameraPhotoTake = 'camera_photo_take',
|
||||||
|
CameraRecordingStart = 'camera_recording_start',
|
||||||
|
CameraRecordingStop = 'camera_recording_stop',
|
||||||
|
CameraFocalLengthSet = 'camera_focal_length_set',
|
||||||
|
GimbalReset = 'gimbal_reset',
|
||||||
|
CameraAim = 'camera_aim'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostCameraModeBody {
|
||||||
|
payload_index: string
|
||||||
|
camera_mode: CameraMode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostCameraPhotoBody {
|
||||||
|
payload_index: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostCameraRecordingBody {
|
||||||
|
payload_index: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCameraRecordingParams {
|
||||||
|
payload_index: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostCameraFocalLengthBody {
|
||||||
|
payload_index: string,
|
||||||
|
camera_type: CameraType,
|
||||||
|
zoom_factor: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostGimbalResetBody{
|
||||||
|
payload_index: string,
|
||||||
|
reset_mode: GimbalResetMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostCameraAimBody{
|
||||||
|
payload_index: string,
|
||||||
|
camera_type: CameraType,
|
||||||
|
locked: boolean,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PostPayloadCommandsBody = {
|
||||||
|
cmd: PayloadCommandsEnum.CameraModeSwitch,
|
||||||
|
data: PostCameraModeBody
|
||||||
|
} | {
|
||||||
|
cmd: PayloadCommandsEnum.CameraPhotoTake,
|
||||||
|
data: PostCameraPhotoBody
|
||||||
|
} | {
|
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStart,
|
||||||
|
data: PostCameraRecordingBody
|
||||||
|
} | {
|
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStop,
|
||||||
|
data: DeleteCameraRecordingParams
|
||||||
|
} | {
|
||||||
|
cmd: PayloadCommandsEnum.CameraFocalLengthSet,
|
||||||
|
data: PostCameraFocalLengthBody
|
||||||
|
} | {
|
||||||
|
cmd: PayloadCommandsEnum.GimbalReset,
|
||||||
|
data: PostGimbalResetBody
|
||||||
|
} | {
|
||||||
|
cmd: PayloadCommandsEnum.CameraAim,
|
||||||
|
data: PostCameraAimBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送负载名称
|
||||||
|
export async function postPayloadCommands (sn: string, body: PostPayloadCommandsBody): Promise<IWorkspaceResponse<null>> {
|
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/payload/commands`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
81
src/api/flight-area/index.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import request from '../http/request'
|
||||||
|
import { IWorkspaceResponse } from '../http/type'
|
||||||
|
import { EFlightAreaType, ESyncStatus, FlightAreaContent } from './../../types/flight-area'
|
||||||
|
import { ELocalStorageKey } from '/@/types/enums'
|
||||||
|
import { GeojsonCoordinate } from '/@/utils/genjson'
|
||||||
|
|
||||||
|
export interface GetFlightArea {
|
||||||
|
area_id: string,
|
||||||
|
name: string,
|
||||||
|
type: EFlightAreaType,
|
||||||
|
content: FlightAreaContent,
|
||||||
|
status: boolean,
|
||||||
|
username: string,
|
||||||
|
create_time: number,
|
||||||
|
update_time: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostFlightAreaBody {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
type: EFlightAreaType,
|
||||||
|
content: {
|
||||||
|
properties: {
|
||||||
|
color: string,
|
||||||
|
clampToGround: boolean,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: string,
|
||||||
|
coordinates: GeojsonCoordinate | GeojsonCoordinate[][],
|
||||||
|
radius?: number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlightAreaStatus {
|
||||||
|
sync_code: number,
|
||||||
|
sync_status: ESyncStatus,
|
||||||
|
sync_msg: string,
|
||||||
|
|
||||||
|
}
|
||||||
|
export interface GetDeviceStatus {
|
||||||
|
device_sn: string,
|
||||||
|
nickname?: string,
|
||||||
|
device_name?: string,
|
||||||
|
online?: boolean,
|
||||||
|
flight_area_status: FlightAreaStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAP_API_PREFIX = '/map/api/v1'
|
||||||
|
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
|
||||||
|
export async function getFlightAreaList (): Promise<IWorkspaceResponse<GetFlightArea[]>> {
|
||||||
|
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-areas`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeFlightAreaStatus (area_id: string, status: boolean): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.put(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`, { status })
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveFlightArea (body: PostFlightAreaBody): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area`, body)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFlightArea (area_id: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.delete(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/${area_id}`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function syncFlightArea (device_sn: string[]): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const resp = await request.post(`${MAP_API_PREFIX}/workspaces/${workspaceId}/flight-area/sync`, { device_sn })
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDeviceStatus (): Promise<IWorkspaceResponse<GetDeviceStatus[]>> {
|
||||||
|
const resp = await request.get(`${MAP_API_PREFIX}/workspaces/${workspaceId}/device-status`)
|
||||||
|
return resp.data
|
||||||
|
}
|
||||||
42
src/api/http.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 职责声明:
|
||||||
|
* 1.提供一个 单一的 axios 实例(方面进行统一拦截)
|
||||||
|
* 2.允许调用方定制自己的配置(例如拦截器等),而不影响其他实例
|
||||||
|
*
|
||||||
|
* 暴露 API:
|
||||||
|
* 1.一个统一的 axios 实例: singleAxiosInstance(绑定了统一的拦截器)
|
||||||
|
* 2.创建 axios 实例的方法 createAxiosInstance,并在参数中允许配置是否绑定统一拦截器
|
||||||
|
* 3.对外暴露统一拦截器绑定方案,允许外界进行定制: bindCommonRequestInterceptors、bindCommonResponseInterceptors
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||||
|
|
||||||
|
// 统一的 request 拦截器
|
||||||
|
export function bindCommonRequestInterceptors (instance: AxiosInstance): void {
|
||||||
|
instance.interceptors.request.use(config => {
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified response interceptor
|
||||||
|
export function bindCommonResponseInterceptors (instance: AxiosInstance): void {
|
||||||
|
instance.interceptors.response.use(config => {
|
||||||
|
return config
|
||||||
|
}, err => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAxiosInstance (config?: AxiosRequestConfig, commonInterceptorConf: { request?: boolean, response?: boolean } = {}): AxiosInstance {
|
||||||
|
const instance = Axios.create(config)
|
||||||
|
|
||||||
|
// Binding a unified interceptor, binding by default
|
||||||
|
commonInterceptorConf.request !== false && bindCommonRequestInterceptors(instance)
|
||||||
|
commonInterceptorConf.response !== false && bindCommonResponseInterceptors(instance)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleAxios = createAxiosInstance({}, { request: true, response: false })
|
||||||
|
|
||||||
|
export default singleAxios
|
||||||
48
src/api/http/config.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export const CURRENT_CONFIG = {
|
||||||
|
|
||||||
|
// license
|
||||||
|
appId: '159035', // You need to go to the development website to apply.
|
||||||
|
appKey: '5132f8002819503ee42bd86208a12f6', // You need to go to the development website to apply.
|
||||||
|
appLicense: 'SjRIEnTeouiw47DcEra+OLNcbqvpGn4Uep8zQi3DJGFsI/us1nEp5hhL/JJ7PfPHmMopXN+Q6daOgXIKqCKBaCX2ntvKfmgxjTAqwS2gu3AxCkBlqTgZZsFNtTRdo98tbMbo+EsOQu/EGWjlrk6yCzfqB2iSRWQiQsbwswdcrlo=', // You need to go to the development website to apply.
|
||||||
|
// appId: '159812', // You need to go to the development website to apply.
|
||||||
|
// appKey: 'db9f0fd8956539e2985ba9156027cfa', // You need to go to the development website to apply.
|
||||||
|
// appLicense: 'GL9Y/UGd0cYI5vCkQWTqIEQsgFWvkIYKqCBPjKYhe7JMObEVsF2ZRUt+emBppV5llg0AAWRJknXQJDSgL0H/qaECdGYZELAUzaDRtbJZHOM2NDlmZUMiJCI95a9KvgS8Bs/u0XYU6cKxCKqZr0uVyF7Dbgk4WgHb6SOD4Dv7Qd8=', // You need to go to the development website to apply.
|
||||||
|
|
||||||
|
// http
|
||||||
|
baseURL: '/', // 修改为根路径
|
||||||
|
// baseURL: 'http://192.168.0.90:12311/', // 修改为根路径
|
||||||
|
// baseURL: 'http://8.137.54.85:11501/', // 修改为根路径
|
||||||
|
// baseURL: 'http://220.167.122.36:12311/', // 修改为根路径
|
||||||
|
// baseURL: 'http://139.155.129.59:6789/', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
|
||||||
|
// baseURL: 'http://47.108.71.9:12311/', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
|
||||||
|
// websocketURL: 'ws://139.155.129.59:6789/api/v1/ws', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
|
||||||
|
// websocketURL: '/api/v1/ws', // 保持现有配置
|
||||||
|
websocketURL: '', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
|
||||||
|
// websocketURL: 'ws://192.168.31.197:8083/mqtt', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
|
||||||
|
|
||||||
|
// livestreaming
|
||||||
|
// RTMP Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
|
||||||
|
// rtmpURL: 'rtmp://47.108.71.9:1935/live/', // Example: 'rtmp://192.168.1.1/live/'
|
||||||
|
rtmpURL: '', // Example: 'rtmp://192.168.1.1/live/'
|
||||||
|
// GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
|
||||||
|
gbServerIp: 'Please enter the server ip.',
|
||||||
|
gbServerPort: 'Please enter the server port.',
|
||||||
|
gbServerId: 'Please enter the server id.',
|
||||||
|
gbAgentId: 'Please enter the agent id',
|
||||||
|
gbPassword: 'Please enter the agent password',
|
||||||
|
gbAgentPort: 'Please enter the local port.',
|
||||||
|
gbAgentChannel: 'Please enter the channel.',
|
||||||
|
// RTSP
|
||||||
|
rtspUserName: 'Please enter the username.',
|
||||||
|
rtspPassword: 'Please enter the password.',
|
||||||
|
rtspPort: '8554',
|
||||||
|
// Agora
|
||||||
|
agoraAPPID: 'Please enter the agora app id.',
|
||||||
|
agoraToken: 'Please enter the agora temporary token.',
|
||||||
|
agoraChannel: 'Please enter the agora channel.',
|
||||||
|
|
||||||
|
// map
|
||||||
|
// You can apply on the AMap website.
|
||||||
|
amapKey: 'Please enter the amap key.',
|
||||||
|
|
||||||
|
}
|
||||||
84
src/api/http/request.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { uuidv4 } from '/@/utils/uuid'
|
||||||
|
import { CURRENT_CONFIG } from './config'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import router from '/@/router'
|
||||||
|
import { ELocalStorageKey, ERouterName, EUserType } from '/@/types/enums'
|
||||||
|
export * from './type'
|
||||||
|
|
||||||
|
const REQUEST_ID = 'X-Request-Id'
|
||||||
|
function getAuthToken () {
|
||||||
|
return localStorage.getItem(ELocalStorageKey.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
// withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
// timeout: 12000,
|
||||||
|
})
|
||||||
|
|
||||||
|
instance.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
config.headers[ELocalStorageKey.Token] = getAuthToken()
|
||||||
|
// config.headers[REQUEST_ID] = uuidv4()
|
||||||
|
config.baseURL = CURRENT_CONFIG.baseURL
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
console.info('URL: ' + response.config.baseURL + response.config.url, '\nData: ', response.data, '\nResponse:', response)
|
||||||
|
/* if (response.data.code && response.data.code !== 1) {
|
||||||
|
console.error(response.data.message)
|
||||||
|
} */
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
const requestId = err?.config?.headers && err?.config?.headers[REQUEST_ID]
|
||||||
|
if (requestId) {
|
||||||
|
console.info(REQUEST_ID, ':', requestId)
|
||||||
|
}
|
||||||
|
console.info('url: ', err?.config?.url, `【${err?.config?.method}】 \n>>>> err: `, err)
|
||||||
|
|
||||||
|
let description = '-'
|
||||||
|
if (err.response?.data && err.response.data.message) {
|
||||||
|
description = err.response.data.message
|
||||||
|
}
|
||||||
|
if (err.response?.data && err.response.data.result) {
|
||||||
|
description = err.response.data.result.message
|
||||||
|
}
|
||||||
|
// @See: https://github.com/axios/axios/issues/383
|
||||||
|
if (!err.response || !err.response.status) {
|
||||||
|
console.error('The network is abnormal, please check the backend service and try again')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (err.response?.status !== 200) {
|
||||||
|
console.error(`ERROR_CODE: ${err.response?.status}`)
|
||||||
|
}
|
||||||
|
// if (err.response?.status === 403) {
|
||||||
|
// // window.location.href = '/'
|
||||||
|
// }
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
console.error(err.response)
|
||||||
|
const flag: number = Number(localStorage.getItem(ELocalStorageKey.Flag))
|
||||||
|
switch (flag) {
|
||||||
|
case EUserType.Web:
|
||||||
|
router.push(ERouterName.PROJECT)
|
||||||
|
break
|
||||||
|
case EUserType.Pilot:
|
||||||
|
router.push(ERouterName.PILOT)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(err)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export default instance
|
||||||
38
src/api/http/type.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
export interface IResult {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPage {
|
||||||
|
page: number;
|
||||||
|
total: number;
|
||||||
|
page_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IListWorkspaceResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
list: T[];
|
||||||
|
pagination: IPage;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Workspace
|
||||||
|
export interface IWorkspaceResponse<T> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IStatus = 'WAITING' | 'DOING' | 'SUCCESS' | 'FAILED';
|
||||||
|
|
||||||
|
export interface CommonListResponse<T> extends IResult {
|
||||||
|
data: {
|
||||||
|
list: T[];
|
||||||
|
pagination: IPage;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonResponse<T> extends IResult {
|
||||||
|
data: T
|
||||||
|
}
|
||||||
53
src/api/layer.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ELocalStorageKey } from '../types/enums'
|
||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { mapLayers } from '/@/constants/mock-layers'
|
||||||
|
import { elementGroupsReq, PostElementsBody, PutElementsBody } from '/@/types/mapLayer'
|
||||||
|
const PREFIX = '/map/api/v1'
|
||||||
|
const workspace_id = localStorage.getItem(ELocalStorageKey.WorkspaceId)
|
||||||
|
type UnknownResponse = Promise<IWorkspaceResponse<unknown>>
|
||||||
|
// get elements group
|
||||||
|
// export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
|
||||||
|
// const url = `${PREFIX}/workspaces/${workspace_id}/element_groups`
|
||||||
|
// const result = await request.get(url, {
|
||||||
|
// params: {
|
||||||
|
// group_id: reqParams.groupId,
|
||||||
|
// is_distributed: reqParams.isDistributed
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// return result.data
|
||||||
|
// }
|
||||||
|
export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
|
||||||
|
return mapLayers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get elements groups request
|
||||||
|
export const getElementGroupsReq = async (body: elementGroupsReq): Promise<IWorkspaceResponse<any>> => {
|
||||||
|
const url = `${PREFIX}/workspaces/` + workspace_id + '/element-groups'
|
||||||
|
const result = await request.get(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
// add element
|
||||||
|
export const postElementsReq = async (pid: string, body: PostElementsBody): Promise<IWorkspaceResponse<{ id: string }>> => {
|
||||||
|
const url = `${PREFIX}/workspaces/` + workspace_id + `/element-groups/${pid}/elements`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
// Update map element request
|
||||||
|
export const updateElementsReq = async (id: string, body: PutElementsBody): Promise<IWorkspaceResponse<{ id: string }>> => {
|
||||||
|
const url = `${PREFIX}/workspaces/` + workspace_id + `/elements/${id}`
|
||||||
|
const result = await request.put(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
// Delete map element
|
||||||
|
export const deleteElementReq = async (id: string, body: {}): Promise<any> => {
|
||||||
|
const url = `${PREFIX}/workspaces/` + workspace_id + `/elements/${id}`
|
||||||
|
const result = await request.delete(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete layer elements
|
||||||
|
export const deleteLayerEleReq = async (id: string, body: {}): Promise<any> => {
|
||||||
|
const url = `${PREFIX}/workspaces/` + workspace_id + `/element-groups/${id}/elements`
|
||||||
|
const result = await request.delete(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
196
src/api/manage.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import { Firmware, FirmwareQueryParam, FirmwareUploadParam } from '/@/types/device-firmware'
|
||||||
|
import request, { CommonListResponse, IListWorkspaceResponse, IPage, IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { Device } from '/@/types/device'
|
||||||
|
|
||||||
|
const HTTP_PREFIX = '/manage/api/v1'
|
||||||
|
const HTTP_V1 = '/api/v1'
|
||||||
|
const HTTP_V2 = '/api/v2'
|
||||||
|
|
||||||
|
// login
|
||||||
|
export interface LoginBody {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
flag: number,
|
||||||
|
}
|
||||||
|
export interface BindBody {
|
||||||
|
device_sn: string,
|
||||||
|
user_id: string,
|
||||||
|
workspace_id: string,
|
||||||
|
domain?: string
|
||||||
|
}
|
||||||
|
export interface HmsQueryBody {
|
||||||
|
sns: string[],
|
||||||
|
children_sn: string,
|
||||||
|
device_sn: string,
|
||||||
|
language: string,
|
||||||
|
level: number | string,
|
||||||
|
begin_time: number,
|
||||||
|
end_time: number,
|
||||||
|
message: string,
|
||||||
|
domain: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const login = async function (body: LoginBody): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/login`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Token
|
||||||
|
export const refreshToken = async function (body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/token/refresh`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Platform Info
|
||||||
|
export const getPlatformInfo = async function (): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/current`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get User Info
|
||||||
|
export const getUserInfo = async function (): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/users/current`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Device Topo
|
||||||
|
export const getDeviceTopo = async function (workspace_id: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Livestream Capacity
|
||||||
|
export const getLiveCapacity = async function (body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/live/capacity`
|
||||||
|
const result = await request.get(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Livestream
|
||||||
|
export const startLivestream = async function (body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/live/streams/start`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop Livestream
|
||||||
|
export const stopLivestream = async function (body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/live/streams/stop`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
// Update Quality
|
||||||
|
export const setLivestreamQuality = async function (body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/live/streams/update`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllUsersInfo = async function (wid: string, body: IPage): Promise<CommonListResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/users/${wid}/users?&page=${body.page}&page_size=${body.page_size}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateUserInfo = async function (wid: string, user_id: string, body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/users/${wid}/users/${user_id}`
|
||||||
|
const result = await request.put(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bindDevice = async function (body: BindBody): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${body.device_sn}/binding`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unbindDevice = async function (device_sn: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${device_sn}/unbinding`
|
||||||
|
const result = await request.delete(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDeviceBySn = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWayLineList = async function (workspace_id: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_V1}/wayline/api/v1/workspaces/${workspace_id}/waylines`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取绑定设备信息
|
||||||
|
* @param workspace_id
|
||||||
|
* @param body
|
||||||
|
* @param domain
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getBindingDevices = async function (workspace_id: string, body: IPage, domain: number): Promise<IListWorkspaceResponse<Device>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}&domain=${domain}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDevice = async function (body: {}, workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/${device_sn}`
|
||||||
|
const result = await request.put(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUnreadDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDeviceHms = async function (workspace_id: string, device_sn: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms/${device_sn}`
|
||||||
|
const result = await request.put(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDeviceHms = async function (body: HmsQueryBody, workspace_id: string, pagination: IPage): Promise<IListWorkspaceResponse<any>> {
|
||||||
|
let url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/hms?page=${pagination.page}&page_size=${pagination.page_size}` +
|
||||||
|
`&level=${body.level ?? ''}&begin_time=${body.begin_time ?? ''}&end_time=${body.end_time ?? ''}&message=${body.message ?? ''}&language=${body.language}`
|
||||||
|
body.sns.forEach((sn: string) => {
|
||||||
|
if (sn !== '') {
|
||||||
|
url = url.concat(`&device_sn=${sn}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeLivestreamLens = async function (body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/live/streams/switch`
|
||||||
|
const result = await request.post(url, body)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFirmwares = async function (workspace_id: string, page: IPage, body: FirmwareQueryParam): Promise<IListWorkspaceResponse<Firmware>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspace_id}/firmwares?page=${page.page}&page_size=${page.page_size}` +
|
||||||
|
`&device_name=${body.device_name}&product_version=${body.product_version}&status=${body.firmware_status ?? ''}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importFirmareFile = async function (workspaceId: string, param: FormData): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/firmwares/file/upload`
|
||||||
|
const result = await request.post(url, param)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeFirmareStatus = async function (workspaceId: string, firmwareId: string, param: {status: boolean}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/firmwares/${firmwareId}`
|
||||||
|
const result = await request.put(url, param)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
26
src/api/media.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
const HTTP_PREFIX = '/media/api/v1'
|
||||||
|
|
||||||
|
// Get Media Files
|
||||||
|
export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
// Download Media File
|
||||||
|
export const downloadMediaFile = async function (workspaceId: string, fileId: string): Promise<any> {
|
||||||
|
const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fileId}/url`
|
||||||
|
const result = await request.get(url, { responseType: 'blob' })
|
||||||
|
if (result.data.type === 'application/json') {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = function (e) {
|
||||||
|
const text = reader.result as string
|
||||||
|
const result = JSON.parse(text)
|
||||||
|
console.error(result.message)
|
||||||
|
}
|
||||||
|
reader.readAsText(result.data, 'utf-8')
|
||||||
|
} else {
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
283
src/api/pilot-bridge.ts
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { EComponentName, EPhotoType, ERouterName } from '../types'
|
||||||
|
import { CURRENT_CONFIG } from './http/config'
|
||||||
|
import { EVideoPublishType, LiveStreamStatus } from '../types/live-stream'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
|
||||||
|
const root = getRoot()
|
||||||
|
export const components = new Map()
|
||||||
|
declare let window:any
|
||||||
|
interface JsResponse{
|
||||||
|
code:number,
|
||||||
|
message:string,
|
||||||
|
data:any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThingParam {
|
||||||
|
host: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
connectCallback: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiveshareParam {
|
||||||
|
videoPublishType: string, // video-on-demand、video-by-manual、video-demand-aux-manual
|
||||||
|
statusCallback: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MapParam {
|
||||||
|
userName: string,
|
||||||
|
elementPreName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WsParam {
|
||||||
|
host: string,
|
||||||
|
token: string,
|
||||||
|
connectCallback: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiParam {
|
||||||
|
host: string,
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaParam {
|
||||||
|
autoUploadPhoto: boolean, // 是否自动上传图片, 非必需
|
||||||
|
autoUploadPhotoType: number, // 自动上传的照片类型,0:原图, 1:缩略图, 非必需
|
||||||
|
autoUploadVideo: boolean // 是否自动上传视频, 非必需
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnBool (response: string): boolean {
|
||||||
|
const res: JsResponse = JSON.parse(response)
|
||||||
|
const isError = errorHint(res)
|
||||||
|
if (JSON.stringify(res.data) !== '{}') {
|
||||||
|
return isError && res.data
|
||||||
|
}
|
||||||
|
return isError
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnString (response: string): string {
|
||||||
|
const res: JsResponse = JSON.parse(response)
|
||||||
|
return errorHint(res) ? res.data : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function returnNumber (response: string): number {
|
||||||
|
const res: JsResponse = JSON.parse(response)
|
||||||
|
return errorHint(res) ? res.data : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorHint (response: JsResponse): boolean {
|
||||||
|
if (response.code !== 0) {
|
||||||
|
console.error(response.message)
|
||||||
|
console.error(response.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
init (): Map<EComponentName, any> {
|
||||||
|
const thingParam: ThingParam = {
|
||||||
|
host: '',
|
||||||
|
connectCallback: '',
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
components.set(EComponentName.Thing, thingParam)
|
||||||
|
const liveshareParam: LiveshareParam = {
|
||||||
|
videoPublishType: EVideoPublishType.VideoDemandAuxManual,
|
||||||
|
statusCallback: 'liveStatusCallback'
|
||||||
|
}
|
||||||
|
components.set(EComponentName.Liveshare, liveshareParam)
|
||||||
|
const mapParam: MapParam = {
|
||||||
|
userName: '',
|
||||||
|
elementPreName: 'PILOT'
|
||||||
|
}
|
||||||
|
components.set(EComponentName.Map, mapParam)
|
||||||
|
const wsParam: WsParam = {
|
||||||
|
host: CURRENT_CONFIG.websocketURL,
|
||||||
|
token: '',
|
||||||
|
connectCallback: 'wsConnectCallback'
|
||||||
|
}
|
||||||
|
components.set(EComponentName.Ws, wsParam)
|
||||||
|
const apiParam: ApiParam = {
|
||||||
|
host: '',
|
||||||
|
token: ''
|
||||||
|
}
|
||||||
|
components.set(EComponentName.Api, apiParam)
|
||||||
|
components.set(EComponentName.Tsa, {})
|
||||||
|
const mediaParam: MediaParam = {
|
||||||
|
autoUploadPhoto: true,
|
||||||
|
autoUploadPhotoType: EPhotoType.Preview,
|
||||||
|
autoUploadVideo: true
|
||||||
|
}
|
||||||
|
components.set(EComponentName.Media, mediaParam)
|
||||||
|
components.set(EComponentName.Mission, {})
|
||||||
|
|
||||||
|
return components
|
||||||
|
},
|
||||||
|
|
||||||
|
getComponentParam (key:EComponentName): any {
|
||||||
|
return components.get(key)
|
||||||
|
},
|
||||||
|
setComponentParam (key:EComponentName, value:any) {
|
||||||
|
components.set(key, value)
|
||||||
|
},
|
||||||
|
loadComponent (name:string, param:any):string {
|
||||||
|
return returnString(window.djiBridge.platformLoadComponent(name, JSON.stringify(param)))
|
||||||
|
},
|
||||||
|
unloadComponent (name:string) :string {
|
||||||
|
return returnString(window.djiBridge.platformUnloadComponent(name))
|
||||||
|
},
|
||||||
|
isComponentLoaded (module:string): boolean {
|
||||||
|
return returnBool(window.djiBridge.platformIsComponentLoaded(module))
|
||||||
|
},
|
||||||
|
setWorkspaceId (uuid:string):string {
|
||||||
|
return returnString(window.djiBridge.platformSetWorkspaceId(uuid))
|
||||||
|
},
|
||||||
|
setPlatformMessage (platformName:string, title:string, desc:string): boolean {
|
||||||
|
return returnBool(window.djiBridge.platformSetInformation(platformName, title, desc))
|
||||||
|
},
|
||||||
|
getRemoteControllerSN () :string {
|
||||||
|
return returnString(window.djiBridge.platformGetRemoteControllerSN())
|
||||||
|
},
|
||||||
|
getAircraftSN ():string {
|
||||||
|
return returnString(window.djiBridge.platformGetAircraftSN())
|
||||||
|
},
|
||||||
|
stopwebview ():string {
|
||||||
|
return returnString(window.djiBridge.platformStopSelf())
|
||||||
|
},
|
||||||
|
setLogEncryptKey (key:string):string {
|
||||||
|
return window.djiBridge.platformSetLogEncryptKey(key)
|
||||||
|
},
|
||||||
|
clearLogEncryptKey ():string {
|
||||||
|
return window.djiBridge.platformClearLogEncryptKey()
|
||||||
|
},
|
||||||
|
getLogPath ():string {
|
||||||
|
return returnString(window.djiBridge.platformGetLogPath())
|
||||||
|
},
|
||||||
|
platformVerifyLicense (appId:string, appKey:string, appLicense:string): boolean {
|
||||||
|
return returnBool(window.djiBridge.platformVerifyLicense(appId, appKey, appLicense))
|
||||||
|
},
|
||||||
|
isPlatformVerifySuccess (): boolean {
|
||||||
|
return returnBool(window.djiBridge.platformIsVerified())
|
||||||
|
},
|
||||||
|
isAppInstalled (pkgName: string): boolean {
|
||||||
|
return returnBool(window.djiBridge.platformIsAppInstalled(pkgName))
|
||||||
|
},
|
||||||
|
getVersion (): string {
|
||||||
|
return window.djiBridge.platformGetVersion()
|
||||||
|
},
|
||||||
|
|
||||||
|
// thing
|
||||||
|
thingGetConnectState (): boolean {
|
||||||
|
return returnBool(window.djiBridge.thingGetConnectState())
|
||||||
|
},
|
||||||
|
|
||||||
|
thingGetConfigs (): ThingParam {
|
||||||
|
const thingParam = JSON.parse(window.djiBridge.thingGetConfigs())
|
||||||
|
return thingParam.code === 0 ? JSON.parse(thingParam.data) : {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// api
|
||||||
|
getToken () : string {
|
||||||
|
return returnString(window.djiBridge.apiGetToken())
|
||||||
|
},
|
||||||
|
setToken (token:string):string {
|
||||||
|
return returnString(window.djiBridge.apiSetToken(token))
|
||||||
|
},
|
||||||
|
getHost (): string {
|
||||||
|
return returnString(window.djiBridge.apiGetHost())
|
||||||
|
},
|
||||||
|
|
||||||
|
// liveshare
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* video-on-demand: 服务器点播,依赖于thing模块,具体的点播命令参见设备物模型的直播服务
|
||||||
|
* video-by-manual:手动点播,配置好直播类型参数之后,在图传页面可修改直播参数,停止直播
|
||||||
|
* video-demand-aux-manual: 混合模式,支持服务器点播,以及图传页面修改直播参数,停止直播
|
||||||
|
*/
|
||||||
|
setVideoPublishType (type:string): boolean {
|
||||||
|
return returnBool(window.djiBridge.liveshareSetVideoPublishType(type))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
* type: liveshare type, 0:unknown, 1:agora, 2:rtmp, 3:rtsp, 4:gb28181
|
||||||
|
*/
|
||||||
|
getLiveshareConfig (): string {
|
||||||
|
return returnString(window.djiBridge.liveshareGetConfig())
|
||||||
|
},
|
||||||
|
|
||||||
|
setLiveshareConfig (type:number, params:string):string {
|
||||||
|
return window.djiBridge.liveshareSetConfig(type, params)
|
||||||
|
},
|
||||||
|
|
||||||
|
setLiveshareStatusCallback (callbackFunc:string) :string {
|
||||||
|
return window.djiBridge.liveshareSetStatusCallback(callbackFunc)
|
||||||
|
},
|
||||||
|
getLiveshareStatus (): LiveStreamStatus {
|
||||||
|
return JSON.parse(JSON.parse(window.djiBridge.liveshareGetStatus()).data)
|
||||||
|
},
|
||||||
|
startLiveshare (): boolean {
|
||||||
|
return returnBool(window.djiBridge.liveshareStartLive())
|
||||||
|
},
|
||||||
|
stopLiveshare (): boolean {
|
||||||
|
return returnBool(window.djiBridge.liveshareStopLive())
|
||||||
|
},
|
||||||
|
// WebSocket
|
||||||
|
wsGetConnectState (): boolean {
|
||||||
|
return returnBool(window.djiBridge.wsGetConnectState())
|
||||||
|
},
|
||||||
|
wsConnect (host: string, token: string, callback: string): string {
|
||||||
|
return window.djiBridge.wsConnect(host, token, callback)
|
||||||
|
},
|
||||||
|
wsDisconnect (): string {
|
||||||
|
return window.djiBridge.wsConnect()
|
||||||
|
},
|
||||||
|
wsSend (message: string): string {
|
||||||
|
return window.djiBridge.wsSend(message)
|
||||||
|
},
|
||||||
|
// media
|
||||||
|
setAutoUploadPhoto (auto:boolean):string {
|
||||||
|
return window.djiBridge.mediaSetAutoUploadPhoto(auto)
|
||||||
|
},
|
||||||
|
getAutoUploadPhoto (): boolean {
|
||||||
|
return returnBool(window.djiBridge.mediaGetAutoUploadPhoto())
|
||||||
|
},
|
||||||
|
setUploadPhotoType (type:number):string {
|
||||||
|
return window.djiBridge.mediaSetUploadPhotoType(type)
|
||||||
|
},
|
||||||
|
getUploadPhotoType (): number {
|
||||||
|
return returnNumber(window.djiBridge.mediaGetUploadPhotoType())
|
||||||
|
},
|
||||||
|
setAutoUploadVideo (auto:boolean):string {
|
||||||
|
return window.djiBridge.mediaSetAutoUploadVideo(auto)
|
||||||
|
},
|
||||||
|
getAutoUploadVideo (): boolean {
|
||||||
|
return returnBool(window.djiBridge.mediaGetAutoUploadVideo())
|
||||||
|
},
|
||||||
|
setDownloadOwner (rcIndex:number):string {
|
||||||
|
return window.djiBridge.mediaSetDownloadOwner(rcIndex)
|
||||||
|
},
|
||||||
|
getDownloadOwner (): number {
|
||||||
|
return returnNumber(window.djiBridge.mediaGetDownloadOwner())
|
||||||
|
},
|
||||||
|
onBackClickReg () {
|
||||||
|
window.djiBridge.onBackClick = () => {
|
||||||
|
if (root.$router.currentRoute.value.path === '/' + ERouterName.PILOT_HOME) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
history.go(-1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onStopPlatform () {
|
||||||
|
window.djiBridge.onStopPlatform = () => {
|
||||||
|
localStorage.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/api/wayline.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import request, { IPage, IWorkspaceResponse, IListWorkspaceResponse } from '/@/api/http/request'
|
||||||
|
import { TaskType, TaskStatus, OutOfControlAction } from '/@/types/task'
|
||||||
|
import { WaylineType } from '/@/types/wayline'
|
||||||
|
|
||||||
|
const HTTP_PREFIX = '/wayline/api/v1'
|
||||||
|
|
||||||
|
// Get Wayline Files
|
||||||
|
export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?order_by=${body.order_by}&page=${body.page}&page_size=${body.page_size}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download Wayline File
|
||||||
|
export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url`
|
||||||
|
const result = await request.get(url, { responseType: 'blob' })
|
||||||
|
if (result.data.type === 'application/json') {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = function (e) {
|
||||||
|
const text = reader.result as string
|
||||||
|
const result = JSON.parse(text)
|
||||||
|
console.error(result.message)
|
||||||
|
}
|
||||||
|
reader.readAsText(result.data, 'utf-8')
|
||||||
|
} else {
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Wayline File
|
||||||
|
export const deleteWaylineFile = async function (workspaceId: string, waylineId: string): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}`
|
||||||
|
const result = await request.delete(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatePlan {
|
||||||
|
name: string,
|
||||||
|
file_id: string,
|
||||||
|
dock_sn: string,
|
||||||
|
task_type: TaskType, // 任务类型
|
||||||
|
wayline_type: WaylineType, // 航线类型
|
||||||
|
task_days: number[] // 执行任务的日期(秒)
|
||||||
|
task_periods: number[][] // 执行任务的时间点(秒)
|
||||||
|
rth_altitude: number // 相对机场返航高度 20 - 500
|
||||||
|
out_of_control_action: OutOfControlAction // 失控动作
|
||||||
|
min_battery_capacity?: number, // The minimum battery capacity of aircraft.
|
||||||
|
min_storage_capacity?: number, // The minimum storage capacity of dock and aircraft.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Wayline Job
|
||||||
|
export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks`
|
||||||
|
const result = await request.post(url, plan)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
job_id: string,
|
||||||
|
job_name: string,
|
||||||
|
task_type: TaskType, // 任务类型
|
||||||
|
file_id: string, // 航线文件id
|
||||||
|
file_name: string, // 航线名称
|
||||||
|
wayline_type: WaylineType, // 航线类型
|
||||||
|
dock_sn: string,
|
||||||
|
dock_name: string,
|
||||||
|
workspace_id: string,
|
||||||
|
username: string,
|
||||||
|
begin_time: string,
|
||||||
|
end_time: string,
|
||||||
|
execute_time: string,
|
||||||
|
completed_time: string,
|
||||||
|
status: TaskStatus, // 任务状态
|
||||||
|
progress: number, // 执行进度
|
||||||
|
code: number, // 错误码
|
||||||
|
rth_altitude: number // 相对机场返航高度 20 - 500
|
||||||
|
out_of_control_action: OutOfControlAction // 失控动作
|
||||||
|
media_count: number // 媒体数量
|
||||||
|
uploading:boolean // 是否正在上传媒体
|
||||||
|
uploaded_count: number // 已上传媒体数量
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Wayline Jobs
|
||||||
|
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IListWorkspaceResponse<Task>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}`
|
||||||
|
const result = await request.get(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteTaskParams {
|
||||||
|
job_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除机场任务
|
||||||
|
export async function deleteTask (workspaceId: string, params: DeleteTaskParams): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs`
|
||||||
|
const result = await request.delete(url, {
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UpdateTaskStatus {
|
||||||
|
Suspend = 0, // 暂停
|
||||||
|
Resume = 1, // 恢复
|
||||||
|
}
|
||||||
|
export interface UpdateTaskStatusBody {
|
||||||
|
job_id: string
|
||||||
|
status: UpdateTaskStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新机场任务状态
|
||||||
|
export async function updateTaskStatus (workspaceId: string, body: UpdateTaskStatusBody): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${body.job_id}`
|
||||||
|
const result = await request.put(url, {
|
||||||
|
status: body.status
|
||||||
|
})
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload Wayline file
|
||||||
|
export const importKmzFile = async function (workspaceId: string, file: {}): Promise<IWorkspaceResponse<any>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/file/upload`
|
||||||
|
const result = await request.post(url, file, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 媒体立即上传
|
||||||
|
export const uploadMediaFileNow = async function (workspaceId: string, jobId: string): Promise<IWorkspaceResponse<{}>> {
|
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${jobId}/media-highest`
|
||||||
|
const result = await request.post(url)
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
11
src/assets/icons/check.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>ic/panel/checkbox_active</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="ic/panel/checkbox_active" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Group-3" fill="#FFFFFF">
|
||||||
|
<polygon id="Shape-Copy" points="11.2103387 4.23529412 6.47425817 8.97137463 4.50089127 6.99800776 2.82352941 8.65563591 4.79689632 10.6290028 6.37558979 12.2076964 6.47425817 12.286631 12.8877005 5.8929223"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 731 B |
BIN
src/assets/icons/cloudapi.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
16
src/assets/icons/dji-logo-vector.svg
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="layer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 652 652" style="enable-background:new 0 0 652 652;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#241F1F;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M464.9,404.7l44.3-183.9h-95l-40.5,164.9c-5.9,32.2-40.4,47.2-64.9,47.6h-67.3l-22.9,66.3h141.5
|
||||||
|
C395,499.6,446.5,481.8,464.9,404.7"/>
|
||||||
|
<path class="st0" d="M265.5,339.9L310.1,153h97.8l-50.8,212.6c-9.8,41.1-40.3,50.9-68.5,50.9H63.2c-24.8,0-45.6-10.6-34.4-58
|
||||||
|
l20.3-84.8c10.3-43,42.3-52.9,65.4-52.9h157.3l-12.7,53h-80.3c-11.8,0-18.3,2.6-21.6,16.3l-13,54.1c-4.6,19.4,2.2,20.8,16.4,20.8
|
||||||
|
h73.6C247.8,365.2,259.6,364.3,265.5,339.9"/>
|
||||||
|
<polygon class="st0" points="530.7,220.9 484.6,416.5 579.7,416.5 625.7,220.9 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 952 B |
BIN
src/assets/icons/dji_logo.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/icons/dock.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/icons/drone.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
4
src/assets/icons/layer.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs><path d="m14.042 0 14.043 8.055-5.128 2.941 5.128 2.941-5.127 2.94 5.127 2.942-14.043 8.055L0 19.82l5.126-2.942L0 13.937l5.127-2.941L0 8.056 14.042 0zM7.638 18.318l-2.614 1.5 9.018 5.175 9.017-5.174-2.614-1.5-6.403 3.674-6.404-3.675zm0-5.882-2.615 1.5 9.02 5.175 9.017-5.173-2.615-1.501-6.403 3.674-6.404-3.675zm6.404-9.554L5.024 8.056l9.018 5.174 9.017-5.174-9.017-5.174z" id="layer_a"></path></defs><g transform="translate(2 2)" fill-rule="evenodd"><mask id="layer_b"><use xlink:href="#layer_a"></use></mask><use fill-rule="nonzero" xlink:href="#layer_a"></use><g mask="url(#layer_b)"><path d="M-19-20h68v68h-68z"></path></g></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 830 B |
BIN
src/assets/icons/m30.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
4
src/assets/icons/media.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs><path d="M29.5 4v21.5h-4v2H3V8h3V4h23.5zM10.553 18.859 5.5 22.399V25H23v-6.088l-7.011 5.108-5.436-5.161zM27 6.5H8.499L8.5 8h17v15H27V6.5zm-4 4H5.5v8.847l5.293-3.707 5.406 5.133L23 15.819V10.5zm-6.362 1.956a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" id="media_a"></path></defs><g fill-rule="evenodd"><mask id="media_b"><use xlink:href="#media_a"></use></mask><use fill-rule="nonzero" xlink:href="#media_a"></use><g mask="url(#media_b)"><path d="M-17-18h68v68h-68z"></path></g></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 666 B |
BIN
src/assets/icons/no-data.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
22
src/assets/icons/pin-19be6b.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>2图标/24px/pin</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#19BE6B" fill-rule="evenodd">
|
||||||
|
<g id="编组" transform="translate(5.000000, 2.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="形状" fill="#19BE6B" fill-rule="nonzero" xlink:href="#path-1"></use>
|
||||||
|
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#19BE6B">
|
||||||
|
<g transform="translate(-26.000000, -26.000000)">
|
||||||
|
<rect x="0" y="0" width="68" height="68"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
22
src/assets/icons/pin-212121.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>2图标/24px/pin</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#212121" fill-rule="evenodd">
|
||||||
|
<g id="编组" transform="translate(5.000000, 2.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="形状" fill="#212121" fill-rule="nonzero" xlink:href="#path-1"></use>
|
||||||
|
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#212121">
|
||||||
|
<g transform="translate(-26.000000, -26.000000)">
|
||||||
|
<rect x="0" y="0" width="68" height="68"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
22
src/assets/icons/pin-2d8cf0.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>2图标/24px/pin</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#2D8CF0" fill-rule="evenodd">
|
||||||
|
<g id="编组" transform="translate(5.000000, 2.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="形状" fill="#2D8CF0" fill-rule="nonzero" xlink:href="#path-1"></use>
|
||||||
|
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#2D8CF0">
|
||||||
|
<g transform="translate(-26.000000, -26.000000)">
|
||||||
|
<rect x="0" y="0" width="68" height="68"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
22
src/assets/icons/pin-b620e0.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>2图标/24px/pin</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#b620e0" fill-rule="evenodd">
|
||||||
|
<g id="编组" transform="translate(5.000000, 2.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="形状" fill="#b620e0" fill-rule="nonzero" xlink:href="#path-1"></use>
|
||||||
|
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#b620e0">
|
||||||
|
<g transform="translate(-26.000000, -26.000000)">
|
||||||
|
<rect x="0" y="0" width="68" height="68"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
22
src/assets/icons/pin-e23c39.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>2图标/24px/pin</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#e23c39" fill-rule="evenodd">
|
||||||
|
<g id="编组" transform="translate(5.000000, 2.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="形状" fill="#e23c39" fill-rule="nonzero" xlink:href="#path-1"></use>
|
||||||
|
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#e23c39">
|
||||||
|
<g transform="translate(-26.000000, -26.000000)">
|
||||||
|
<rect x="0" y="0" width="68" height="68"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
22
src/assets/icons/pin-ffbb00.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
|
||||||
|
<title>2图标/24px/pin</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs>
|
||||||
|
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
|
||||||
|
</defs>
|
||||||
|
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#FFBB00" fill-rule="evenodd">
|
||||||
|
<g id="编组" transform="translate(5.000000, 2.000000)">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1"></use>
|
||||||
|
</mask>
|
||||||
|
<use id="形状" fill="#FFBB00" fill-rule="nonzero" xlink:href="#path-1"></use>
|
||||||
|
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#FFBB00">
|
||||||
|
<g transform="translate(-26.000000, -26.000000)">
|
||||||
|
<rect x="0" y="0" width="68" height="68"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/icons/rc.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
14
src/assets/icons/tsa.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<defs>
|
||||||
|
<path d="m19.87 10.131-1.01 2.518-.86-.012v-.006l-7.045.002-.068.004a1.75 1.75 0 0 0-1.614 1.737l.006.144.656 8.388h4.804l-1.005 2.5h-6.11l-.168-2.137H.544l-.53-6.896a4.25 4.25 0 0 1 4.03-4.598l.206-.005h2.48a4.25 4.25 0 0 1 .873.09 4.238 4.238 0 0 1 3.089-1.716l.166-.01.166-.003h8.846zm1.106 1.7 5.476 13.566-5.454-1.781-5.497 1.78 5.475-13.565zm.008 4.028.011 6.18H21l2.854.931-2.87-7.111zm-14.21-1.588L4.25 14.27a1.75 1.75 0 0 0-1.75 1.75l.007.153.352 4.597 4.401-.001-.473-6.056a4.3 4.3 0 0 1-.012-.442zM6.193 3.748a3.25 3.25 0 1 1 0 6.5 3.25 3.25 0 0 1 0-6.5zM14.3 0a4.25 4.25 0 1 1 0 8.5 4.25 4.25 0 0 1 0-8.5zM6.192 5.748a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5zM14.3 2a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5z" id="team_a">
|
||||||
|
</path>
|
||||||
|
</defs>
|
||||||
|
<g transform="translate(3 3)" fill-rule="evenodd">
|
||||||
|
<mask id="team_b">
|
||||||
|
<use xlink:href="#team_a"></use>
|
||||||
|
</mask>
|
||||||
|
<use fill-rule="nonzero" xlink:href="#team_a"></use>
|
||||||
|
<g mask="url(#team_b)"><path d="M-20-21h68v68h-68z"></path>
|
||||||
|
</g></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
1014
src/components/GMap.vue
Normal file
217
src/components/LayersTree.vue
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<a-tree
|
||||||
|
draggable
|
||||||
|
:defaultExpandAll="true"
|
||||||
|
class="device-map-layers"
|
||||||
|
@drop="onDrop"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<a-tree-node
|
||||||
|
:title="layer.name"
|
||||||
|
:id="layer.id"
|
||||||
|
v-for="layer in getTreeData"
|
||||||
|
:key="layer.id"
|
||||||
|
>
|
||||||
|
<!-- <template #title>
|
||||||
|
{{layer.name}}
|
||||||
|
</template> -->
|
||||||
|
<template v-if="layer.elements">
|
||||||
|
<a-tree-node
|
||||||
|
v-for="resource in layer.elements"
|
||||||
|
:id="getLayerTreeKey('resource', resource.id)"
|
||||||
|
:key="getLayerTreeKey('resource', resource.id)"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ resource.name }}
|
||||||
|
</template>
|
||||||
|
</a-tree-node>
|
||||||
|
</template>
|
||||||
|
</a-tree-node>
|
||||||
|
</a-tree>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineProps, PropType, reactive } from 'vue'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { DropEvent, mapLayer } from '/@/types/mapLayer'
|
||||||
|
import { getLayerTreeKey } from '/@/utils/layer-tree'
|
||||||
|
const store = useMyStore()
|
||||||
|
const props = defineProps({
|
||||||
|
layerData: Array as PropType<mapLayer[]>
|
||||||
|
})
|
||||||
|
const state = reactive({
|
||||||
|
checkedKeys: [] as string[],
|
||||||
|
expandedKeys: [] as string[]
|
||||||
|
})
|
||||||
|
const getTreeData = computed(() => {
|
||||||
|
// console.log('props.treeData', JSON.parse(JSON.stringify(props.layerData)))
|
||||||
|
return JSON.parse(JSON.stringify(props.layerData))
|
||||||
|
})
|
||||||
|
const shareId = computed(() => {
|
||||||
|
return store.state.layerBaseInfo.share
|
||||||
|
})
|
||||||
|
const defaultId = computed(() => {
|
||||||
|
return store.state.layerBaseInfo.default
|
||||||
|
})
|
||||||
|
async function onDrop ({ node, dragNode, dropPosition, dropToGap }: DropEvent) {
|
||||||
|
let _treeData = props.layerData || []
|
||||||
|
let dragKey = dragNode.eventKey
|
||||||
|
dragKey = dragKey.replaceAll('resource__', '')
|
||||||
|
const dropPos = node.pos.split('-')
|
||||||
|
let dropKey =
|
||||||
|
node.eventKey.includes(shareId.value) ||
|
||||||
|
node.eventKey.includes(defaultId.value)
|
||||||
|
? node.eventKey
|
||||||
|
: node.$parent.eventKey
|
||||||
|
if (!dragKey || !dropKey) return
|
||||||
|
dropKey = dropKey.replaceAll('resource__', '')
|
||||||
|
const loop = (data: mapLayer[], key: string, callback: Function) => {
|
||||||
|
data.forEach((item, index, arr) => {
|
||||||
|
if (item.id === key) {
|
||||||
|
return callback(item, index, arr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.elements) {
|
||||||
|
return loop(item.elements, key, callback)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const data = [..._treeData] as mapLayer[]
|
||||||
|
// Find dragObject
|
||||||
|
let dragObj = {} as mapLayer
|
||||||
|
loop(data, dragKey, (item: mapLayer, index: number, arr: mapLayer[]) => {
|
||||||
|
arr.splice(index, 1)
|
||||||
|
dragObj = item
|
||||||
|
})
|
||||||
|
if (!dropToGap) {
|
||||||
|
// Drop on the content
|
||||||
|
loop(data, dropKey, (item: mapLayer) => {
|
||||||
|
item.elements = item.elements || []
|
||||||
|
// where to insert 示例添加到尾部,可以是随意位置
|
||||||
|
item.elements.push(dragObj)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_treeData = data
|
||||||
|
// console.log('_treeData', _treeData)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
$antPrefix: 'ant';
|
||||||
|
.device-map-layers.#{$antPrefix}-tree {
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.#{$antPrefix}-tree-checkbox:not(.#{$antPrefix}-tree-checkbox-checked)
|
||||||
|
.#{$antPrefix}-tree-checkbox-inner {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一个层级的 li,有左边距 16px
|
||||||
|
> li {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.#{$antPrefix}-tree-treenode-disabled
|
||||||
|
> .#{$antPrefix}-tree-node-content-wrapper {
|
||||||
|
height: 20px;
|
||||||
|
span {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> ul {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$antPrefix}-tree-switcher {
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$antPrefix}-tree-checkbox {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.#{$antPrefix}-tree-checkbox:hover::after,
|
||||||
|
.#{$antPrefix}-tree-checkbox-wrapper:hover
|
||||||
|
.#{$antPrefix}-tree-checkbox::after {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$antPrefix}-tree-title {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$antPrefix}-tree-node-content-wrapper {
|
||||||
|
color: #fff;
|
||||||
|
width: calc(100% - 46px);
|
||||||
|
flex: 1;
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 20px;
|
||||||
|
min-width: 0; // 解决文字溢出不会省略的问题
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
|
&:not([draggable='true']) {
|
||||||
|
border-top: 2px transparent solid;
|
||||||
|
border-bottom: 2px transparent solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
&::before {
|
||||||
|
// position: absolute;
|
||||||
|
// right: 0;
|
||||||
|
// left: 0;
|
||||||
|
height: 28px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度条组件需要相对最外层定位,进度条组件的position不能设置为relative
|
||||||
|
> *:not(.progress-wrapper) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.#{$antPrefix}-tree-node-selected {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #2d8cf0;
|
||||||
|
> span {
|
||||||
|
&::before {
|
||||||
|
background-color: #4f4f4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span.#{$antPrefix}-tree-switcher.#{$antPrefix}-tree-switcher_open
|
||||||
|
.#{$antPrefix}-tree-switcher-icon {
|
||||||
|
transform: rotate(0deg) !important;
|
||||||
|
}
|
||||||
|
span.#{$antPrefix}-tree-switcher.#{$antPrefix}-tree-switcher_close
|
||||||
|
.#{$antPrefix}-tree-switcher-icon {
|
||||||
|
transform: rotate(0deg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
168
src/components/MediaPanel.vue
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header">Media Files</div>
|
||||||
|
<a-spin :spinning="loading" :delay="1000" tip="downloading" size="large">
|
||||||
|
<div class="media-panel-wrapper">
|
||||||
|
<a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint"
|
||||||
|
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
|
||||||
|
<template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col">
|
||||||
|
<a-tooltip :title="text">
|
||||||
|
<a v-if="col === 'name'">{{ text }}</a>
|
||||||
|
<span v-else>{{ text }}</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #original="{ text }">
|
||||||
|
{{ text }}
|
||||||
|
</template>
|
||||||
|
<template #action="{ record }">
|
||||||
|
<a-tooltip title="download">
|
||||||
|
<a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from '@vue/reactivity'
|
||||||
|
import { TableState } from 'ant-design-vue/lib/table/interface'
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
import { IPage } from '../api/http/type'
|
||||||
|
import { ELocalStorageKey } from '../types/enums'
|
||||||
|
import { downloadFile } from '../utils/common'
|
||||||
|
import { downloadMediaFile, getMediaFiles } from '/@/api/media'
|
||||||
|
import { DownloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { message, Pagination } from 'ant-design-vue'
|
||||||
|
import { load } from '@amap/amap-jsapi-loader'
|
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'File Name',
|
||||||
|
dataIndex: 'file_name',
|
||||||
|
ellipsis: true,
|
||||||
|
slots: { customRender: 'name' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'File Path',
|
||||||
|
dataIndex: 'file_path',
|
||||||
|
ellipsis: true,
|
||||||
|
slots: { customRender: 'path' }
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: 'FileSize',
|
||||||
|
// dataIndex: 'size',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: 'Drone',
|
||||||
|
dataIndex: 'drone'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Payload Type',
|
||||||
|
dataIndex: 'payload'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Original',
|
||||||
|
dataIndex: 'is_original',
|
||||||
|
slots: { customRender: 'original' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Created',
|
||||||
|
dataIndex: 'create_time'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
slots: { customRender: 'action' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const body: IPage = {
|
||||||
|
page: 1,
|
||||||
|
total: 0,
|
||||||
|
page_size: 50
|
||||||
|
}
|
||||||
|
const paginationProp = reactive({
|
||||||
|
pageSizeOptions: ['20', '50', '100'],
|
||||||
|
showQuickJumper: true,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSize: 50,
|
||||||
|
current: 1,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
type Pagination = TableState['pagination']
|
||||||
|
|
||||||
|
interface MediaFile {
|
||||||
|
fingerprint: string,
|
||||||
|
drone: string,
|
||||||
|
payload: string,
|
||||||
|
is_original: string,
|
||||||
|
file_name: string,
|
||||||
|
file_path: string,
|
||||||
|
create_time: string,
|
||||||
|
file_id: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaData = reactive({
|
||||||
|
data: [] as MediaFile[]
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getFiles()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getFiles () {
|
||||||
|
getMediaFiles(workspaceId, body).then(res => {
|
||||||
|
mediaData.data = res.data.list
|
||||||
|
paginationProp.total = res.data.pagination.total
|
||||||
|
paginationProp.current = res.data.pagination.page
|
||||||
|
console.info(mediaData.data[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshData (page: Pagination) {
|
||||||
|
body.page = page?.current!
|
||||||
|
body.page_size = page?.pageSize!
|
||||||
|
getFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadMedia (media: MediaFile) {
|
||||||
|
loading.value = true
|
||||||
|
downloadMediaFile(workspaceId, media.file_id).then(res => {
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = new Blob([res])
|
||||||
|
downloadFile(data, media.file_name)
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.media-panel-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
.media-table {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.action-area {
|
||||||
|
color: $primary;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: start;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
124
src/components/common/sidebar.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div class="demo-project-sidebar-wrapper flex-justify-between">
|
||||||
|
<div>
|
||||||
|
<router-link
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.key"
|
||||||
|
:to="item.path"
|
||||||
|
:class="{
|
||||||
|
'menu-item': true,
|
||||||
|
selected: selectedRoute(item),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a-tooltip :title="item.label" placement="right">
|
||||||
|
<Icon class="fz20" style="width: 50px;" :icon="item.icon"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="mb20 flex-display flex-column flex-align-center flex-justify-between">
|
||||||
|
<a-tooltip title="Back to home" placement="right">
|
||||||
|
<a @click="goHome"> <Icon icon="ImportOutlined" style="font-size: 22px; color: white"/></a>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { createVNode, defineComponent } from 'vue'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
import * as icons from '@ant-design/icons-vue'
|
||||||
|
import { ERouterName } from '/@/types'
|
||||||
|
|
||||||
|
interface IOptions {
|
||||||
|
key: number
|
||||||
|
label: string
|
||||||
|
path:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
path: string
|
||||||
|
query?: any
|
||||||
|
}
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = (props: {icon: string}) => {
|
||||||
|
return createVNode((icons as any)[props.icon])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
name: 'Sidebar',
|
||||||
|
setup () {
|
||||||
|
const root = getRoot()
|
||||||
|
const options = [
|
||||||
|
{ key: 0, label: 'Tsa', path: '/' + ERouterName.TSA, icon: 'TeamOutlined' },
|
||||||
|
{ key: 1, label: 'Livestream', path: '/' + ERouterName.LIVESTREAM, icon: 'VideoCameraOutlined' },
|
||||||
|
{ key: 2, label: 'Annotations', path: '/' + ERouterName.LAYER, icon: 'EnvironmentOutlined' },
|
||||||
|
{ key: 3, label: 'Media Files', path: '/' + ERouterName.MEDIA, icon: 'PictureOutlined' },
|
||||||
|
{ key: 4, label: 'Flight Route Library', path: '/' + ERouterName.WAYLINE, icon: 'NodeIndexOutlined' },
|
||||||
|
{ key: 5, label: 'Task Plan Library', path: '/' + ERouterName.TASK, icon: 'CalendarOutlined' },
|
||||||
|
{ key: 6, label: 'Flight Area', path: '/' + ERouterName.FLIGHT_AREA, icon: 'GroupOutlined' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function selectedRoute (item: IOptions) {
|
||||||
|
const path = typeof item.path === 'string' ? item.path : item.path.path
|
||||||
|
return root.$route.path?.indexOf(path) === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome () {
|
||||||
|
root.$router.push('/' + ERouterName.MEMBERS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
selectedRoute,
|
||||||
|
goHome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.demo-project-sidebar-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 50px;
|
||||||
|
border-right: 1px solid #4f4f4f;
|
||||||
|
color: $text-white-basic;
|
||||||
|
// flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
.menu-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: $text-white-basic;
|
||||||
|
cursor: pointer;
|
||||||
|
&.selected {
|
||||||
|
background-color: #101010;
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filling {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: $text-white-basic;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.ant-tooltip-open {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
96
src/components/common/topbar.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<div class="width-100 flex-row flex-justify-between flex-align-center" style="height: 60px;">
|
||||||
|
<div class="height-100">
|
||||||
|
<a-avatar :size="40" shape="square" :src="cloudapi" />
|
||||||
|
<span class="ml10 fontBold">{{ workspaceName }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-space class="fz16 height-100" size="large">
|
||||||
|
<router-link
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.key"
|
||||||
|
:to="item.path"
|
||||||
|
:class="{
|
||||||
|
'menu-item': true,
|
||||||
|
}">
|
||||||
|
<span @click="selectedRoute(item.path)" :style="selected === item.path ? 'color: #2d8cf0;' : 'color: white'">{{ item.label }}</span>
|
||||||
|
</router-link>
|
||||||
|
</a-space>
|
||||||
|
|
||||||
|
<div class="height-100 fz16 flex-row flex-justify-between flex-align-center">
|
||||||
|
<a-dropdown>
|
||||||
|
<div class="height-100">
|
||||||
|
<span class="fz20 mt20" style="border: 2px solid white; border-radius: 50%; display: inline-flex;"><UserOutlined /></span>
|
||||||
|
<span class="ml10 mr10" style="float: right;">{{ username }}</span>
|
||||||
|
</div>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu theme="dark" class="flex-column flex-justify-between flex-align-center">
|
||||||
|
<a-menu-item>
|
||||||
|
<span class="mr10" style="font-size: 16px;"><ExportOutlined /></span>
|
||||||
|
<span @click="logout">Log Out</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { defineComponent, onMounted, ref } from 'vue'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
import { getPlatformInfo } from '/@/api/manage'
|
||||||
|
import { ELocalStorageKey, ERouterName } from '/@/types'
|
||||||
|
import { UserOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||||
|
import cloudapi from '/@/assets/icons/cloudapi.png'
|
||||||
|
|
||||||
|
const root = getRoot()
|
||||||
|
|
||||||
|
interface IOptions {
|
||||||
|
key: number
|
||||||
|
label: string
|
||||||
|
path:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
path: string
|
||||||
|
query?: any
|
||||||
|
}
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
const username = ref(localStorage.getItem(ELocalStorageKey.Username))
|
||||||
|
const workspaceName = ref('')
|
||||||
|
const options = [
|
||||||
|
{ key: 0, label: ERouterName.WORKSPACE.charAt(0).toUpperCase() + ERouterName.WORKSPACE.substr(1), path: '/' + ERouterName.WORKSPACE },
|
||||||
|
{ key: 1, label: ERouterName.MEMBERS.charAt(0).toUpperCase() + ERouterName.MEMBERS.substr(1), path: '/' + ERouterName.MEMBERS },
|
||||||
|
{ key: 2, label: ERouterName.DEVICES.charAt(0).toUpperCase() + ERouterName.DEVICES.substr(1), path: '/' + ERouterName.DEVICES },
|
||||||
|
{ key: 3, label: ERouterName.FIRMWARES.charAt(0).toUpperCase() + ERouterName.FIRMWARES.substr(1), path: '/' + ERouterName.FIRMWARES },
|
||||||
|
]
|
||||||
|
|
||||||
|
const selected = ref<string>(root.$route.path)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
/* getPlatformInfo().then(res => {
|
||||||
|
workspaceName.value = res.data.workspace_name
|
||||||
|
}) */
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectedRoute (path: string) {
|
||||||
|
selected.value = path
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.clear()
|
||||||
|
root.$router.push(ERouterName.PROJECT)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '/@/styles/index.scss';
|
||||||
|
|
||||||
|
.fontBold {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
61
src/components/devices/DeviceFirmwareStatus.vue
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="status-tag pointer">
|
||||||
|
<a-popconfirm
|
||||||
|
:title="getTitle()"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
placement="left"
|
||||||
|
@confirm="onFirmwareStatusClick(firmware)"
|
||||||
|
>
|
||||||
|
<a-tag :color="firmware.firmware_status ? commonColor.NORMAL : commonColor.FAIL"
|
||||||
|
:class="firmware.firmware_status ? 'border-corner ' : 'status-disable border-corner'">
|
||||||
|
{{ getText(firmware.firmware_status) }}
|
||||||
|
</a-tag>
|
||||||
|
</a-popconfirm>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
|
||||||
|
import { changeFirmareStatus } from '/@/api/manage'
|
||||||
|
import { ELocalStorageKey } from '/@/types'
|
||||||
|
import { Firmware, FirmwareStatusEnum } from '/@/types/device-firmware'
|
||||||
|
import { commonColor } from '/@/utils/color'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
firmware: Firmware
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
|
||||||
|
|
||||||
|
function getTitle () {
|
||||||
|
return `Are you sure to set this firmware to ${getText(!props.firmware.firmware_status)}?`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getText (status: boolean) {
|
||||||
|
return status ? FirmwareStatusEnum.TRUE : FirmwareStatusEnum.FALSE
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFirmwareStatusClick (record: Firmware) {
|
||||||
|
changeFirmareStatus(workspaceId, record.firmware_id, { status: !record.firmware_status }).then((res) => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
record.firmware_status = !record.firmware_status
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.status-disable{
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.border-corner {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
274
src/components/devices/device-hms/DeviceHmsDrawer.vue
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<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">
|
||||||
|
<div >{{ text }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #domain="{text}">
|
||||||
|
<a-tooltip :title="EDeviceTypeName[text]">
|
||||||
|
<div >{{ EDeviceTypeName[text] }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 暂时只抽取该组件 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { watchEffect, reactive, ref, defineProps, defineEmits, watch } 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)
|
||||||
|
|
||||||
|
watch(props, () => {
|
||||||
|
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', slots: { customRender: 'domain' } },
|
||||||
|
{ title: 'Error Code', dataIndex: 'key', width: '20%', className: 'titleStyle', ellipsis: true, slots: { customRender: 'code' } },
|
||||||
|
{ title: 'Hms Message', dataIndex: 'message_en', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } },
|
||||||
|
{ title: 'Hms Message', dataIndex: 'message_zh', 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: -1,
|
||||||
|
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: -1
|
||||||
|
}, {
|
||||||
|
label: EDeviceTypeName[EDeviceTypeName.Aircraft],
|
||||||
|
value: EDeviceTypeName.Aircraft
|
||||||
|
}, {
|
||||||
|
label: EDeviceTypeName[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: number) {
|
||||||
|
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
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="日志上传详情"
|
||||||
|
v-model:visible="sVisible"
|
||||||
|
width="900px"
|
||||||
|
:footer="null"
|
||||||
|
@update:visible="onVisibleChange">
|
||||||
|
<div class="device-log-detail-wrap">
|
||||||
|
<div class="device-log-list">
|
||||||
|
<div class="log-list-item">
|
||||||
|
<a-button type="primary" class="download-btn" :disabled="!airportTableLogState.logList?.file_id || !airportTableLogState.logList?.object_key" 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 || !droneTableLogState.logList?.object_key" 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
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
title="设备日志上传"
|
||||||
|
v-model:visible="sVisible"
|
||||||
|
width="900px"
|
||||||
|
:footer="null"
|
||||||
|
@update:visible="onVisibleChange">
|
||||||
|
<div class="device-log-upload-wrap">
|
||||||
|
<div class="page-action-row">
|
||||||
|
<a-button type="primary" :disabled="deviceLogUploadBtnDisabled" @click="uploadDeviceLog">上传日志</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="device-log-list">
|
||||||
|
<div class="log-list-item">
|
||||||
|
<a-table :columns="airportLogColumns"
|
||||||
|
:scroll="{ x: '100%', y: 600 }"
|
||||||
|
:data-source="airportTableLogState.logList?.list"
|
||||||
|
:loading="airportTableLogState.tableLoading"
|
||||||
|
:row-selection="airportTableLogState.rowSelection"
|
||||||
|
rowKey="boot_index"
|
||||||
|
:pagination = "false">
|
||||||
|
<template #log_time="{record}">
|
||||||
|
<div>{{getLogTime(record)}}</div>
|
||||||
|
</template>
|
||||||
|
<template #size="{record}">
|
||||||
|
<div>{{getLogSize(record.size)}}</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
<div class="log-list-item">
|
||||||
|
<a-table :columns="droneLogColumns"
|
||||||
|
:scroll="{ x: '100%', y: 600 }"
|
||||||
|
:data-source="droneTableLogState.logList?.list"
|
||||||
|
:loading="droneTableLogState.tableLoading"
|
||||||
|
:row-selection="droneTableLogState.rowSelection"
|
||||||
|
rowKey="boot_index"
|
||||||
|
:pagination = "false">
|
||||||
|
<template #log_time="{record}">
|
||||||
|
<div>{{getLogTime(record)}}</div>
|
||||||
|
</template>
|
||||||
|
<template #size="{record}">
|
||||||
|
<div>{{getLogSize(record.size)}}</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { watchEffect, reactive, ref, computed, defineProps, defineEmits } from 'vue'
|
||||||
|
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface'
|
||||||
|
import { IPage } from '/@/api/http/type'
|
||||||
|
import { Device, DOMAIN } from '/@/types/device'
|
||||||
|
import { getDeviceLogList, postDeviceUpgrade, DeviceLogFileInfo, UploadDeviceLogBody, DeviceLogItem } from '/@/api/device-log'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { useDeviceLogUploadDetail } from './use-device-log-upload-detail'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean,
|
||||||
|
device: null | Device,
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits(['update:visible', 'upload-log-ok'])
|
||||||
|
|
||||||
|
const sVisible = ref(false)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
sVisible.value = props.visible
|
||||||
|
// 显示弹框时,获取设备日志信息
|
||||||
|
if (props.visible) {
|
||||||
|
getDeviceLogInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onVisibleChange (sVisible: boolean) {
|
||||||
|
setVisible(sVisible)
|
||||||
|
if (!sVisible) {
|
||||||
|
resetTableLogState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVisible (v: boolean, e?: Event) {
|
||||||
|
sVisible.value = v
|
||||||
|
emit('update:visible', v, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
const airportLogColumns: ColumnProps[] = [
|
||||||
|
{ title: '机场日志', dataIndex: 'time', width: 100, slots: { customRender: 'log_time' } },
|
||||||
|
{ title: '文件大小', dataIndex: 'size', width: 25, slots: { customRender: 'size' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
const droneLogColumns: ColumnProps[] = [
|
||||||
|
{ title: '飞行器日志', dataIndex: 'time', width: 100, slots: { customRender: 'log_time' } },
|
||||||
|
{ title: '文件大小', dataIndex: 'size', width: 25, slots: { customRender: 'size' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
const airportTableLogState = reactive({
|
||||||
|
logList: {} as DeviceLogFileInfo,
|
||||||
|
tableLoading: false,
|
||||||
|
selectRow: [],
|
||||||
|
rowSelection: {
|
||||||
|
columnWidth: 15,
|
||||||
|
selectedRowKeys: [] as number[],
|
||||||
|
onChange: (selectedRowKeys:number[], selectedRows: []) => {
|
||||||
|
airportTableLogState.rowSelection.selectedRowKeys = selectedRowKeys
|
||||||
|
airportTableLogState.selectRow = selectedRows
|
||||||
|
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetTableLogState () {
|
||||||
|
airportTableLogState.logList = {} as DeviceLogFileInfo
|
||||||
|
airportTableLogState.selectRow = []
|
||||||
|
airportTableLogState.tableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const droneTableLogState = reactive({
|
||||||
|
logList: {} as DeviceLogFileInfo,
|
||||||
|
tableLoading: false,
|
||||||
|
selectRow: [],
|
||||||
|
rowSelection: {
|
||||||
|
columnWidth: 15,
|
||||||
|
selectedRowKeys: [] as number[],
|
||||||
|
onChange: (selectedRowKeys: number[], selectedRows: []) => {
|
||||||
|
droneTableLogState.rowSelection.selectedRowKeys = selectedRowKeys
|
||||||
|
droneTableLogState.selectRow = selectedRows
|
||||||
|
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deviceLogUploadBtnDisabled = computed(() => {
|
||||||
|
return (airportTableLogState.rowSelection.selectedRowKeys && airportTableLogState.rowSelection.selectedRowKeys.length <= 0) &&
|
||||||
|
(droneTableLogState.rowSelection.selectedRowKeys && droneTableLogState.rowSelection.selectedRowKeys.length <= 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取设备内日志
|
||||||
|
async function getDeviceLogInfo () {
|
||||||
|
airportTableLogState.tableLoading = true
|
||||||
|
droneTableLogState.tableLoading = true
|
||||||
|
try {
|
||||||
|
const { code, data } = await getDeviceLogList({
|
||||||
|
device_sn: props.device?.device_sn || '',
|
||||||
|
domain: [DOMAIN.DOCK, DOMAIN.DRONE]
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
const { files } = data
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
files.forEach(file => {
|
||||||
|
if (file.module === DOMAIN.DOCK) {
|
||||||
|
airportTableLogState.logList = file
|
||||||
|
} else if (file.module === DOMAIN.DRONE) {
|
||||||
|
droneTableLogState.logList = file
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
airportTableLogState.tableLoading = false
|
||||||
|
droneTableLogState.tableLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志上传
|
||||||
|
async function uploadDeviceLog () {
|
||||||
|
const body = {
|
||||||
|
device_sn: props.device?.device_sn || '',
|
||||||
|
files: [] as any
|
||||||
|
} as UploadDeviceLogBody
|
||||||
|
if (airportTableLogState.selectRow && airportTableLogState.selectRow.length > 0) {
|
||||||
|
body.files.push({
|
||||||
|
list: airportTableLogState.selectRow,
|
||||||
|
device_sn: airportTableLogState.logList.device_sn,
|
||||||
|
module: airportTableLogState.logList.module
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (droneTableLogState.selectRow && droneTableLogState.selectRow.length > 0) {
|
||||||
|
body.files.push({
|
||||||
|
list: droneTableLogState.selectRow,
|
||||||
|
device_sn: droneTableLogState.logList.device_sn,
|
||||||
|
module: droneTableLogState.logList.module
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const { code } = await postDeviceUpgrade(body)
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('日志上传任务执行成功')
|
||||||
|
emit('upload-log-ok')
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getLogTime, getLogSize } = useDeviceLogUploadDetail()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.device-log-upload-wrap{
|
||||||
|
|
||||||
|
.device-log-list{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
.log-list-item{
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -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.device_model_key]}}</div>
|
||||||
|
<div v-if="getDeviceInfo(record).hosts && getDeviceInfo(record).hosts.length > 0">{{ DEVICE_NAME[getDeviceInfo(record).hosts[0].device_model.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>
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Ref, ref } from 'vue'
|
||||||
|
import { Device } from '/@/types/device'
|
||||||
|
import { postDeviceUpgrade, DeviceUpgradeBody } from '/@/api/device-upgrade'
|
||||||
|
|
||||||
|
export function useDeviceFirmwareUpgrade (workspaceId: string) {
|
||||||
|
const deviceFirmwareUpgradeModalVisible = ref(false)
|
||||||
|
const selectedDevice: Ref<null | Device> = ref(null)
|
||||||
|
|
||||||
|
function setDeviceFirmwareUpgradeModalVisible (visible: boolean) {
|
||||||
|
deviceFirmwareUpgradeModalVisible.value = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedDevice (device: null | Device) {
|
||||||
|
selectedDevice.value = device
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击设备升级
|
||||||
|
function onDeviceUpgrade (record: Device) {
|
||||||
|
if (!record) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSelectedDevice(record)
|
||||||
|
setDeviceFirmwareUpgradeModalVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认设备升级
|
||||||
|
async function onUpgradeDeviceOk (deviceUpgradeBody: DeviceUpgradeBody) {
|
||||||
|
const { code } = await postDeviceUpgrade(workspaceId, deviceUpgradeBody)
|
||||||
|
if (code === 0) {
|
||||||
|
// setDeviceFirmwareUpgradeModalVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deviceFirmwareUpgradeModalVisible,
|
||||||
|
setDeviceFirmwareUpgradeModalVisible,
|
||||||
|
selectedDevice,
|
||||||
|
setSelectedDevice,
|
||||||
|
onDeviceUpgrade,
|
||||||
|
onUpgradeDeviceOk,
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/components/flight-area/FlightAreaActionIcon.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div @click="selectCurrent">
|
||||||
|
<a-dropdown class="height-100 width-100 icon-panel">
|
||||||
|
<FlightAreaIcon :type="actionMap[selectedKey].type" :is-circle="actionMap[selectedKey].isCircle" :hide-title="true"/>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="selectAction" mode="vertical-right" :selectedKeys="[selectedKey]">
|
||||||
|
<a-menu-item v-for="(v, k) in actionMap" :key="k">
|
||||||
|
<FlightAreaIcon :type="v.type" :is-circle="v.isCircle"/>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, defineEmits } from 'vue'
|
||||||
|
import { EFlightAreaType } from '../../types/flight-area'
|
||||||
|
import FlightAreaIcon from './FlightAreaIcon.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['select-action', 'click'])
|
||||||
|
|
||||||
|
const actionMap: Record<string, { type: EFlightAreaType, isCircle: boolean}> = {
|
||||||
|
1: {
|
||||||
|
type: EFlightAreaType.DFENCE,
|
||||||
|
isCircle: true,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
type: EFlightAreaType.DFENCE,
|
||||||
|
isCircle: false,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
type: EFlightAreaType.NFZ,
|
||||||
|
isCircle: true,
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
type: EFlightAreaType.NFZ,
|
||||||
|
isCircle: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedKey = ref<string>('1')
|
||||||
|
const selectAction = (item: any) => {
|
||||||
|
selectedKey.value = item.key
|
||||||
|
emit('select-action', actionMap[item.key])
|
||||||
|
}
|
||||||
|
const selectCurrent = () => {
|
||||||
|
emit('click', actionMap[selectedKey.value])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon-panel {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
197
src/components/flight-area/FlightAreaDevicePanel.vue
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flight-area-device-panel">
|
||||||
|
<Title title="Choose Synchronous Devices">
|
||||||
|
<div style="position: absolute; right: 10px;">
|
||||||
|
<a style="color: white;" @click="closePanel"><CloseOutlined /></a>
|
||||||
|
</div>
|
||||||
|
</Title>
|
||||||
|
<div class="scrollbar">
|
||||||
|
<div id="data" v-if="data.length !== 0">
|
||||||
|
<div v-for="dock in data" :key="dock.device_sn">
|
||||||
|
<div class="pt5 panel flex-row" @click="selectDock(dock)" :style="{opacity: selectedDocksMap[dock.device_sn] ? 1 : 0.5 }">
|
||||||
|
<div style="width: 88%">
|
||||||
|
<div class="title">
|
||||||
|
<RobotFilled class="fz20"/>
|
||||||
|
<a-tooltip :title="dock.nickname">
|
||||||
|
<div class="pr10 ml5" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="ml10 mr10 pr5 pl5 flex-align-center flex-row flex-justify-between" style="background: #595959;">
|
||||||
|
<div>
|
||||||
|
Custom Flight Area
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div v-if="!dock.status">
|
||||||
|
<a-tooltip title="Dock offline">
|
||||||
|
<ApiOutlined />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZED">
|
||||||
|
<a-tooltip title="Data synced">
|
||||||
|
<CheckCircleTwoTone twoToneColor="#28d445"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING
|
||||||
|
|| deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC">
|
||||||
|
<a-tooltip title="To be synced">
|
||||||
|
<SyncOutlined spin />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a-tooltip :title="deviceStatusMap[dock.device_sn]?.flight_area_status?.sync_msg || 'No synchronization'">
|
||||||
|
<ExclamationCircleTwoTone twoToneColor="#e70102" />
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box" v-if="selectedDocksMap[dock.device_sn]">
|
||||||
|
<CheckOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DividerLine style="position: absolute; bottom: 68px;" />
|
||||||
|
<div class="flex-row flex-justify-between footer">
|
||||||
|
<a-button class="mr10" @click="closePanel">Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :disabled="confirmDisabled" @click="syncDeviceFlightArea">Sync
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { CloseOutlined, RobotFilled, CheckOutlined, ApiOutlined, CheckCircleTwoTone, SyncOutlined, ExclamationCircleTwoTone } from '@ant-design/icons-vue'
|
||||||
|
import Title from '/@/components/workspace/Title.vue'
|
||||||
|
import { defineEmits, onMounted, ref, defineProps, computed } from 'vue'
|
||||||
|
import { getBindingDevices } from '/@/api/manage'
|
||||||
|
import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
|
||||||
|
import { IPage } from '/@/api/http/type'
|
||||||
|
import { Device } from '/@/types/device'
|
||||||
|
import DividerLine from '../workspace/DividerLine.vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { GetDeviceStatus, syncFlightArea } from '/@/api/flight-area'
|
||||||
|
import { ESyncStatus } from '/@/types/flight-area'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: GetDeviceStatus[]
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits(['closePanel'])
|
||||||
|
const closePanel = () => {
|
||||||
|
emit('closePanel', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDisabled = ref(false)
|
||||||
|
|
||||||
|
const deviceStatusMap = computed(() => props.data.reduce((obj: Record<string, GetDeviceStatus>, val: GetDeviceStatus) => {
|
||||||
|
obj[val.device_sn] = val
|
||||||
|
return obj
|
||||||
|
}, {} as Record<string, GetDeviceStatus>))
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
|
||||||
|
const body: IPage = {
|
||||||
|
page: 1,
|
||||||
|
total: 0,
|
||||||
|
page_size: 10,
|
||||||
|
}
|
||||||
|
const data = ref<Device[]>([])
|
||||||
|
const selectedDocksMap = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
const getDocks = async () => {
|
||||||
|
await getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => {
|
||||||
|
if (res.code !== 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.value.push(...res.data.list)
|
||||||
|
body.page = res.data.pagination.page
|
||||||
|
body.page_size = res.data.pagination.page_size
|
||||||
|
body.total = res.data.pagination.total
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectDock = (dock: Device) => {
|
||||||
|
if (!dock.status) {
|
||||||
|
message.info(`Dock(${dock.nickname}) is offline.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.SYNCHRONIZING ||
|
||||||
|
deviceStatusMap.value[dock.device_sn]?.flight_area_status?.sync_status === ESyncStatus.WAIT_SYNC) {
|
||||||
|
message.info('The dock is synchronizing.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedDocksMap.value[dock.device_sn] = !selectedDocksMap.value[dock.device_sn]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDocks()
|
||||||
|
const key = setInterval(() => {
|
||||||
|
if (body.total === 0 || Math.ceil(body.total / body.page_size) <= body.page) {
|
||||||
|
clearInterval(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body.page++
|
||||||
|
getDocks()
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
const syncDeviceFlightArea = () => {
|
||||||
|
const keys = Object.keys(selectedDocksMap.value)
|
||||||
|
if (keys.length === 0) {
|
||||||
|
message.warn('Please select the docks that need to be synchronized.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmDisabled.value = true
|
||||||
|
Object.keys(selectedDocksMap.value).forEach(k => {
|
||||||
|
const device = deviceStatusMap.value[k]
|
||||||
|
if (device) {
|
||||||
|
device.flight_area_status = { sync_code: 0, sync_status: ESyncStatus.WAIT_SYNC, sync_msg: '' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
syncFlightArea(keys).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('The devices are synchronizing...')
|
||||||
|
selectedDocksMap.value = {}
|
||||||
|
}
|
||||||
|
}).finally(() => setTimeout(() => {
|
||||||
|
confirmDisabled.value = false
|
||||||
|
}, 3000))
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flight-area-device-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: 285px;
|
||||||
|
width: 280px;
|
||||||
|
height: 100vh;
|
||||||
|
float: right;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
color: white;
|
||||||
|
background: #282828;
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
button {
|
||||||
|
width: 45%;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollbar {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
.box {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
src/components/flight-area/FlightAreaIcon.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-row flex-align-center">
|
||||||
|
<div class="shape" :class="type" :style="isCircle ? 'border-radius: 50%;' : ''"></div>
|
||||||
|
<div class="ml5" v-if="!hideTitle">{{ FlightAreaTypeTitleMap[type][isCircle ? EGeometryType.CIRCLE : EGeometryType.POLYGON] }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps } from 'vue'
|
||||||
|
import { EFlightAreaType, EGeometryType, FlightAreaTypeTitleMap } from '../../types/flight-area'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: EFlightAreaType,
|
||||||
|
isCircle: boolean,
|
||||||
|
hideTitle?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.nfz {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
.dfence {
|
||||||
|
border-color: $tag-green;
|
||||||
|
}
|
||||||
|
.shape {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-width: 3px;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
89
src/components/flight-area/FlightAreaItem.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div class="panel" style="padding-top: 5px;" :class="{disable: !flightArea.status}">
|
||||||
|
<div class="title">
|
||||||
|
<a-tooltip :title="flightArea.name">
|
||||||
|
<div class="pr10" style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ flightArea.name }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
|
||||||
|
<span class="mr10">Update at {{ formatDateTime(flightArea.update_time).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row flex-justify-between flex-align-center ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
|
||||||
|
<FlightAreaIcon :type="flightArea.type" :isCircle="EGeometryType.CIRCLE === flightArea.content.geometry.type"/>
|
||||||
|
<div class="mr10 operate">
|
||||||
|
<a-popconfirm v-if="flightArea.status" title="Is it determined to disable the current area?" okText="Disable" @confirm="changeAreaStatus(false)">
|
||||||
|
<stop-outlined />
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-popconfirm v-else @confirm="changeAreaStatus(true)" title="Is it determined to enable the current area?" okText="Enable" >
|
||||||
|
<check-circle-outlined />
|
||||||
|
</a-popconfirm>
|
||||||
|
<EnvironmentFilled class="ml10" @click="clickLocation"/>
|
||||||
|
<a-popconfirm title="Is it determined to delete the current area?" okText="Delete" okType="danger" @confirm="deleteArea">
|
||||||
|
<delete-outlined class="ml10" />
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, reactive, defineEmits, computed } from 'vue'
|
||||||
|
import { GetFlightArea, changeFlightAreaStatus } from '../../api/flight-area'
|
||||||
|
import FlightAreaIcon from './FlightAreaIcon.vue'
|
||||||
|
import { formatDateTime } from '../../utils/time'
|
||||||
|
import { EGeometryType } from '../../types/flight-area'
|
||||||
|
import { StopOutlined, CheckCircleOutlined, DeleteOutlined, EnvironmentFilled } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: GetFlightArea
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits(['delete', 'update', 'location'])
|
||||||
|
|
||||||
|
const flightArea = computed(() => props.data)
|
||||||
|
const changeAreaStatus = (status: boolean) => {
|
||||||
|
changeFlightAreaStatus(props.data.area_id, status).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
flightArea.value.status = status
|
||||||
|
emit('update', flightArea)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const deleteArea = () => {
|
||||||
|
emit('delete', flightArea.value.area_id)
|
||||||
|
}
|
||||||
|
const clickLocation = () => {
|
||||||
|
emit('location', flightArea.value.area_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.panel {
|
||||||
|
background: #3c3c3c;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 90px;
|
||||||
|
width: 95%;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0px 10px 0 10px;
|
||||||
|
}
|
||||||
|
.operate > *{
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disable {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
43
src/components/flight-area/FlightAreaPanel.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flight-area-panel">
|
||||||
|
<div v-if="data.length === 0">
|
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" />
|
||||||
|
</div>
|
||||||
|
<div v-else v-for="area in flightAreaList" :key="area.area_id">
|
||||||
|
<FlightAreaItem :data="area" @delete="deleteArea" @update="updateArea" @location="clickLocation(area)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, defineEmits, ref, computed } from 'vue'
|
||||||
|
import FlightAreaItem from './FlightAreaItem.vue'
|
||||||
|
import { GetFlightArea } from '/@/api/flight-area'
|
||||||
|
|
||||||
|
const emit = defineEmits(['deleteArea', 'updateArea', 'locationArea'])
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: GetFlightArea[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const flightAreaList = computed(() => props.data)
|
||||||
|
|
||||||
|
const deleteArea = (areaId: string) => {
|
||||||
|
emit('deleteArea', areaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateArea = (area: GetFlightArea) => {
|
||||||
|
emit('updateArea', area)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickLocation = (area: GetFlightArea) => {
|
||||||
|
emit('locationArea', area)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flight-area-panel {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
src/components/flight-area/FlightAreaSyncPanel.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flight-area-sync-panel p10 flex-row flex-align-center" >
|
||||||
|
<RobotFilled class="fz30" twoToneColor="red" fill="#00ff00"/>
|
||||||
|
<div class="ml20 mr10 flex-column" @click="switchPanel">
|
||||||
|
<div class="fz18">Sync Across Devices</div>
|
||||||
|
<div v-if="syncDevicesCount > 0"><a-spin /> Syncing to {{ syncDevicesCount }} devices</div>
|
||||||
|
</div>
|
||||||
|
<RightOutlined class="fz18" @click="switchPanel"/>
|
||||||
|
<FlightAreaDevicePanel v-if="visible" @close-panel="closePanel" :data="syncDevices"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { RobotFilled, RightOutlined } from '@ant-design/icons-vue'
|
||||||
|
import FlightAreaDevicePanel from '/@/components/flight-area/FlightAreaDevicePanel.vue'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { GetDeviceStatus, getDeviceStatus } from '/@/api/flight-area'
|
||||||
|
import { ESyncStatus, FlightAreaSyncProgress } from '/@/types/flight-area'
|
||||||
|
import { useFlightAreaSyncProgressEvent } from './use-flight-area-sync-progress-event'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const syncDevices = ref<GetDeviceStatus[]>([])
|
||||||
|
const syncDevicesCount = computed(() => syncDevices.value.filter(device =>
|
||||||
|
device.flight_area_status.sync_status === ESyncStatus.SYNCHRONIZING || device.flight_area_status.sync_status === ESyncStatus.WAIT_SYNC).length)
|
||||||
|
const getAllDeviceStatus = () => {
|
||||||
|
getDeviceStatus().then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
syncDevices.value = res.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getAllDeviceStatus()
|
||||||
|
})
|
||||||
|
const switchPanel = () => {
|
||||||
|
visible.value = !visible.value
|
||||||
|
}
|
||||||
|
const closePanel = (val: boolean) => {
|
||||||
|
visible.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSyncProgress = (data: FlightAreaSyncProgress) => {
|
||||||
|
let has = false
|
||||||
|
const status = { sync_code: data.result, sync_status: data.status, sync_msg: data.message }
|
||||||
|
syncDevices.value.forEach(device => {
|
||||||
|
if (data.sn === device.device_sn) {
|
||||||
|
device.flight_area_status = status
|
||||||
|
has = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!has) {
|
||||||
|
syncDevices.value.push({ device_sn: data.sn, flight_area_status: status })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useFlightAreaSyncProgressEvent(handleSyncProgress)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.flight-area-sync-panel {
|
||||||
|
height: 70px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { FlightAreasDroneLocation } from '/@/types/flight-area'
|
||||||
|
import { CommonHostWs } from '/@/websocket'
|
||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
export function useFlightAreaDroneLocationEvent (onFlightAreaDroneLocationWs: (data: CommonHostWs<FlightAreasDroneLocation>) => void): void {
|
||||||
|
function handleDroneLocationEvent (data: any) {
|
||||||
|
onFlightAreaDroneLocationWs(data.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightAreasDroneLocationWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightAreasDroneLocationWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { FlightAreaSyncProgress } from '/@/types/flight-area'
|
||||||
|
|
||||||
|
export function useFlightAreaSyncProgressEvent (onFlightAreaSyncProgressWs: (data: FlightAreaSyncProgress) => void): void {
|
||||||
|
function handleSyncProgressEvent (data: FlightAreaSyncProgress) {
|
||||||
|
onFlightAreaSyncProgressWs(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightAreasSyncProgressWs', handleSyncProgressEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightAreasSyncProgressWs', handleSyncProgressEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
30
src/components/flight-area/use-flight-area-update.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { EFlightAreaUpdate, FlightAreaUpdate, FlightAreasDroneLocation } from '/@/types/flight-area'
|
||||||
|
import { CommonHostWs } from '/@/websocket'
|
||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
function doNothing (data: FlightAreaUpdate) {
|
||||||
|
}
|
||||||
|
export function useFlightAreaUpdateEvent (addFunc = doNothing, deleteFunc = doNothing, updateFunc = doNothing): void {
|
||||||
|
function handleDroneLocationEvent (data: FlightAreaUpdate) {
|
||||||
|
switch (data.operation) {
|
||||||
|
case EFlightAreaUpdate.ADD:
|
||||||
|
addFunc(data)
|
||||||
|
break
|
||||||
|
case EFlightAreaUpdate.UPDATE:
|
||||||
|
updateFunc(data)
|
||||||
|
break
|
||||||
|
case EFlightAreaUpdate.DELETE:
|
||||||
|
deleteFunc(data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightAreasUpdateWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightAreasUpdateWs', handleDroneLocationEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
155
src/components/flight-area/use-flight-area.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { message, notification } from 'ant-design-vue'
|
||||||
|
import { MapDoodleEnum } from '/@/types/map-enum'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
import { PostFlightAreaBody, saveFlightArea } from '/@/api/flight-area'
|
||||||
|
import { generateCircleContent, generatePolyContent } from '/@/utils/map-layer-utils'
|
||||||
|
import { GeojsonCoordinate } from '/@/utils/genjson'
|
||||||
|
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform.js'
|
||||||
|
import { uuidv4 } from '/@/utils/uuid'
|
||||||
|
import { CommonHostWs } from '/@/websocket'
|
||||||
|
import { FlightAreasDroneLocation } from '/@/types/flight-area'
|
||||||
|
import rootStore from '/@/store'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import { useGMapCover } from '/@/hooks/use-g-map-cover'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { DATE_FORMAT } from '/@/utils/constants'
|
||||||
|
|
||||||
|
export function useFlightArea () {
|
||||||
|
const root = getRoot()
|
||||||
|
const store = rootStore
|
||||||
|
const coverMap = store.state.coverMap
|
||||||
|
|
||||||
|
let useGMapCoverHook = useGMapCover()
|
||||||
|
|
||||||
|
const MIN_RADIUS = 10
|
||||||
|
function checkCircle (obj: any): boolean {
|
||||||
|
if (obj.getRadius() < MIN_RADIUS) {
|
||||||
|
console.error(`The radius must be greater than ${MIN_RADIUS}m.`)
|
||||||
|
root.$map.remove(obj)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPolygon (obj: any): boolean {
|
||||||
|
const path: any[][] = obj.getPath()
|
||||||
|
if (path.length < 3) {
|
||||||
|
console.error('The path of the polygon cannot be crossed.')
|
||||||
|
root.$map.remove(obj)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// root.$aMap.GeometryUtil.doesLineLineIntersect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setExtData (obj: any) {
|
||||||
|
let ext = obj.getExtData()
|
||||||
|
const id = uuidv4()
|
||||||
|
const name = `${ext.type}-${moment().format(DATE_FORMAT)}`
|
||||||
|
ext = Object.assign({}, ext, { id, name })
|
||||||
|
obj.setExtData(ext)
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
function createFlightArea (obj: any) {
|
||||||
|
const ext = obj.getExtData()
|
||||||
|
const data = {
|
||||||
|
id: ext.id,
|
||||||
|
type: ext.type,
|
||||||
|
name: ext.name,
|
||||||
|
}
|
||||||
|
let coordinates: GeojsonCoordinate | GeojsonCoordinate[][]
|
||||||
|
let content
|
||||||
|
switch (ext.mapType) {
|
||||||
|
case 'circle':
|
||||||
|
content = generateCircleContent(obj.getCenter(), obj.getRadius())
|
||||||
|
coordinates = getWgs84(content.geometry.coordinates as GeojsonCoordinate)
|
||||||
|
break
|
||||||
|
case 'polygon':
|
||||||
|
content = generatePolyContent(obj.getPath()).content
|
||||||
|
coordinates = [getWgs84(content.geometry.coordinates[0] as GeojsonCoordinate[])]
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error(`Invalid type: ${obj.mapType}`)
|
||||||
|
root.$map.remove(obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content.geometry.coordinates = coordinates
|
||||||
|
|
||||||
|
saveFlightArea(Object.assign({}, data, { content }) as PostFlightAreaBody).then(res => {
|
||||||
|
if (res.code !== 1) {
|
||||||
|
useGMapCoverHook.removeCoverFromMap(ext.id)
|
||||||
|
}
|
||||||
|
}).finally(() => root.$map.remove(obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDrawFlightAreaCallback (obj: any) {
|
||||||
|
useGMapCoverHook = useGMapCover()
|
||||||
|
const ext = setExtData(obj)
|
||||||
|
switch (ext.mapType) {
|
||||||
|
case MapDoodleEnum.CIRCLE:
|
||||||
|
if (!checkCircle(obj)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case MapDoodleEnum.POLYGON:
|
||||||
|
if (!checkPolygon(obj)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
createFlightArea(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getWgs84 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => {
|
||||||
|
if (coordinate[0] instanceof Array) {
|
||||||
|
return (coordinate as GeojsonCoordinate[]).map(c => gcj02towgs84(c[0], c[1])) as T
|
||||||
|
}
|
||||||
|
return gcj02towgs84(coordinate[0], coordinate[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGcj02 = <T extends GeojsonCoordinate | GeojsonCoordinate[]>(coordinate: T): T => {
|
||||||
|
if (coordinate[0] instanceof Array) {
|
||||||
|
return (coordinate as GeojsonCoordinate[]).map(c => wgs84togcj02(c[0], c[1])) as T
|
||||||
|
}
|
||||||
|
return wgs84togcj02(coordinate[0], coordinate[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFlightAreaDroneLocationWs = (data: CommonHostWs<FlightAreasDroneLocation>) => {
|
||||||
|
const nearArea = data.host.drone_locations.filter(val => !val.is_in_area)
|
||||||
|
const inArea = data.host.drone_locations.filter(val => val.is_in_area)
|
||||||
|
notification.warning({
|
||||||
|
key: `flight-area-${data.sn}`,
|
||||||
|
message: `Drone(${data.sn}) flight area information`,
|
||||||
|
description: h('div',
|
||||||
|
[
|
||||||
|
h('div', [
|
||||||
|
h('span', { class: 'fz18' }, 'In the flight area: '),
|
||||||
|
h('ul', [
|
||||||
|
...inArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
h('div', [
|
||||||
|
h('span', { class: 'fz18' }, 'Near the flight area: '),
|
||||||
|
h('ul', [
|
||||||
|
...nearArea.map(val => h('li', `There are ${val.area_distance} meters from the edge of the area(${coverMap[val.area_id][1]?.getText() || val.area_id}).`))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
duration: null,
|
||||||
|
style: {
|
||||||
|
width: '420px',
|
||||||
|
marginTop: '-8px',
|
||||||
|
marginLeft: '-28px',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getDrawFlightAreaCallback,
|
||||||
|
getGcj02,
|
||||||
|
getWgs84,
|
||||||
|
onFlightAreaDroneLocationWs,
|
||||||
|
}
|
||||||
|
}
|
||||||
242
src/components/g-map/DeviceSettingBox.vue
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<template>
|
||||||
|
<div class="device-setting-wrapper">
|
||||||
|
<div class="device-setting-header">Device Property Set</div>
|
||||||
|
<div class="device-setting-box">
|
||||||
|
<!-- 飞行器夜航灯 -->
|
||||||
|
<div class="control-setting-item">
|
||||||
|
<div class="control-setting-item-left">
|
||||||
|
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].label }}</div>
|
||||||
|
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-setting-item-right">
|
||||||
|
<DeviceSettingPopover
|
||||||
|
:visible="deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].popConfirm.visible"
|
||||||
|
:loading="deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].popConfirm.loading"
|
||||||
|
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)"
|
||||||
|
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].label }}:</span>
|
||||||
|
<a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.nightLightsState" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET].settingKey)">Edit</a>
|
||||||
|
</DeviceSettingPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 限高 -->
|
||||||
|
<div class="control-setting-item">
|
||||||
|
<div class="control-setting-item-left">
|
||||||
|
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].label }}</div>
|
||||||
|
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-setting-item-right">
|
||||||
|
<DeviceSettingPopover
|
||||||
|
:visible="deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].popConfirm.visible"
|
||||||
|
:loading="deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].popConfirm.loading"
|
||||||
|
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)"
|
||||||
|
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].label }}:</span>
|
||||||
|
<a-input-number v-model:value="deviceSettingFormModel.heightLimit" :min="20" :max="1500" />
|
||||||
|
m
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.HEIGHT_LIMIT_SET].settingKey)">Edit</a>
|
||||||
|
</DeviceSettingPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 限远 -->
|
||||||
|
<div class="control-setting-item">
|
||||||
|
<div class="control-setting-item-left">
|
||||||
|
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].label }}</div>
|
||||||
|
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-setting-item-right">
|
||||||
|
<DeviceSettingPopover
|
||||||
|
:visible="deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].popConfirm.visible"
|
||||||
|
:loading="deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].popConfirm.loading"
|
||||||
|
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)"
|
||||||
|
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].label }}:</span>
|
||||||
|
<a-switch style="margin-right: 10px;" checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.distanceLimitStatus.state" />
|
||||||
|
<a-input-number v-model:value="deviceSettingFormModel.distanceLimitStatus.distanceLimit" :min="15" :max="8000" />
|
||||||
|
m
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.DISTANCE_LIMIT_SET].settingKey)">Edit</a>
|
||||||
|
</DeviceSettingPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 水平避障 -->
|
||||||
|
<div class="control-setting-item">
|
||||||
|
<div class="control-setting-item-left">
|
||||||
|
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].label }}</div>
|
||||||
|
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-setting-item-right">
|
||||||
|
<DeviceSettingPopover
|
||||||
|
:visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].popConfirm.visible"
|
||||||
|
:loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].popConfirm.loading"
|
||||||
|
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)"
|
||||||
|
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].label }}:</span>
|
||||||
|
<a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.obstacleAvoidanceHorizon" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON].settingKey)">Edit</a>
|
||||||
|
</DeviceSettingPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 上视避障 -->
|
||||||
|
<div class="control-setting-item">
|
||||||
|
<div class="control-setting-item-left">
|
||||||
|
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].label }}</div>
|
||||||
|
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-setting-item-right">
|
||||||
|
<DeviceSettingPopover
|
||||||
|
:visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].popConfirm.visible"
|
||||||
|
:loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].popConfirm.loading"
|
||||||
|
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)"
|
||||||
|
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].label }}:</span>
|
||||||
|
<a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.obstacleAvoidanceUpside" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE].settingKey)">Edit</a>
|
||||||
|
</DeviceSettingPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 下视避障 -->
|
||||||
|
<div class="control-setting-item">
|
||||||
|
<div class="control-setting-item-left">
|
||||||
|
<div class="item-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].label }}</div>
|
||||||
|
<div class="item-status">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].value }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-setting-item-right">
|
||||||
|
<DeviceSettingPopover
|
||||||
|
:visible="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].popConfirm.visible"
|
||||||
|
:loading="deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].popConfirm.loading"
|
||||||
|
@confirm="onConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)"
|
||||||
|
@cancel="onCancel(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<span class="form-label">{{ deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].label }}:</span>
|
||||||
|
<a-switch checked-children="开" un-checked-children="关" v-model:checked="deviceSettingFormModel.obstacleAvoidanceDownside" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<a @click="onShowPopConfirm(deviceSetting[DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE].settingKey)">Edit</a>
|
||||||
|
</DeviceSettingPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref, watch } from 'vue'
|
||||||
|
import { DeviceInfoType } from '/@/types/device'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
import { initDeviceSetting, initDeviceSettingFormModel, DeviceSettingKeyEnum } from '/@/types/device-setting'
|
||||||
|
import { updateDeviceSettingInfoByOsd, updateDeviceSettingFormModelByOsd } from '/@/utils/device-setting'
|
||||||
|
import { useDeviceSetting } from './use-device-setting'
|
||||||
|
import DeviceSettingPopover from './DeviceSettingPopover.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sn: string,
|
||||||
|
deviceInfo: DeviceInfoType,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const store = useMyStore()
|
||||||
|
const deviceSetting = ref(cloneDeep(initDeviceSetting))
|
||||||
|
const deviceSettingFormModelFromOsd = ref(cloneDeep(initDeviceSettingFormModel))
|
||||||
|
const deviceSettingFormModel = ref(cloneDeep(initDeviceSettingFormModel)) // 真实使用的formModel
|
||||||
|
|
||||||
|
// 根据设备osd信息更新信息
|
||||||
|
watch(() => props.deviceInfo, (value) => {
|
||||||
|
updateDeviceSettingInfoByOsd(deviceSetting.value, value)
|
||||||
|
updateDeviceSettingFormModelByOsd(deviceSettingFormModelFromOsd.value, value)
|
||||||
|
// console.log('deviceInfo', value)
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function onShowPopConfirm (settingKey: DeviceSettingKeyEnum) {
|
||||||
|
deviceSetting.value[settingKey].popConfirm.visible = true
|
||||||
|
deviceSettingFormModel.value = cloneDeep(deviceSettingFormModelFromOsd.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel (settingKey: DeviceSettingKeyEnum) {
|
||||||
|
deviceSetting.value[settingKey].popConfirm.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirm (settingKey: DeviceSettingKeyEnum) {
|
||||||
|
deviceSetting.value[settingKey].popConfirm.loading = true
|
||||||
|
const body = genDevicePropsBySettingKey(settingKey, deviceSettingFormModel.value)
|
||||||
|
await setDeviceProps(props.sn, body)
|
||||||
|
deviceSetting.value[settingKey].popConfirm.loading = false
|
||||||
|
deviceSetting.value[settingKey].popConfirm.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备属性
|
||||||
|
const {
|
||||||
|
genDevicePropsBySettingKey,
|
||||||
|
setDeviceProps,
|
||||||
|
} = useDeviceSetting()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.device-setting-wrapper{
|
||||||
|
border-bottom: 1px solid #515151;
|
||||||
|
|
||||||
|
.device-setting-header{
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-setting-box{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 10px;
|
||||||
|
|
||||||
|
.control-setting-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-setting-item-left{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.item-label{
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
src/components/g-map/DeviceSettingPopover.vue
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<a-popover :visible="state.sVisible"
|
||||||
|
trigger="click"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:overlay-class-name="overlayClassName"
|
||||||
|
placement="bottom"
|
||||||
|
@visibleChange=";"
|
||||||
|
v-on="$attrs">
|
||||||
|
<template #content>
|
||||||
|
<div class="title-content">
|
||||||
|
</div>
|
||||||
|
<slot name="formContent" />
|
||||||
|
<div class="uranus-popconfirm-btns">
|
||||||
|
<a-button size="sm"
|
||||||
|
@click="onCancel">
|
||||||
|
{{ cancelText || '取消'}}
|
||||||
|
</a-button>
|
||||||
|
<a-button size="sm"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
class="confirm-btn"
|
||||||
|
@click="onConfirm">
|
||||||
|
{{ okText || '确定' }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots.default">
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, defineEmits, reactive, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible?: boolean,
|
||||||
|
loading?: Boolean,
|
||||||
|
disabled?: Boolean,
|
||||||
|
title?: String,
|
||||||
|
okText?: String,
|
||||||
|
cancelText?: String,
|
||||||
|
width?: Number,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['cancel', 'confirm'])
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
sVisible: false,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
state.sVisible = val || false
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = computed(() => {
|
||||||
|
return props.loading
|
||||||
|
})
|
||||||
|
const okLabel = computed(() => {
|
||||||
|
return props.loading ? '' : '确定'
|
||||||
|
})
|
||||||
|
|
||||||
|
const overlayClassName = computed(() => {
|
||||||
|
const classList = ['device-setting-popconfirm']
|
||||||
|
return classList.join(' ')
|
||||||
|
})
|
||||||
|
|
||||||
|
function onConfirm (e: Event) {
|
||||||
|
if (props.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('confirm', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel (e: Event) {
|
||||||
|
state.sVisible = false
|
||||||
|
emit('cancel', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.device-setting-popconfirm {
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
.uranus-popconfirm-btns{
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 0px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.confirm-btn{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content{
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.form-label{
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
172
src/components/g-map/DockControlPanel.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<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>Device Control<span class="fz12 pl15">{{ props.sn}}</span></span>
|
||||||
|
<span @click="closeControlPanel">
|
||||||
|
<CloseOutlined />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- setting -->
|
||||||
|
<DeviceSettingBox :sn="props.sn" :deviceInfo="props.deviceInfo"></DeviceSettingBox>
|
||||||
|
<!-- cmd -->
|
||||||
|
<div class="control-cmd-wrapper">
|
||||||
|
<div class="control-cmd-header">
|
||||||
|
Device Remote Debug
|
||||||
|
<a-switch class="debug-btn" checked-children="开" un-checked-children="关" v-model:checked="debugStatus" @change="onDeviceStatusChange"/>
|
||||||
|
</div>
|
||||||
|
<div class="control-cmd-box">
|
||||||
|
<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 :disabled="!debugStatus || cmdItem.disabled" :loading="cmdItem.loading" size="small" type="primary" @click="sendControlCmd(cmdItem, index)">
|
||||||
|
{{ cmdItem.operateText }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</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 './use-dock-control'
|
||||||
|
import { DeviceInfoType, EDockModeCode } from '/@/types/device'
|
||||||
|
import { cmdList as baseCmdList, DeviceCmdItem } from '/@/types/device-cmd'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { updateDeviceCmdInfoByOsd, updateDeviceCmdInfoByExecuteInfo } from '/@/utils/device-cmd'
|
||||||
|
import DeviceSettingBox from './DeviceSettingBox.vue'
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
// dock 控制指令
|
||||||
|
const debugStatus = ref(props.deviceInfo.dock?.basic_osd?.mode_code === EDockModeCode.Remote_Debugging)
|
||||||
|
const emit = defineEmits(['close-control-panel'])
|
||||||
|
|
||||||
|
function closeControlPanel () {
|
||||||
|
emit('close-control-panel', props.sn, debugStatus.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDeviceStatusChange (status: boolean) {
|
||||||
|
let result = false
|
||||||
|
if (status) {
|
||||||
|
result = await dockDebugOnOff(props.sn, true)
|
||||||
|
} else {
|
||||||
|
result = await dockDebugOnOff(props.sn, false)
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
if (status) {
|
||||||
|
debugStatus.value = false
|
||||||
|
} else {
|
||||||
|
debugStatus.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
sendDockControlCmd,
|
||||||
|
dockDebugOnOff
|
||||||
|
} = useDockControl()
|
||||||
|
|
||||||
|
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
|
||||||
|
const success = await sendDockControlCmd({
|
||||||
|
sn: props.sn,
|
||||||
|
cmd: cmdItem.cmdKey,
|
||||||
|
action: cmdItem.action
|
||||||
|
}, 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{
|
||||||
|
.control-cmd-header{
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 10px 0px;
|
||||||
|
|
||||||
|
.debug-btn{
|
||||||
|
margin-left: 10px;
|
||||||
|
border:1px solid #585858;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-cmd-box{
|
||||||
|
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>
|
||||||
34
src/components/g-map/DroneControlInfoPanel.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="drone-control-info-wrap">
|
||||||
|
<a-textarea v-model:value="info" placeholder="drc info" :rows="5" disabled/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, defineProps, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
message?: string,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const info = ref('')
|
||||||
|
watch(() => props.message, message => {
|
||||||
|
info.value = message || ''
|
||||||
|
}, {
|
||||||
|
immediate: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// const emit = defineEmits(['cancel', 'confirm'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.drone-control-info-wrap {
|
||||||
|
&::v-deep{
|
||||||
|
textarea.ant-input {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
837
src/components/g-map/DroneControlPanel.vue
Normal file
@ -0,0 +1,837 @@
|
|||||||
|
<template>
|
||||||
|
<div class="drone-control-wrapper">
|
||||||
|
<div class="drone-control-header">Drone Flight Control</div>
|
||||||
|
<div class="drone-control-box">
|
||||||
|
<div class="box">
|
||||||
|
<div class="row">
|
||||||
|
<div class="drone-control"><Button :ghost="!flightController" size="small" @click="onClickFightControl">{{ flightController ? 'Exit Remote Control' : 'Enter Remote Control'}}</Button></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="drone-control-direction">
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_Q)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><UndoOutlined /></template><span class="word">Q</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_W)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><UpOutlined/></template><span class="word">W</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_E)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><RedoOutlined /></template><span class="word">E</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_UP)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><ArrowUpOutlined /></template>
|
||||||
|
</Button>
|
||||||
|
<br />
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_A)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><LeftOutlined/></template><span class="word">A</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_S)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><DownOutlined/></template><span class="word">S</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_D)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><RightOutlined/></template><span class="word">D</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_DOWN)" @onmouseup="onMouseUp">
|
||||||
|
<template #icon><ArrowDownOutlined /></template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" size="small" danger ghost @click="handleEmergencyStop" >
|
||||||
|
<template #icon><PauseCircleOutlined/></template><span>Break</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<DroneControlPopover
|
||||||
|
:visible="flyToPointPopoverData.visible"
|
||||||
|
:loading="flyToPointPopoverData.loading"
|
||||||
|
@confirm="($event) => onFlyToConfirm(true)"
|
||||||
|
@cancel="($event) =>onFlyToConfirm(false)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">latitude:</span>
|
||||||
|
<a-input-number v-model:value="flyToPointPopoverData.latitude"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">longitude:</span>
|
||||||
|
<a-input-number v-model:value="flyToPointPopoverData.longitude"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">height(m):</span>
|
||||||
|
<a-input-number v-model:value="flyToPointPopoverData.height"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button size="small" ghost @click="onShowFlyToPopover" >
|
||||||
|
<span>Fly to</span>
|
||||||
|
</Button>
|
||||||
|
</DroneControlPopover>
|
||||||
|
<Button size="small" ghost @click="onStopFlyToPoint" >
|
||||||
|
<span>Stop Fly to</span>
|
||||||
|
</Button>
|
||||||
|
<DroneControlPopover
|
||||||
|
:visible="takeoffToPointPopoverData.visible"
|
||||||
|
:loading="takeoffToPointPopoverData.loading"
|
||||||
|
@confirm="($event) => onTakeoffToPointConfirm(true)"
|
||||||
|
@cancel="($event) =>onTakeoffToPointConfirm(false)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">latitude:</span>
|
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.latitude"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">longitude:</span>
|
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.longitude"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">height(m):</span>
|
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.height"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Safe Takeoff Altitude(m):</span>
|
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.securityTakeoffHeight"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Return-to-Home Altitude(m):</span>
|
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.rthAltitude"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Lost Action:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="takeoffToPointPopoverData.rcLostAction"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="LostControlActionInCommandFLightOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Wayline Lost Action:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="takeoffToPointPopoverData.exitWaylineWhenRcLost"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="WaylineLostControlActionInCommandFlightOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Return-to-Home Mode:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="takeoffToPointPopoverData.rthMode"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="RthModeInCommandFlightOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Commander Mode Lost Action:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="takeoffToPointPopoverData.commanderModeLostAction"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="CommanderModeLostActionInCommandFlightOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Commander Flight Mode:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="takeoffToPointPopoverData.commanderFlightMode"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="CommanderFlightModeInCommandFlightOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">Commander Flight Height(m):</span>
|
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.commanderFlightHeight"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button size="small" ghost @click="onShowTakeoffToPointPopover" >
|
||||||
|
<span>Take off</span>
|
||||||
|
</Button>
|
||||||
|
<div v-for="(cmdItem) in cmdList" :key="cmdItem.cmdKey" class="control-cmd-item">
|
||||||
|
<Button :loading="cmdItem.loading" size="small" ghost @click="sendControlCmd(cmdItem, 0)">
|
||||||
|
{{ cmdItem.operateText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button size="small" ghost @click="openLivestreamAgora" >
|
||||||
|
<span>Agora Live</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @click="openLivestreamOthers" >
|
||||||
|
<span>RTMP/GB28181 Live</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DroneControlPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="row">
|
||||||
|
<Select v-model:value="payloadSelectInfo.value" style="width: 110px; marginRight: 5px" :options="payloadSelectInfo.options" @change="handlePayloadChange"/>
|
||||||
|
<div class="drone-control">
|
||||||
|
<Button type="primary" size="small" @click="onAuthPayload">Payload Control</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<DroneControlPopover
|
||||||
|
:visible="gimbalResetPopoverData.visible"
|
||||||
|
:loading="gimbalResetPopoverData.loading"
|
||||||
|
@confirm="($event) => onGimbalResetConfirm(true)"
|
||||||
|
@cancel="($event) =>onGimbalResetConfirm(false)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">reset mode:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="gimbalResetPopoverData.resetMode"
|
||||||
|
style="width: 180px"
|
||||||
|
:options="GimbalResetModeOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button size="small" ghost @click="onShowGimbalResetPopover">
|
||||||
|
<span>Gimbal Reset</span>
|
||||||
|
</Button>
|
||||||
|
</DroneControlPopover>
|
||||||
|
<Button size="small" ghost @click="onSwitchCameraMode">
|
||||||
|
<span>Camera Mode Switch</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Button size="small" ghost @click="onStartCameraRecording">
|
||||||
|
<span>Start Recording</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="small" ghost @click="onStopCameraRecording">
|
||||||
|
<span>Stop Recording</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Button size="small" ghost @click="onTakeCameraPhoto">
|
||||||
|
<span>Take Photo</span>
|
||||||
|
</Button>
|
||||||
|
<DroneControlPopover
|
||||||
|
:visible="zoomFactorPopoverData.visible"
|
||||||
|
:loading="zoomFactorPopoverData.loading"
|
||||||
|
@confirm="($event) => onZoomFactorConfirm(true)"
|
||||||
|
@cancel="($event) =>onZoomFactorConfirm(false)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">camera type:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="zoomFactorPopoverData.cameraType"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="ZoomCameraTypeOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">zoom factor:</span>
|
||||||
|
<a-input-number v-model:value="zoomFactorPopoverData.zoomFactor" :min="2" :max="200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button size="small" ghost @click="($event) => onShowZoomFactorPopover()">
|
||||||
|
<span class="word" @click=";">Zoom</span>
|
||||||
|
</Button>
|
||||||
|
</DroneControlPopover>
|
||||||
|
<DroneControlPopover
|
||||||
|
:visible="cameraAimPopoverData.visible"
|
||||||
|
:loading="cameraAimPopoverData.loading"
|
||||||
|
@confirm="($event) => onCameraAimConfirm(true)"
|
||||||
|
@cancel="($event) =>onCameraAimConfirm(false)"
|
||||||
|
>
|
||||||
|
<template #formContent>
|
||||||
|
<div class="form-content">
|
||||||
|
<div>
|
||||||
|
<span class="form-label">camera type:</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="cameraAimPopoverData.cameraType"
|
||||||
|
style="width: 120px"
|
||||||
|
:options="CameraTypeOptions"
|
||||||
|
></a-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">locked:</span>
|
||||||
|
<a-switch v-model:checked="cameraAimPopoverData.locked"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">x:</span>
|
||||||
|
<a-input-number v-model:value="cameraAimPopoverData.x" :min="0" :max="1"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="form-label">y:</span>
|
||||||
|
<a-input-number v-model:value="cameraAimPopoverData.y" :min="0" :max="1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button size="small" ghost @click="($event) => onShowCameraAimPopover()">
|
||||||
|
<span class="word" @click=";">AIM</span>
|
||||||
|
</Button>
|
||||||
|
</DroneControlPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 信息提示 -->
|
||||||
|
<DroneControlInfoPanel :message="drcInfo"></DroneControlInfoPanel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, reactive, ref, watch, computed, onMounted, watchEffect } from 'vue'
|
||||||
|
import { Select, message, Button } from 'ant-design-vue'
|
||||||
|
import { PayloadInfo, DeviceInfoType, ControlSource, DeviceOsdCamera, DrcStateEnum } from '/@/types/device'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { postDrcEnter, postDrcExit } from '/@/api/drc'
|
||||||
|
import { useMqtt, DeviceTopicInfo } from './use-mqtt'
|
||||||
|
import { DownOutlined, UpOutlined, LeftOutlined, RightOutlined, PauseCircleOutlined, UndoOutlined, RedoOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { useManualControl, KeyCode } from './use-manual-control'
|
||||||
|
import { usePayloadControl } from './use-payload-control'
|
||||||
|
import { CameraMode, CameraType, CameraTypeOptions, ZoomCameraTypeOptions, CameraListItem } from '/@/types/live-stream'
|
||||||
|
import { useDroneControlWsEvent } from './use-drone-control-ws-event'
|
||||||
|
import { useDroneControlMqttEvent } from './use-drone-control-mqtt-event'
|
||||||
|
import {
|
||||||
|
postFlightAuth, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight, ERthMode,
|
||||||
|
ECommanderModeLostAction, ECommanderFlightMode
|
||||||
|
} from '/@/api/drone-control/drone'
|
||||||
|
import { useDroneControl } from './use-drone-control'
|
||||||
|
import {
|
||||||
|
GimbalResetMode, GimbalResetModeOptions, LostControlActionInCommandFLightOptions, WaylineLostControlActionInCommandFlightOptions,
|
||||||
|
RthModeInCommandFlightOptions, CommanderModeLostActionInCommandFlightOptions, CommanderFlightModeInCommandFlightOptions
|
||||||
|
} from '/@/types/drone-control'
|
||||||
|
import DroneControlPopover from './DroneControlPopover.vue'
|
||||||
|
import DroneControlInfoPanel from './DroneControlInfoPanel.vue'
|
||||||
|
import { noDebugCmdList as baseCmdList, DeviceCmdItem, DeviceCmd } from '/@/types/device-cmd'
|
||||||
|
import { useDockControl } from './use-dock-control'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sn: string,
|
||||||
|
deviceInfo: DeviceInfoType,
|
||||||
|
payloads: null | PayloadInfo[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const store = useMyStore()
|
||||||
|
const clientId = computed(() => {
|
||||||
|
return store.state.clientId
|
||||||
|
})
|
||||||
|
|
||||||
|
const initCmdList = baseCmdList.map(cmdItem => Object.assign({}, cmdItem))
|
||||||
|
const cmdList = ref(initCmdList)
|
||||||
|
|
||||||
|
const {
|
||||||
|
sendDockControlCmd
|
||||||
|
} = useDockControl()
|
||||||
|
|
||||||
|
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) {
|
||||||
|
cmdItem.loading = true
|
||||||
|
const result = await sendDockControlCmd({
|
||||||
|
sn: props.sn,
|
||||||
|
cmd: cmdItem.cmdKey,
|
||||||
|
action: cmdItem.action
|
||||||
|
}, false)
|
||||||
|
if (result) {
|
||||||
|
message.success('Return home successful')
|
||||||
|
if (flightController.value) {
|
||||||
|
exitFlightCOntrol()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Failed to return home')
|
||||||
|
}
|
||||||
|
cmdItem.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { flyToPoint, stopFlyToPoint, takeoffToPoint } = useDroneControl()
|
||||||
|
const MAX_SPEED = 14
|
||||||
|
|
||||||
|
const flyToPointPopoverData = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
latitude: null as null | number,
|
||||||
|
longitude: null as null | number,
|
||||||
|
height: null as null | number,
|
||||||
|
maxSpeed: MAX_SPEED,
|
||||||
|
})
|
||||||
|
|
||||||
|
function onShowFlyToPopover () {
|
||||||
|
flyToPointPopoverData.visible = !flyToPointPopoverData.visible
|
||||||
|
flyToPointPopoverData.loading = false
|
||||||
|
flyToPointPopoverData.latitude = null
|
||||||
|
flyToPointPopoverData.longitude = null
|
||||||
|
flyToPointPopoverData.height = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onFlyToConfirm (confirm: boolean) {
|
||||||
|
if (confirm) {
|
||||||
|
if (!flyToPointPopoverData.height || !flyToPointPopoverData.latitude || !flyToPointPopoverData.longitude) {
|
||||||
|
console.error('Input error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await flyToPoint(props.sn, {
|
||||||
|
max_speed: flyToPointPopoverData.maxSpeed,
|
||||||
|
points: [
|
||||||
|
{
|
||||||
|
latitude: flyToPointPopoverData.latitude,
|
||||||
|
longitude: flyToPointPopoverData.longitude,
|
||||||
|
height: flyToPointPopoverData.height
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flyToPointPopoverData.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStopFlyToPoint () {
|
||||||
|
await stopFlyToPoint(props.sn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const takeoffToPointPopoverData = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
latitude: null as null | number,
|
||||||
|
longitude: null as null | number,
|
||||||
|
height: null as null | number,
|
||||||
|
securityTakeoffHeight: null as null | number,
|
||||||
|
maxSpeed: MAX_SPEED,
|
||||||
|
rthAltitude: null as null | number,
|
||||||
|
rcLostAction: LostControlActionInCommandFLight.RETURN_HOME,
|
||||||
|
exitWaylineWhenRcLost: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION,
|
||||||
|
rthMode: ERthMode.SETTING,
|
||||||
|
commanderModeLostAction: ECommanderModeLostAction.CONTINUE,
|
||||||
|
commanderFlightMode: ECommanderFlightMode.SETTING,
|
||||||
|
commanderFlightHeight: null as null | number,
|
||||||
|
})
|
||||||
|
|
||||||
|
function onShowTakeoffToPointPopover () {
|
||||||
|
takeoffToPointPopoverData.visible = !takeoffToPointPopoverData.visible
|
||||||
|
takeoffToPointPopoverData.loading = false
|
||||||
|
takeoffToPointPopoverData.latitude = null
|
||||||
|
takeoffToPointPopoverData.longitude = null
|
||||||
|
takeoffToPointPopoverData.securityTakeoffHeight = null
|
||||||
|
takeoffToPointPopoverData.rthAltitude = null
|
||||||
|
takeoffToPointPopoverData.rcLostAction = LostControlActionInCommandFLight.RETURN_HOME
|
||||||
|
takeoffToPointPopoverData.exitWaylineWhenRcLost = WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION
|
||||||
|
takeoffToPointPopoverData.rthMode = ERthMode.SETTING
|
||||||
|
takeoffToPointPopoverData.commanderModeLostAction = ECommanderModeLostAction.CONTINUE
|
||||||
|
takeoffToPointPopoverData.commanderFlightMode = ECommanderFlightMode.SETTING
|
||||||
|
takeoffToPointPopoverData.commanderFlightHeight = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onTakeoffToPointConfirm (confirm: boolean) {
|
||||||
|
if (confirm) {
|
||||||
|
if (!takeoffToPointPopoverData.height ||
|
||||||
|
!takeoffToPointPopoverData.latitude ||
|
||||||
|
!takeoffToPointPopoverData.longitude ||
|
||||||
|
!takeoffToPointPopoverData.securityTakeoffHeight ||
|
||||||
|
!takeoffToPointPopoverData.rthAltitude ||
|
||||||
|
!takeoffToPointPopoverData.commanderFlightHeight) {
|
||||||
|
console.error('Input error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await takeoffToPoint(props.sn, {
|
||||||
|
target_latitude: takeoffToPointPopoverData.latitude,
|
||||||
|
target_longitude: takeoffToPointPopoverData.longitude,
|
||||||
|
target_height: takeoffToPointPopoverData.height,
|
||||||
|
security_takeoff_height: takeoffToPointPopoverData.securityTakeoffHeight,
|
||||||
|
rth_altitude: takeoffToPointPopoverData.rthAltitude,
|
||||||
|
max_speed: takeoffToPointPopoverData.maxSpeed,
|
||||||
|
rc_lost_action: takeoffToPointPopoverData.rcLostAction,
|
||||||
|
exit_wayline_when_rc_lost: takeoffToPointPopoverData.exitWaylineWhenRcLost,
|
||||||
|
rth_mode: takeoffToPointPopoverData.rthMode,
|
||||||
|
commander_mode_lost_action: takeoffToPointPopoverData.commanderModeLostAction,
|
||||||
|
commander_flight_mode: takeoffToPointPopoverData.commanderFlightMode,
|
||||||
|
commander_flight_height: takeoffToPointPopoverData.commanderFlightHeight,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
takeoffToPointPopoverData.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceTopicInfo: DeviceTopicInfo = reactive({
|
||||||
|
sn: props.sn,
|
||||||
|
pubTopic: '',
|
||||||
|
subTopic: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
useMqtt(deviceTopicInfo)
|
||||||
|
|
||||||
|
// 飞行控制
|
||||||
|
// const drcState = computed(() => {
|
||||||
|
// return store.state.deviceState?.dockInfo[props.sn]?.link_osd?.drc_state === DrcStateEnum.CONNECTED
|
||||||
|
// })
|
||||||
|
const flightController = ref(false)
|
||||||
|
|
||||||
|
async function onClickFightControl () {
|
||||||
|
if (flightController.value) {
|
||||||
|
exitFlightCOntrol()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enterFlightControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入飞行控制
|
||||||
|
async function enterFlightControl () {
|
||||||
|
try {
|
||||||
|
const { code, data } = await postDrcEnter({
|
||||||
|
client_id: clientId.value,
|
||||||
|
dock_sn: props.sn,
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
flightController.value = true
|
||||||
|
if (data.sub && data.sub.length > 0) {
|
||||||
|
deviceTopicInfo.subTopic = data.sub[0]
|
||||||
|
}
|
||||||
|
if (data.pub && data.pub.length > 0) {
|
||||||
|
deviceTopicInfo.pubTopic = data.pub[0]
|
||||||
|
}
|
||||||
|
// 获取飞行控制权
|
||||||
|
if (droneControlSource.value !== ControlSource.A) {
|
||||||
|
await postFlightAuth(props.sn)
|
||||||
|
}
|
||||||
|
message.success('Get flight control successfully')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出飞行控制
|
||||||
|
async function exitFlightCOntrol () {
|
||||||
|
try {
|
||||||
|
const { code } = await postDrcExit({
|
||||||
|
client_id: clientId.value,
|
||||||
|
dock_sn: props.sn,
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
flightController.value = false
|
||||||
|
deviceTopicInfo.subTopic = ''
|
||||||
|
deviceTopicInfo.pubTopic = ''
|
||||||
|
message.success('Exit flight control')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drc mqtt message
|
||||||
|
const { drcInfo, errorInfo } = useDroneControlMqttEvent(props.sn)
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleKeyup,
|
||||||
|
handleEmergencyStop,
|
||||||
|
resetControlState,
|
||||||
|
} = useManualControl(deviceTopicInfo, flightController)
|
||||||
|
|
||||||
|
function onMouseDown (type: KeyCode) {
|
||||||
|
handleKeyup(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp () {
|
||||||
|
resetControlState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 负载控制
|
||||||
|
const payloadSelectInfo = {
|
||||||
|
value: null as any,
|
||||||
|
controlSource: undefined as undefined | ControlSource,
|
||||||
|
options: [] as any,
|
||||||
|
payloadIndex: '' as string,
|
||||||
|
camera: undefined as undefined | DeviceOsdCamera // 当前负载osd信息
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePayloadChange = (value: string) => {
|
||||||
|
const payload = props.payloads?.find(item => item.payload_sn === value)
|
||||||
|
if (payload) {
|
||||||
|
payloadSelectInfo.payloadIndex = payload.payload_index || ''
|
||||||
|
payloadSelectInfo.controlSource = payload.control_source
|
||||||
|
payloadSelectInfo.camera = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// function getCurrentCamera (cameraList: CameraListItem[], cameraIndex?: string):CameraListItem | null {
|
||||||
|
// let camera = null
|
||||||
|
// cameraList.forEach(item => {
|
||||||
|
// if (item.camera_index === cameraIndex) {
|
||||||
|
// camera = item
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// return camera
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const currentCamera = computed(() => {
|
||||||
|
// return getCurrentCamera(props.deviceInfo.dock.basic_osd.live_capacity?.device_list[0]?.camera_list as CameraListItem[], camera_index)
|
||||||
|
// })
|
||||||
|
// 更新负载信息
|
||||||
|
watch(() => props.payloads, (payloads) => {
|
||||||
|
if (payloads && payloads.length > 0) {
|
||||||
|
payloadSelectInfo.value = payloads[0].payload_sn
|
||||||
|
payloadSelectInfo.controlSource = payloads[0].control_source || ControlSource.B
|
||||||
|
payloadSelectInfo.payloadIndex = payloads[0].payload_index || ''
|
||||||
|
payloadSelectInfo.options = payloads.map(item => ({ label: item.payload_name, value: item.payload_sn }))
|
||||||
|
payloadSelectInfo.camera = undefined
|
||||||
|
} else {
|
||||||
|
payloadSelectInfo.value = null
|
||||||
|
payloadSelectInfo.controlSource = undefined
|
||||||
|
payloadSelectInfo.options = []
|
||||||
|
payloadSelectInfo.payloadIndex = ''
|
||||||
|
payloadSelectInfo.camera = undefined
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
watch(() => props.deviceInfo.device, (droneOsd) => {
|
||||||
|
if (droneOsd && droneOsd.cameras) {
|
||||||
|
payloadSelectInfo.camera = droneOsd.cameras.find(item => item.payload_index === payloadSelectInfo.payloadIndex)
|
||||||
|
} else {
|
||||||
|
payloadSelectInfo.camera = undefined
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// ws 消息通知
|
||||||
|
const { droneControlSource, payloadControlSource } = useDroneControlWsEvent(props.sn, payloadSelectInfo.value)
|
||||||
|
watch(() => payloadControlSource, (controlSource) => {
|
||||||
|
payloadSelectInfo.controlSource = controlSource.value
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
checkPayloadAuth,
|
||||||
|
authPayload,
|
||||||
|
resetGimbal,
|
||||||
|
switchCameraMode,
|
||||||
|
takeCameraPhoto,
|
||||||
|
startCameraRecording,
|
||||||
|
stopCameraRecording,
|
||||||
|
changeCameraFocalLength,
|
||||||
|
cameraAim,
|
||||||
|
} = usePayloadControl()
|
||||||
|
|
||||||
|
async function onAuthPayload () {
|
||||||
|
const result = await authPayload(props.sn, payloadSelectInfo.payloadIndex)
|
||||||
|
if (result) {
|
||||||
|
payloadControlSource.value = ControlSource.A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gimbalResetPopoverData = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
resetMode: null as null | GimbalResetMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
function onShowGimbalResetPopover () {
|
||||||
|
gimbalResetPopoverData.visible = !gimbalResetPopoverData.visible
|
||||||
|
gimbalResetPopoverData.loading = false
|
||||||
|
gimbalResetPopoverData.resetMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onGimbalResetConfirm (confirm: boolean) {
|
||||||
|
if (confirm) {
|
||||||
|
if (gimbalResetPopoverData.resetMode === null) {
|
||||||
|
console.error('Please select reset mode')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gimbalResetPopoverData.loading = true
|
||||||
|
try {
|
||||||
|
await resetGimbal(props.sn, {
|
||||||
|
payload_index: payloadSelectInfo.payloadIndex,
|
||||||
|
reset_mode: gimbalResetPopoverData.resetMode
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gimbalResetPopoverData.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSwitchCameraMode () {
|
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentCameraMode = payloadSelectInfo.camera?.camera_mode
|
||||||
|
await switchCameraMode(props.sn, {
|
||||||
|
payload_index: payloadSelectInfo.payloadIndex,
|
||||||
|
camera_mode: currentCameraMode === CameraMode.Photo ? CameraMode.Video : CameraMode.Photo
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onTakeCameraPhoto () {
|
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await takeCameraPhoto(props.sn, payloadSelectInfo.payloadIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStartCameraRecording () {
|
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await startCameraRecording(props.sn, payloadSelectInfo.payloadIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStopCameraRecording () {
|
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await stopCameraRecording(props.sn, payloadSelectInfo.payloadIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomFactorPopoverData = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
cameraType: null as null | CameraType,
|
||||||
|
zoomFactor: null as null | number,
|
||||||
|
})
|
||||||
|
|
||||||
|
function onShowZoomFactorPopover () {
|
||||||
|
zoomFactorPopoverData.visible = !zoomFactorPopoverData.visible
|
||||||
|
zoomFactorPopoverData.loading = false
|
||||||
|
zoomFactorPopoverData.cameraType = null
|
||||||
|
zoomFactorPopoverData.zoomFactor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onZoomFactorConfirm (confirm: boolean) {
|
||||||
|
if (confirm) {
|
||||||
|
if (!zoomFactorPopoverData.zoomFactor || zoomFactorPopoverData.cameraType === null) {
|
||||||
|
console.error('Please input Zoom Factor')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
zoomFactorPopoverData.loading = true
|
||||||
|
try {
|
||||||
|
await changeCameraFocalLength(props.sn, {
|
||||||
|
payload_index: payloadSelectInfo.payloadIndex,
|
||||||
|
camera_type: zoomFactorPopoverData.cameraType,
|
||||||
|
zoom_factor: zoomFactorPopoverData.zoomFactor
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zoomFactorPopoverData.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const cameraAimPopoverData = reactive({
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
cameraType: null as null | CameraType,
|
||||||
|
locked: false,
|
||||||
|
x: null as null | number,
|
||||||
|
y: null as null | number,
|
||||||
|
})
|
||||||
|
|
||||||
|
function onShowCameraAimPopover () {
|
||||||
|
cameraAimPopoverData.visible = !cameraAimPopoverData.visible
|
||||||
|
cameraAimPopoverData.loading = false
|
||||||
|
cameraAimPopoverData.cameraType = null
|
||||||
|
cameraAimPopoverData.locked = false
|
||||||
|
cameraAimPopoverData.x = null
|
||||||
|
cameraAimPopoverData.y = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLivestreamOthers () {
|
||||||
|
store.commit('SET_LIVESTREAM_OTHERS_VISIBLE', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLivestreamAgora () {
|
||||||
|
store.commit('SET_LIVESTREAM_AGORA_VISIBLE', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onCameraAimConfirm (confirm: boolean) {
|
||||||
|
if (confirm) {
|
||||||
|
if (cameraAimPopoverData.cameraType === null || cameraAimPopoverData.x === null || cameraAimPopoverData.y === null) {
|
||||||
|
console.error('Input error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await cameraAim(props.sn, {
|
||||||
|
payload_index: payloadSelectInfo.payloadIndex,
|
||||||
|
camera_type: cameraAimPopoverData.cameraType,
|
||||||
|
locked: cameraAimPopoverData.locked,
|
||||||
|
x: cameraAimPopoverData.x,
|
||||||
|
y: cameraAimPopoverData.y,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cameraAimPopoverData.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => errorInfo, (errorInfo) => {
|
||||||
|
if (errorInfo.value) {
|
||||||
|
console.error(errorInfo.value)
|
||||||
|
console.error(errorInfo.value)
|
||||||
|
errorInfo.value = ''
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss' scoped>
|
||||||
|
.drone-control-wrapper{
|
||||||
|
// border-bottom: 1px solid #515151;
|
||||||
|
|
||||||
|
.drone-control-header{
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drone-control-box {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: 1;
|
||||||
|
.box {
|
||||||
|
width: 50%;
|
||||||
|
padding: 5px;
|
||||||
|
border: 0.5px solid rgba(255,255,255,0.3);
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
+ .row{
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep{
|
||||||
|
.ant-btn{
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0px 4px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drone-control{
|
||||||
|
&::v-deep{
|
||||||
|
|
||||||
|
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector{
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drone-control-direction{
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
// padding: 0px 1px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word{
|
||||||
|
width: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
src/components/g-map/DroneControlPopover.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<a-popover :visible="state.sVisible"
|
||||||
|
trigger="click"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:overlay-class-name="overlayClassName"
|
||||||
|
placement="bottom"
|
||||||
|
@visibleChange=";"
|
||||||
|
v-on="$attrs">
|
||||||
|
<template #content>
|
||||||
|
<div class="title-content">
|
||||||
|
</div>
|
||||||
|
<slot name="formContent" />
|
||||||
|
<div class="uranus-popconfirm-btns">
|
||||||
|
<a-button size="sm"
|
||||||
|
@click="onCancel">
|
||||||
|
{{ cancelText || 'cancel'}}
|
||||||
|
</a-button>
|
||||||
|
<a-button size="sm"
|
||||||
|
:loading="loading"
|
||||||
|
type="primary"
|
||||||
|
class="confirm-btn"
|
||||||
|
@click="onConfirm">
|
||||||
|
{{ okText || 'ok' }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots.default">
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineProps, defineEmits, reactive, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible?: boolean,
|
||||||
|
loading?: Boolean,
|
||||||
|
disabled?: Boolean,
|
||||||
|
title?: String,
|
||||||
|
okText?: String,
|
||||||
|
cancelText?: String,
|
||||||
|
width?: Number,
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['cancel', 'confirm'])
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
sVisible: false,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.visible, (val) => {
|
||||||
|
state.sVisible = val || false
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = computed(() => {
|
||||||
|
return props.loading
|
||||||
|
})
|
||||||
|
const okLabel = computed(() => {
|
||||||
|
return props.loading ? '' : '确定'
|
||||||
|
})
|
||||||
|
|
||||||
|
const overlayClassName = computed(() => {
|
||||||
|
const classList = ['drone-control-popconfirm']
|
||||||
|
return classList.join(' ')
|
||||||
|
})
|
||||||
|
|
||||||
|
function onConfirm (e: Event) {
|
||||||
|
if (props.disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('confirm', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel (e: Event) {
|
||||||
|
state.sVisible = false
|
||||||
|
emit('cancel', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.drone-control-popconfirm {
|
||||||
|
min-width: 300px;
|
||||||
|
|
||||||
|
.uranus-popconfirm-btns{
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 0px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.confirm-btn{
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
flex: 1 0 60px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
71
src/components/g-map/use-connect-mqtt.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
computed,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { postDrc } from '/@/api/drc'
|
||||||
|
import {
|
||||||
|
UranusMqtt,
|
||||||
|
} from '/@/mqtt'
|
||||||
|
|
||||||
|
type StatusOptions = {
|
||||||
|
status: 'close';
|
||||||
|
event?: CloseEvent;
|
||||||
|
} | {
|
||||||
|
status: 'open';
|
||||||
|
retryCount: number;
|
||||||
|
} | {
|
||||||
|
status: 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConnectMqtt () {
|
||||||
|
const store = useMyStore()
|
||||||
|
const dockOsdVisible = computed(() => {
|
||||||
|
return store.state.osdVisible && store.state.osdVisible.visible && store.state.osdVisible.is_dock
|
||||||
|
})
|
||||||
|
const mqttState = ref<UranusMqtt | null>(null)
|
||||||
|
|
||||||
|
// 监听已打开的设备小窗 窗口数量
|
||||||
|
watch(() => dockOsdVisible.value, async (val) => {
|
||||||
|
// 1.打开小窗
|
||||||
|
// 2.设备拥有飞行控制权
|
||||||
|
// 3.请求建立mqtt连接的认证信息
|
||||||
|
if (val) {
|
||||||
|
if (mqttState.value) return
|
||||||
|
const result = await postDrc({})
|
||||||
|
if (result?.code === 0) {
|
||||||
|
const { address, client_id, username, password, expire_time } = result.data
|
||||||
|
// @TODO: 校验 expire_time
|
||||||
|
mqttState.value = new UranusMqtt(address, {
|
||||||
|
clientId: client_id,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
mqttState.value?.initMqtt()
|
||||||
|
mqttState.value?.on('onStatus', (statusOptions: StatusOptions) => {
|
||||||
|
// @TODO: 异常case
|
||||||
|
})
|
||||||
|
|
||||||
|
store.commit('SET_MQTT_STATE', mqttState.value)
|
||||||
|
store.commit('SET_CLIENT_ID', client_id)
|
||||||
|
}
|
||||||
|
// @TODO: 认证失败case
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 关闭所有小窗后
|
||||||
|
// 1.销毁mqtt连接重置mqtt状态
|
||||||
|
if (mqttState?.value) {
|
||||||
|
mqttState.value?.destroyed()
|
||||||
|
mqttState.value = null
|
||||||
|
store.commit('SET_MQTT_STATE', null)
|
||||||
|
store.commit('SET_CLIENT_ID', '')
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
mqttState.value?.destroyed()
|
||||||
|
})
|
||||||
|
}
|
||||||
56
src/components/g-map/use-device-setting.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { putDeviceProps, PutDevicePropsBody } from '/@/api/device-setting'
|
||||||
|
import { DeviceSettingKeyEnum, DeviceSettingFormModel, ObstacleAvoidanceStatusEnum, NightLightsStateEnum, DistanceLimitStatusEnum } from '/@/types/device-setting'
|
||||||
|
|
||||||
|
export function useDeviceSetting () {
|
||||||
|
// 生成参数
|
||||||
|
function genDevicePropsBySettingKey (key: DeviceSettingKeyEnum, fromModel: DeviceSettingFormModel) {
|
||||||
|
const body = {} as PutDevicePropsBody
|
||||||
|
if (key === DeviceSettingKeyEnum.NIGHT_LIGHTS_MODE_SET) {
|
||||||
|
body.night_lights_state = fromModel.nightLightsState ? NightLightsStateEnum.OPEN : NightLightsStateEnum.CLOSE
|
||||||
|
} else if (key === DeviceSettingKeyEnum.HEIGHT_LIMIT_SET) {
|
||||||
|
body.height_limit = fromModel.heightLimit
|
||||||
|
} else if (key === DeviceSettingKeyEnum.DISTANCE_LIMIT_SET) {
|
||||||
|
body.distance_limit_status = {}
|
||||||
|
if (fromModel.distanceLimitStatus.state) {
|
||||||
|
body.distance_limit_status.state = DistanceLimitStatusEnum.SET
|
||||||
|
body.distance_limit_status.distance_limit = fromModel.distanceLimitStatus.distanceLimit
|
||||||
|
} else {
|
||||||
|
body.distance_limit_status.state = DistanceLimitStatusEnum.UNSET
|
||||||
|
}
|
||||||
|
} else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_HORIZON) {
|
||||||
|
body.obstacle_avoidance = {
|
||||||
|
horizon: fromModel.obstacleAvoidanceHorizon ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
|
||||||
|
}
|
||||||
|
} else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_UPSIDE) {
|
||||||
|
body.obstacle_avoidance = {
|
||||||
|
upside: fromModel.obstacleAvoidanceUpside ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
|
||||||
|
}
|
||||||
|
} else if (key === DeviceSettingKeyEnum.OBSTACLE_AVOIDANCE_DOWNSIDE) {
|
||||||
|
body.obstacle_avoidance = {
|
||||||
|
downside: fromModel.obstacleAvoidanceDownside ? ObstacleAvoidanceStatusEnum.OPEN : ObstacleAvoidanceStatusEnum.CLOSE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置设备属性
|
||||||
|
async function setDeviceProps (sn: string, body: PutDevicePropsBody) {
|
||||||
|
try {
|
||||||
|
const { code, message: msg } = await putDeviceProps(sn, body)
|
||||||
|
if (code === 0) {
|
||||||
|
// message.success('指令发送成功')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
throw (msg)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('设备属性设置失败')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
genDevicePropsBySettingKey,
|
||||||
|
setDeviceProps
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/components/g-map/use-dock-control.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { postSendCmd } from '/@/api/device-cmd'
|
||||||
|
import { DeviceCmd, DeviceCmdItemAction } from '/@/types/device-cmd'
|
||||||
|
|
||||||
|
export function useDockControl () {
|
||||||
|
const dockControlPanelVisible = ref(false)
|
||||||
|
|
||||||
|
function setDockControlPanelVisible (visible: boolean) {
|
||||||
|
dockControlPanelVisible.value = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// 远程调试开关
|
||||||
|
async function dockDebugOnOff (sn: string, on: boolean) {
|
||||||
|
const result = await sendDockControlCmd({
|
||||||
|
sn: sn,
|
||||||
|
cmd: on ? DeviceCmd.DebugModeOpen : DeviceCmd.DebugModeClose
|
||||||
|
}, false)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送指令
|
||||||
|
async function sendDockControlCmd (params: {
|
||||||
|
sn: string,
|
||||||
|
cmd: DeviceCmd
|
||||||
|
action?: DeviceCmdItemAction
|
||||||
|
}, tip = true) {
|
||||||
|
try {
|
||||||
|
let body = undefined as any
|
||||||
|
if (params.action !== undefined) {
|
||||||
|
body = {
|
||||||
|
action: params.action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { code, message: msg } = await postSendCmd({ dock_sn: params.sn, device_cmd: params.cmd }, body)
|
||||||
|
if (code === 0) {
|
||||||
|
tip && message.success('指令发送成功')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
throw (msg)
|
||||||
|
} catch (e) {
|
||||||
|
tip && console.error('指令发送失败')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制面板关闭
|
||||||
|
async function onCloseControlPanel (sn: string, debugging: boolean) {
|
||||||
|
if (debugging) {
|
||||||
|
await dockDebugOnOff(sn, false)
|
||||||
|
}
|
||||||
|
setDockControlPanelVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dockControlPanelVisible,
|
||||||
|
setDockControlPanelVisible,
|
||||||
|
sendDockControlCmd,
|
||||||
|
dockDebugOnOff,
|
||||||
|
onCloseControlPanel,
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/components/g-map/use-drone-control-mqtt-event.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import {
|
||||||
|
DRC_METHOD,
|
||||||
|
DRCHsiInfo,
|
||||||
|
DRCOsdInfo,
|
||||||
|
DRCDelayTimeInfo,
|
||||||
|
DrcResponseInfo,
|
||||||
|
} from '/@/types/drc'
|
||||||
|
|
||||||
|
export function useDroneControlMqttEvent (sn: string) {
|
||||||
|
const drcInfo = ref('')
|
||||||
|
const hsiInfo = ref('')
|
||||||
|
const osdInfo = ref('')
|
||||||
|
const delayInfo = ref('')
|
||||||
|
const errorInfo = ref('')
|
||||||
|
|
||||||
|
function handleHsiInfo (data: DRCHsiInfo) {
|
||||||
|
hsiInfo.value = `method: ${DRC_METHOD.HSI_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n `
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOsdInfo (data: DRCOsdInfo) {
|
||||||
|
osdInfo.value = `method: ${DRC_METHOD.OSD_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n `
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelayTimeInfo (data: DRCDelayTimeInfo) {
|
||||||
|
delayInfo.value = `method: ${DRC_METHOD.DELAY_TIME_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n `
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDroneControlErrorInfo (data: DrcResponseInfo) {
|
||||||
|
if (!data.result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorInfo.value = `Drc error code: ${data.result}, seq: ${data.output?.seq}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDroneControlMqttEvent (payload: any) {
|
||||||
|
if (!payload || !payload.method) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.method) {
|
||||||
|
case DRC_METHOD.HSI_INFO_PUSH: {
|
||||||
|
handleHsiInfo(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case DRC_METHOD.OSD_INFO_PUSH: {
|
||||||
|
handleOsdInfo(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case DRC_METHOD.DELAY_TIME_INFO_PUSH: {
|
||||||
|
handleDelayTimeInfo(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case DRC_METHOD.DRONE_EMERGENCY_STOP:
|
||||||
|
case DRC_METHOD.DRONE_CONTROL: {
|
||||||
|
handleDroneControlErrorInfo(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drcInfo.value = hsiInfo.value + osdInfo.value + delayInfo.value
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('droneControlMqttInfo', handleDroneControlMqttEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('droneControlMqttInfo', handleDroneControlMqttEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
drcInfo: drcInfo,
|
||||||
|
errorInfo: errorInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/components/g-map/use-drone-control-ws-event.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { message, notification } from 'ant-design-vue'
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { EBizCode } from '/@/types'
|
||||||
|
import { ControlSource } from '/@/types/device'
|
||||||
|
import { ControlSourceChangeType, ControlSourceChangeInfo, FlyToPointMessage, TakeoffToPointMessage, DrcModeExitNotifyMessage, DrcStatusNotifyMessage } from '/@/types/drone-control'
|
||||||
|
|
||||||
|
export interface UseDroneControlWsEventParams {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDroneControlWsEvent (sn: string, payloadSn: string, funcs?: UseDroneControlWsEventParams) {
|
||||||
|
const droneControlSource = ref(ControlSource.A)
|
||||||
|
const payloadControlSource = ref(ControlSource.B)
|
||||||
|
function onControlSourceChange (data: ControlSourceChangeInfo) {
|
||||||
|
if (data.type === ControlSourceChangeType.Flight && data.sn === sn) {
|
||||||
|
droneControlSource.value = data.control_source
|
||||||
|
message.info(`Flight control is changed to ${droneControlSource.value}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data.type === ControlSourceChangeType.Payload && data.sn === payloadSn) {
|
||||||
|
payloadControlSource.value = data.control_source
|
||||||
|
message.info(`Payload control is changed to ${payloadControlSource.value}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProgress (key: string, message: string, error: number) {
|
||||||
|
if (error !== 0) {
|
||||||
|
notification.error({
|
||||||
|
key: key,
|
||||||
|
message: key + 'Error code:' + error,
|
||||||
|
description: message,
|
||||||
|
duration: null
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notification.info({
|
||||||
|
key: key,
|
||||||
|
message: key,
|
||||||
|
description: message,
|
||||||
|
duration: 30
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDroneControlWsEvent (payload: any) {
|
||||||
|
if (!payload) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.biz_code) {
|
||||||
|
case EBizCode.ControlSourceChange: {
|
||||||
|
onControlSourceChange(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.FlyToPointProgress: {
|
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as FlyToPointMessage
|
||||||
|
if (deviceSn !== sn) return
|
||||||
|
handleProgress(EBizCode.FlyToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.TakeoffToPointProgress: {
|
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as TakeoffToPointMessage
|
||||||
|
if (deviceSn !== sn) return
|
||||||
|
handleProgress(EBizCode.TakeoffToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.JoystickInvalidNotify: {
|
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as DrcModeExitNotifyMessage
|
||||||
|
if (deviceSn !== sn) return
|
||||||
|
handleProgress(EBizCode.JoystickInvalidNotify, `device(sn: ${deviceSn}) ${msg}`, result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.DrcStatusNotify: {
|
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as DrcStatusNotifyMessage
|
||||||
|
// handleProgress(EBizCode.DrcStatusNotify, `device(sn: ${deviceSn}) ${msg}`, result)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
// console.log('payload.biz_code', payload.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('droneControlWs', handleDroneControlWsEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('droneControlWs', handleDroneControlWsEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
droneControlSource: droneControlSource,
|
||||||
|
payloadControlSource: payloadControlSource
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/components/g-map/use-drone-control.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { postFlyToPoint, PostFlyToPointBody, deleteFlyToPoint, postTakeoffToPoint, PostTakeoffToPointBody } from '/@/api/drone-control/drone'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export function useDroneControl () {
|
||||||
|
const droneControlPanelVisible = ref(false)
|
||||||
|
|
||||||
|
function setDroneControlPanelVisible (visible: boolean) {
|
||||||
|
droneControlPanelVisible.value = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flyToPoint (sn: string, body: PostFlyToPointBody) {
|
||||||
|
const { code } = await postFlyToPoint(sn, body)
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Fly to')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopFlyToPoint (sn: string) {
|
||||||
|
const { code } = await deleteFlyToPoint(sn)
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Stop fly to')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takeoffToPoint (sn: string, body: PostTakeoffToPointBody) {
|
||||||
|
const { code } = await postTakeoffToPoint(sn, body)
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Take off successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
droneControlPanelVisible,
|
||||||
|
setDroneControlPanelVisible,
|
||||||
|
flyToPoint,
|
||||||
|
stopFlyToPoint,
|
||||||
|
takeoffToPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/components/g-map/use-manual-control.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import {
|
||||||
|
ref,
|
||||||
|
onUnmounted,
|
||||||
|
watch,
|
||||||
|
Ref,
|
||||||
|
} from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
DRC_METHOD,
|
||||||
|
DroneControlProtocol,
|
||||||
|
} from '/@/types/drc'
|
||||||
|
import {
|
||||||
|
useMqtt,
|
||||||
|
DeviceTopicInfo
|
||||||
|
} from './use-mqtt'
|
||||||
|
|
||||||
|
let myInterval: any
|
||||||
|
|
||||||
|
export enum KeyCode {
|
||||||
|
KEY_W = 'KeyW',
|
||||||
|
KEY_A = 'KeyA',
|
||||||
|
KEY_S = 'KeyS',
|
||||||
|
KEY_D = 'KeyD',
|
||||||
|
KEY_Q = 'KeyQ',
|
||||||
|
KEY_E = 'KeyE',
|
||||||
|
ARROW_UP = 'ArrowUp',
|
||||||
|
ARROW_DOWN = 'ArrowDown',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useManualControl (deviceTopicInfo: DeviceTopicInfo, isCurrentFlightController: Ref<boolean>) {
|
||||||
|
const activeCodeKey = ref(null) as Ref<KeyCode | null>
|
||||||
|
const mqttHooks = useMqtt(deviceTopicInfo)
|
||||||
|
let seq = 0
|
||||||
|
function handlePublish (params: DroneControlProtocol) {
|
||||||
|
const body = {
|
||||||
|
method: DRC_METHOD.DRONE_CONTROL,
|
||||||
|
data: params,
|
||||||
|
}
|
||||||
|
handleClearInterval()
|
||||||
|
myInterval = setInterval(() => {
|
||||||
|
body.data.seq = seq++
|
||||||
|
seq++
|
||||||
|
window.console.log('keyCode>>>>', activeCodeKey.value, body)
|
||||||
|
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyup (keyCode: KeyCode) {
|
||||||
|
if (!deviceTopicInfo.pubTopic) {
|
||||||
|
console.error('请确保已经建立DRC链路')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const SPEED = 5 // check
|
||||||
|
const HEIGHT = 5 // check
|
||||||
|
const W_SPEED = 20 // 机头角速度
|
||||||
|
seq = 0
|
||||||
|
switch (keyCode) {
|
||||||
|
case 'KeyA':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ y: -SPEED })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'KeyW':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ x: SPEED })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'KeyS':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ x: -SPEED })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'KeyD':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ y: SPEED })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ h: HEIGHT })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'ArrowDown':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ h: -HEIGHT })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'KeyQ':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ w: -W_SPEED })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
case 'KeyE':
|
||||||
|
if (activeCodeKey.value === keyCode) return
|
||||||
|
handlePublish({ w: W_SPEED })
|
||||||
|
activeCodeKey.value = keyCode
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClearInterval () {
|
||||||
|
clearInterval(myInterval)
|
||||||
|
myInterval = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetControlState () {
|
||||||
|
activeCodeKey.value = null
|
||||||
|
seq = 0
|
||||||
|
handleClearInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyup () {
|
||||||
|
resetControlState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown (e: KeyboardEvent) {
|
||||||
|
handleKeyup(e.code as KeyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startKeyboardManualControl () {
|
||||||
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
window.addEventListener('keyup', onKeyup)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeKeyboardManualControl () {
|
||||||
|
resetControlState()
|
||||||
|
window.removeEventListener('keydown', onKeydown)
|
||||||
|
window.removeEventListener('keyup', onKeyup)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => isCurrentFlightController.value, (val) => {
|
||||||
|
if (val && deviceTopicInfo.pubTopic) {
|
||||||
|
startKeyboardManualControl()
|
||||||
|
} else {
|
||||||
|
closeKeyboardManualControl()
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
closeKeyboardManualControl()
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleEmergencyStop () {
|
||||||
|
if (!deviceTopicInfo.pubTopic) {
|
||||||
|
console.error('请确保已经建立DRC链路')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
method: DRC_METHOD.DRONE_EMERGENCY_STOP,
|
||||||
|
data: {}
|
||||||
|
}
|
||||||
|
resetControlState()
|
||||||
|
window.console.log('handleEmergencyStop>>>>', deviceTopicInfo.pubTopic, body)
|
||||||
|
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeCodeKey,
|
||||||
|
handleKeyup,
|
||||||
|
handleEmergencyStop,
|
||||||
|
resetControlState,
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/components/g-map/use-mqtt.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import {
|
||||||
|
ref,
|
||||||
|
reactive,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
onUnmounted,
|
||||||
|
} from 'vue'
|
||||||
|
import {
|
||||||
|
IClientPublishOptions,
|
||||||
|
IPublishPacket,
|
||||||
|
} from '/@/mqtt'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import {
|
||||||
|
DRC_METHOD,
|
||||||
|
} from '/@/types/drc'
|
||||||
|
import EventBus from '/@/event-bus'
|
||||||
|
|
||||||
|
export interface DeviceTopicInfo{
|
||||||
|
sn: string
|
||||||
|
pubTopic: string
|
||||||
|
subTopic: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageMqtt = (topic: string, payload: Buffer, packet: IPublishPacket) => void | Promise<void>
|
||||||
|
|
||||||
|
export function useMqtt (deviceTopicInfo: DeviceTopicInfo) {
|
||||||
|
let cacheSubscribeArr: {
|
||||||
|
topic: string;
|
||||||
|
callback?: MessageMqtt;
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
const store = useMyStore()
|
||||||
|
|
||||||
|
const mqttState = computed(() => {
|
||||||
|
return store.state.mqttState
|
||||||
|
})
|
||||||
|
|
||||||
|
function publishMqtt (topic: string, body: object, ots?: IClientPublishOptions) {
|
||||||
|
// const buffer = Buffer.from(JSON.stringify(body))
|
||||||
|
mqttState.value?.publishMqtt(topic, JSON.stringify(body), ots)
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeMqtt (topic: string, handleMessageMqtt?: MessageMqtt) {
|
||||||
|
mqttState.value?.subscribeMqtt(topic)
|
||||||
|
const handler = handleMessageMqtt || onMessageMqtt
|
||||||
|
mqttState.value?.on('onMessageMqtt', handler)
|
||||||
|
cacheSubscribeArr.push({
|
||||||
|
topic,
|
||||||
|
callback: handler,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessageMqtt (message: any) {
|
||||||
|
if (cacheSubscribeArr.findIndex(item => item.topic === message?.topic) !== -1) {
|
||||||
|
const payloadStr = new TextDecoder('utf-8').decode(message?.payload)
|
||||||
|
const payloadObj = JSON.parse(payloadStr)
|
||||||
|
switch (payloadObj?.method) {
|
||||||
|
case DRC_METHOD.HEART_BEAT:
|
||||||
|
break
|
||||||
|
case DRC_METHOD.DELAY_TIME_INFO_PUSH:
|
||||||
|
case DRC_METHOD.HSI_INFO_PUSH:
|
||||||
|
case DRC_METHOD.OSD_INFO_PUSH:
|
||||||
|
case DRC_METHOD.DRONE_CONTROL:
|
||||||
|
case DRC_METHOD.DRONE_EMERGENCY_STOP:
|
||||||
|
EventBus.emit('droneControlMqttInfo', payloadObj)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribeDrc () {
|
||||||
|
// 销毁已订阅事件
|
||||||
|
cacheSubscribeArr.forEach(item => {
|
||||||
|
mqttState.value?.off('onMessageMqtt', item.callback)
|
||||||
|
mqttState.value?.unsubscribeMqtt(item.topic)
|
||||||
|
})
|
||||||
|
cacheSubscribeArr = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心跳
|
||||||
|
const heartBeatSeq = ref(0)
|
||||||
|
const state = reactive({
|
||||||
|
heartState: new Map<string, {
|
||||||
|
pingInterval: any;
|
||||||
|
}>(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听云控控制权
|
||||||
|
watch(() => deviceTopicInfo, (val, oldVal) => {
|
||||||
|
if (val.subTopic !== '') {
|
||||||
|
// 1.订阅topic
|
||||||
|
subscribeMqtt(deviceTopicInfo.subTopic)
|
||||||
|
// 2.发心跳
|
||||||
|
publishDrcPing(deviceTopicInfo.sn)
|
||||||
|
} else {
|
||||||
|
clearInterval(state.heartState.get(deviceTopicInfo.sn)?.pingInterval)
|
||||||
|
state.heartState.delete(deviceTopicInfo.sn)
|
||||||
|
heartBeatSeq.value = 0
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
|
function publishDrcPing (sn: string) {
|
||||||
|
const body = {
|
||||||
|
method: DRC_METHOD.HEART_BEAT,
|
||||||
|
data: {
|
||||||
|
ts: new Date().getTime(),
|
||||||
|
seq: heartBeatSeq.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const pingInterval = setInterval(() => {
|
||||||
|
if (!mqttState.value) return
|
||||||
|
heartBeatSeq.value += 1
|
||||||
|
body.data.ts = new Date().getTime()
|
||||||
|
body.data.seq = heartBeatSeq.value
|
||||||
|
publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 })
|
||||||
|
}, 1000)
|
||||||
|
state.heartState.set(sn, {
|
||||||
|
pingInterval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unsubscribeDrc()
|
||||||
|
heartBeatSeq.value = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
mqttState,
|
||||||
|
publishMqtt,
|
||||||
|
subscribeMqtt,
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/components/g-map/use-payload-control.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
postPayloadAuth,
|
||||||
|
postPayloadCommands,
|
||||||
|
PayloadCommandsEnum,
|
||||||
|
PostCameraModeBody,
|
||||||
|
PostCameraFocalLengthBody,
|
||||||
|
PostGimbalResetBody,
|
||||||
|
PostCameraAimBody,
|
||||||
|
} from '/@/api/drone-control/payload'
|
||||||
|
import { ControlSource } from '/@/types/device'
|
||||||
|
|
||||||
|
export function usePayloadControl () {
|
||||||
|
function checkPayloadAuth (controlSource?: ControlSource) {
|
||||||
|
if (controlSource !== ControlSource.A) {
|
||||||
|
console.error('Get Payload Control first')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authPayload (sn: string, payloadIndx: string) {
|
||||||
|
const { code } = await postPayloadAuth(sn, {
|
||||||
|
payload_index: payloadIndx
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Get Payload Control successfully')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetGimbal (sn: string, data: PostGimbalResetBody) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.GimbalReset,
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Gimbal Reset successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchCameraMode (sn: string, data: PostCameraModeBody) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.CameraModeSwitch,
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Camera Mode Switch successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function takeCameraPhoto (sn: string, payloadIndx: string) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.CameraPhotoTake,
|
||||||
|
data: {
|
||||||
|
payload_index: payloadIndx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Take Photo successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCameraRecording (sn: string, payloadIndx: string) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStart,
|
||||||
|
data: {
|
||||||
|
payload_index: payloadIndx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Start Recording successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopCameraRecording (sn: string, payloadIndx: string) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStop,
|
||||||
|
data: {
|
||||||
|
payload_index: payloadIndx
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Stop Recording successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeCameraFocalLength (sn: string, data: PostCameraFocalLengthBody) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.CameraFocalLengthSet,
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Zoom successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cameraAim (sn: string, data: PostCameraAimBody) {
|
||||||
|
const { code } = await postPayloadCommands(sn, {
|
||||||
|
cmd: PayloadCommandsEnum.CameraAim,
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Zoom Aim successfully')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkPayloadAuth,
|
||||||
|
authPayload,
|
||||||
|
resetGimbal,
|
||||||
|
switchCameraMode,
|
||||||
|
takeCameraPhoto,
|
||||||
|
startCameraRecording,
|
||||||
|
stopCameraRecording,
|
||||||
|
changeCameraFocalLength,
|
||||||
|
cameraAim,
|
||||||
|
}
|
||||||
|
}
|
||||||
398
src/components/livestream-agora.vue
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-column flex-justify-start flex-align-center">
|
||||||
|
<div id="player" style="width: 720px; height: 420px; border: 1px solid"></div>
|
||||||
|
<p class="fz24">Live streaming source selection</p>
|
||||||
|
<div class="flex-row flex-justify-center flex-align-center mt10">
|
||||||
|
<template v-if="livePara.liveState && dronePara.isDockLive">
|
||||||
|
<span class="mr10">Lens:</span>
|
||||||
|
<a-radio-group v-model:value="dronePara.lensSelected" button-style="solid">
|
||||||
|
<a-radio-button v-for="lens in dronePara.lensList" :key="lens" :value="lens">{{lens}}</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-select
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Drone"
|
||||||
|
v-model:value="dronePara.droneSelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in dronePara.droneList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
@click="onDroneSelect(item)"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
<a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Camera"
|
||||||
|
v-model:value="dronePara.cameraSelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in dronePara.cameraList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
@click="onCameraSelect(item)"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
<!-- <a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Lens"
|
||||||
|
@select="onVideoSelect"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in dronePara.videoList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select> -->
|
||||||
|
</template>
|
||||||
|
<a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Clarity"
|
||||||
|
@select="onClaritySelect"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in clarityList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<p class="fz16 mt10">
|
||||||
|
Note: Obtain The Following Parameters From https://console.agora.io
|
||||||
|
</p>
|
||||||
|
<div class="flex-row flex-justify-center flex-align-center">
|
||||||
|
<span class="mr10">AppId:</span>
|
||||||
|
<a-input v-model:value="agoraPara.appid" placeholder="APP ID"></a-input>
|
||||||
|
<span class="ml10">Token:</span>
|
||||||
|
<a-input
|
||||||
|
class="ml10"
|
||||||
|
v-model:value="agoraPara.token"
|
||||||
|
placeholder="Token"
|
||||||
|
></a-input>
|
||||||
|
<span class="ml10">Channel:</span>
|
||||||
|
<a-input
|
||||||
|
class="ml10"
|
||||||
|
v-model:value="agoraPara.channel"
|
||||||
|
placeholder="Channel"
|
||||||
|
></a-input>
|
||||||
|
</div>
|
||||||
|
<div class="mt20 flex-row flex-justify-center flex-align-center">
|
||||||
|
<a-button v-if="livePara.liveState && dronePara.isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
|
||||||
|
<a-button v-else type="primary" large @click="onStart">Play</a-button>
|
||||||
|
<a-button class="ml20" type="primary" large @click="onStop"
|
||||||
|
>Stop</a-button
|
||||||
|
>
|
||||||
|
<a-button class="ml20" type="primary" large @click="onUpdateQuality"
|
||||||
|
>Update Clarity</a-button
|
||||||
|
>
|
||||||
|
<a-button v-if="!livePara.liveState || !dronePara.isDockLive" class="ml20" type="primary" large @click="onRefresh"
|
||||||
|
>Refresh Live Capacity</a-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import AgoraRTC, { IAgoraRTCClient, IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
import { uuidv4 } from '../utils/uuid'
|
||||||
|
import { CURRENT_CONFIG as config } from '/@/api/http/config'
|
||||||
|
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
|
||||||
|
const root = getRoot()
|
||||||
|
|
||||||
|
const clarityList = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: 'Adaptive'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: 'Smooth'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: 'Standard'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: 'HD'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: 'Super Clear'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
value: any,
|
||||||
|
label: string,
|
||||||
|
more?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
let agoraClient = {} as IAgoraRTCClient
|
||||||
|
const agoraPara = reactive({
|
||||||
|
appid: config.agoraAPPID,
|
||||||
|
token: config.agoraToken,
|
||||||
|
channel: config.agoraChannel,
|
||||||
|
uid: 123456,
|
||||||
|
stream: {}
|
||||||
|
})
|
||||||
|
const dronePara = reactive({
|
||||||
|
livestreamSource: [],
|
||||||
|
droneList: [] as SelectOption[],
|
||||||
|
cameraList: [] as SelectOption[],
|
||||||
|
videoList: [] as SelectOption[],
|
||||||
|
droneSelected: undefined as string | undefined,
|
||||||
|
cameraSelected: undefined as string | undefined,
|
||||||
|
videoSelected: undefined as string | undefined,
|
||||||
|
claritySelected: 0,
|
||||||
|
lensList: [] as string[],
|
||||||
|
lensSelected: undefined as string | undefined,
|
||||||
|
isDockLive: false
|
||||||
|
})
|
||||||
|
const livePara = reactive({
|
||||||
|
url: '',
|
||||||
|
webrtc: {} as any,
|
||||||
|
videoId: '',
|
||||||
|
liveState: false
|
||||||
|
})
|
||||||
|
const nonSwitchable = 'normal'
|
||||||
|
const onRefresh = async () => {
|
||||||
|
dronePara.droneList = []
|
||||||
|
dronePara.cameraList = []
|
||||||
|
dronePara.videoList = []
|
||||||
|
dronePara.droneSelected = undefined
|
||||||
|
dronePara.cameraSelected = undefined
|
||||||
|
dronePara.videoSelected = undefined
|
||||||
|
await getLiveCapacity({})
|
||||||
|
.then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
if (res.data === null) {
|
||||||
|
console.warn('warning: get live capacity is null!!!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dronePara.livestreamSource = res.data
|
||||||
|
dronePara.droneList = []
|
||||||
|
|
||||||
|
console.log('live_capacity:', dronePara.livestreamSource)
|
||||||
|
|
||||||
|
if (dronePara.livestreamSource) {
|
||||||
|
dronePara.livestreamSource.forEach((ele: any) => {
|
||||||
|
dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onRefresh()
|
||||||
|
agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'vp8' })
|
||||||
|
agoraClient.setClientRole('audience', { level: 2 })
|
||||||
|
if (agoraClient.connectionState === 'DISCONNECTED') {
|
||||||
|
agoraClient.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
|
||||||
|
}
|
||||||
|
// Subscribe when a remote user publishes a stream
|
||||||
|
agoraClient.on('user-joined', async (user: IAgoraRTCRemoteUser) => {
|
||||||
|
message.info('user[' + user.uid + '] join')
|
||||||
|
})
|
||||||
|
agoraClient.on('user-published', async (user: IAgoraRTCRemoteUser, mediaType: 'audio' | 'video') => {
|
||||||
|
await agoraClient.subscribe(user, mediaType)
|
||||||
|
if (mediaType === 'video') {
|
||||||
|
console.log('subscribe success')
|
||||||
|
// Get `RemoteVideoTrack` in the `user` object.
|
||||||
|
const remoteVideoTrack = user.videoTrack!
|
||||||
|
// Dynamically create a container in the form of a DIV element for playing the remote video track.
|
||||||
|
remoteVideoTrack.play(document.getElementById('player') as HTMLElement)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
agoraClient.on('user-unpublished', async (user: any) => {
|
||||||
|
console.log('unpublish live:', user)
|
||||||
|
message.info('unpublish live')
|
||||||
|
})
|
||||||
|
agoraClient.on('exception', async (e: any) => {
|
||||||
|
console.log(e)
|
||||||
|
console.error(e.msg)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const handleError = (err: any) => {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
const handleJoinChannel = (uid: any) => {
|
||||||
|
agoraPara.uid = uid
|
||||||
|
}
|
||||||
|
const onStart = async () => {
|
||||||
|
const that = this
|
||||||
|
console.log(
|
||||||
|
'drone parameter:',
|
||||||
|
dronePara.droneSelected,
|
||||||
|
dronePara.cameraSelected,
|
||||||
|
dronePara.videoSelected,
|
||||||
|
dronePara.claritySelected
|
||||||
|
)
|
||||||
|
const timestamp = new Date().getTime().toString()
|
||||||
|
const liveTimestamp = timestamp
|
||||||
|
if (
|
||||||
|
dronePara.droneSelected == null ||
|
||||||
|
dronePara.cameraSelected == null ||
|
||||||
|
dronePara.claritySelected == null
|
||||||
|
) {
|
||||||
|
message.warn('waring: not select live para!!!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
agoraClient.setClientRole('audience', { level: 2 })
|
||||||
|
if (agoraClient.connectionState === 'DISCONNECTED') {
|
||||||
|
await agoraClient.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
|
||||||
|
}
|
||||||
|
livePara.videoId =
|
||||||
|
dronePara.droneSelected +
|
||||||
|
'/' +
|
||||||
|
dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
|
||||||
|
console.log(agoraPara)
|
||||||
|
|
||||||
|
livePara.url =
|
||||||
|
'channel=' +
|
||||||
|
agoraPara.channel +
|
||||||
|
'&sn=' +
|
||||||
|
dronePara.droneSelected +
|
||||||
|
'&token=' +
|
||||||
|
encodeURIComponent(agoraPara.token) +
|
||||||
|
'&uid=' +
|
||||||
|
agoraPara.uid
|
||||||
|
|
||||||
|
startLivestream({
|
||||||
|
url: livePara.url,
|
||||||
|
video_id: livePara.videoId,
|
||||||
|
url_type: 0,
|
||||||
|
video_quality: dronePara.claritySelected
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.code !== 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
livePara.liveState = true
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onStop = async () => {
|
||||||
|
if (
|
||||||
|
dronePara.droneSelected == null ||
|
||||||
|
dronePara.cameraSelected == null ||
|
||||||
|
dronePara.claritySelected == null
|
||||||
|
) {
|
||||||
|
message.warn('waring: not select live para!!!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
livePara.videoId =
|
||||||
|
dronePara.droneSelected +
|
||||||
|
'/' +
|
||||||
|
dronePara.cameraSelected + '/' + (dronePara.videoSelected || nonSwitchable + '-0')
|
||||||
|
|
||||||
|
stopLivestream({
|
||||||
|
video_id: livePara.videoId
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success(res.message)
|
||||||
|
livePara.liveState = false
|
||||||
|
dronePara.lensSelected = ''
|
||||||
|
console.log('stop play livestream')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onDroneSelect = (val: SelectOption) => {
|
||||||
|
dronePara.cameraList = []
|
||||||
|
dronePara.videoList = []
|
||||||
|
dronePara.lensList = []
|
||||||
|
|
||||||
|
dronePara.cameraSelected = undefined
|
||||||
|
dronePara.videoSelected = undefined
|
||||||
|
dronePara.lensSelected = undefined
|
||||||
|
dronePara.droneSelected = val.value
|
||||||
|
if (!val.more) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val.more.forEach((ele: any) => {
|
||||||
|
dronePara.cameraList.push({ label: ele.name, value: ele.index, more: ele.videos_list })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onCameraSelect = (val: SelectOption) => {
|
||||||
|
dronePara.cameraSelected = val.value
|
||||||
|
dronePara.videoSelected = undefined
|
||||||
|
dronePara.lensSelected = undefined
|
||||||
|
dronePara.videoList = []
|
||||||
|
dronePara.lensList = []
|
||||||
|
if (!val.more) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val.more.forEach((ele: any) => {
|
||||||
|
dronePara.videoList.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
|
||||||
|
})
|
||||||
|
if (dronePara.videoList.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const firstVideo: SelectOption = dronePara.videoList[0]
|
||||||
|
dronePara.videoSelected = firstVideo.value
|
||||||
|
dronePara.lensList = firstVideo.more
|
||||||
|
dronePara.lensSelected = firstVideo.label
|
||||||
|
dronePara.isDockLive = dronePara.lensList?.length > 0
|
||||||
|
}
|
||||||
|
const onVideoSelect = (val: SelectOption) => {
|
||||||
|
dronePara.videoSelected = val.value
|
||||||
|
dronePara.lensList = val.more
|
||||||
|
dronePara.lensSelected = val.label
|
||||||
|
}
|
||||||
|
const onClaritySelect = (val: any) => {
|
||||||
|
dronePara.claritySelected = val
|
||||||
|
}
|
||||||
|
const onUpdateQuality = () => {
|
||||||
|
if (!livePara.liveState) {
|
||||||
|
message.info('Please turn on the livestream first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLivestreamQuality({
|
||||||
|
video_id: livePara.videoId,
|
||||||
|
video_quality: dronePara.claritySelected
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('Set the clarity to ' + clarityList[dronePara.claritySelected].label)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSwitch = () => {
|
||||||
|
if (dronePara.lensSelected === undefined || dronePara.lensSelected === nonSwitchable) {
|
||||||
|
message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changeLivestreamLens({
|
||||||
|
video_id: livePara.videoId,
|
||||||
|
video_type: dronePara.lensSelected
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('Switching live camera successfully.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '/@/styles/index.scss';
|
||||||
|
</style>
|
||||||
441
src/components/livestream-others.vue
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-column flex-justify-start flex-align-center">
|
||||||
|
<video
|
||||||
|
:style="{ width: '720px', height: '480px' }"
|
||||||
|
id="video-webrtc"
|
||||||
|
ref="videowebrtc"
|
||||||
|
controls
|
||||||
|
autoplay
|
||||||
|
class="mt20"
|
||||||
|
></video>
|
||||||
|
<p class="fz24">Live streaming source selection</p>
|
||||||
|
|
||||||
|
<div class="flex-row flex-justify-center flex-align-center mt10">
|
||||||
|
<template v-if="liveState && isDockLive">
|
||||||
|
<span class="mr10">Lens:</span>
|
||||||
|
<a-radio-group v-model:value="lensSelected" button-style="solid">
|
||||||
|
<a-radio-button v-for="lens in lensList" :key="lens" :value="lens">{{lens}}</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-select
|
||||||
|
style="width: 150px"
|
||||||
|
placeholder="Select Live Type"
|
||||||
|
@select="onLiveTypeSelect"
|
||||||
|
v-model:value="livetypeSelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in liveTypeList"
|
||||||
|
:key="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Drone"
|
||||||
|
v-model:value="droneSelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in droneList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
@click="onDroneSelect(item)"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
<a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Camera"
|
||||||
|
v-model:value="cameraSelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in cameraList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
@click="onCameraSelect(item)"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
<!-- <a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Lens"
|
||||||
|
v-model:value="videoSelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in videoList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
@click="onVideoSelect(item)"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select> -->
|
||||||
|
</template>
|
||||||
|
<a-select
|
||||||
|
class="ml10"
|
||||||
|
style="width:150px"
|
||||||
|
placeholder="Select Clarity"
|
||||||
|
@select="onClaritySelect"
|
||||||
|
v-model:value="claritySelected"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="item in clarityList"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>{{ item.label }}</a-select-option
|
||||||
|
>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="mt20">
|
||||||
|
<p class="fz10" v-if="livetypeSelected == 2">
|
||||||
|
Please use VLC media player to play the RTSP livestream !!!
|
||||||
|
</p>
|
||||||
|
<p class="fz10" v-if="livetypeSelected == 2">
|
||||||
|
RTSP Parameter:{{ rtspData }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt10 flex-row flex-justify-center flex-align-center">
|
||||||
|
<a-button v-if="liveState && isDockLive" type="primary" large @click="onSwitch">Switch Lens</a-button>
|
||||||
|
<a-button v-else type="primary" large @click="onStart">Play</a-button>
|
||||||
|
<a-button class="ml20" type="primary" large @click="onStop"
|
||||||
|
>Stop</a-button
|
||||||
|
>
|
||||||
|
<a-button class="ml20" type="primary" large @click="onUpdateQuality"
|
||||||
|
>Update Clarity</a-button
|
||||||
|
>
|
||||||
|
<a-button v-if="!liveState || !isDockLive" class="ml20" type="primary" large @click="onRefresh"
|
||||||
|
>Refresh Live Capacity</a-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { CURRENT_CONFIG as config } from '/@/api/http/config'
|
||||||
|
import { changeLivestreamLens, getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
import jswebrtc from '/@/vendors/jswebrtc.min.js'
|
||||||
|
import srs from '/@/vendors/srs.sdk.js'
|
||||||
|
|
||||||
|
const root = getRoot()
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
value: any,
|
||||||
|
label: string,
|
||||||
|
more?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveTypeList: SelectOption[] = [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: 'RTMP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: 'RTSP'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: 'GB28181'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: 'WEBRTC'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const clarityList: SelectOption[] = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: 'Adaptive'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: 'Smooth'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: 'Standard'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: 'HD'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: 'Super Clear'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const videowebrtc = ref(null)
|
||||||
|
const livestreamSource = ref()
|
||||||
|
const droneList = ref()
|
||||||
|
const cameraList = ref()
|
||||||
|
const videoList = ref()
|
||||||
|
const droneSelected = ref()
|
||||||
|
const cameraSelected = ref()
|
||||||
|
const videoSelected = ref()
|
||||||
|
const claritySelected = ref()
|
||||||
|
const videoId = ref()
|
||||||
|
const liveState = ref<boolean>(false)
|
||||||
|
const livetypeSelected = ref()
|
||||||
|
const rtspData = ref()
|
||||||
|
const lensList = ref<string[]>([])
|
||||||
|
const lensSelected = ref<String>()
|
||||||
|
const isDockLive = ref(false)
|
||||||
|
const nonSwitchable = 'normal'
|
||||||
|
let webrtc: any = null
|
||||||
|
|
||||||
|
const onRefresh = async () => {
|
||||||
|
droneList.value = []
|
||||||
|
cameraList.value = []
|
||||||
|
videoList.value = []
|
||||||
|
droneSelected.value = null
|
||||||
|
cameraSelected.value = null
|
||||||
|
videoSelected.value = null
|
||||||
|
await getLiveCapacity({})
|
||||||
|
.then(res => {
|
||||||
|
console.log(res)
|
||||||
|
if (res.code === 1) {
|
||||||
|
if (res.data === null) {
|
||||||
|
console.warn('warning: get live capacity is null!!!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const resData: Array<[]> = res.data
|
||||||
|
console.log('live_capacity:', resData)
|
||||||
|
livestreamSource.value = resData
|
||||||
|
|
||||||
|
const temp: Array<SelectOption> = []
|
||||||
|
if (livestreamSource.value) {
|
||||||
|
livestreamSource.value.forEach((ele: any) => {
|
||||||
|
temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })
|
||||||
|
})
|
||||||
|
droneList.value = temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
console.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onRefresh()
|
||||||
|
})
|
||||||
|
const onStart = async () => {
|
||||||
|
console.log(
|
||||||
|
'Param:',
|
||||||
|
livetypeSelected.value,
|
||||||
|
droneSelected.value,
|
||||||
|
cameraSelected.value,
|
||||||
|
videoSelected.value,
|
||||||
|
claritySelected.value
|
||||||
|
)
|
||||||
|
const timestamp = new Date().getTime().toString()
|
||||||
|
if (
|
||||||
|
livetypeSelected.value == null ||
|
||||||
|
droneSelected.value == null ||
|
||||||
|
cameraSelected.value == null ||
|
||||||
|
claritySelected.value == null
|
||||||
|
) {
|
||||||
|
message.warn('waring: not select live para!!!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
videoId.value =
|
||||||
|
droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
|
||||||
|
|
||||||
|
let liveURL = ''
|
||||||
|
switch (livetypeSelected.value) {
|
||||||
|
case 1: {
|
||||||
|
// RTMP
|
||||||
|
liveURL = config.rtmpURL + timestamp
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
// RTSP
|
||||||
|
liveURL = `userName=${config.rtspUserName}&password=${config.rtspPassword}&port=${config.rtspPort}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.warn('warning: live type is not correct!!!')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
await startLivestream({
|
||||||
|
url: liveURL,
|
||||||
|
video_id: videoId.value,
|
||||||
|
url_type: livetypeSelected.value,
|
||||||
|
video_quality: claritySelected.value
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.code !== 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (livetypeSelected.value === 3) {
|
||||||
|
const url = res.data.url
|
||||||
|
const videoElement = videowebrtc.value
|
||||||
|
// gb28181,it will fail if not wait.
|
||||||
|
message.loading({
|
||||||
|
content: 'Loding...',
|
||||||
|
duration: 4,
|
||||||
|
onClose () {
|
||||||
|
const player = new jswebrtc.Player(url, {
|
||||||
|
video: videoElement,
|
||||||
|
autoplay: true,
|
||||||
|
onPlay: (obj: any) => {
|
||||||
|
console.log('start play livestream')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (livetypeSelected.value === 2) {
|
||||||
|
console.log(res)
|
||||||
|
rtspData.value = 'url:' + res.data.url
|
||||||
|
} else if (livetypeSelected.value === 1) {
|
||||||
|
const url = res.data.url
|
||||||
|
const videoElement = videowebrtc.value
|
||||||
|
console.log('start live:', url)
|
||||||
|
console.log(videoElement)
|
||||||
|
const player = new jswebrtc.Player(url, {
|
||||||
|
video: videoElement,
|
||||||
|
autoplay: true,
|
||||||
|
onPlay: (obj: any) => {
|
||||||
|
console.log('start play livestream')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (livetypeSelected.value === 4) {
|
||||||
|
const videoElement = videowebrtc.value as unknown as HTMLMediaElement
|
||||||
|
videoElement.muted = true
|
||||||
|
playWebrtc(videoElement, res.data.url)
|
||||||
|
}
|
||||||
|
liveState.value = true
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onStop = () => {
|
||||||
|
videoId.value =
|
||||||
|
droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')
|
||||||
|
|
||||||
|
stopLivestream({
|
||||||
|
video_id: videoId.value
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success(res.message)
|
||||||
|
liveState.value = false
|
||||||
|
lensSelected.value = undefined
|
||||||
|
console.log('stop play livestream')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateQuality = () => {
|
||||||
|
if (!liveState.value) {
|
||||||
|
message.info('Please turn on the livestream first.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLivestreamQuality({
|
||||||
|
video_id: videoId.value,
|
||||||
|
video_quality: claritySelected.value
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('Set the clarity to ' + clarityList[claritySelected.value].label)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLiveTypeSelect = (val: any) => {
|
||||||
|
livetypeSelected.value = val
|
||||||
|
}
|
||||||
|
const onDroneSelect = (val: SelectOption) => {
|
||||||
|
droneSelected.value = val.value
|
||||||
|
const temp: Array<SelectOption> = []
|
||||||
|
cameraList.value = []
|
||||||
|
cameraSelected.value = undefined
|
||||||
|
videoSelected.value = undefined
|
||||||
|
videoList.value = []
|
||||||
|
lensList.value = []
|
||||||
|
if (!val.more) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val.more.forEach((ele: any) => {
|
||||||
|
temp.push({ label: ele.name, value: ele.index, more: ele.videos_list })
|
||||||
|
})
|
||||||
|
cameraList.value = temp
|
||||||
|
}
|
||||||
|
const onCameraSelect = (val: SelectOption) => {
|
||||||
|
cameraSelected.value = val.value
|
||||||
|
const result: Array<SelectOption> = []
|
||||||
|
videoSelected.value = undefined
|
||||||
|
videoList.value = []
|
||||||
|
lensList.value = []
|
||||||
|
if (!val.more) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val.more.forEach((ele: any) => {
|
||||||
|
result.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })
|
||||||
|
})
|
||||||
|
videoList.value = result
|
||||||
|
if (videoList.value.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const firstVideo: SelectOption = videoList.value[0]
|
||||||
|
videoSelected.value = firstVideo.value
|
||||||
|
lensList.value = firstVideo.more
|
||||||
|
lensSelected.value = firstVideo.label
|
||||||
|
isDockLive.value = lensList.value?.length > 0
|
||||||
|
}
|
||||||
|
const onVideoSelect = (val: SelectOption) => {
|
||||||
|
videoSelected.value = val.value
|
||||||
|
lensList.value = val.more
|
||||||
|
lensSelected.value = val.label
|
||||||
|
}
|
||||||
|
const onClaritySelect = (val: any) => {
|
||||||
|
claritySelected.value = val
|
||||||
|
}
|
||||||
|
const onSwitch = () => {
|
||||||
|
if (lensSelected.value === undefined || lensSelected.value === nonSwitchable) {
|
||||||
|
message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changeLivestreamLens({
|
||||||
|
video_id: videoId.value,
|
||||||
|
video_type: lensSelected.value
|
||||||
|
}).then(res => {
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('Switching live camera successfully.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const playWebrtc = (videoElement: HTMLMediaElement, url: string) => {
|
||||||
|
if (webrtc) {
|
||||||
|
webrtc.close()
|
||||||
|
}
|
||||||
|
webrtc = new srs.SrsRtcWhipWhepAsync()
|
||||||
|
videoElement.srcObject = webrtc.stream
|
||||||
|
webrtc.play(url).then(function (session: any) {
|
||||||
|
console.info(session)
|
||||||
|
}).catch(function (reason: any) {
|
||||||
|
webrtc.close()
|
||||||
|
console.error(reason)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '/@/styles/index.scss';
|
||||||
|
</style>
|
||||||
42
src/components/svgIcon.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<svg :class="svgClass" :aria-hidden="true" :style="{color: color, width:computedWidth, height:computedWidth}">
|
||||||
|
<use :xlink:href="iconName" :fill="color"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, computed } from 'vue'
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const iconName = computed(() => `#icon-${props.name}`)
|
||||||
|
const svgClass = computed(() => {
|
||||||
|
console.log(props.name, 'props.name')
|
||||||
|
if (props.name) {
|
||||||
|
return `svg-icon icon-${props.name}`
|
||||||
|
}
|
||||||
|
return 'svg-icon'
|
||||||
|
})
|
||||||
|
const computedWidth = computed(() => {
|
||||||
|
const result = props.width || props.size
|
||||||
|
return result ? result + 'px' : '1em'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang='scss'>
|
||||||
|
.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
450
src/components/task/CreatePlan.vue
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
<template>
|
||||||
|
<div class="create-plan-wrapper">
|
||||||
|
<div class="header">
|
||||||
|
Create Plan
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<a-form ref="valueRef" layout="horizontal" :hideRequiredMark="true" :rules="rules" :model="planBody" labelAlign="left">
|
||||||
|
<a-form-item label="Plan Name" name="name" :labelCol="{span: 23}">
|
||||||
|
<a-input style="background: black;" placeholder="Please enter plan name" v-model:value="planBody.name"/>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- 航线 -->
|
||||||
|
<a-form-item label="Flight Route" :wrapperCol="{offset: 7}" name="file_id">
|
||||||
|
<router-link
|
||||||
|
:to="{name: 'select-plan'}"
|
||||||
|
@click="selectRoute"
|
||||||
|
>
|
||||||
|
Select Route
|
||||||
|
</router-link>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="planBody.file_id" style="margin-top: -15px;">
|
||||||
|
<div class="wayline-panel" style="padding-top: 5px;">
|
||||||
|
<div class="title">
|
||||||
|
<a-tooltip :title="wayline.name">
|
||||||
|
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
<div class="ml10"><UserOutlined /></div>
|
||||||
|
<a-tooltip :title="wayline.user_name">
|
||||||
|
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
|
||||||
|
<span><RocketOutlined /></span>
|
||||||
|
<span class="ml5">{{ DEVICE_NAME[wayline.drone_model_key] }}</span>
|
||||||
|
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span>
|
||||||
|
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id">
|
||||||
|
{{ DEVICE_NAME[payload] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);">
|
||||||
|
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- 设备 -->
|
||||||
|
<a-form-item label="Device" :wrapperCol="{offset: 10}" v-model:value="planBody.dock_sn" name="dock_sn">
|
||||||
|
<router-link
|
||||||
|
:to="{name: 'select-plan'}"
|
||||||
|
@click="selectDevice"
|
||||||
|
>Select Device</router-link>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;">
|
||||||
|
<div class="panel" style="padding-top: 5px;">
|
||||||
|
<div class="title">
|
||||||
|
<a-tooltip :title="dock.nickname">
|
||||||
|
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);">
|
||||||
|
<span><RocketOutlined /></span>
|
||||||
|
<span class="ml5">{{ dock.children?.nickname ?? 'No drone' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- 任务类型 -->
|
||||||
|
<a-form-item label="Plan Timer" class="plan-timer-form-item">
|
||||||
|
<div style="white-space: nowrap;">
|
||||||
|
<a-radio-group v-model:value="planBody.task_type" button-style="solid">
|
||||||
|
<a-radio-button v-for="type in TaskTypeOptions" :value="type.value" :key="type.value">{{ type.label }}</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- execute date -->
|
||||||
|
<a-form-item label="Date" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition" name="select_execute_date" :labelCol="{span: 23}">
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="planBody.select_execute_date"
|
||||||
|
:disabledDate="(current: Moment) => current < moment().subtract(1, 'days')"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
:placeholder="['Start Time', 'End Time']"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- execute time -->
|
||||||
|
<a-form-item label="Time" v-if="planBody.task_type === TaskType.Timed || planBody.task_type === TaskType.Condition"
|
||||||
|
name="select_execute_time" ref="select_execute_time" :labelCol="{span: 23}" :autoLink="false">
|
||||||
|
<div class="mb10 flex-row flex-align-center flex-justify-around" v-for="n in planBody.select_time_number" :key="n">
|
||||||
|
<a-time-picker
|
||||||
|
v-model:value="planBody.select_time[n - 1][0]"
|
||||||
|
format="HH:mm:ss"
|
||||||
|
show-time
|
||||||
|
placeholder="Start Time"
|
||||||
|
:style="planBody.task_type === TaskType.Condition ? 'width: 40%' : 'width: 82%'"
|
||||||
|
@change="() => $refs.select_execute_time.onFieldChange()"
|
||||||
|
/>
|
||||||
|
<template v-if="planBody.task_type === TaskType.Condition">
|
||||||
|
<div><span style="color: white;">-</span></div>
|
||||||
|
<a-time-picker
|
||||||
|
v-model:value="planBody.select_time[n - 1][1]"
|
||||||
|
format="HH:mm:ss"
|
||||||
|
show-time
|
||||||
|
placeholder="End Time"
|
||||||
|
style="width: 40%;"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div class="ml5" style="font-size:18px">
|
||||||
|
<PlusCircleOutlined class="mr5" style="color: #1890ff" @click="addTime"/>
|
||||||
|
<MinusCircleOutlined :style="planBody.select_time_number === 1 ? 'color: gray' : 'color: red;'" @click="removeTime"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<template v-if="planBody.task_type === TaskType.Condition">
|
||||||
|
<!-- battery capacity -->
|
||||||
|
<a-form-item label="Start task when battery level reaches" :labelCol="{span: 23}" name="min_battery_capacity">
|
||||||
|
<a-input-number class="width-100" v-model:value="planBody.min_battery_capacity" :min="50" :max="100"
|
||||||
|
:formatter="(value: number) => `${value}%`" :parser="(value: string) => value.replace('%', '')">
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- storage capacity -->
|
||||||
|
<a-form-item label="Start task when storage level reaches (MB)" :labelCol="{span: 23}" name="storage_capacity">
|
||||||
|
<a-input-number v-model:value="planBody.min_storage_capacity" class="width-100">
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
|
<!-- RTH Altitude Relative to Dock -->
|
||||||
|
<a-form-item label="RTH Altitude Relative to Dock (m)" :labelCol="{span: 23}" name="rth_altitude">
|
||||||
|
<a-input-number v-model:value="planBody.rth_altitude" :min="20" :max="1500" class="width-100" required>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<!-- Lost Action -->
|
||||||
|
<a-form-item label="Lost Action" :labelCol="{span: 23}" name="out_of_control_action">
|
||||||
|
<div style="white-space: nowrap;">
|
||||||
|
<a-radio-group v-model:value="planBody.out_of_control_action" button-style="solid">
|
||||||
|
<a-radio-button v-for="action in OutOfControlActionOptions" :value="action.value" :key="action.value">
|
||||||
|
{{ action.label }}
|
||||||
|
</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item class="width-100" style="margin-bottom: 40px;">
|
||||||
|
<div class="footer">
|
||||||
|
<a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="onSubmit" :disabled="disabled">OK
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="drawerVisible" style="position: absolute; left: 335px; width: 280px; height: 100vh; float: right; top: 0; z-index: 1000; color: white; background: #282828;">
|
||||||
|
<div>
|
||||||
|
<router-view :name="routeName"/>
|
||||||
|
</div>
|
||||||
|
<div style="position: absolute; top: 15px; right: 10px;">
|
||||||
|
<a style="color: white;" @click="closePanel"><CloseOutlined /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, UnwrapRef } from 'vue'
|
||||||
|
import { CloseOutlined, RocketOutlined, CameraFilled, UserOutlined, PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { ELocalStorageKey, ERouterName } from '/@/types'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { WaylineType, WaylineFile } from '/@/types/wayline'
|
||||||
|
import { Device, DEVICE_NAME } from '/@/types/device'
|
||||||
|
import { createPlan, CreatePlan } from '/@/api/wayline'
|
||||||
|
import { getRoot } from '/@/root'
|
||||||
|
import { TaskType, OutOfControlActionOptions, OutOfControlAction, TaskTypeOptions } from '/@/types/task'
|
||||||
|
import moment, { Moment } from 'moment'
|
||||||
|
import { RuleObject } from 'ant-design-vue/es/form/interface'
|
||||||
|
|
||||||
|
const root = getRoot()
|
||||||
|
const store = useMyStore()
|
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
|
||||||
|
|
||||||
|
const wayline = computed<WaylineFile>(() => {
|
||||||
|
return store.state.waylineInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
const dock = computed<Device>(() => {
|
||||||
|
return store.state.dockInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
const disabled = ref(false)
|
||||||
|
|
||||||
|
const routeName = ref('')
|
||||||
|
const planBody = reactive({
|
||||||
|
name: '',
|
||||||
|
file_id: computed(() => store.state?.waylineInfo.id),
|
||||||
|
dock_sn: computed(() => store.state?.dockInfo.device_sn),
|
||||||
|
task_type: TaskType.Immediate,
|
||||||
|
select_execute_date: [moment(), moment()] as Moment[],
|
||||||
|
select_time_number: 1,
|
||||||
|
select_time: [[]] as Moment[][],
|
||||||
|
rth_altitude: '',
|
||||||
|
out_of_control_action: OutOfControlAction.ReturnToHome,
|
||||||
|
min_battery_capacity: 90 as number,
|
||||||
|
min_storage_capacity: undefined as number | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
const drawerVisible = ref(false)
|
||||||
|
const valueRef = ref()
|
||||||
|
const rules = {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: 'Please enter plan name.' },
|
||||||
|
{ max: 20, message: 'Length should be 1 to 20' }
|
||||||
|
],
|
||||||
|
file_id: [{ required: true, message: 'Select Route' }],
|
||||||
|
dock_sn: [{ required: true, message: 'Select Device' }],
|
||||||
|
select_execute_time: [{
|
||||||
|
validator: async (rule: RuleObject, value: Moment[]) => {
|
||||||
|
validEndTime()
|
||||||
|
validStartTime()
|
||||||
|
if (planBody.select_time.length < planBody.select_time_number) {
|
||||||
|
throw new Error('Select time')
|
||||||
|
}
|
||||||
|
validOverlapped()
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
select_execute_date: [{ required: true, message: 'Select date' }],
|
||||||
|
rth_altitude: [
|
||||||
|
{
|
||||||
|
validator: async (rule: RuleObject, value: string) => {
|
||||||
|
if (!/^[0-9]{1,}$/.test(value)) {
|
||||||
|
throw new Error('RTH Altitude Require number')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
min_battery_capacity: [
|
||||||
|
{
|
||||||
|
validator: async (rule: RuleObject, value: any) => {
|
||||||
|
if (TaskType.Condition === planBody.task_type && !value) {
|
||||||
|
throw new Error('Please enter battery capacity')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
out_of_control_action: [{ required: true, message: 'Select Lost Action' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function validStartTime (): Error | void {
|
||||||
|
for (let i = 0; i < planBody.select_time.length; i++) {
|
||||||
|
if (!planBody.select_time[i][0]) {
|
||||||
|
throw new Error('Select start time')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validEndTime (): Error | void {
|
||||||
|
if (TaskType.Condition !== planBody.task_type) return
|
||||||
|
for (let i = 0; i < planBody.select_time.length; i++) {
|
||||||
|
if (!planBody.select_time[i][1]) {
|
||||||
|
throw new Error('Select end time')
|
||||||
|
}
|
||||||
|
if (planBody.select_time[i][0] && planBody.select_time[i][1].isSameOrBefore(planBody.select_time[i][0])) {
|
||||||
|
throw new Error('End time should be later than start time')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validOverlapped (): Error | void {
|
||||||
|
if (TaskType.Condition !== planBody.task_type) return
|
||||||
|
const arr = planBody.select_time.slice()
|
||||||
|
arr.sort((a, b) => a[0].unix() - b[0].unix())
|
||||||
|
arr.forEach((v, i, arr) => {
|
||||||
|
if (i > 0 && v[0] < arr[i - 1][1]) {
|
||||||
|
throw new Error('Overlapping time periods.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit () {
|
||||||
|
console.info(dock, '12131231')
|
||||||
|
valueRef.value.validate().then(() => {
|
||||||
|
disabled.value = true
|
||||||
|
const createPlanBody = { ...planBody } as unknown as CreatePlan
|
||||||
|
if (planBody.select_execute_date.length === 2) {
|
||||||
|
createPlanBody.task_days = []
|
||||||
|
for (let i = planBody.select_execute_date[0]; i.isSameOrBefore(planBody.select_execute_date[1]); i.add(1, 'days')) {
|
||||||
|
createPlanBody.task_days.push(i.unix())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createPlanBody.task_periods = []
|
||||||
|
if (TaskType.Immediate !== planBody.task_type) {
|
||||||
|
for (let i = 0; i < planBody.select_time.length; i++) {
|
||||||
|
const result = []
|
||||||
|
result.push(planBody.select_time[i][0].unix())
|
||||||
|
if (TaskType.Condition === planBody.task_type) {
|
||||||
|
result.push(planBody.select_time[i][1].unix())
|
||||||
|
}
|
||||||
|
createPlanBody.task_periods.push(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createPlanBody.rth_altitude = Number(createPlanBody.rth_altitude)
|
||||||
|
if (wayline.value && wayline.value.template_types && wayline.value.template_types.length > 0) {
|
||||||
|
createPlanBody.wayline_type = wayline.value.template_types[0]
|
||||||
|
}
|
||||||
|
createPlan(workspaceId, createPlanBody)
|
||||||
|
.then(res => {
|
||||||
|
disabled.value = false
|
||||||
|
}).finally(() => {
|
||||||
|
closePlan()
|
||||||
|
})
|
||||||
|
}).catch((e: any) => {
|
||||||
|
console.log('validate err', e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePlan () {
|
||||||
|
root.$router.push('/' + ERouterName.TASK)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel () {
|
||||||
|
drawerVisible.value = false
|
||||||
|
routeName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRoute () {
|
||||||
|
drawerVisible.value = true
|
||||||
|
routeName.value = 'WaylinePanel'
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDevice () {
|
||||||
|
drawerVisible.value = true
|
||||||
|
routeName.value = 'DockPanel'
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTime () {
|
||||||
|
valueRef.value.validateFields(['select_execute_time']).then(() => {
|
||||||
|
planBody.select_time_number++
|
||||||
|
planBody.select_time.push([])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function removeTime () {
|
||||||
|
if (planBody.select_time_number === 1) return
|
||||||
|
planBody.select_time_number--
|
||||||
|
planBody.select_time.splice(planBody.select_time_number)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.create-plan-wrapper {
|
||||||
|
background-color: #232323;
|
||||||
|
color: fff;
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 285px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 52px;
|
||||||
|
border-bottom: 1px solid #4f4f4f;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
padding-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: calc(100% - 54px);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label, input, .ant-input, .ant-calendar-range-picker-separator,
|
||||||
|
.ant-input:hover, .ant-time-picker .anticon, .ant-calendar-picker .anticon {
|
||||||
|
background-color: #232323;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-suffix {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-timer-form-item {
|
||||||
|
|
||||||
|
.ant-radio-button-wrapper{
|
||||||
|
background-color: #232323;
|
||||||
|
color: #fff;
|
||||||
|
width: 33%;
|
||||||
|
text-align: center;
|
||||||
|
&.ant-radio-button-wrapper-checked{
|
||||||
|
background-color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
padding:10px 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 45%;
|
||||||
|
color: #fff ;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wayline-panel {
|
||||||
|
background: #3c3c3c;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 90px;
|
||||||
|
width: 95%;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
color: white;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0px 10px 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #3c3c3c;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 70px;
|
||||||
|
width: 95%;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
color: white;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0px 10px 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
365
src/components/task/TaskPanel.vue
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
<template>
|
||||||
|
<div class="header">Task Plan Library</div>
|
||||||
|
<div class="plan-panel-wrapper">
|
||||||
|
<a-table class="plan-table" :columns="columns" :data-source="plansData.data" row-key="job_id"
|
||||||
|
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData">
|
||||||
|
<!-- 执行时间 -->
|
||||||
|
<template #duration="{ record }">
|
||||||
|
<div class="flex-row" style="white-space: pre-wrap">
|
||||||
|
<div>
|
||||||
|
<div>{{ formatTaskTime(record.begin_time) }}</div>
|
||||||
|
<div>{{ formatTaskTime(record.end_time) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml10">
|
||||||
|
<div>{{ formatTaskTime(record.execute_time) }}</div>
|
||||||
|
<div>{{ formatTaskTime(record.completed_time) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 状态 -->
|
||||||
|
<template #status="{ record }">
|
||||||
|
<div>
|
||||||
|
<div class="flex-display flex-align-center">
|
||||||
|
<span class="circle-icon" :style="{backgroundColor: formatTaskStatus(record).color}"></span>
|
||||||
|
{{ formatTaskStatus(record).text }}
|
||||||
|
<a-tooltip v-if="!!record.code" placement="bottom" arrow-point-at-center >
|
||||||
|
<template #title>
|
||||||
|
<div>{{ getCodeMessage(record.code) }}</div>
|
||||||
|
</template>
|
||||||
|
<exclamation-circle-outlined class="ml5" :style="{color: commonColor.WARN, fontSize: '16px' }"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<div v-if="record.status === TaskStatus.Carrying">
|
||||||
|
<a-progress :percent="record.progress || 0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 任务类型 -->
|
||||||
|
<template #taskType="{ record }">
|
||||||
|
<div>{{ formatTaskType(record) }}</div>
|
||||||
|
</template>
|
||||||
|
<!-- 失控动作 -->
|
||||||
|
<template #lostAction="{ record }">
|
||||||
|
<div>{{ formatLostAction(record) }}</div>
|
||||||
|
</template>
|
||||||
|
<!-- 媒体上传状态 -->
|
||||||
|
<template #media_upload="{ record }">
|
||||||
|
<div>
|
||||||
|
<div class="flex-display flex-align-center">
|
||||||
|
<span class="circle-icon" :style="{backgroundColor: formatMediaTaskStatus(record).color}"></span>
|
||||||
|
{{ formatMediaTaskStatus(record).text }}
|
||||||
|
</div>
|
||||||
|
<div class="pl15">
|
||||||
|
{{ formatMediaTaskStatus(record).number }}
|
||||||
|
<a-tooltip v-if="formatMediaTaskStatus(record).status === MediaStatus.ToUpload" placement="bottom" arrow-point-at-center >
|
||||||
|
<template #title>
|
||||||
|
<div>Upload now</div>
|
||||||
|
</template>
|
||||||
|
<UploadOutlined class="ml5" :style="{color: commonColor.BLUE, fontSize: '16px' }" @click="onUploadMediaFileNow(record.job_id)"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 操作 -->
|
||||||
|
<template #action="{ record }">
|
||||||
|
<div class="action-area">
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="record.status === TaskStatus.Wait"
|
||||||
|
title="Are you sure you want to delete flight task?"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
@confirm="onDeleteTask(record.job_id)"
|
||||||
|
>
|
||||||
|
<a-button type="primary" size="small">Delete</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="record.status === TaskStatus.Carrying"
|
||||||
|
title="Are you sure you want to suspend?"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
@confirm="onSuspendTask(record.job_id)"
|
||||||
|
>
|
||||||
|
<a-button type="primary" size="small">Suspend</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-popconfirm
|
||||||
|
v-if="record.status === TaskStatus.Paused"
|
||||||
|
title="Are you sure you want to resume?"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
@confirm="onResumeTask(record.job_id)"
|
||||||
|
>
|
||||||
|
<a-button type="primary" size="small">Resume</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from '@vue/reactivity'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { TableState } from 'ant-design-vue/lib/table/interface'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { IPage } from '/@/api/http/type'
|
||||||
|
import { deleteTask, updateTaskStatus, UpdateTaskStatus, getWaylineJobs, Task, uploadMediaFileNow } from '/@/api/wayline'
|
||||||
|
import { useMyStore } from '/@/store'
|
||||||
|
import { ELocalStorageKey } from '/@/types/enums'
|
||||||
|
import { useFormatTask } from './use-format-task'
|
||||||
|
import { TaskStatus, TaskProgressInfo, TaskProgressStatus, TaskProgressWsStatusMap, MediaStatus, MediaStatusProgressInfo, TaskMediaHighestPriorityProgressInfo } from '/@/types/task'
|
||||||
|
import { useTaskWsEvent } from './use-task-ws-event'
|
||||||
|
import { getErrorMessage } from '/@/utils/error-code/index'
|
||||||
|
import { commonColor } from '/@/utils/color'
|
||||||
|
import { ExclamationCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const store = useMyStore()
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
|
||||||
|
|
||||||
|
const body: IPage = {
|
||||||
|
page: 1,
|
||||||
|
total: 0,
|
||||||
|
page_size: 50
|
||||||
|
}
|
||||||
|
const paginationProp = reactive({
|
||||||
|
pageSizeOptions: ['20', '50', '100'],
|
||||||
|
showQuickJumper: true,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSize: 50,
|
||||||
|
current: 1,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Planned/Actual Time',
|
||||||
|
dataIndex: 'duration',
|
||||||
|
width: 200,
|
||||||
|
slots: { customRender: 'duration' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
key: 'status',
|
||||||
|
width: 150,
|
||||||
|
slots: { customRender: 'status' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Plan Name',
|
||||||
|
dataIndex: 'job_name',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Type',
|
||||||
|
dataIndex: 'taskType',
|
||||||
|
width: 100,
|
||||||
|
slots: { customRender: 'taskType' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Flight Route Name',
|
||||||
|
dataIndex: 'file_name',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dock Name',
|
||||||
|
dataIndex: 'dock_name',
|
||||||
|
width: 100,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RTH Altitude Relative to Dock (m)',
|
||||||
|
dataIndex: 'rth_altitude',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Lost Action',
|
||||||
|
dataIndex: 'out_of_control_action',
|
||||||
|
width: 120,
|
||||||
|
slots: { customRender: 'lostAction' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Creator',
|
||||||
|
dataIndex: 'username',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Media File Upload',
|
||||||
|
key: 'media_upload',
|
||||||
|
width: 160,
|
||||||
|
slots: { customRender: 'media_upload' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
width: 120,
|
||||||
|
slots: { customRender: 'action' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
type Pagination = TableState['pagination']
|
||||||
|
|
||||||
|
const plansData = reactive({
|
||||||
|
data: [] as Task[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const { formatTaskType, formatTaskTime, formatLostAction, formatTaskStatus, formatMediaTaskStatus } = useFormatTask()
|
||||||
|
|
||||||
|
// 设备任务执行进度更新
|
||||||
|
function onTaskProgressWs (data: TaskProgressInfo) {
|
||||||
|
const { bid, output } = data
|
||||||
|
if (output) {
|
||||||
|
const { status, progress } = output || {}
|
||||||
|
const taskItem = plansData.data.find(task => task.job_id === bid)
|
||||||
|
if (!taskItem) return
|
||||||
|
if (status) {
|
||||||
|
taskItem.status = TaskProgressWsStatusMap[status]
|
||||||
|
// 执行中,更新进度
|
||||||
|
if (status === TaskProgressStatus.Sent || status === TaskProgressStatus.inProgress) {
|
||||||
|
taskItem.progress = progress?.percent || 0
|
||||||
|
} else if ([TaskProgressStatus.Rejected, TaskProgressStatus.Canceled, TaskProgressStatus.Timeout, TaskProgressStatus.Failed, TaskProgressStatus.OK].includes(status)) {
|
||||||
|
getPlans()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 媒体上传进度更新
|
||||||
|
function onTaskMediaProgressWs (data: MediaStatusProgressInfo) {
|
||||||
|
const { media_count: mediaCount, uploaded_count: uploadedCount, job_id: jobId } = data
|
||||||
|
if (isNaN(mediaCount) || isNaN(uploadedCount) || !jobId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const taskItem = plansData.data.find(task => task.job_id === jobId)
|
||||||
|
if (!taskItem) return
|
||||||
|
if (mediaCount === uploadedCount) {
|
||||||
|
taskItem.uploading = false
|
||||||
|
} else {
|
||||||
|
taskItem.uploading = true
|
||||||
|
}
|
||||||
|
taskItem.media_count = mediaCount
|
||||||
|
taskItem.uploaded_count = uploadedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
function onoTaskMediaHighestPriorityWS (data: TaskMediaHighestPriorityProgressInfo) {
|
||||||
|
const { pre_job_id: preJobId, job_id: jobId } = data
|
||||||
|
const preTaskItem = plansData.data.find(task => task.job_id === preJobId)
|
||||||
|
const taskItem = plansData.data.find(task => task.job_id === jobId)
|
||||||
|
if (preTaskItem) {
|
||||||
|
preTaskItem.uploading = false
|
||||||
|
}
|
||||||
|
if (taskItem) {
|
||||||
|
taskItem.uploading = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodeMessage (code: number) {
|
||||||
|
return getErrorMessage(code) + `(code: ${code})`
|
||||||
|
}
|
||||||
|
|
||||||
|
useTaskWsEvent({
|
||||||
|
onTaskProgressWs,
|
||||||
|
onTaskMediaProgressWs,
|
||||||
|
onoTaskMediaHighestPriorityWS,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getPlans()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getPlans () {
|
||||||
|
getWaylineJobs(workspaceId, body).then(res => {
|
||||||
|
if (res.code !== 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
plansData.data = res.data.list
|
||||||
|
paginationProp.total = res.data.pagination.total
|
||||||
|
paginationProp.current = res.data.pagination.page
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshData (page: Pagination) {
|
||||||
|
body.page = page?.current!
|
||||||
|
body.page_size = page?.pageSize!
|
||||||
|
getPlans()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
async function onDeleteTask (jobId: string) {
|
||||||
|
const { code } = await deleteTask(workspaceId, {
|
||||||
|
job_id: jobId
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Deleted successfully')
|
||||||
|
getPlans()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 挂起任务
|
||||||
|
async function onSuspendTask (jobId: string) {
|
||||||
|
const { code } = await updateTaskStatus(workspaceId, {
|
||||||
|
job_id: jobId,
|
||||||
|
status: UpdateTaskStatus.Suspend
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Suspended successfully')
|
||||||
|
getPlans()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解除挂起任务
|
||||||
|
async function onResumeTask (jobId: string) {
|
||||||
|
const { code } = await updateTaskStatus(workspaceId, {
|
||||||
|
job_id: jobId,
|
||||||
|
status: UpdateTaskStatus.Resume
|
||||||
|
})
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Resumed successfully')
|
||||||
|
getPlans()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即上传媒体
|
||||||
|
async function onUploadMediaFileNow (jobId: string) {
|
||||||
|
const { code } = await uploadMediaFileNow(workspaceId, jobId)
|
||||||
|
if (code === 0) {
|
||||||
|
message.success('Upload Media File successfully')
|
||||||
|
getPlans()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.plan-panel-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
.plan-table {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.action-area {
|
||||||
|
|
||||||
|
&::v-deep {
|
||||||
|
.ant-btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-right: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: start;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
73
src/components/task/use-format-task.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { DEFAULT_PLACEHOLDER } from '/@/utils/constants'
|
||||||
|
import { Task } from '/@/api/wayline'
|
||||||
|
import { TaskStatusColor, TaskStatusMap, TaskTypeMap, OutOfControlActionMap, MediaStatusMap, MediaStatusColorMap, MediaStatus } from '/@/types/task'
|
||||||
|
import { isNil } from 'lodash'
|
||||||
|
|
||||||
|
export function useFormatTask () {
|
||||||
|
function formatTaskType (task: Task) {
|
||||||
|
return TaskTypeMap[task.task_type] || DEFAULT_PLACEHOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskTime (time: string) {
|
||||||
|
return time || DEFAULT_PLACEHOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLostAction (task: Task) {
|
||||||
|
return OutOfControlActionMap[task.out_of_control_action] || DEFAULT_PLACEHOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskStatus (task: Task) {
|
||||||
|
const statusObj = {
|
||||||
|
text: '',
|
||||||
|
color: ''
|
||||||
|
}
|
||||||
|
const { status } = task
|
||||||
|
statusObj.text = TaskStatusMap[status]
|
||||||
|
statusObj.color = TaskStatusColor[status]
|
||||||
|
return statusObj
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMediaTaskStatus (task: Task) {
|
||||||
|
const statusObj = {
|
||||||
|
text: '',
|
||||||
|
color: '',
|
||||||
|
number: '',
|
||||||
|
status: MediaStatus.Empty,
|
||||||
|
}
|
||||||
|
const { media_count, uploaded_count, uploading } = task
|
||||||
|
if (isNil(media_count) || isNaN(media_count)) {
|
||||||
|
return statusObj
|
||||||
|
}
|
||||||
|
const expectedFileCount = media_count || 0
|
||||||
|
const uploadedFileCount = uploaded_count || 0
|
||||||
|
if (media_count === 0) {
|
||||||
|
statusObj.text = MediaStatusMap[MediaStatus.Empty]
|
||||||
|
statusObj.color = MediaStatusColorMap[MediaStatus.Empty]
|
||||||
|
} else if (media_count === uploaded_count) {
|
||||||
|
statusObj.text = MediaStatusMap[MediaStatus.Success]
|
||||||
|
statusObj.color = MediaStatusColorMap[MediaStatus.Success]
|
||||||
|
statusObj.number = `(${uploadedFileCount}/${expectedFileCount})`
|
||||||
|
statusObj.status = MediaStatus.Success
|
||||||
|
} else {
|
||||||
|
if (uploading) {
|
||||||
|
statusObj.text = MediaStatusMap[MediaStatus.Uploading]
|
||||||
|
statusObj.color = MediaStatusColorMap[MediaStatus.Uploading]
|
||||||
|
statusObj.status = MediaStatus.Uploading
|
||||||
|
} else {
|
||||||
|
statusObj.text = MediaStatusMap[MediaStatus.ToUpload]
|
||||||
|
statusObj.color = MediaStatusColorMap[MediaStatus.ToUpload]
|
||||||
|
statusObj.status = MediaStatus.ToUpload
|
||||||
|
}
|
||||||
|
statusObj.number = `(${uploadedFileCount}/${expectedFileCount})`
|
||||||
|
}
|
||||||
|
return statusObj
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatTaskType,
|
||||||
|
formatTaskTime,
|
||||||
|
formatLostAction,
|
||||||
|
formatTaskStatus,
|
||||||
|
formatMediaTaskStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/components/task/use-task-ws-event.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import EventBus from '/@/event-bus/'
|
||||||
|
import { onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { TaskProgressInfo, MediaStatusProgressInfo, TaskMediaHighestPriorityProgressInfo } from '/@/types/task'
|
||||||
|
import { EBizCode } from '/@/types'
|
||||||
|
|
||||||
|
export interface UseTaskWsEventParams {
|
||||||
|
onTaskProgressWs: (data: TaskProgressInfo) => void,
|
||||||
|
onTaskMediaProgressWs: (data: MediaStatusProgressInfo) => void
|
||||||
|
onoTaskMediaHighestPriorityWS: (data: TaskMediaHighestPriorityProgressInfo) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTaskWsEvent (funcs: UseTaskWsEventParams): void {
|
||||||
|
function handleTaskWsEvent (payload: any) {
|
||||||
|
if (!payload) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.biz_code) {
|
||||||
|
case EBizCode.FlightTaskProgress: {
|
||||||
|
funcs?.onTaskProgressWs(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.FlightTaskMediaProgress: {
|
||||||
|
funcs?.onTaskMediaProgressWs(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EBizCode.FlightTaskMediaHighestPriority: {
|
||||||
|
funcs?.onoTaskMediaHighestPriorityWS(payload.data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
// console.log('payload', payload.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
EventBus.on('flightTaskWs', handleTaskWsEvent)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
EventBus.off('flightTaskWs', handleTaskWsEvent)
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/components/workspace/DividerLine.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<Divider class="divider" />
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Divider } from 'ant-design-vue'
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.divider {
|
||||||
|
margin: 10px 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #4f4f4f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||