@ -1,20 +1,36 @@ |
|||||||
export const CURRENT_CONFIG = { |
export const CURRENT_CONFIG = { |
||||||
|
|
||||||
|
// license
|
||||||
|
appId: 'Please enter the app id.', // You need to go to the development website to apply.
|
||||||
|
appKey: 'Please enter the app key.', // You need to go to the development website to apply.
|
||||||
|
appLicense: 'Please enter the app license.' // You need to go to the development website to apply.
|
||||||
|
|
||||||
|
// http
|
||||||
baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
|
baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
|
||||||
websocketURL: 'Please enter the WebSocket access address.', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
|
websocketURL: 'Please enter the WebSocket access address.', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
|
||||||
|
|
||||||
rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
|
// livestreaming
|
||||||
gb28181Para: |
// 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.
|
||||||
'serverIP=Please enter the server ip.&serverPort=Please enter the server port.&serverID=Please enter the server id.' + |
rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
|
||||||
'&agentID=Please enter the agent id.&agentPassword=Please enter the agent password' + |
// 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.
|
||||||
'&localPort=Please enter the local port.&channel=Please enter the channel.', |
gbServerIp: 'Please enter the server ip.', |
||||||
rtspPara: 'userName=Please enter the username.&password=Please enter the password&port=Please enter the port.', |
gbServerPort: 'Please enter the server port.', |
||||||
amapKey: 'Please enter the amap key.', |
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.', |
agoraAPPID: 'Please enter the agora app id.', |
||||||
agoraToken: 'Please enter the agora token.', |
agoraToken: 'Please enter the agora temporary token.', |
||||||
agoraChannel: 'Please enter the agora channel.', |
agoraChannel: 'Please enter the agora channel.', |
||||||
|
|
||||||
appId: 'Please enter the app id.', // You need to go to the development website to apply.
|
// map
|
||||||
appKey: 'Please enter the app key.', // You need to go to the development website to apply.
|
// You can apply on the AMap website.
|
||||||
appLicense: 'Please enter the app license.' // You need to go to the development website to apply.
|
amapKey: 'Please enter the amap key.', |
||||||
|
|
||||||
} |
} |
||||||
|
@ -1,9 +1,18 @@ |
|||||||
import request from '/@/api/http/request' |
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request' |
||||||
const HTTP_PREFIX = '/media/api/v1' |
const HTTP_PREFIX = '/media/api/v1' |
||||||
|
|
||||||
// Get Media Files
|
// Get Media Files
|
||||||
export const getMediaFiles = async function (wid: string, body: {}): Promise<any> { |
export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> { |
||||||
const url = `${HTTP_PREFIX}/files/${wid}/files` |
const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}` |
||||||
const result = await request.get(url, body) |
const result = await request.get(url) |
||||||
return result.data |
return result.data |
||||||
} |
} |
||||||
|
// Download Media File
|
||||||
|
export const downloadMediaFile = async function (workspaceId: string, fingerprint: string): Promise<any> { |
||||||
|
const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fingerprint}/url` |
||||||
|
const result = await request.get(url, { responseType: 'blob' }) |
||||||
|
if (result.data.code) { |
||||||
|
return result.data |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
@ -1,9 +1,55 @@ |
|||||||
import request from '/@/api/http/request' |
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request' |
||||||
const HTTP_PREFIX = '/wayline/api/v1' |
const HTTP_PREFIX = '/wayline/api/v1' |
||||||
|
|
||||||
|
export interface CreatePlan { |
||||||
|
name: string, |
||||||
|
file_id: string, |
||||||
|
dock_sn: string, |
||||||
|
immediate: boolean, |
||||||
|
type: string, |
||||||
|
} |
||||||
|
|
||||||
// Get Wayline Files
|
// Get Wayline Files
|
||||||
export const getWaylineFiles = async function (wid: string, body: {}): Promise<any> { |
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 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) |
const result = await request.get(url) |
||||||
return result.data |
return result.data |
||||||
} |
} |
||||||
|
|
||||||
|
// Download Wayline File
|
||||||
|
export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> { |
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url` |
||||||
|
const result = await request.get(url, { responseType: 'blob' }) |
||||||
|
if (result.data.code) { |
||||||
|
return result.data |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
// Delete Wayline File
|
||||||
|
export const deleteWaylineFile = async function (workspaceId: string, waylineId: string): Promise<IWorkspaceResponse<any>> { |
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}` |
||||||
|
const result = await request.delete(url) |
||||||
|
return result.data |
||||||
|
} |
||||||
|
|
||||||
|
// Create Wayline Job
|
||||||
|
export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> { |
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks` |
||||||
|
const result = await request.post(url, plan) |
||||||
|
return result.data |
||||||
|
} |
||||||
|
|
||||||
|
// Get Wayline Jobs
|
||||||
|
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IWorkspaceResponse<any>> { |
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}` |
||||||
|
const result = await request.get(url) |
||||||
|
return result.data |
||||||
|
} |
||||||
|
|
||||||
|
// Execute Wayline Job
|
||||||
|
export const executeWaylineJobs = async function (workspaceId: string, plan_id: string): Promise<IWorkspaceResponse<any>> { |
||||||
|
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${plan_id}` |
||||||
|
const result = await request.post(url) |
||||||
|
return result.data |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 8.8 KiB |
@ -1,92 +1,168 @@ |
|||||||
<template> |
<template> |
||||||
<div class="media-panel-wrapper"> |
<div class="header">Media Files</div> |
||||||
<div class="header">Media</div> |
<a-spin :spinning="loading" :delay="1000" tip="downloading" size="large"> |
||||||
<a-button type="primary" style="margin-top:20px" @click="onRefresh" |
<div class="media-panel-wrapper"> |
||||||
>Refresh</a-button |
<a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint" |
||||||
> |
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData"> |
||||||
<a-table class="media-table" :columns="columns" :data-source="data"> |
<template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col"> |
||||||
<template #name="{ text, record }"> |
<a-tooltip :title="text"> |
||||||
<a :href="record.preview_url">{{ text }}</a> |
<a v-if="col === 'name'">{{ text }}</a> |
||||||
</template> |
<span v-else>{{ text }}</span> |
||||||
<template #action> |
</a-tooltip> |
||||||
<span class="action-area"> |
</template> |
||||||
action |
<template #original="{ text }"> |
||||||
</span> |
{{ text }} |
||||||
</template> |
</template> |
||||||
</a-table> |
<template #action="{ record }"> |
||||||
</div> |
<a-tooltip title="download"> |
||||||
|
<a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a> |
||||||
|
</a-tooltip> |
||||||
|
</template> |
||||||
|
</a-table> |
||||||
|
</div> |
||||||
|
</a-spin> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import { ref } from '@vue/reactivity' |
import { ref } from '@vue/reactivity' |
||||||
import { getMediaFiles } from '/@/api/media' |
import { TableState } from 'ant-design-vue/lib/table/interface' |
||||||
|
import { onMounted, reactive } from 'vue' |
||||||
|
import { IPage } from '../api/http/type' |
||||||
|
import { ELocalStorageKey } from '../types/enums' |
||||||
|
import { downloadFile } from '../utils/common' |
||||||
|
import { downloadMediaFile, getMediaFiles } from '/@/api/media' |
||||||
|
import { DownloadOutlined } from '@ant-design/icons-vue' |
||||||
|
import { Pagination } from 'ant-design-vue' |
||||||
|
import { load } from '@amap/amap-jsapi-loader' |
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
const loading = ref(false) |
||||||
|
|
||||||
const columns = [ |
const columns = [ |
||||||
{ |
{ |
||||||
title: 'FileName', |
title: 'File Name', |
||||||
dataIndex: 'name', |
dataIndex: 'file_name', |
||||||
key: 'name', |
ellipsis: true, |
||||||
slots: { customRender: 'name' } |
slots: { customRender: 'name' } |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
title: 'FileSize', |
title: 'File Path', |
||||||
dataIndex: 'size', |
dataIndex: 'file_path', |
||||||
key: 'size' |
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: 'PayloadType', |
title: 'Created', |
||||||
dataIndex: 'payload_type', |
dataIndex: 'create_time' |
||||||
key: 'payload_type', |
|
||||||
ellipsis: true |
|
||||||
}, |
}, |
||||||
{ |
{ |
||||||
title: 'Action', |
title: 'Action', |
||||||
key: 'action', |
|
||||||
slots: { customRender: 'action' } |
slots: { customRender: 'action' } |
||||||
} |
} |
||||||
] |
] |
||||||
const data = ref([ |
const body: IPage = { |
||||||
{ |
page: 1, |
||||||
key: '1', |
total: 0, |
||||||
name: 'name1', |
page_size: 50 |
||||||
size: 32, |
} |
||||||
payload_type: 'PM320_DUAL', |
const paginationProp = reactive({ |
||||||
preview_url: '' |
pageSizeOptions: ['20', '50', '100'], |
||||||
} |
showQuickJumper: true, |
||||||
]) |
showSizeChanger: true, |
||||||
const onRefresh = async () => { |
pageSize: 50, |
||||||
const wid = localStorage.getItem('workspace-id') |
current: 1, |
||||||
data.value = [] |
total: 0 |
||||||
const index = 1 |
}) |
||||||
const res = await getMediaFiles(wid, {}) |
|
||||||
res.data.forEach(ele => { |
type Pagination = TableState['pagination'] |
||||||
data.value.push({ |
|
||||||
key: index.toString(), |
interface MediaFile { |
||||||
name: ele.file_name |
fingerprint: string, |
||||||
}) |
drone: string, |
||||||
|
payload: string, |
||||||
|
is_original: string, |
||||||
|
file_name: string, |
||||||
|
file_path: string, |
||||||
|
create_time: string, |
||||||
|
} |
||||||
|
|
||||||
|
const mediaData = reactive({ |
||||||
|
data: [] as MediaFile[] |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getFiles() |
||||||
|
}) |
||||||
|
|
||||||
|
function getFiles () { |
||||||
|
getMediaFiles(workspaceId, body).then(res => { |
||||||
|
mediaData.data = res.data.list |
||||||
|
paginationProp.total = res.data.pagination.total |
||||||
|
paginationProp.current = res.data.pagination.page |
||||||
|
console.info(mediaData.data[0]) |
||||||
}) |
}) |
||||||
console.log(res) |
|
||||||
} |
} |
||||||
|
|
||||||
|
function refreshData (page: Pagination) { |
||||||
|
body.page = page?.current! |
||||||
|
body.page_size = page?.pageSize! |
||||||
|
getFiles() |
||||||
|
} |
||||||
|
|
||||||
|
function downloadMedia (media: MediaFile) { |
||||||
|
loading.value = true |
||||||
|
downloadMediaFile(workspaceId, media.fingerprint).then(res => { |
||||||
|
if (res.code && res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
const suffix = media.file_name.substring(media.file_name.lastIndexOf('.')) |
||||||
|
const data = new Blob([res.data], { type: suffix === '.mp4' ? 'video/mp4' : 'image/jpeg' }) |
||||||
|
downloadFile(data, media.file_name) |
||||||
|
}).finally(() => { |
||||||
|
loading.value = false |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
</script> |
</script> |
||||||
|
|
||||||
<style lang="scss" scoped> |
<style lang="scss" scoped> |
||||||
.media-panel-wrapper { |
.media-panel-wrapper { |
||||||
width: 100%; |
width: 100%; |
||||||
|
padding: 16px; |
||||||
.media-table { |
.media-table { |
||||||
background: #fff; |
background: #fff; |
||||||
margin-top: 32px; |
margin-top: 10px; |
||||||
} |
|
||||||
.header { |
|
||||||
width: 100%; |
|
||||||
height: 60px; |
|
||||||
background: #fff; |
|
||||||
padding: 16px 24px; |
|
||||||
font-size: 20px; |
|
||||||
text-align: start; |
|
||||||
color: #000; |
|
||||||
} |
} |
||||||
.action-area { |
.action-area { |
||||||
color: $primary; |
color: $primary; |
||||||
cursor: pointer; |
cursor: pointer; |
||||||
} |
} |
||||||
} |
} |
||||||
|
.header { |
||||||
|
width: 100%; |
||||||
|
height: 60px; |
||||||
|
background: #fff; |
||||||
|
padding: 16px; |
||||||
|
font-size: 20px; |
||||||
|
font-weight: bold; |
||||||
|
text-align: start; |
||||||
|
color: #000; |
||||||
|
} |
||||||
</style> |
</style> |
||||||
|
@ -0,0 +1,182 @@ |
|||||||
|
<template> |
||||||
|
<div class="header">Task Plan Library</div> |
||||||
|
<div class="plan-panel-wrapper"> |
||||||
|
<!-- <router-link :to=" '/' + ERouterName.CREATE_PLAN"> |
||||||
|
<a-button type="primary">Create Plan</a-button> |
||||||
|
</router-link> --> |
||||||
|
<a-table class="plan-table" :columns="columns" :data-source="plansData.data" row-key="job_id" |
||||||
|
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData"> |
||||||
|
<template #status="{ record }"> |
||||||
|
<span v-if="taskProgressMap[record.bid]"> |
||||||
|
<a-progress type="line" :percent="taskProgressMap[record.bid]?.progress?.percent" |
||||||
|
:status="taskProgressMap[record.bid]?.status === ETaskStatus.FAILED ? 'exception' : taskProgressMap[record.bid]?.status === ETaskStatus.OK ? 'success' : 'normal'"> |
||||||
|
<template #format="percent"> |
||||||
|
<a-tooltip :title="taskProgressMap[record.bid]?.status"> |
||||||
|
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden; position: absolute; left: 5px; top: -12px;"> |
||||||
|
{{ percent }}% {{ taskProgressMap[record.bid]?.status }} |
||||||
|
</div> |
||||||
|
</a-tooltip> |
||||||
|
</template> |
||||||
|
</a-progress> |
||||||
|
</span> |
||||||
|
</template> |
||||||
|
<template #action="{ record }"> |
||||||
|
<span class="action-area"> |
||||||
|
<a-popconfirm |
||||||
|
title="Are you sure execute this task?" |
||||||
|
ok-text="Yes" |
||||||
|
cancel-text="No" |
||||||
|
@confirm="executePlan(record.job_id)" |
||||||
|
> |
||||||
|
<a-button type="primary" size="small">Execute</a-button> |
||||||
|
</a-popconfirm> |
||||||
|
</span> |
||||||
|
</template> |
||||||
|
</a-table> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { reactive, ref } from '@vue/reactivity' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { TableState } from 'ant-design-vue/lib/table/interface' |
||||||
|
import { computed, onMounted, watch } from 'vue' |
||||||
|
import { IPage } from '../api/http/type' |
||||||
|
import { executeWaylineJobs, getWaylineJobs } from '../api/wayline' |
||||||
|
import { getRoot } from '../root' |
||||||
|
import { useMyStore } from '../store' |
||||||
|
import { ELocalStorageKey, ERouterName } from '../types/enums' |
||||||
|
import router from '/@/router' |
||||||
|
import { ETaskStatus } from '/@/types/wayline' |
||||||
|
|
||||||
|
const store = useMyStore() |
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
const body: IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 50 |
||||||
|
} |
||||||
|
const paginationProp = reactive({ |
||||||
|
pageSizeOptions: ['20', '50', '100'], |
||||||
|
showQuickJumper: true, |
||||||
|
showSizeChanger: true, |
||||||
|
pageSize: 50, |
||||||
|
current: 1, |
||||||
|
total: 0 |
||||||
|
}) |
||||||
|
|
||||||
|
const columns = [ |
||||||
|
{ |
||||||
|
title: 'Plan Name', |
||||||
|
dataIndex: 'job_name' |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Flight Route Name', |
||||||
|
dataIndex: 'file_name', |
||||||
|
ellipsis: true |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Dock Name', |
||||||
|
dataIndex: 'dock_name', |
||||||
|
ellipsis: true |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Creator', |
||||||
|
dataIndex: 'username', |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Updated', |
||||||
|
dataIndex: 'update_time' |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Status', |
||||||
|
key: 'status', |
||||||
|
width: 200, |
||||||
|
slots: { customRender: 'status' } |
||||||
|
}, |
||||||
|
|
||||||
|
{ |
||||||
|
title: 'Action', |
||||||
|
slots: { customRender: 'action' } |
||||||
|
} |
||||||
|
] |
||||||
|
type Pagination = TableState['pagination'] |
||||||
|
|
||||||
|
interface TaskPlan { |
||||||
|
bid: string, |
||||||
|
job_id: string, |
||||||
|
job_name: string, |
||||||
|
file_name: string, |
||||||
|
dock_name: string, |
||||||
|
username: string, |
||||||
|
create_time: string, |
||||||
|
} |
||||||
|
|
||||||
|
const plansData = reactive({ |
||||||
|
data: [] as TaskPlan[] |
||||||
|
}) |
||||||
|
|
||||||
|
function createPlan () { |
||||||
|
root.$router.push('/' + ERouterName.CREATE_PLAN) |
||||||
|
} |
||||||
|
|
||||||
|
const taskProgressMap = computed(() => store.state.taskProgressInfo) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getPlans() |
||||||
|
}) |
||||||
|
|
||||||
|
function getPlans () { |
||||||
|
getWaylineJobs(workspaceId, body).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
plansData.data = res.data.list |
||||||
|
paginationProp.total = res.data.pagination.total |
||||||
|
paginationProp.current = res.data.pagination.page |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function refreshData (page: Pagination) { |
||||||
|
body.page = page?.current! |
||||||
|
body.page_size = page?.pageSize! |
||||||
|
getPlans() |
||||||
|
} |
||||||
|
|
||||||
|
function executePlan (jobId: string) { |
||||||
|
executeWaylineJobs(workspaceId, jobId).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.success('Executed Successfully') |
||||||
|
getPlans() |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.plan-panel-wrapper { |
||||||
|
width: 100%; |
||||||
|
padding: 16px; |
||||||
|
.plan-table { |
||||||
|
background: #fff; |
||||||
|
margin-top: 10px; |
||||||
|
} |
||||||
|
.action-area { |
||||||
|
color: $primary; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
} |
||||||
|
.header { |
||||||
|
width: 100%; |
||||||
|
height: 60px; |
||||||
|
background: #fff; |
||||||
|
padding: 16px; |
||||||
|
font-size: 20px; |
||||||
|
font-weight: bold; |
||||||
|
text-align: start; |
||||||
|
color: #000; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,350 @@ |
|||||||
|
<template> |
||||||
|
<div class="mt20 flex-column flex-justify-start flex-align-center"> |
||||||
|
<div id="player" style="width: 720px; height: 420px; border: 1px solid"></div> |
||||||
|
<p class="fz24">Live streaming source selection</p> |
||||||
|
<div class="flex-row flex-justify-center flex-align-center mt10"> |
||||||
|
<a-select |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Drone" |
||||||
|
@select="onDroneSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in dronePara.droneList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> |
||||||
|
<a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Camera" |
||||||
|
@select="onCameraSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in dronePara.cameraList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> |
||||||
|
<!-- <a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Lens" |
||||||
|
@select="onVideoSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
class="ml10" |
||||||
|
v-for="item in dronePara.videoList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> --> |
||||||
|
<a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Clarity" |
||||||
|
@select="onClaritySelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in clarityList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> |
||||||
|
</div> |
||||||
|
<p class="fz16 mt10"> |
||||||
|
Note: Obtain The Following Parameters From https://console.agora.io |
||||||
|
</p> |
||||||
|
<div class="flex-row flex-justify-center flex-align-center"> |
||||||
|
<a-input v-model:value="agoraPara.appid" placeholder="APP ID"></a-input> |
||||||
|
<a-input |
||||||
|
class="ml10" |
||||||
|
v-model:value="agoraPara.token" |
||||||
|
placeholder="Token" |
||||||
|
></a-input> |
||||||
|
<a-input |
||||||
|
class="ml10" |
||||||
|
v-model:value="agoraPara.channel" |
||||||
|
placeholder="Channel" |
||||||
|
></a-input> |
||||||
|
</div> |
||||||
|
<div class="mt20 flex-row flex-justify-center flex-align-center"> |
||||||
|
<a-button type="primary" large @click="onStart">Play</a-button> |
||||||
|
<a-button class="ml20" type="primary" large @click="onStop" |
||||||
|
>Stop</a-button |
||||||
|
> |
||||||
|
<a-button class="ml20" type="primary" large @click="onUpdateQuality" |
||||||
|
>Update Clarity</a-button |
||||||
|
> |
||||||
|
<a-button class="ml20" type="primary" large @click="onRefresh" |
||||||
|
>Refresh Live Capacity</a-button |
||||||
|
> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import AgoraRTC, { IAgoraRTCClient, IAgoraRTCRemoteUser } from 'agora-rtc-sdk-ng' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { onMounted, reactive } from 'vue' |
||||||
|
import { CURRENT_CONFIG as config } from '/@/api/http/config' |
||||||
|
import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
|
||||||
|
const clarityList = [ |
||||||
|
{ |
||||||
|
value: 0, |
||||||
|
label: 'Adaptive' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 1, |
||||||
|
label: 'Smooth' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 2, |
||||||
|
label: 'Standard' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 3, |
||||||
|
label: 'HD' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 4, |
||||||
|
label: 'Super Clear' |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
let agoraClient = {} as IAgoraRTCClient |
||||||
|
const agoraPara = reactive({ |
||||||
|
appid: config.agoraAPPID, |
||||||
|
token: config.agoraToken, |
||||||
|
channel: config.agoraChannel, |
||||||
|
uid: 123456, |
||||||
|
stream: {} |
||||||
|
}) |
||||||
|
const dronePara = reactive({ |
||||||
|
livestreamSource: [], |
||||||
|
droneList: [] as any[], |
||||||
|
cameraList: [] as any[], |
||||||
|
videoList: [] as any[], |
||||||
|
droneSelected: '', |
||||||
|
cameraSelected: '', |
||||||
|
videoSelected: '', |
||||||
|
claritySelected: 0 |
||||||
|
}) |
||||||
|
const livePara = reactive({ |
||||||
|
url: '', |
||||||
|
webrtc: {} as any, |
||||||
|
videoId: '', |
||||||
|
liveState: false |
||||||
|
}) |
||||||
|
|
||||||
|
const onRefresh = async () => { |
||||||
|
dronePara.droneList = [] |
||||||
|
dronePara.cameraList = [] |
||||||
|
dronePara.videoList = [] |
||||||
|
dronePara.droneSelected = '' |
||||||
|
dronePara.cameraSelected = '' |
||||||
|
dronePara.videoSelected = '' |
||||||
|
await getLiveCapacity({}) |
||||||
|
.then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
if (res.data === null) { |
||||||
|
console.warn('warning: get live capacity is null!!!') |
||||||
|
return |
||||||
|
} |
||||||
|
dronePara.livestreamSource = res.data |
||||||
|
dronePara.droneList = [] |
||||||
|
|
||||||
|
console.log('live_capacity:', dronePara.livestreamSource) |
||||||
|
|
||||||
|
if (dronePara.livestreamSource) { |
||||||
|
dronePara.livestreamSource.forEach((ele: any) => { |
||||||
|
dronePara.droneList.push({ label: ele.name + '-' + ele.sn, value: ele.sn }) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(error => { |
||||||
|
console.error(error) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
onRefresh() |
||||||
|
agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'vp8' }) |
||||||
|
// Subscribe when a remote user publishes a stream |
||||||
|
agoraClient.on('user-joined', async (user: IAgoraRTCRemoteUser) => { |
||||||
|
message.info('user[' + user.uid + '] join') |
||||||
|
}) |
||||||
|
agoraClient.on('user-published', async (user: IAgoraRTCRemoteUser, mediaType: 'audio' | 'video') => { |
||||||
|
await agoraClient.subscribe(user, mediaType) |
||||||
|
if (mediaType === 'video') { |
||||||
|
console.log('subscribe success') |
||||||
|
// Get `RemoteVideoTrack` in the `user` object. |
||||||
|
const remoteVideoTrack = user.videoTrack! |
||||||
|
// Dynamically create a container in the form of a DIV element for playing the remote video track. |
||||||
|
const remotePlayerContainer: any = document.getElementById('player') |
||||||
|
// remotePlayerContainer.id = agoraPara.uid |
||||||
|
remoteVideoTrack.play(remotePlayerContainer) |
||||||
|
} |
||||||
|
}) |
||||||
|
agoraClient.on('user-unpublished', async (user: any) => { |
||||||
|
console.log('unpublish live:', user) |
||||||
|
message.info('unpublish live') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
const handleError = (err: any) => { |
||||||
|
console.error(err) |
||||||
|
} |
||||||
|
const handleJoinChannel = (uid: any) => { |
||||||
|
agoraPara.uid = uid |
||||||
|
} |
||||||
|
|
||||||
|
const onStart = async () => { |
||||||
|
const that = this |
||||||
|
console.log( |
||||||
|
'drone parameter:', |
||||||
|
dronePara.droneSelected, |
||||||
|
dronePara.cameraSelected, |
||||||
|
dronePara.videoSelected, |
||||||
|
dronePara.claritySelected |
||||||
|
) |
||||||
|
const timestamp = new Date().getTime().toString() |
||||||
|
const liveTimestamp = timestamp |
||||||
|
if ( |
||||||
|
dronePara.droneSelected == null || |
||||||
|
dronePara.cameraSelected == null || |
||||||
|
dronePara.videoSelected == null || |
||||||
|
dronePara.claritySelected == null |
||||||
|
) { |
||||||
|
message.warn('waring: not select live para!!!') |
||||||
|
return |
||||||
|
} |
||||||
|
agoraClient.setClientRole('audience', { level: 1 }) |
||||||
|
if (agoraClient.connectionState === 'DISCONNECTED') { |
||||||
|
agoraClient |
||||||
|
.join(agoraPara.appid, agoraPara.channel, agoraPara.token) |
||||||
|
} |
||||||
|
livePara.videoId = |
||||||
|
dronePara.droneSelected + |
||||||
|
'/' + |
||||||
|
dronePara.cameraSelected + |
||||||
|
'/' + |
||||||
|
dronePara.videoSelected |
||||||
|
console.log(agoraPara) |
||||||
|
agoraPara.token = encodeURIComponent(agoraPara.token) |
||||||
|
|
||||||
|
livePara.url = |
||||||
|
'channel=' + |
||||||
|
agoraPara.channel + |
||||||
|
'&sn=' + |
||||||
|
dronePara.droneSelected + |
||||||
|
'&token=' + |
||||||
|
agoraPara.token + |
||||||
|
'&uid=' + |
||||||
|
agoraPara.uid |
||||||
|
|
||||||
|
startLivestream({ |
||||||
|
url: livePara.url, |
||||||
|
video_id: livePara.videoId, |
||||||
|
url_type: 0, |
||||||
|
video_quality: dronePara.claritySelected |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
livePara.liveState = true |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
console.error(err) |
||||||
|
}) |
||||||
|
} |
||||||
|
const onStop = async () => { |
||||||
|
livePara.videoId = |
||||||
|
dronePara.droneSelected + |
||||||
|
'/' + |
||||||
|
dronePara.cameraSelected + |
||||||
|
'/' + |
||||||
|
dronePara.videoSelected |
||||||
|
stopLivestream({ |
||||||
|
video_id: livePara.videoId |
||||||
|
}).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.success(res.message) |
||||||
|
} |
||||||
|
livePara.liveState = false |
||||||
|
console.log('stop play livestream') |
||||||
|
}) |
||||||
|
} |
||||||
|
const onDroneSelect = (val: any) => { |
||||||
|
dronePara.droneSelected = val |
||||||
|
if (dronePara.droneSelected) { |
||||||
|
const droneTemp = dronePara.livestreamSource |
||||||
|
dronePara.cameraList = [] |
||||||
|
|
||||||
|
droneTemp.forEach((ele: any) => { |
||||||
|
const drone = ele |
||||||
|
if (drone.cameras_list && drone.sn === dronePara.droneSelected) { |
||||||
|
const cameraListTemp = drone.cameras_list |
||||||
|
cameraListTemp.forEach((ele: any) => { |
||||||
|
dronePara.cameraList.push({ label: ele.name, value: ele.index }) |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
const onCameraSelect = (val: any) => { |
||||||
|
dronePara.cameraSelected = val |
||||||
|
|
||||||
|
if (dronePara.cameraSelected) { |
||||||
|
const droneTemp = dronePara.livestreamSource |
||||||
|
droneTemp.forEach((ele: any) => { |
||||||
|
const drone = ele |
||||||
|
if (drone.sn === dronePara.droneSelected) { |
||||||
|
const cameraListTemp = drone.cameras_list |
||||||
|
cameraListTemp.forEach((ele: any) => { |
||||||
|
const camera = ele |
||||||
|
if (camera.index === dronePara.cameraSelected) { |
||||||
|
const videoListTemp = camera.videos_list |
||||||
|
dronePara.videoList = [] |
||||||
|
videoListTemp.forEach((ele: any) => { |
||||||
|
dronePara.videoList.push({ label: ele.type, value: ele.index }) |
||||||
|
}) |
||||||
|
dronePara.videoSelected = dronePara.videoList[0]?.value |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
const onVideoSelect = (val: any) => { |
||||||
|
dronePara.videoSelected = val |
||||||
|
} |
||||||
|
const onClaritySelect = (val: any) => { |
||||||
|
dronePara.claritySelected = val |
||||||
|
} |
||||||
|
const onUpdateQuality = () => { |
||||||
|
if (!livePara.liveState) { |
||||||
|
message.info('Please turn on the livestream first.') |
||||||
|
return |
||||||
|
} |
||||||
|
setLivestreamQuality({ |
||||||
|
video_id: livePara.videoId, |
||||||
|
video_quality: dronePara.claritySelected |
||||||
|
}).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.success('Set the clarity to ' + clarityList[dronePara.claritySelected].label) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import '/@/styles/index.scss'; |
||||||
|
</style> |
@ -0,0 +1,378 @@ |
|||||||
|
<template> |
||||||
|
<div class="flex-column flex-justify-start flex-align-center mt20"> |
||||||
|
<video |
||||||
|
:style="{ width: '720px', height: '480px' }" |
||||||
|
id="video-webrtc" |
||||||
|
ref="videowebrtc" |
||||||
|
controls |
||||||
|
class="mt20" |
||||||
|
></video> |
||||||
|
<p class="fz24">Live streaming source selection</p> |
||||||
|
<div class="flex-row flex-justify-center flex-align-center mt10"> |
||||||
|
<a-select |
||||||
|
style="width: 150px" |
||||||
|
placeholder="Select Live Type" |
||||||
|
@select="onLiveTypeSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in liveTypeList" |
||||||
|
:key="item.label" |
||||||
|
:value="item.value" |
||||||
|
> |
||||||
|
{{ item.label }} |
||||||
|
</a-select-option> |
||||||
|
</a-select> |
||||||
|
<a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Drone" |
||||||
|
@select="onDroneSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in droneList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> |
||||||
|
<a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Camera" |
||||||
|
@select="onCameraSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in cameraList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> |
||||||
|
<!-- <a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Lens" |
||||||
|
@select="onVideoSelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
class="ml10" |
||||||
|
v-for="item in videoList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> --> |
||||||
|
<a-select |
||||||
|
class="ml10" |
||||||
|
style="width:150px" |
||||||
|
placeholder="Select Clarity" |
||||||
|
@select="onClaritySelect" |
||||||
|
> |
||||||
|
<a-select-option |
||||||
|
v-for="item in clarityList" |
||||||
|
:key="item.value" |
||||||
|
:value="item.value" |
||||||
|
>{{ item.label }}</a-select-option |
||||||
|
> |
||||||
|
</a-select> |
||||||
|
</div> |
||||||
|
<div class="mt20"> |
||||||
|
<p class="fz10" v-if="livetypeSelected == 2"> |
||||||
|
Please use VLC media player to play the RTSP livestream !!! |
||||||
|
</p> |
||||||
|
<p class="fz10" v-if="livetypeSelected == 2"> |
||||||
|
RTSP Parameter:{{ rtspData }} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
<div class="mt10 flex-row flex-justify-center flex-align-center"> |
||||||
|
<a-button type="primary" large @click="onStart">Play</a-button> |
||||||
|
<a-button class="ml20" type="primary" large @click="onStop" |
||||||
|
>Stop</a-button |
||||||
|
> |
||||||
|
<a-button class="ml20" type="primary" large @click="onUpdateQuality" |
||||||
|
>Update Clarity</a-button |
||||||
|
> |
||||||
|
<a-button class="ml20" type="primary" large @click="onRefresh" |
||||||
|
>Refresh Live Capacity</a-button |
||||||
|
> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { onMounted, reactive, ref } from 'vue' |
||||||
|
import { CURRENT_CONFIG as config } from '/@/api/http/config' |
||||||
|
import { getLiveCapacity, setLivestreamQuality, startLivestream, stopLivestream } from '/@/api/manage' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import jswebrtc from '/@/vendors/jswebrtc.min.js' |
||||||
|
const root = getRoot() |
||||||
|
|
||||||
|
const liveTypeList = [ |
||||||
|
{ |
||||||
|
value: 1, |
||||||
|
label: 'RTMP' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 2, |
||||||
|
label: 'RTSP' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 3, |
||||||
|
label: 'GB28181' |
||||||
|
} |
||||||
|
] |
||||||
|
const clarityList = [ |
||||||
|
{ |
||||||
|
value: 0, |
||||||
|
label: 'Adaptive' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 1, |
||||||
|
label: 'Smooth' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 2, |
||||||
|
label: 'Standard' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 3, |
||||||
|
label: 'HD' |
||||||
|
}, |
||||||
|
{ |
||||||
|
value: 4, |
||||||
|
label: 'Super Clear' |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
const videowebrtc = ref(null) |
||||||
|
const livestreamSource = ref() |
||||||
|
const droneList = ref() |
||||||
|
const cameraList = ref() |
||||||
|
const videoList = ref() |
||||||
|
const droneSelected = ref() |
||||||
|
const cameraSelected = ref() |
||||||
|
const videoSeleted = ref() |
||||||
|
const claritySeleted = ref() |
||||||
|
const videoId = ref() |
||||||
|
const liveState = ref<boolean>(false) |
||||||
|
const livetypeSelected = ref() |
||||||
|
const rtspData = ref() |
||||||
|
|
||||||
|
const onRefresh = async () => { |
||||||
|
droneList.value = [] |
||||||
|
cameraList.value = [] |
||||||
|
videoList.value = [] |
||||||
|
droneSelected.value = null |
||||||
|
cameraSelected.value = null |
||||||
|
videoSeleted.value = null |
||||||
|
await getLiveCapacity({}) |
||||||
|
.then(res => { |
||||||
|
console.log(res) |
||||||
|
if (res.code === 0) { |
||||||
|
if (res.data === null) { |
||||||
|
console.warn('warning: get live capacity is null!!!') |
||||||
|
return |
||||||
|
} |
||||||
|
const resData: Array<[]> = res.data |
||||||
|
console.log('live_capacity:', resData) |
||||||
|
livestreamSource.value = resData |
||||||
|
|
||||||
|
const temp: Array<{}> = [] |
||||||
|
if (livestreamSource.value) { |
||||||
|
livestreamSource.value.forEach((ele: any) => { |
||||||
|
temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn }) |
||||||
|
}) |
||||||
|
droneList.value = temp |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(error => { |
||||||
|
console.error(error) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
onRefresh() |
||||||
|
}) |
||||||
|
const onStart = async () => { |
||||||
|
console.log( |
||||||
|
'Param:', |
||||||
|
livetypeSelected.value, |
||||||
|
droneSelected.value, |
||||||
|
cameraSelected.value, |
||||||
|
videoSeleted.value, |
||||||
|
claritySeleted.value |
||||||
|
) |
||||||
|
const timestamp = new Date().getTime().toString() |
||||||
|
if ( |
||||||
|
livetypeSelected.value == null || |
||||||
|
droneSelected.value == null || |
||||||
|
cameraSelected.value == null || |
||||||
|
videoSeleted.value == null || |
||||||
|
claritySeleted.value == null |
||||||
|
) { |
||||||
|
message.warn('waring: not select live para!!!') |
||||||
|
return |
||||||
|
} |
||||||
|
videoId.value = |
||||||
|
droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value |
||||||
|
let liveURL = '' |
||||||
|
switch (livetypeSelected.value) { |
||||||
|
case 1: { |
||||||
|
// RTMP |
||||||
|
liveURL = config.rtmpURL + timestamp |
||||||
|
break |
||||||
|
} |
||||||
|
case 2: { |
||||||
|
// RTSP |
||||||
|
liveURL = `userName=${config.rtspUserName}&password=${config.rtspPassword}&port=${config.rtspPort}` |
||||||
|
break |
||||||
|
} |
||||||
|
case 3: { |
||||||
|
liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}` |
||||||
|
break |
||||||
|
} |
||||||
|
default: |
||||||
|
console.warn('warning: live type is not correct!!!') |
||||||
|
break |
||||||
|
} |
||||||
|
await startLivestream({ |
||||||
|
url: liveURL, |
||||||
|
video_id: videoId.value, |
||||||
|
url_type: livetypeSelected.value, |
||||||
|
video_quality: claritySeleted.value |
||||||
|
}) |
||||||
|
.then(res => { |
||||||
|
if (livetypeSelected.value === 3) { |
||||||
|
const url = res.data.url |
||||||
|
const videoElement = videowebrtc.value |
||||||
|
// gb28181,it will fail if not wait. |
||||||
|
message.loading({ |
||||||
|
content: 'Loding...', |
||||||
|
duration: 4, |
||||||
|
onClose () { |
||||||
|
const player = new jswebrtc.Player(url, { |
||||||
|
video: videoElement, |
||||||
|
autoplay: true, |
||||||
|
onPlay: (obj: any) => { |
||||||
|
console.log('start play livestream') |
||||||
|
} |
||||||
|
}) |
||||||
|
liveState.value = true |
||||||
|
} |
||||||
|
}) |
||||||
|
} else if (livetypeSelected.value === 2) { |
||||||
|
console.log(res) |
||||||
|
rtspData.value = |
||||||
|
'url:' + |
||||||
|
res.data.url + |
||||||
|
'&username:' + |
||||||
|
res.data.username + |
||||||
|
'&password:' + |
||||||
|
res.data.password |
||||||
|
} else if (livetypeSelected.value === 1) { |
||||||
|
const url = res.data.url |
||||||
|
const videoElement = videowebrtc.value |
||||||
|
console.log('start live:', url) |
||||||
|
console.log(videoElement) |
||||||
|
const player = new jswebrtc.Player(url, { |
||||||
|
video: videoElement, |
||||||
|
autoplay: true, |
||||||
|
onPlay: (obj: any) => { |
||||||
|
console.log('start play livestream') |
||||||
|
liveState.value = true |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch(err => { |
||||||
|
console.error(err) |
||||||
|
}) |
||||||
|
} |
||||||
|
const onStop = () => { |
||||||
|
videoId.value = |
||||||
|
droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value |
||||||
|
stopLivestream({ |
||||||
|
video_id: videoId.value |
||||||
|
}).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.info(res.message) |
||||||
|
liveState.value = false |
||||||
|
console.log('stop play livestream') |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const onUpdateQuality = () => { |
||||||
|
if (!liveState.value) { |
||||||
|
message.info('Please turn on the livestream first.') |
||||||
|
return |
||||||
|
} |
||||||
|
setLivestreamQuality({ |
||||||
|
video_id: videoId.value, |
||||||
|
video_quality: claritySeleted.value |
||||||
|
}).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.success('Set the clarity to ' + clarityList[claritySeleted.value].label) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const onLiveTypeSelect = (val: any) => { |
||||||
|
livetypeSelected.value = val |
||||||
|
} |
||||||
|
const onDroneSelect = (val: any) => { |
||||||
|
droneSelected.value = val |
||||||
|
const temp: Array<{}> = [] |
||||||
|
cameraList.value = [] |
||||||
|
if (droneSelected.value) { |
||||||
|
const droneTemp = livestreamSource.value |
||||||
|
droneTemp.forEach((ele: any) => { |
||||||
|
const drone = ele |
||||||
|
if (drone.cameras_list && drone.sn === droneSelected.value) { |
||||||
|
const cameraListTemp = drone.cameras_list |
||||||
|
console.info(ele) |
||||||
|
cameraListTemp.forEach((ele: any) => { |
||||||
|
temp.push({ label: ele.name, value: ele.index }) |
||||||
|
}) |
||||||
|
cameraList.value = temp |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
const onCameraSelect = (val: any) => { |
||||||
|
cameraSelected.value = val |
||||||
|
const result: Array<{}> = [] |
||||||
|
if (cameraSelected.value) { |
||||||
|
const droneTemp = livestreamSource.value |
||||||
|
droneTemp.forEach((ele: any) => { |
||||||
|
const drone = ele |
||||||
|
if (drone.sn === droneSelected.value) { |
||||||
|
const cameraListTemp = drone.cameras_list |
||||||
|
cameraListTemp.forEach((ele: any) => { |
||||||
|
const camera = ele |
||||||
|
if (camera.index === cameraSelected.value) { |
||||||
|
const videoListTemp = camera.videos_list |
||||||
|
videoListTemp.forEach((ele: any) => { |
||||||
|
result.push({ label: ele.type, value: ele.index }) |
||||||
|
}) |
||||||
|
videoList.value = result |
||||||
|
videoSeleted.value = videoList.value[0]?.value |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
const onVideoSelect = (val: any) => { |
||||||
|
videoSeleted.value = val |
||||||
|
} |
||||||
|
const onClaritySelect = (val: any) => { |
||||||
|
claritySeleted.value = val |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import '/@/styles/index.scss'; |
||||||
|
</style> |
@ -0,0 +1,96 @@ |
|||||||
|
import store from '/@/store' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import { ELocalStorageKey } from '/@/types' |
||||||
|
import { getDeviceBySn } from '/@/api/manage' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
|
||||||
|
export function deviceTsaUpdate () { |
||||||
|
const root = getRoot() |
||||||
|
const AMap = root.$aMapObj |
||||||
|
|
||||||
|
const map = root.$aMap |
||||||
|
const icons: { |
||||||
|
[key: string]: string |
||||||
|
} = { |
||||||
|
'sub-device' : '/@/assets/icons/drone.png', |
||||||
|
'gateway': '/@/assets/icons/rc.png', |
||||||
|
'dock': '/@/assets/icons/dock.png' |
||||||
|
} |
||||||
|
const markers = store.state.markerInfo.coverMap |
||||||
|
const paths = store.state.markerInfo.pathMap |
||||||
|
|
||||||
|
const passedPolyline = new AMap.Polyline({ |
||||||
|
map: map, |
||||||
|
strokeColor: '#939393' // 线颜色
|
||||||
|
}) |
||||||
|
|
||||||
|
function initIcon (type: string) { |
||||||
|
return new AMap.Icon({ |
||||||
|
image: icons[type], |
||||||
|
imageSize: new AMap.Size(40, 40) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function initMarker (type: string, name: string, sn: string, lng?: number, lat?: number) { |
||||||
|
if (markers[sn]) { |
||||||
|
return |
||||||
|
} |
||||||
|
markers[sn] = new AMap.Marker({ |
||||||
|
position: new AMap.LngLat(lng ? lng : 113.935913, lat ? lat : 22.525335), |
||||||
|
icon: initIcon(type), |
||||||
|
title: name, |
||||||
|
anchor: 'top-center', |
||||||
|
offset: [0, -20], |
||||||
|
}) |
||||||
|
root.$aMap.add(markers[sn]) |
||||||
|
|
||||||
|
// markers[sn].on('moving', function (e: any) {
|
||||||
|
// let path = paths[sn]
|
||||||
|
// if (!path) {
|
||||||
|
// paths[sn] = e.passedPath
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// path.push(e.passedPath[0])
|
||||||
|
// path.push(e.passedPath[1])
|
||||||
|
// passedPolyline.setPath(path)
|
||||||
|
// })
|
||||||
|
} |
||||||
|
function removeMarker (sn: string) { |
||||||
|
if (!markers[sn]) { |
||||||
|
return |
||||||
|
} |
||||||
|
root.$aMap.remove(markers[sn]) |
||||||
|
passedPolyline.setPath([]) |
||||||
|
delete markers[sn] |
||||||
|
delete paths[sn] |
||||||
|
} |
||||||
|
function addMarker(sn: string, lng?: number, lat?: number) { |
||||||
|
getDeviceBySn(localStorage.getItem(ELocalStorageKey.WorkspaceId)!, sn) |
||||||
|
.then(data => { |
||||||
|
if (data.code !== 0) { |
||||||
|
message.error(data.message) |
||||||
|
return |
||||||
|
} |
||||||
|
initMarker(data.data.domain, data.data.nickname, sn, lng, lat) |
||||||
|
}) |
||||||
|
} |
||||||
|
function moveTo(sn: string, lng: number, lat: number) { |
||||||
|
let marker = markers[sn] |
||||||
|
if (!marker) { |
||||||
|
addMarker(sn, lng, lat) |
||||||
|
marker = markers[sn] |
||||||
|
return |
||||||
|
} |
||||||
|
marker.moveTo([lng, lat], { |
||||||
|
duration: 1800, |
||||||
|
autoRotation: true |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
marker: markers, |
||||||
|
initMarker, |
||||||
|
removeMarker, |
||||||
|
moveTo |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
<template> |
||||||
|
<a-layout class="flex-display" style="height: 100vh; background-color: white;"> |
||||||
|
<div class="height100 width100 flex-column flex-justify-start flex-align-start"> |
||||||
|
<a-row class="pt20 pl20" style="height: 45px; width: 100vw" align="middle"> |
||||||
|
<a-col :span="1"> |
||||||
|
<span style="color: #1fa3f6" class="fz26"><SendOutlined rotate="90" /></span> |
||||||
|
</a-col> |
||||||
|
<a-col :span="20"> |
||||||
|
<span class="fz20 pl5">{{ drone.data.model }}</span> |
||||||
|
</a-col> |
||||||
|
<a-col :span="3"> |
||||||
|
<span class="fz16" v-if="drone.data.bound_status" style="color: #737373">Bound</span> |
||||||
|
<a-button type="primary" @click="onBindDevice" v-else>Bind</a-button> |
||||||
|
</a-col> |
||||||
|
</a-row> |
||||||
|
</div> |
||||||
|
</a-layout> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { SendOutlined } from '@ant-design/icons-vue' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { onMounted, reactive, ref } from 'vue' |
||||||
|
import { BindBody, bindDevice } from '/@/api/manage' |
||||||
|
import apiPilot from '/@/api/pilot-bridge' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import { ELocalStorageKey } from '/@/types' |
||||||
|
import { DeviceStatus } from '/@/types/device' |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
interface DeviceStatusData { |
||||||
|
data: DeviceStatus |
||||||
|
} |
||||||
|
const drone = reactive<DeviceStatusData>({ |
||||||
|
data: JSON.parse(localStorage.getItem(ELocalStorageKey.Device)!) |
||||||
|
}) |
||||||
|
|
||||||
|
function onBindDevice () { |
||||||
|
const bindParam: BindBody = { |
||||||
|
device_sn: drone.data.sn, |
||||||
|
user_id: localStorage.getItem(ELocalStorageKey.UserId)!, |
||||||
|
workspace_id: localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
} |
||||||
|
bindDevice(bindParam).then(bindRes => { |
||||||
|
if (bindRes.code !== 0) { |
||||||
|
message.error('bind failed:' + bindRes.message) |
||||||
|
console.error(bindRes.message) |
||||||
|
return |
||||||
|
} |
||||||
|
drone.data.bound_status = true |
||||||
|
localStorage.setItem(ELocalStorageKey.Device, JSON.stringify(drone.data)) |
||||||
|
}) |
||||||
|
} |
||||||
|
</script> |
@ -0,0 +1,57 @@ |
|||||||
|
<template> |
||||||
|
<a-layout class="width-100 flex-display" style="height: 100vh"> |
||||||
|
<a-layout-header class="header"> |
||||||
|
<Topbar /> |
||||||
|
</a-layout-header> |
||||||
|
<a-layout-content> |
||||||
|
<router-view /> |
||||||
|
</a-layout-content> |
||||||
|
|
||||||
|
</a-layout> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import Topbar from './topbar.vue' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { onMounted, reactive, ref, UnwrapRef, watch } from 'vue' |
||||||
|
import { getPlatformInfo, getUserInfo } from '/@/api/manage' |
||||||
|
import websocket from '/@/api/websocket' |
||||||
|
import { useGMapCover } from '/@/hooks/use-g-map-cover' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { ELocalStorageKey, ERouterName } from '/@/types' |
||||||
|
import ReconnectingWebSocket from 'reconnecting-websocket' |
||||||
|
|
||||||
|
interface FormState { |
||||||
|
user: string |
||||||
|
password: string |
||||||
|
} |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
const showLogin = ref(true) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
const token = localStorage.getItem(ELocalStorageKey.Token) |
||||||
|
if (!token) { |
||||||
|
root.$router.push(ERouterName.PROJECT) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import '/@/styles/index.scss'; |
||||||
|
|
||||||
|
.fontBold { |
||||||
|
font-weight: 500; |
||||||
|
font-size: 18px; |
||||||
|
} |
||||||
|
|
||||||
|
.header { |
||||||
|
background-color: black; |
||||||
|
color: white; |
||||||
|
height: 60px; |
||||||
|
font-size: 15px; |
||||||
|
padding: 0 20px; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,251 @@ |
|||||||
|
<template> |
||||||
|
<div class="plan"> |
||||||
|
<div class="header"> |
||||||
|
Create Plan |
||||||
|
</div> |
||||||
|
<div class="content"> |
||||||
|
<a-form ref="valueRef" layout="horizontal" :hideRequiredMark="true" :rules="rules" :model="planBody"> |
||||||
|
<a-form-item label="Plan Name" name="name" :labelCol="{span: 24}"> |
||||||
|
<a-input style="background: black;" placeholder="Please enter plan name" v-model:value="planBody.name"/> |
||||||
|
</a-form-item> |
||||||
|
<a-form-item label="Flight Route" :wrapperCol="{offset: 7}" name="file_id"> |
||||||
|
<router-link |
||||||
|
:to="{name: 'select-plan'}" |
||||||
|
@click="selectRoute" |
||||||
|
> |
||||||
|
Select Route |
||||||
|
</router-link> |
||||||
|
</a-form-item> |
||||||
|
<a-form-item v-if="planBody.file_id" style="margin-top: -15px;"> |
||||||
|
<div class="wayline-panel" style="padding-top: 5px;"> |
||||||
|
<div class="title"> |
||||||
|
<a-tooltip :title="wayline.name"> |
||||||
|
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div> |
||||||
|
</a-tooltip> |
||||||
|
<div class="ml10"><UserOutlined /></div> |
||||||
|
<a-tooltip :title="wayline.user_name"> |
||||||
|
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||||
|
<span><RocketOutlined /></span> |
||||||
|
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span> |
||||||
|
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> |
||||||
|
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> |
||||||
|
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||||
|
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a-form-item> |
||||||
|
<a-form-item label="Device" :wrapperCol="{offset: 10}" v-model:value="planBody.dock_sn" name="dock_sn"> |
||||||
|
<router-link |
||||||
|
:to="{name: 'select-plan'}" |
||||||
|
@click="selectDevice" |
||||||
|
>Select Device</router-link> |
||||||
|
</a-form-item> |
||||||
|
<a-form-item v-if="planBody.dock_sn" style="margin-top: -15px;"> |
||||||
|
<div class="panel" style="padding-top: 5px;" @click="selectDock(dock)"> |
||||||
|
<div class="title"> |
||||||
|
<a-tooltip :title="dock.nickname"> |
||||||
|
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||||
|
<span><RocketOutlined /></span> |
||||||
|
<span class="ml5">{{ dock.children?.nickname }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a-form-item> |
||||||
|
<a-form-item label="Immediate"> |
||||||
|
<a-switch v-model:checked="planBody.immediate"> |
||||||
|
<template #checkedChildren><CheckOutlined /></template> |
||||||
|
<template #unCheckedChildren><CloseOutlined /></template> |
||||||
|
</a-switch> |
||||||
|
</a-form-item> |
||||||
|
<a-form-item style="position: absolute; bottom: 0px; margin-bottom: 0; margin-left: -10px; width: 280px;"> |
||||||
|
<div class="footer"> |
||||||
|
<a-button class="mr10" style="background: #3c3c3c;" @click="closePlan">Cancel |
||||||
|
</a-button> |
||||||
|
<a-button type="primary" @click="onSubmit" :disabled="disabled">OK |
||||||
|
</a-button> |
||||||
|
</div> |
||||||
|
</a-form-item> |
||||||
|
</a-form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-if="drawerVisible" style="position: absolute; left: 330px; width: 280px; height: 100vh; float: right; top: 0; z-index: 1000; color: white; background: #282828;"> |
||||||
|
<div> |
||||||
|
<router-view :name="routeName"/> |
||||||
|
</div> |
||||||
|
<div style="position: absolute; top: 15px; right: 10px;"> |
||||||
|
<a style="color: white;" @click="closePanel"><CloseOutlined /></a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, UnwrapRef } from 'vue' |
||||||
|
import { CheckOutlined, CloseOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { ELocalStorageKey, ERouterName } from '/@/types' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { WaylineFile } from '/@/types/wayline' |
||||||
|
import { Device, EDeviceType } from '/@/types/device' |
||||||
|
import { createPlan, CreatePlan } from '/@/api/wayline' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
const store = useMyStore() |
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
|
||||||
|
const wayline = computed<WaylineFile>(() => { |
||||||
|
return store.state.waylineInfo |
||||||
|
}) |
||||||
|
|
||||||
|
const dock = computed<Device>(() => { |
||||||
|
return store.state.dockInfo |
||||||
|
}) |
||||||
|
|
||||||
|
const disabled = ref(false) |
||||||
|
|
||||||
|
const routeName = ref('') |
||||||
|
const planBody: UnwrapRef<CreatePlan> = reactive({ |
||||||
|
name: '', |
||||||
|
file_id: computed(() => store.state.waylineInfo.id), |
||||||
|
dock_sn: computed(() => store.state.dockInfo.device_sn), |
||||||
|
immediate: false, |
||||||
|
type: 'wayline' |
||||||
|
}) |
||||||
|
const drawerVisible = ref(false) |
||||||
|
const valueRef = ref() |
||||||
|
const rules = { |
||||||
|
name: [ |
||||||
|
{ required: true, message: 'Please enter plan name.' }, |
||||||
|
{ max: 20, message: 'Length should be 1 to 20', trigger: 'blur' } |
||||||
|
], |
||||||
|
file_id: [{ required: true, message: 'Select Route' }], |
||||||
|
dock_sn: [{ required: true, message: 'Select Device' }] |
||||||
|
} |
||||||
|
|
||||||
|
function onSubmit () { |
||||||
|
valueRef.value.validate().then(() => { |
||||||
|
disabled.value = true |
||||||
|
createPlan(workspaceId, planBody) |
||||||
|
.then(res => { |
||||||
|
message.success('Saved Successfully') |
||||||
|
setTimeout(() => { |
||||||
|
disabled.value = false |
||||||
|
}, 1500) |
||||||
|
}).finally(() => { |
||||||
|
closePlan() |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function closePlan () { |
||||||
|
root.$router.push('/' + ERouterName.TASK) |
||||||
|
} |
||||||
|
|
||||||
|
function closePanel () { |
||||||
|
drawerVisible.value = false |
||||||
|
routeName.value = '' |
||||||
|
} |
||||||
|
|
||||||
|
function selectRoute () { |
||||||
|
drawerVisible.value = true |
||||||
|
routeName.value = 'WaylinePanel' |
||||||
|
} |
||||||
|
|
||||||
|
function selectDevice () { |
||||||
|
drawerVisible.value = true |
||||||
|
routeName.value = 'DockPanel' |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
|
||||||
|
.plan { |
||||||
|
background-color: #232323; |
||||||
|
color: white; |
||||||
|
padding-bottom: 0; |
||||||
|
height: 100vh; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
.header { |
||||||
|
height: 53px; |
||||||
|
border-bottom: 1px solid #4f4f4f; |
||||||
|
font-weight: 700; |
||||||
|
font-size: 16px; |
||||||
|
padding-left: 10px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
.content { |
||||||
|
height: 100%; |
||||||
|
form { |
||||||
|
margin: 10px; |
||||||
|
} |
||||||
|
form label, input { |
||||||
|
color: white; |
||||||
|
} |
||||||
|
} |
||||||
|
.footer { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
border-top: 1px solid #4f4f4f; |
||||||
|
min-height: 65px; |
||||||
|
margin-bottom: 0; |
||||||
|
padding-bottom: 0; |
||||||
|
button { |
||||||
|
width: 45%; |
||||||
|
color: white; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.wayline-panel { |
||||||
|
background: #3c3c3c; |
||||||
|
margin-left: auto; |
||||||
|
margin-right: auto; |
||||||
|
margin-top: 10px; |
||||||
|
height: 90px; |
||||||
|
width: 95%; |
||||||
|
font-size: 13px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
.title { |
||||||
|
display: flex; |
||||||
|
color: white; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
height: 30px; |
||||||
|
font-weight: bold; |
||||||
|
margin: 0px 10px 0 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
.panel { |
||||||
|
background: #3c3c3c; |
||||||
|
margin-left: auto; |
||||||
|
margin-right: auto; |
||||||
|
margin-top: 10px; |
||||||
|
height: 70px; |
||||||
|
width: 95%; |
||||||
|
font-size: 13px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
.title { |
||||||
|
display: flex; |
||||||
|
color: white; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
height: 30px; |
||||||
|
font-weight: bold; |
||||||
|
margin: 0px 10px 0 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,518 @@ |
|||||||
|
|
||||||
|
<template> |
||||||
|
<a-menu v-model:selectedKeys="current" mode="horizontal" @select="select"> |
||||||
|
<a-menu-item :key="EDeviceTypeName.Aircraft" class="ml20"> |
||||||
|
Aircraft |
||||||
|
</a-menu-item> |
||||||
|
<a-menu-item :key="EDeviceTypeName.Dock"> |
||||||
|
Dock |
||||||
|
</a-menu-item> |
||||||
|
</a-menu> |
||||||
|
<div class="table flex-display flex-column"> |
||||||
|
<a-table :columns="columns" :data-source="data.device" :pagination="paginationProp" @change="refreshData" row-key="device_sn" :expandedRowKeys="expandRows" |
||||||
|
:row-selection="rowSelection" :rowClassName="rowClassName" :scroll="{ x: '100%', y: 600 }" |
||||||
|
:expandIcon="expandIcon" :loading="loading"> |
||||||
|
<template v-for="col in ['nickname']" #[col]="{ text, record }" :key="col"> |
||||||
|
<div> |
||||||
|
<a-input |
||||||
|
v-if="editableData[record.device_sn]" |
||||||
|
v-model:value="editableData[record.device_sn][col]" |
||||||
|
style="margin: -5px 0" |
||||||
|
/> |
||||||
|
<template v-else> |
||||||
|
{{ text }} |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-for="col in ['sn', 'workspace']" #[col]="{ text }" :key="col"> |
||||||
|
<a-tooltip :title="text"> |
||||||
|
<span>{{ text }}</span> |
||||||
|
</a-tooltip> |
||||||
|
</template> |
||||||
|
<template #status="{ text }"> |
||||||
|
<span v-if="text" class="flex-row flex-align-center"> |
||||||
|
<span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: green;" /> |
||||||
|
<span>Online</span> |
||||||
|
</span> |
||||||
|
<span class="flex-row flex-align-center" v-else> |
||||||
|
<span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: red;" /> |
||||||
|
<span>Offline</span> |
||||||
|
</span> |
||||||
|
</template> |
||||||
|
<template #action="{ record }"> |
||||||
|
<div class="editable-row-operations"> |
||||||
|
<span v-if="editableData[record.device_sn]"> |
||||||
|
<a-tooltip title="Confirm changes"> |
||||||
|
<span @click="save(record)" style="color: #28d445;"><CheckOutlined /></span> |
||||||
|
</a-tooltip> |
||||||
|
<a-tooltip title="Modification canceled"> |
||||||
|
<span @click="() => delete editableData[record.device_sn]" class="ml15" style="color: #e70102;"><CloseOutlined /></span> |
||||||
|
</a-tooltip> |
||||||
|
</span> |
||||||
|
<span v-else class="flex-align-center flex-row" style="color: #2d8cf0"> |
||||||
|
<a-tooltip v-if="current.indexOf(EDeviceTypeName.Dock) !== -1" title="Hms Info"> |
||||||
|
<FileSearchOutlined @click="showHms(record)"/> |
||||||
|
</a-tooltip> |
||||||
|
<a-tooltip title="Edit"> |
||||||
|
<EditOutlined @click="edit(record)" class="ml10" /> |
||||||
|
</a-tooltip> |
||||||
|
<a-tooltip title="Delete"> |
||||||
|
<DeleteOutlined @click="() => { deleteTip = true, deleteSn = record.device_sn }" class="ml15" /> |
||||||
|
</a-tooltip> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
</a-table> |
||||||
|
<a-modal v-model:visible="deleteTip" width="450px" :closable="false" centered :okButtonProps="{ danger: true }" @ok="unbind"> |
||||||
|
<p class="pt10 pl20" style="height: 50px;">Delete device from workspace?</p> |
||||||
|
<template #title> |
||||||
|
<div class="flex-row flex-justify-center"> |
||||||
|
<span>Delete devices</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</a-modal> |
||||||
|
|
||||||
|
<a-drawer |
||||||
|
title="Hms Info" |
||||||
|
placement="right" |
||||||
|
v-model:visible="hmsVisible" |
||||||
|
:width="800"> |
||||||
|
<div class="flex-row flex-align-center"> |
||||||
|
<div style="width: 240px;"> |
||||||
|
<a-range-picker |
||||||
|
v-model:value="time" |
||||||
|
format="YYYY-MM-DD" |
||||||
|
:placeholder="['Start Time', 'End Time']" |
||||||
|
@change="onTimeChange"/> |
||||||
|
</div> |
||||||
|
<div class="ml5"> |
||||||
|
<a-select |
||||||
|
style="width: 150px" |
||||||
|
v-model:value="param.level" |
||||||
|
@select="onLevelSelect"> |
||||||
|
<a-select-option |
||||||
|
v-for="item in levels" |
||||||
|
:key="item.label" |
||||||
|
:value="item.value" |
||||||
|
> |
||||||
|
{{ item.label }} |
||||||
|
</a-select-option> |
||||||
|
</a-select> |
||||||
|
</div> |
||||||
|
<div class="ml5"> |
||||||
|
<a-select |
||||||
|
v-model:value="param.domain" |
||||||
|
:disabled="!param.children_sn || !param.device_sn" |
||||||
|
style="width: 150px" |
||||||
|
@select="onDeviceTypeSelect"> |
||||||
|
<a-select-option |
||||||
|
v-for="item in deviceTypes" |
||||||
|
:key="item.label" |
||||||
|
:value="item.value" |
||||||
|
> |
||||||
|
{{ item.label }} |
||||||
|
</a-select-option> |
||||||
|
</a-select> |
||||||
|
</div> |
||||||
|
<div class="ml5"> |
||||||
|
<a-input-search |
||||||
|
v-model:value="param.message" |
||||||
|
placeholder="input search message" |
||||||
|
style="width: 200px" |
||||||
|
@search="getHms"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<a-table :columns="hmsColumns" :scroll="{ x: '100%', y: 600 }" :data-source="hmsData.data" :pagination="hmsPaginationProp" @change="refreshHmsData" row-key="hms_id" |
||||||
|
:rowClassName="rowClassName" :loading="loading"> |
||||||
|
<template #time="{ record }"> |
||||||
|
<div>{{ record.create_time }}</div> |
||||||
|
<div :style="record.update_time ? '' : record.level === EHmsLevel.CAUTION ? 'color: orange;' : |
||||||
|
record.level === EHmsLevel.WARN ? 'color: red;' : 'color: #28d445;'">{{ record.update_time ?? 'It is happening...' }}</div> |
||||||
|
</template> |
||||||
|
<template #level="{ text }"> |
||||||
|
<div class="flex-row flex-align-center"> |
||||||
|
<div :class="text === EHmsLevel.CAUTION ? 'caution' : text === EHmsLevel.WARN ? 'warn' : 'notice'" style="width: 10px; height: 10px; border-radius: 50%;"></div> |
||||||
|
<div style="margin-left: 3px;">{{ EHmsLevel[text] }}</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-for="col in ['code', 'message']" #[col]="{ text }" :key="col"> |
||||||
|
<a-tooltip :title="text"> |
||||||
|
<span>{{ text }}</span> |
||||||
|
</a-tooltip> |
||||||
|
</template> |
||||||
|
</a-table> |
||||||
|
</div> |
||||||
|
</a-drawer> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<script lang="ts" setup> |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { ColumnProps, TableState } from 'ant-design-vue/lib/table/interface' |
||||||
|
import { h, onMounted, reactive, ref, UnwrapRef } from 'vue' |
||||||
|
import { IPage } from '/@/api/http/type' |
||||||
|
import { BindBody, bindDevice, getBindingDevices, getDeviceHms, HmsQueryBody, unbindDevice, updateDevice } from '/@/api/manage' |
||||||
|
import { EDeviceTypeName, EHmsLevel, ELocalStorageKey } from '/@/types' |
||||||
|
import { EditOutlined, CheckOutlined, CloseOutlined, DeleteOutlined, FileSearchOutlined } from '@ant-design/icons-vue' |
||||||
|
import { Device, DeviceHms } from '/@/types/device' |
||||||
|
import moment, { Moment } from 'moment' |
||||||
|
|
||||||
|
interface DeviceData { |
||||||
|
device: Device[] |
||||||
|
} |
||||||
|
const loading = ref(true) |
||||||
|
const deleteTip = ref<boolean>(false) |
||||||
|
const deleteSn = ref<string>() |
||||||
|
const hmsVisible = ref<boolean>(false) |
||||||
|
const columns: ColumnProps[] = [ |
||||||
|
{ title: 'Model', dataIndex: 'device_name', width: '10%', className: 'titleStyle' }, |
||||||
|
{ title: 'SN', dataIndex: 'device_sn', width: '10%', className: 'titleStyle', ellipsis: true, slots: { customRender: 'sn' } }, |
||||||
|
{ |
||||||
|
title: 'Name', |
||||||
|
dataIndex: 'nickname', |
||||||
|
width: '15%', |
||||||
|
sorter: (a: Device, b: Device) => a.nickname.localeCompare(b.nickname), |
||||||
|
className: 'titleStyle', |
||||||
|
ellipsis: true, |
||||||
|
slots: { customRender: 'nickname' } |
||||||
|
}, |
||||||
|
{ title: 'Firmware Version', dataIndex: 'firmware_version', width: '10%', className: 'titleStyle' }, |
||||||
|
{ title: 'Status', dataIndex: 'status', width: '100px', className: 'titleStyle', slots: { customRender: 'status' } }, |
||||||
|
{ |
||||||
|
title: 'Workspace', |
||||||
|
dataIndex: 'workspace_name', |
||||||
|
width: '10%', |
||||||
|
className: 'titleStyle', |
||||||
|
ellipsis: true, |
||||||
|
slots: { customRender: 'workspace' }, |
||||||
|
customRender: ({ text, record, index }) => { |
||||||
|
const obj = { |
||||||
|
children: text, |
||||||
|
props: {} as any, |
||||||
|
} |
||||||
|
if (current.value.indexOf(EDeviceTypeName.Aircraft) !== -1 || (!record.child_device_sn && record.domain === EDeviceTypeName.Dock)) { |
||||||
|
return obj |
||||||
|
} |
||||||
|
|
||||||
|
obj.props.rowSpan = record.domain === EDeviceTypeName.Dock ? 2 : 0 |
||||||
|
return obj |
||||||
|
} |
||||||
|
}, |
||||||
|
{ title: 'Joined', dataIndex: 'bound_time', width: '15%', sorter: (a: Device, b: Device) => a.bound_time.localeCompare(b.bound_time), className: 'titleStyle' }, |
||||||
|
{ title: 'Last Online', dataIndex: 'login_time', width: '15%', sorter: (a: Device, b: Device) => a.login_time.localeCompare(b.login_time), className: 'titleStyle' }, |
||||||
|
{ |
||||||
|
title: 'Actions', |
||||||
|
dataIndex: 'actions', |
||||||
|
className: 'titleStyle', |
||||||
|
slots: { customRender: 'action' } |
||||||
|
}, |
||||||
|
] |
||||||
|
const expandIcon = (props: any) => { |
||||||
|
if (judgeCurrentType(EDeviceTypeName.Dock) && !props.expanded) { |
||||||
|
return h('div', |
||||||
|
{ |
||||||
|
style: 'border-left: 2px solid rgb(200,200,200); border-bottom: 2px solid rgb(200,200,200); height: 16px; width: 16px; float: left;', |
||||||
|
class: 'mt-5 ml0', |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
const rowClassName = (record: any, index: number) => { |
||||||
|
const className = [] |
||||||
|
if ((index & 1) === 0) { |
||||||
|
className.push('table-striped') |
||||||
|
} |
||||||
|
if (record.domain !== EDeviceTypeName.Dock) { |
||||||
|
className.push('child-row') |
||||||
|
} |
||||||
|
return className.toString().replaceAll(',', ' ') |
||||||
|
} |
||||||
|
|
||||||
|
const expandRows = ref<string[]>([]) |
||||||
|
const data = reactive<DeviceData>({ |
||||||
|
device: [] |
||||||
|
}) |
||||||
|
|
||||||
|
const paginationProp = reactive({ |
||||||
|
pageSizeOptions: ['20', '50', '100'], |
||||||
|
showQuickJumper: true, |
||||||
|
showSizeChanger: true, |
||||||
|
pageSize: 50, |
||||||
|
current: 1, |
||||||
|
total: 0 |
||||||
|
}) |
||||||
|
|
||||||
|
const rowSelection = { |
||||||
|
onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => { |
||||||
|
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows) |
||||||
|
}, |
||||||
|
onSelect: (record: any, selected: boolean, selectedRows: []) => { |
||||||
|
console.log(record, selected, selectedRows) |
||||||
|
}, |
||||||
|
onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => { |
||||||
|
console.log(selected, selectedRows, changeRows) |
||||||
|
}, |
||||||
|
getCheckboxProps: (record: any) => ({ |
||||||
|
disabled: judgeCurrentType(EDeviceTypeName.Dock) && record.domain !== EDeviceTypeName.Dock, |
||||||
|
style: judgeCurrentType(EDeviceTypeName.Dock) && record.domain !== EDeviceTypeName.Dock ? 'display: none' : '' |
||||||
|
}), |
||||||
|
} |
||||||
|
type Pagination = TableState['pagination'] |
||||||
|
|
||||||
|
const body: IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 50 |
||||||
|
} |
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
|
||||||
|
const editableData: UnwrapRef<Record<string, Device>> = reactive({}) |
||||||
|
|
||||||
|
const current = ref([EDeviceTypeName.Aircraft]) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getDevices(workspaceId, body, current.value[0]) |
||||||
|
}) |
||||||
|
|
||||||
|
function judgeCurrentType (type: EDeviceTypeName): boolean { |
||||||
|
return current.value.indexOf(type) !== -1 |
||||||
|
} |
||||||
|
|
||||||
|
function getDevices (workspaceId: string, body: IPage, domain: string) { |
||||||
|
loading.value = true |
||||||
|
getBindingDevices(workspaceId, body, domain).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
const resData: Device[] = res.data.list |
||||||
|
expandRows.value = [] |
||||||
|
resData.forEach((val: any) => { |
||||||
|
if (val.children) { |
||||||
|
val.children = [val.children] |
||||||
|
} |
||||||
|
if (judgeCurrentType(EDeviceTypeName.Dock)) { |
||||||
|
expandRows.value.push(val.device_sn) |
||||||
|
} |
||||||
|
}) |
||||||
|
data.device = resData |
||||||
|
paginationProp.total = res.data.pagination.total |
||||||
|
paginationProp.current = res.data.pagination.page |
||||||
|
loading.value = false |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function refreshData (page: Pagination) { |
||||||
|
body.page = page?.current! |
||||||
|
body.page_size = page?.pageSize! |
||||||
|
getDevices(workspaceId, body, current.value[0]) |
||||||
|
} |
||||||
|
|
||||||
|
function edit (record: Device) { |
||||||
|
editableData[record.device_sn] = record |
||||||
|
} |
||||||
|
|
||||||
|
function save (record: Device) { |
||||||
|
delete editableData[record.device_sn] |
||||||
|
updateDevice({ nickname: record.nickname }, workspaceId, record.device_sn) |
||||||
|
} |
||||||
|
|
||||||
|
function showDeleteTip (sn: any) { |
||||||
|
deleteTip.value = true |
||||||
|
} |
||||||
|
|
||||||
|
function unbind () { |
||||||
|
deleteTip.value = false |
||||||
|
unbindDevice(deleteSn.value?.toString()!).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
getDevices(workspaceId, body, current.value[0]) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function select (item: any) { |
||||||
|
getDevices(workspaceId, body, item.key) |
||||||
|
} |
||||||
|
|
||||||
|
const hmsColumns: ColumnProps[] = [ |
||||||
|
{ title: 'Alarm Begin | End Time', dataIndex: 'create_time', width: '25%', className: 'titleStyle', slots: { customRender: 'time' } }, |
||||||
|
{ title: 'Level', dataIndex: 'level', width: '120px', className: 'titleStyle', slots: { customRender: 'level' } }, |
||||||
|
{ title: 'Device', dataIndex: 'domain', width: '12%', className: 'titleStyle' }, |
||||||
|
{ title: 'Error Code', dataIndex: 'key', width: '20%', className: 'titleStyle', slots: { customRender: 'code' } }, |
||||||
|
{ title: 'Hms Message', dataIndex: 'message_en', className: 'titleStyle', ellipsis: true, slots: { customRender: 'message' } }, |
||||||
|
] |
||||||
|
|
||||||
|
interface DeviceHmsData { |
||||||
|
data: DeviceHms[] |
||||||
|
} |
||||||
|
const hmsData = reactive<DeviceHmsData>({ |
||||||
|
data: [] |
||||||
|
}) |
||||||
|
|
||||||
|
const hmsPaginationProp = reactive({ |
||||||
|
pageSizeOptions: ['20', '50', '100'], |
||||||
|
showQuickJumper: true, |
||||||
|
showSizeChanger: true, |
||||||
|
pageSize: 50, |
||||||
|
current: 1, |
||||||
|
total: 0 |
||||||
|
}) |
||||||
|
|
||||||
|
const hmsPage: IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 50 |
||||||
|
} |
||||||
|
|
||||||
|
function showHms (dock: Device) { |
||||||
|
hmsVisible.value = true |
||||||
|
if (dock.domain === EDeviceTypeName.Dock) { |
||||||
|
param.domain = '' |
||||||
|
getDeviceHmsBySn(dock.device_sn, dock.children?.[0].device_sn ?? '') |
||||||
|
} |
||||||
|
if (dock.domain === EDeviceTypeName.Aircraft) { |
||||||
|
param.domain = EDeviceTypeName.Aircraft |
||||||
|
getDeviceHmsBySn('', dock.device_sn) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function refreshHmsData (page: Pagination) { |
||||||
|
hmsPage.page = page?.current! |
||||||
|
hmsPage.page_size = page?.pageSize! |
||||||
|
getHms() |
||||||
|
} |
||||||
|
|
||||||
|
const param = reactive<HmsQueryBody>({ |
||||||
|
sns: [], |
||||||
|
device_sn: '', |
||||||
|
children_sn: '', |
||||||
|
language: 'en', |
||||||
|
begin_time: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0), |
||||||
|
end_time: new Date().setHours(23, 59, 59, 999), |
||||||
|
domain: '', |
||||||
|
level: '', |
||||||
|
message: '' |
||||||
|
}) |
||||||
|
|
||||||
|
const levels = [ |
||||||
|
{ |
||||||
|
label: 'All', |
||||||
|
value: '' |
||||||
|
}, { |
||||||
|
label: EHmsLevel[0], |
||||||
|
value: EHmsLevel.NOTICE |
||||||
|
}, { |
||||||
|
label: EHmsLevel[1], |
||||||
|
value: EHmsLevel.CAUTION |
||||||
|
}, { |
||||||
|
label: EHmsLevel[2], |
||||||
|
value: EHmsLevel.WARN |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
const deviceTypes = [ |
||||||
|
{ |
||||||
|
label: 'All', |
||||||
|
value: '' |
||||||
|
}, { |
||||||
|
label: EDeviceTypeName.Aircraft, |
||||||
|
value: EDeviceTypeName.Aircraft |
||||||
|
}, { |
||||||
|
label: EDeviceTypeName.Dock, |
||||||
|
value: EDeviceTypeName.Dock |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
const time = ref([moment(param.begin_time), moment(param.end_time)]) |
||||||
|
|
||||||
|
function getHms () { |
||||||
|
getDeviceHms(param, workspaceId, hmsPage) |
||||||
|
.then(res => { |
||||||
|
hmsPaginationProp.total = res.data.pagination.total |
||||||
|
hmsPaginationProp.current = res.data.pagination.page |
||||||
|
hmsData.data = res.data.list |
||||||
|
hmsData.data.forEach(hms => { |
||||||
|
hms.domain = hms.sn === param.children_sn ? EDeviceTypeName.Aircraft : EDeviceTypeName.Dock |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function getDeviceHmsBySn (sn: string, childSn: string) { |
||||||
|
param.device_sn = sn |
||||||
|
param.children_sn = childSn |
||||||
|
param.sns = [param.device_sn, param.children_sn] |
||||||
|
getHms() |
||||||
|
} |
||||||
|
|
||||||
|
function onTimeChange (newTime: [Moment, Moment]) { |
||||||
|
param.begin_time = newTime[0].valueOf() |
||||||
|
param.end_time = newTime[1].valueOf() |
||||||
|
getHms() |
||||||
|
} |
||||||
|
|
||||||
|
function onDeviceTypeSelect (val: string) { |
||||||
|
param.sns = [param.device_sn, param.children_sn] |
||||||
|
if (val === EDeviceTypeName.Dock) { |
||||||
|
param.sns = [param.device_sn, ''] |
||||||
|
} |
||||||
|
if (val === EDeviceTypeName.Aircraft) { |
||||||
|
param.sns = ['', param.children_sn] |
||||||
|
} |
||||||
|
getHms() |
||||||
|
} |
||||||
|
|
||||||
|
function onLevelSelect (val: number) { |
||||||
|
param.level = val |
||||||
|
getHms() |
||||||
|
} |
||||||
|
</script> |
||||||
|
<style lang="scss"> |
||||||
|
|
||||||
|
.table { |
||||||
|
background-color: white; |
||||||
|
margin: 20px; |
||||||
|
padding: 20px; |
||||||
|
height: 88vh; |
||||||
|
} |
||||||
|
.table-striped { |
||||||
|
background-color: #f7f9fa; |
||||||
|
} |
||||||
|
.ant-table { |
||||||
|
border-top: 1px solid rgb(0,0,0,0.06); |
||||||
|
border-bottom: 1px solid rgb(0,0,0,0.06); |
||||||
|
} |
||||||
|
.ant-table-tbody tr td { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
.ant-table td { |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
.ant-table-thead tr th { |
||||||
|
background: white !important; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
th.ant-table-selection-column { |
||||||
|
background-color: white !important; |
||||||
|
} |
||||||
|
.ant-table-header { |
||||||
|
background-color: white !important; |
||||||
|
} |
||||||
|
.child-row { |
||||||
|
height: 70px; |
||||||
|
} |
||||||
|
.notice { |
||||||
|
background: $success; |
||||||
|
overflow: hidden; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.caution { |
||||||
|
background: orange; |
||||||
|
cursor: pointer; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.warn { |
||||||
|
background: red; |
||||||
|
cursor: pointer; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,100 @@ |
|||||||
|
<template> |
||||||
|
<div> |
||||||
|
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;"> |
||||||
|
<a-row> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
<a-col :span="22">Devices</a-col> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
</a-row> |
||||||
|
</div> |
||||||
|
<div v-if="docksData.data.length !== 0"> |
||||||
|
<div v-for="dock in docksData.data" :key="dock.device_sn"> |
||||||
|
<div v-if="dock.children" class="panel" style="padding-top: 5px;" @click="selectDock(dock)"> |
||||||
|
<div class="title"> |
||||||
|
<a-tooltip :title="dock.nickname"> |
||||||
|
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ dock.nickname }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||||
|
<span><RocketOutlined /></span> |
||||||
|
<span class="ml5">{{ dock.children?.nickname }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-else> |
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { reactive } from '@vue/reactivity' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { onMounted, ref } from 'vue' |
||||||
|
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline' |
||||||
|
import { EDeviceTypeName, ELocalStorageKey } from '/@/types' |
||||||
|
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue' |
||||||
|
import { Device, EDeviceType } from '/@/types/device' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { getBindingDevices } from '/@/api/manage' |
||||||
|
import { IPage } from '/@/api/http/type' |
||||||
|
|
||||||
|
const store = useMyStore() |
||||||
|
|
||||||
|
const docksData = reactive({ |
||||||
|
data: [] as Device[] |
||||||
|
}) |
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getDocks() |
||||||
|
}) |
||||||
|
const body: IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 100 |
||||||
|
} |
||||||
|
function getDocks () { |
||||||
|
getBindingDevices(workspaceId, body, EDeviceTypeName.Dock).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
docksData.data = [] |
||||||
|
res.data.list.forEach((dock: any) => { |
||||||
|
if (dock.child_device_sn) { |
||||||
|
docksData.data.push(dock) |
||||||
|
} |
||||||
|
}) |
||||||
|
console.info(docksData.data) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function selectDock (dock: Device) { |
||||||
|
store.commit('SET_SELECT_DOCK_INFO', dock) |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.panel { |
||||||
|
background: #3c3c3c; |
||||||
|
margin-left: auto; |
||||||
|
margin-right: auto; |
||||||
|
margin-top: 10px; |
||||||
|
height: 70px; |
||||||
|
width: 95%; |
||||||
|
font-size: 13px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
.title { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
height: 30px; |
||||||
|
font-weight: bold; |
||||||
|
margin: 0px 10px 0 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,169 @@ |
|||||||
|
|
||||||
|
<template> |
||||||
|
<div class="table flex-display flex-column"> |
||||||
|
<a-table :columns="columns" :data-source="data.member" :pagination="paginationProp" @change="refreshData" row-key="user_id" |
||||||
|
:row-selection="rowSelection" :rowClassName="(record, index) => ((index % 2) === 0 ? 'table-striped' : null)" :scroll="{ x: '100%', y: 600 }"> |
||||||
|
<template v-for="col in ['mqtt_username', 'mqtt_password']" #[col]="{ text, record }" :key="col"> |
||||||
|
<div> |
||||||
|
<a-input |
||||||
|
v-if="editableData[record.user_id]" |
||||||
|
v-model:value="editableData[record.user_id][col]" |
||||||
|
style="margin: -5px 0" |
||||||
|
/> |
||||||
|
<template v-else> |
||||||
|
{{ text }} |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template #action="{ record }"> |
||||||
|
<div class="editable-row-operations"> |
||||||
|
<span v-if="editableData[record.user_id]"> |
||||||
|
<a-tooltip title="Confirm changes"> |
||||||
|
<span @click="save(record)" style="color: #28d445;"><CheckOutlined /></span> |
||||||
|
</a-tooltip> |
||||||
|
<a-tooltip title="Modification canceled"> |
||||||
|
<span @click="() => delete editableData[record.user_id]" class="ml15" style="color: #e70102;"><CloseOutlined /></span> |
||||||
|
</a-tooltip> |
||||||
|
</span> |
||||||
|
<span v-else class="fz18 flex-align-center flex-row" style="color: #2d8cf0"> |
||||||
|
<EditOutlined @click="edit(record)" /> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
</a-table> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<script lang="ts" setup> |
||||||
|
import { message, PaginationProps } from 'ant-design-vue' |
||||||
|
import { TableState } from 'ant-design-vue/lib/table/interface' |
||||||
|
import { onMounted, reactive, Ref, ref, UnwrapRef } from 'vue' |
||||||
|
import { IPage } from '/@/api/http/type' |
||||||
|
import { getAllUsersInfo, updateUserInfo } from '/@/api/manage' |
||||||
|
import { ELocalStorageKey } from '/@/types' |
||||||
|
import { EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons-vue' |
||||||
|
|
||||||
|
export interface Member { |
||||||
|
user_id: string |
||||||
|
username: string |
||||||
|
user_type: string |
||||||
|
workspace_name: string |
||||||
|
create_time: string |
||||||
|
mqtt_username: string |
||||||
|
mqtt_password: string |
||||||
|
} |
||||||
|
|
||||||
|
interface MemberData { |
||||||
|
member: Member[] |
||||||
|
} |
||||||
|
const columns = [ |
||||||
|
{ title: 'Account', dataIndex: 'username', width: 250, sorter: (a: Member, b: Member) => a.username.localeCompare(b.username), className: 'titleStyle' }, |
||||||
|
{ title: 'User Type', dataIndex: 'user_type', width: 250, className: 'titleStyle' }, |
||||||
|
{ title: 'Workspace Name', dataIndex: 'workspace_name', width: 250, className: 'titleStyle' }, |
||||||
|
{ title: 'Mqtt Username', dataIndex: 'mqtt_username', width: 250, className: 'titleStyle', slots: { customRender: 'mqtt_username' } }, |
||||||
|
{ title: 'Mqtt Password', dataIndex: 'mqtt_password', width: 250, className: 'titleStyle', slots: { customRender: 'mqtt_password' } }, |
||||||
|
{ title: 'Joined', dataIndex: 'create_time', width: 250, sorter: (a: Member, b: Member) => a.create_time.localeCompare(b.create_time), className: 'titleStyle' }, |
||||||
|
{ title: 'Action', dataIndex: 'action', className: 'titleStyle', slots: { customRender: 'action' } }, |
||||||
|
] |
||||||
|
|
||||||
|
const data = reactive<MemberData>({ |
||||||
|
member: [] |
||||||
|
}) |
||||||
|
|
||||||
|
const editableData: UnwrapRef<Record<string, Member>> = reactive({}) |
||||||
|
|
||||||
|
const paginationProp = reactive({ |
||||||
|
pageSizeOptions: ['20', '50', '100'], |
||||||
|
showQuickJumper: true, |
||||||
|
showSizeChanger: true, |
||||||
|
pageSize: 50, |
||||||
|
current: 1, |
||||||
|
total: 0 |
||||||
|
}) |
||||||
|
|
||||||
|
const rowSelection = { |
||||||
|
onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => { |
||||||
|
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows) |
||||||
|
}, |
||||||
|
onSelect: (record: any, selected: boolean, selectedRows: []) => { |
||||||
|
console.log(record, selected, selectedRows) |
||||||
|
}, |
||||||
|
onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => { |
||||||
|
console.log(selected, selectedRows, changeRows) |
||||||
|
}, |
||||||
|
} |
||||||
|
type Pagination = TableState['pagination'] |
||||||
|
|
||||||
|
const body: IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 50 |
||||||
|
} |
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getAllUsers(workspaceId, body) |
||||||
|
}) |
||||||
|
|
||||||
|
function refreshData (page: Pagination) { |
||||||
|
body.page = page?.current! |
||||||
|
body.page_size = page?.pageSize! |
||||||
|
getAllUsers(workspaceId, body) |
||||||
|
} |
||||||
|
|
||||||
|
function getAllUsers (workspaceId: string, page: IPage) { |
||||||
|
getAllUsersInfo(workspaceId, page).then(res => { |
||||||
|
const userList: Member[] = res.data.list |
||||||
|
data.member = userList |
||||||
|
paginationProp.total = res.data.pagination.total |
||||||
|
paginationProp.current = res.data.pagination.page |
||||||
|
|
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function edit (record: Member) { |
||||||
|
editableData[record.user_id] = record |
||||||
|
} |
||||||
|
|
||||||
|
function save (record: Member) { |
||||||
|
delete editableData[record.user_id] |
||||||
|
updateUserInfo(workspaceId, record.user_id, record).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
message.error(res.message) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
<style> |
||||||
|
|
||||||
|
.table { |
||||||
|
background-color: white; |
||||||
|
margin: 20px; |
||||||
|
padding: 20px; |
||||||
|
height: 88vh; |
||||||
|
} |
||||||
|
.table-striped { |
||||||
|
background-color: #f7f9fa; |
||||||
|
} |
||||||
|
.ant-table { |
||||||
|
border-top: 1px solid rgb(0,0,0,0.06); |
||||||
|
border-bottom: 1px solid rgb(0,0,0,0.06); |
||||||
|
} |
||||||
|
.ant-table-tbody tr td { |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
.ant-table td { |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
.ant-table-thead tr th { |
||||||
|
background: white !important; |
||||||
|
border: 0; |
||||||
|
} |
||||||
|
th.ant-table-selection-column { |
||||||
|
background-color: white !important; |
||||||
|
} |
||||||
|
.ant-table-header { |
||||||
|
background-color: white !important; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,38 @@ |
|||||||
|
<template> |
||||||
|
<div> |
||||||
|
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;"> |
||||||
|
<a-row> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
<a-col :span="20">Task Plan Library</a-col> |
||||||
|
<a-col :span="2"> |
||||||
|
<span v-if="!createPlanTip"> |
||||||
|
<router-link :to="{ name: 'create-plan'}"> |
||||||
|
<PlusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = true"/> |
||||||
|
</router-link> |
||||||
|
</span> |
||||||
|
<span v-else> |
||||||
|
<router-link :to="{ name: 'task'}"> |
||||||
|
<MinusOutlined style="color: white; font-size: 16px;" @click="() => createPlanTip = false"/> |
||||||
|
</router-link> |
||||||
|
</span> |
||||||
|
</a-col> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
</a-row> |
||||||
|
</div> |
||||||
|
<div v-if="createPlanTip"> |
||||||
|
<router-view /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { PlusOutlined, MinusOutlined } from '@ant-design/icons-vue' |
||||||
|
import { onMounted, onUnmounted, ref } from 'vue' |
||||||
|
|
||||||
|
const createPlanTip = ref(false) |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
|
||||||
|
</style> |
@ -1,11 +1,463 @@ |
|||||||
<template> |
<template> |
||||||
<div class="project-tsa-wrapper"> |
<div class="project-tsa-wrapper "> |
||||||
TSA |
<div> |
||||||
|
<a-row> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
<a-col :span="11">My Username</a-col> |
||||||
|
<a-col :span="11" align="right" style="font-weight: 700">{{ username }}</a-col> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
</a-row> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;"> |
||||||
|
<a-collapse-panel :key="EDeviceTypeName.Dock" header="Dock" style="border-bottom: 1px solid #4f4f4f;"> |
||||||
|
<div v-if="onlineDocks.data.length === 0" style="height: 150px; color: white;"> |
||||||
|
<a-empty :image="noData" :image-style="{ height: '60px' }" /> |
||||||
|
</div> |
||||||
|
<div v-else class="fz12" style="color: white;"> |
||||||
|
<div v-for="dock in onlineDocks.data" :key="dock.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;"> |
||||||
|
<div style="border-radius: 2px; height: 100%; width: 100%;" class="flex-row flex-justify-between flex-align-center"> |
||||||
|
<div style="float: left; padding: 0px 5px 8px 8px; width: 88%"> |
||||||
|
<div style="width: 80%; height: 30px; line-height: 30px; font-size: 16px;"> |
||||||
|
<a-tooltip :title="dock.gateway.callsign"> |
||||||
|
<span class="text-hidden" style="max-width: 200px;">{{ dock.gateway.callsign }}</span> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> |
||||||
|
<div> |
||||||
|
<span class="ml5 mr5"><RobotOutlined /></span> |
||||||
|
<span class="font-bold" :style="dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'"> |
||||||
|
{{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].mode_code] : EDockModeCode[EDockModeCode.Disconnected] }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;"> |
||||||
|
<div v-if="hmsInfo[dock.gateway.sn]" class="flex-align-center flex-row"> |
||||||
|
<div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' : |
||||||
|
hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;"> |
||||||
|
<span :style="hmsInfo[dock.gateway.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.gateway.sn].length }}</span> |
||||||
|
<span class="fz10">{{ hmsInfo[dock.gateway.sn].length > 99 ? '+' : ''}}</span> |
||||||
|
</div> |
||||||
|
<a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.gateway.sn]" |
||||||
|
@visibleChange="readHms(hmsVisible[dock.gateway.sn], dock.gateway.sn)" |
||||||
|
:overlayStyle="{width: '200px', height: '300px'}"> |
||||||
|
<div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution' : |
||||||
|
hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;"> |
||||||
|
<span class="word-loop">{{ hmsInfo[dock.gateway.sn][0].message_en }}</span> |
||||||
|
</div> |
||||||
|
<template #content> |
||||||
|
<a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true"> |
||||||
|
<a-collapse-panel v-for="hms in hmsInfo[dock.gateway.sn]" :key="hms.hms_id" :showArrow="false" |
||||||
|
style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px" |
||||||
|
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||||
|
> |
||||||
|
<template #header="{ isActive }"> |
||||||
|
<div class="flex-row flex-align-center" style="width: 130px;"> |
||||||
|
<div style="width: 110px;"> |
||||||
|
<span class="word-loop">{{ hms.message_en }}</span> |
||||||
|
</div> |
||||||
|
<div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center" |
||||||
|
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||||
|
> |
||||||
|
<DoubleRightOutlined :rotate="isActive ? 90 : 0" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<a-tooltip :title="hms.create_time"> |
||||||
|
<div style="color: white;" class="text-hidden">{{ hms.create_time }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</a-collapse-panel> |
||||||
|
</a-collapse> |
||||||
|
</template> |
||||||
|
</a-popover> |
||||||
|
</div> |
||||||
|
<div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> |
||||||
|
<div> |
||||||
|
<span class="ml5 mr5"><RocketOutlined /></span> |
||||||
|
<span class="font-bold" :style="deviceInfo[dock.sn] && deviceInfo[dock.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'"> |
||||||
|
{{ deviceInfo[dock.sn] ? EModeCode[deviceInfo[dock.sn].mode_code] : EModeCode[EModeCode.Disconnected] }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;"> |
||||||
|
<div v-if="hmsInfo[dock.sn]" class="flex-align-center flex-row"> |
||||||
|
<div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' : |
||||||
|
hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;"> |
||||||
|
<span :style="hmsInfo[dock.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.sn].length }}</span> |
||||||
|
<span class="fz10">{{ hmsInfo[dock.sn].length > 99 ? '+' : ''}}</span> |
||||||
|
</div> |
||||||
|
<a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.sn]" @visibleChange="readHms(hmsVisible[dock.sn], dock.sn)" |
||||||
|
:overlayStyle="{width: '200px', height: '300px'}"> |
||||||
|
<div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution' : |
||||||
|
hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;"> |
||||||
|
<span class="word-loop">{{ hmsInfo[dock.sn][0].message_en }}</span> |
||||||
|
</div> |
||||||
|
<template #content> |
||||||
|
<a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true"> |
||||||
|
<a-collapse-panel v-for="hms in hmsInfo[dock.sn]" :key="hms.hms_id" :showArrow="false" |
||||||
|
style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px" |
||||||
|
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||||
|
> |
||||||
|
<template #header="{ isActive }"> |
||||||
|
<div class="flex-row flex-align-center" style="width: 130px;"> |
||||||
|
<div style="width: 110px;"> |
||||||
|
<span class="word-loop">{{ hms.message_en }}</span> |
||||||
|
</div> |
||||||
|
<div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center" |
||||||
|
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||||
|
> |
||||||
|
<DoubleRightOutlined :rotate="isActive ? 90 : 0" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<a-tooltip :title="hms.create_time"> |
||||||
|
<div style="color: white;" class="text-hidden">{{ hms.create_time }}</div> |
||||||
|
</a-tooltip> |
||||||
|
</a-collapse-panel> |
||||||
|
</a-collapse> |
||||||
|
</template> |
||||||
|
</a-popover> |
||||||
|
</div> |
||||||
|
<div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div style="float: right; background: #595959; height: 100%; width: 40px;" class="flex-row flex-justify-center flex-align-center"> |
||||||
|
<div class="fz16" @click="switchVisible($event, dock, true, dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected)"> |
||||||
|
<a v-if="osdVisible.gateway_sn === dock.gateway.sn && osdVisible.visible"><EyeOutlined /></a> |
||||||
|
<a v-else><EyeInvisibleOutlined /></a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a-collapse-panel> |
||||||
|
</a-collapse> |
||||||
|
<a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;"> |
||||||
|
<a-collapse-panel :key="EDeviceTypeName.Aircraft" header="Online Devices" style="border-bottom: 1px solid #4f4f4f;"> |
||||||
|
<div v-if="onlineDevices.data.length === 0" style="height: 150px; color: white;"> |
||||||
|
<a-empty :image="noData" :image-style="{ height: '60px' }" /> |
||||||
|
</div> |
||||||
|
<div v-else class="fz12" style="color: white;"> |
||||||
|
<div v-for="device in onlineDevices.data" :key="device.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;"> |
||||||
|
<div class="battery-slide" v-if="deviceInfo[device.sn]"> |
||||||
|
<div style="background: #535759; width: 100%;"></div> |
||||||
|
<div class="capacity-percent" :style="{ width: deviceInfo[device.sn].battery.capacity_percent + '%'}"></div> |
||||||
|
<div class="return-home" :style="{ width: deviceInfo[device.sn].battery.return_home_power + '%'}"></div> |
||||||
|
<div class="landing" :style="{ width: deviceInfo[device.sn].battery.landing_power + '%'}"></div> |
||||||
|
<div class="battery" :style="{ left: deviceInfo[device.sn].battery.capacity_percent + '%' }"></div> |
||||||
|
</div> |
||||||
|
<div style="border-bottom: 1px solid #515151; border-radius: 2px; height: 50px; width: 100%;" class="flex-row flex-justify-between flex-align-center"> |
||||||
|
<div style="float: left; padding: 5px 5px 8px 8px; width: 88%"> |
||||||
|
<div style="width: 100%; height: 100%;"> |
||||||
|
<a-tooltip> |
||||||
|
<template #title>{{ device.model }} - {{ device.callsign }}</template> |
||||||
|
<span class="text-hidden" style="max-width: 200px; display: block; height: 20px;">{{ device.model }} - {{ device.callsign }}</span> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
<div class="mt5" style="background: #595959;"> |
||||||
|
<span class="ml5 mr5"><RocketOutlined /></span> |
||||||
|
<span class="font-bold" :style="deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'"> |
||||||
|
{{ deviceInfo[device.sn] ? EModeCode[deviceInfo[device.sn].mode_code] : EModeCode[EModeCode.Disconnected] }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div style="float: right; background: #595959; height: 50px; width: 40px;" class="flex-row flex-justify-center flex-align-center"> |
||||||
|
<div class="fz16" @click="switchVisible($event, device, false, deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected)"> |
||||||
|
<a v-if="osdVisible.sn === device.sn && osdVisible.visible"><EyeOutlined /></a> |
||||||
|
<a v-else><EyeInvisibleOutlined /></a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="flex-row flex-justify-center flex-align-center" style="height: 40px;"> |
||||||
|
<div style="height: 20px; background: #595959; width: 94%;" > |
||||||
|
<span class="mr5"><a-image style="margin-left: 2px; margin-top: -2px; height: 20px; width: 20px;" :src="rc" /></span> |
||||||
|
<a-tooltip> |
||||||
|
<template #title>{{ device.gateway.callsign }} </template> |
||||||
|
<span>{{ device.gateway.callsign }}</span> |
||||||
|
</a-tooltip> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</a-collapse-panel> |
||||||
|
</a-collapse> |
||||||
|
</div> |
||||||
</div> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script lang="ts" setup> |
<script lang="ts" setup> |
||||||
|
import { computed, onMounted, reactive, ref, watch, WritableComputedRef } from 'vue' |
||||||
|
import { EDeviceTypeName, ELocalStorageKey } from '/@/types' |
||||||
|
import noData from '/@/assets/icons/no-data.png' |
||||||
|
import rc from '/@/assets/icons/rc.png' |
||||||
|
import { DeviceStatus, EModeCode, OSDVisible, EDockModeCode, DeviceOsd } from '/@/types/device' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { getDeviceTopo, getUnreadDeviceHms, updateDeviceHms } from '/@/api/manage' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { RocketOutlined, EyeInvisibleOutlined, EyeOutlined, RobotOutlined, DoubleRightOutlined } from '@ant-design/icons-vue' |
||||||
|
import { EHmsLevel } from '/@/types/enums' |
||||||
|
|
||||||
|
const store = useMyStore() |
||||||
|
const username = ref(localStorage.getItem(ELocalStorageKey.Username)) |
||||||
|
const workspaceId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!) |
||||||
|
const osdVisible = ref({} as OSDVisible) |
||||||
|
const hmsVisible = new Map<string, boolean>() |
||||||
|
|
||||||
|
interface OnlineDevice { |
||||||
|
model: string, |
||||||
|
callsign: string, |
||||||
|
sn: string, |
||||||
|
mode: number, |
||||||
|
gateway: { |
||||||
|
model: string, |
||||||
|
callsign: string, |
||||||
|
sn: string, |
||||||
|
domain: string, |
||||||
|
}, |
||||||
|
payload: { |
||||||
|
model: string |
||||||
|
}[] |
||||||
|
} |
||||||
|
|
||||||
|
const onlineDevices = reactive({ |
||||||
|
data: [] as OnlineDevice[] |
||||||
|
}) |
||||||
|
|
||||||
|
const onlineDocks = reactive({ |
||||||
|
data: [] as OnlineDevice[] |
||||||
|
}) |
||||||
|
|
||||||
|
const deviceInfo = computed(() => store.state.deviceState.deviceInfo) |
||||||
|
const dockInfo = computed(() => store.state.deviceState.dockInfo) |
||||||
|
const hmsInfo = computed({ |
||||||
|
get: () => store.state.hmsInfo, |
||||||
|
set: (val) => { |
||||||
|
return val |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getOnlineTopo() |
||||||
|
setTimeout(() => { |
||||||
|
watch(() => store.state.deviceStatusEvent, |
||||||
|
data => { |
||||||
|
getOnlineTopo() |
||||||
|
if (data.deviceOnline.sn) { |
||||||
|
getUnreadHms(data.deviceOnline.sn) |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
deep: true |
||||||
|
} |
||||||
|
) |
||||||
|
getOnlineDeviceHms() |
||||||
|
}, 3000) |
||||||
|
}) |
||||||
|
|
||||||
|
function getOnlineTopo () { |
||||||
|
getDeviceTopo(workspaceId.value).then((res) => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
onlineDevices.data = [] |
||||||
|
onlineDocks.data = [] |
||||||
|
res.data.forEach((val: any) => { |
||||||
|
const gateway = val.gateways_list.pop() |
||||||
|
const device: OnlineDevice = { |
||||||
|
model: val.device_name, |
||||||
|
callsign: val.nickname, |
||||||
|
sn: val.device_sn, |
||||||
|
mode: EModeCode.Disconnected, |
||||||
|
gateway: { |
||||||
|
model: gateway?.device_name, |
||||||
|
callsign: gateway?.nickname, |
||||||
|
sn: gateway?.device_sn, |
||||||
|
domain: gateway?.domain |
||||||
|
}, |
||||||
|
payload: [] |
||||||
|
} |
||||||
|
val.payloads_list.forEach((payload: any) => { |
||||||
|
device.payload.push({ |
||||||
|
model: payload.payload_name |
||||||
|
}) |
||||||
|
}) |
||||||
|
if (gateway && EDeviceTypeName.Dock === gateway.domain) { |
||||||
|
hmsVisible.set(device.sn, false) |
||||||
|
hmsVisible.set(device.gateway.sn, false) |
||||||
|
onlineDocks.data.push(device) |
||||||
|
} |
||||||
|
if (val.status && EDeviceTypeName.Gateway === gateway.domain) { |
||||||
|
onlineDevices.data.push(device) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function switchVisible (e: any, device: OnlineDevice, isDock: boolean, isClick: boolean) { |
||||||
|
if (!isClick) { |
||||||
|
e.target.style.cursor = 'not-allowed' |
||||||
|
return |
||||||
|
} |
||||||
|
if (device.sn === osdVisible.value.sn) { |
||||||
|
osdVisible.value.visible = !osdVisible.value.visible |
||||||
|
} else { |
||||||
|
osdVisible.value.sn = device.sn |
||||||
|
osdVisible.value.callsign = device.callsign |
||||||
|
osdVisible.value.model = device.model |
||||||
|
osdVisible.value.visible = true |
||||||
|
osdVisible.value.gateway_sn = device.gateway.sn |
||||||
|
osdVisible.value.is_dock = isDock |
||||||
|
osdVisible.value.gateway_callsign = device.gateway.callsign |
||||||
|
} |
||||||
|
store.commit('SET_OSD_VISIBLE_INFO', osdVisible) |
||||||
|
} |
||||||
|
|
||||||
|
function getUnreadHms (sn: string) { |
||||||
|
getUnreadDeviceHms(workspaceId.value, sn).then(res => { |
||||||
|
if (res.data.length !== 0) { |
||||||
|
hmsInfo.value[sn] = res.data |
||||||
|
} |
||||||
|
}) |
||||||
|
console.info(hmsInfo.value) |
||||||
|
} |
||||||
|
|
||||||
|
function getOnlineDeviceHms () { |
||||||
|
const snList = Object.keys(dockInfo.value) |
||||||
|
if (snList.length === 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
snList.forEach(sn => { |
||||||
|
getUnreadHms(sn) |
||||||
|
}) |
||||||
|
const deviceSnList = Object.keys(deviceInfo.value) |
||||||
|
if (deviceSnList.length === 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
deviceSnList.forEach(sn => { |
||||||
|
getUnreadHms(sn) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function readHms (visiable: boolean, sn: string) { |
||||||
|
if (!visiable) { |
||||||
|
updateDeviceHms(workspaceId.value, sn).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
delete hmsInfo.value[sn] |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
</script> |
</script> |
||||||
|
|
||||||
<style lang="scss" scoped> |
<style lang="scss"> |
||||||
|
.project-tsa-wrapper > :first-child { |
||||||
|
height: 50px; |
||||||
|
line-height: 50px; |
||||||
|
align-items: center; |
||||||
|
border-bottom: 1px solid #4f4f4f; |
||||||
|
} |
||||||
|
.ant-collapse > .ant-collapse-item > .ant-collapse-header { |
||||||
|
color: white; |
||||||
|
border: 0; |
||||||
|
padding-left: 14px; |
||||||
|
} |
||||||
|
|
||||||
|
.text-hidden { |
||||||
|
overflow: hidden !important; |
||||||
|
text-overflow: ellipsis !important; |
||||||
|
white-space: nowrap; |
||||||
|
-o-text-overflow: ellipsis; |
||||||
|
} |
||||||
|
.font-bold { |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
|
||||||
|
.battery-slide { |
||||||
|
width: 100%; |
||||||
|
.capacity-percent { |
||||||
|
background: #00ee8b; |
||||||
|
} |
||||||
|
.return-home { |
||||||
|
background: #ff9f0a; |
||||||
|
} |
||||||
|
.landing { |
||||||
|
background: #f5222d; |
||||||
|
} |
||||||
|
.battery { |
||||||
|
background: white; |
||||||
|
border-radius: 1px; |
||||||
|
width: 8px; |
||||||
|
height: 4px; |
||||||
|
margin-top: -3px; |
||||||
|
} |
||||||
|
} |
||||||
|
.battery-slide > div { |
||||||
|
position: relative; |
||||||
|
margin-top: -2px; |
||||||
|
min-height: 2px; |
||||||
|
border-radius: 2px; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
.disable { |
||||||
|
cursor: not-allowed; |
||||||
|
} |
||||||
|
|
||||||
|
.notice-blink { |
||||||
|
background: $success; |
||||||
|
animation: blink 500ms infinite; |
||||||
|
} |
||||||
|
.caution-blink { |
||||||
|
background: orange; |
||||||
|
animation: blink 500ms infinite; |
||||||
|
} |
||||||
|
.warn-blink { |
||||||
|
background: red; |
||||||
|
animation: blink 500ms infinite; |
||||||
|
} |
||||||
|
.notice { |
||||||
|
background: $success; |
||||||
|
overflow: hidden; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.caution { |
||||||
|
background: orange; |
||||||
|
cursor: pointer; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.warn { |
||||||
|
background: red; |
||||||
|
cursor: pointer; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.word-loop { |
||||||
|
white-space: nowrap; |
||||||
|
display: inline-block; |
||||||
|
animation: 10s loop linear infinite normal; |
||||||
|
} |
||||||
|
@keyframes blink { |
||||||
|
from { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
50% { |
||||||
|
opacity: 0.35; |
||||||
|
} |
||||||
|
to { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
@keyframes loop { |
||||||
|
0% { |
||||||
|
transform: translateX(20px); |
||||||
|
-webkit-transform: translateX(20px); |
||||||
|
} |
||||||
|
100% { |
||||||
|
transform: translateX(-100%); |
||||||
|
-webkit-transform: translateX(-100%); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
</style> |
</style> |
||||||
|
@ -1,9 +1,205 @@ |
|||||||
<template> |
<template> |
||||||
<div class="project-wayline-wrapper"> |
<div class="project-wayline-wrapper height-100"> |
||||||
wayline |
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;"> |
||||||
|
<a-row> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
<a-col :span="22">Flight Route Library</a-col> |
||||||
|
<a-col :span="1"></a-col> |
||||||
|
</a-row> |
||||||
|
</div> |
||||||
|
<div class="height-100"> |
||||||
|
<div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll"> |
||||||
|
<div v-for="wayline in waylinesData.data" :key="wayline.id"> |
||||||
|
<div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)"> |
||||||
|
<div class="title"> |
||||||
|
<a-tooltip :title="wayline.name"> |
||||||
|
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div> |
||||||
|
</a-tooltip> |
||||||
|
<div class="ml10"><UserOutlined /></div> |
||||||
|
<a-tooltip :title="wayline.user_name"> |
||||||
|
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div> |
||||||
|
</a-tooltip> |
||||||
|
<div class="fz20"> |
||||||
|
<a-dropdown> |
||||||
|
<a style="color: white;"> |
||||||
|
<EllipsisOutlined /> |
||||||
|
</a> |
||||||
|
<template #overlay> |
||||||
|
<a-menu theme="dark" class="more" style="background: #3c3c3c;"> |
||||||
|
<a-menu-item @click="downloadWayline(wayline.id, wayline.name)"> |
||||||
|
<span>Download</span> |
||||||
|
</a-menu-item> |
||||||
|
<a-menu-item @click="showWaylineTip(wayline.id)"> |
||||||
|
<span>Delete</span> |
||||||
|
</a-menu-item> |
||||||
|
</a-menu> |
||||||
|
</template> |
||||||
|
</a-dropdown> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||||
|
<span><RocketOutlined /></span> |
||||||
|
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span> |
||||||
|
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> |
||||||
|
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> |
||||||
|
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||||
|
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div v-else> |
||||||
|
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||||
|
</div> |
||||||
|
<a-modal v-model:visible="deleteTip" width="450px" :closable="false" :maskClosable="false" centered :okButtonProps="{ danger: true }" @ok="deleteWayline"> |
||||||
|
<p class="pt10 pl20" style="height: 50px;">Wayline file is unrecoverable once deleted. Continue?</p> |
||||||
|
<template #title> |
||||||
|
<div class="flex-row flex-justify-center"> |
||||||
|
<span>Delete</span> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</a-modal> |
||||||
|
</div> |
||||||
</div> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<script lang="ts" setup></script> |
<script lang="ts" setup> |
||||||
|
import { reactive } from '@vue/reactivity' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { onMounted, onUpdated, ref } from 'vue' |
||||||
|
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline' |
||||||
|
import { ELocalStorageKey } from '/@/types' |
||||||
|
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue' |
||||||
|
import { EDeviceType } from '/@/types/device' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { WaylineFile } from '/@/types/wayline' |
||||||
|
import { downloadFile } from '/@/utils/common' |
||||||
|
import { IPage } from '/@/api/http/type' |
||||||
|
|
||||||
<style lang="scss" scoped></style> |
const store = useMyStore() |
||||||
|
const pagination :IPage = { |
||||||
|
page: 1, |
||||||
|
total: 0, |
||||||
|
page_size: 10 |
||||||
|
} |
||||||
|
|
||||||
|
const waylinesData = reactive({ |
||||||
|
data: [] as WaylineFile[] |
||||||
|
}) |
||||||
|
|
||||||
|
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||||
|
const deleteTip = ref(false) |
||||||
|
const deleteWaylineId = ref<string>('') |
||||||
|
const canRefresh = ref(true) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getWaylines() |
||||||
|
}) |
||||||
|
|
||||||
|
onUpdated(() => { |
||||||
|
const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement |
||||||
|
const parent = element?.parentNode as HTMLDivElement |
||||||
|
setTimeout(() => { |
||||||
|
if (element?.scrollHeight < parent?.clientHeight && pagination.total > waylinesData.data.length) { |
||||||
|
if (canRefresh.value) { |
||||||
|
pagination.page++ |
||||||
|
getWaylines() |
||||||
|
} |
||||||
|
} else if (element && element.className.indexOf('height-100') === -1) { |
||||||
|
element.className = element.className + ' height-100' |
||||||
|
} |
||||||
|
}, 300) |
||||||
|
}) |
||||||
|
|
||||||
|
function getWaylines () { |
||||||
|
if (!canRefresh.value) { |
||||||
|
return |
||||||
|
} |
||||||
|
canRefresh.value = false |
||||||
|
getWaylineFiles(workspaceId, { |
||||||
|
page: pagination.page, |
||||||
|
page_size: pagination.page_size, |
||||||
|
order_by: 'update_time desc' |
||||||
|
}).then(res => { |
||||||
|
if (res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
res.data.list.forEach((wayline: WaylineFile) => waylinesData.data.push(wayline)) |
||||||
|
pagination.total = res.data.pagination.total |
||||||
|
pagination.page = res.data.pagination.page |
||||||
|
}).finally(() => { |
||||||
|
canRefresh.value = true |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function showWaylineTip (waylineId: string) { |
||||||
|
deleteWaylineId.value = waylineId |
||||||
|
deleteTip.value = true |
||||||
|
} |
||||||
|
|
||||||
|
function deleteWayline () { |
||||||
|
deleteWaylineFile(workspaceId, deleteWaylineId.value).then(res => { |
||||||
|
if (res.code === 0) { |
||||||
|
message.success('Wayline file deleted') |
||||||
|
} |
||||||
|
deleteWaylineId.value = '' |
||||||
|
deleteTip.value = false |
||||||
|
pagination.total-- |
||||||
|
waylinesData.data = [] |
||||||
|
setTimeout(getWaylines, 500) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function downloadWayline (waylineId: string, fileName: string) { |
||||||
|
downloadWaylineFile(workspaceId, waylineId).then(res => { |
||||||
|
if (res.code && res.code !== 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
const data = new Blob([res.data], { type: 'application/zip' }) |
||||||
|
downloadFile(data, fileName + '.kmz') |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function selectRoute (wayline: WaylineFile) { |
||||||
|
store.commit('SET_SELECT_WAYLINE_INFO', wayline) |
||||||
|
} |
||||||
|
|
||||||
|
function onScroll (e: any) { |
||||||
|
const element = e.srcElement |
||||||
|
if (element.scrollTop + element.clientHeight === element.scrollHeight && Math.ceil(pagination.total / pagination.page_size) > pagination.page && canRefresh.value) { |
||||||
|
pagination.page++ |
||||||
|
getWaylines() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.wayline-panel { |
||||||
|
background: #3c3c3c; |
||||||
|
margin-left: auto; |
||||||
|
margin-right: auto; |
||||||
|
margin-top: 10px; |
||||||
|
height: 90px; |
||||||
|
width: 95%; |
||||||
|
font-size: 13px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
.title { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
height: 30px; |
||||||
|
font-weight: bold; |
||||||
|
margin: 0px 10px 0 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
.uranus-scrollbar { |
||||||
|
overflow: auto; |
||||||
|
scrollbar-width: thin; |
||||||
|
scrollbar-color: #c5c8cc transparent; |
||||||
|
} |
||||||
|
</style> |
||||||
|
@ -0,0 +1,140 @@ |
|||||||
|
<template> |
||||||
|
<div class="project-app-wrapper"> |
||||||
|
<div class="left"> |
||||||
|
<Sidebar /> |
||||||
|
<div class="main-content uranus-scrollbar dark"> |
||||||
|
<router-view /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="right"> |
||||||
|
<div class="map-wrapper"> |
||||||
|
<GMap /> |
||||||
|
</div> |
||||||
|
<div class="media-wrapper" v-if="root.$route.name === ERouterName.MEDIA"> |
||||||
|
<MediaPanel /> |
||||||
|
</div> |
||||||
|
<div class="media-wrapper" v-if="root.$route.name === ERouterName.TASK"> |
||||||
|
<TaskPanel /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<script lang="ts" setup> |
||||||
|
|
||||||
|
import Sidebar from '../sidebar.vue' |
||||||
|
import MediaPanel from '@/components/MediaPanel.vue' |
||||||
|
import TaskPanel from '@/components/TaskPanel.vue' |
||||||
|
import GMap from '/@/components/GMap.vue' |
||||||
|
import { EBizCode, ERouterName } from '/@/types' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import { onMounted, onUnmounted, watch } from 'vue' |
||||||
|
import ReconnectingWebSocket from 'reconnecting-websocket' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import websocket from '/@/api/websocket' |
||||||
|
// import { enableAgoraLive, enableOthersLive } from '/@/pages/project-app/projects/livestream.vue' |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
|
||||||
|
const wsGetMsg = async (res: any) => { |
||||||
|
const payload = JSON.parse(res.data) |
||||||
|
switch (payload.biz_code) { |
||||||
|
case EBizCode.GatewayOsd: { |
||||||
|
store.commit('SET_GATEWAY_INFO', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.DeviceOsd: { |
||||||
|
store.commit('SET_DEVICE_INFO', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.DockOsd: { |
||||||
|
store.commit('SET_DOCK_INFO', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.MapElementCreate: { |
||||||
|
store.commit('SET_MAP_ELEMENT_CREATE', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.MapElementUpdate: { |
||||||
|
store.commit('SET_MAP_ELEMENT_UPDATE', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.MapElementDelete: { |
||||||
|
store.commit('SET_MAP_ELEMENT_DELETE', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.DeviceOnline: { |
||||||
|
store.commit('SET_DEVICE_ONLINE', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.DeviceOffline: { |
||||||
|
store.commit('SET_DEVICE_OFFLINE', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.FlightTaskProgress: { |
||||||
|
store.commit('SET_FLIGHT_TASK_PROGRESS', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.DeviceHms: { |
||||||
|
store.commit('SET_DEVICE_HMS_INFO', payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
default: |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const store = useMyStore() |
||||||
|
|
||||||
|
let socket: ReconnectingWebSocket |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
socket = websocket.init(wsGetMsg) |
||||||
|
}) |
||||||
|
onUnmounted(() => { |
||||||
|
socket.close() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
<style lang="scss" scoped> |
||||||
|
@import '/@/styles/index.scss'; |
||||||
|
|
||||||
|
.project-app-wrapper { |
||||||
|
display: flex; |
||||||
|
position: absolute; |
||||||
|
transition: width 0.2s ease; |
||||||
|
height: 100%; |
||||||
|
width: 100%; |
||||||
|
.left { |
||||||
|
width: 400px; |
||||||
|
display: flex; |
||||||
|
background-color: #232323; |
||||||
|
float: left; |
||||||
|
} |
||||||
|
.right { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
.map-wrapper { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
} |
||||||
|
.main-content { |
||||||
|
flex: 1; |
||||||
|
color: $text-white-basic; |
||||||
|
} |
||||||
|
.media-wrapper { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
bottom: 0; |
||||||
|
z-index: 100; |
||||||
|
background: #f6f8fa; |
||||||
|
} |
||||||
|
.wayline-wrapper { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
bottom: 0; |
||||||
|
z-index: 100; |
||||||
|
background: #f6f8fa; |
||||||
|
padding: 16px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,97 @@ |
|||||||
|
<template> |
||||||
|
<div class="width-100 flex-row flex-justify-between flex-align-center" style="height: 60px;"> |
||||||
|
<div class="height-100"> |
||||||
|
<a-avatar :size="40" shape="square" :src="cloudapi" /> |
||||||
|
<span class="ml10 fontBold">{{ workspaceName }}</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<a-space class="fz16 height-100" size="large"> |
||||||
|
<router-link |
||||||
|
v-for="item in options" |
||||||
|
:key="item.key" |
||||||
|
:to="item.path" |
||||||
|
:class="{ |
||||||
|
'menu-item': true, |
||||||
|
}"> |
||||||
|
<span @click="selectedRoute(item.path)" :style="selected === item.path ? 'color: #2d8cf0;' : 'color: white'">{{ item.label }}</span> |
||||||
|
</router-link> |
||||||
|
</a-space> |
||||||
|
|
||||||
|
<div class="height-100 fz16 flex-row flex-justify-between flex-align-center"> |
||||||
|
<a-dropdown> |
||||||
|
<div class="height-100"> |
||||||
|
<span class="fz20 mt20" style="border: 2px solid white; border-radius: 50%; display: inline-flex;"><UserOutlined /></span> |
||||||
|
<span class="ml10 mr10" style="float: right;">{{ username }}</span> |
||||||
|
</div> |
||||||
|
<template #overlay> |
||||||
|
<a-menu theme="dark" class="flex-column flex-justify-between flex-align-center"> |
||||||
|
<a-menu-item> |
||||||
|
<span class="mr10" style="font-size: 16px;"><ExportOutlined /></span> |
||||||
|
<span @click="logout">Log Out</span> |
||||||
|
</a-menu-item> |
||||||
|
</a-menu> |
||||||
|
</template> |
||||||
|
</a-dropdown> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { defineComponent, onMounted, ref } from 'vue' |
||||||
|
import { getRoot } from '/@/root' |
||||||
|
import { getPlatformInfo } from '/@/api/manage' |
||||||
|
import { ELocalStorageKey, ERouterName } from '/@/types' |
||||||
|
import { UserOutlined, ExportOutlined } from '@ant-design/icons-vue' |
||||||
|
import cloudapi from '/@/assets/icons/cloudapi.png' |
||||||
|
import ReconnectingWebSocket from 'reconnecting-websocket' |
||||||
|
import websocket from '/@/api/websocket' |
||||||
|
|
||||||
|
const root = getRoot() |
||||||
|
|
||||||
|
interface IOptions { |
||||||
|
key: number |
||||||
|
label: string |
||||||
|
path: |
||||||
|
| string |
||||||
|
| { |
||||||
|
path: string |
||||||
|
query?: any |
||||||
|
} |
||||||
|
icon: string |
||||||
|
} |
||||||
|
const username = ref(localStorage.getItem(ELocalStorageKey.Username)) |
||||||
|
const workspaceName = ref('') |
||||||
|
const options = [ |
||||||
|
{ key: 0, label: ERouterName.WORKSPACE.charAt(0).toUpperCase() + ERouterName.WORKSPACE.substr(1), path: '/' + ERouterName.WORKSPACE }, |
||||||
|
{ key: 1, label: ERouterName.MEMBERS.charAt(0).toUpperCase() + ERouterName.MEMBERS.substr(1), path: '/' + ERouterName.MEMBERS }, |
||||||
|
{ key: 2, label: ERouterName.DEVICES.charAt(0).toUpperCase() + ERouterName.DEVICES.substr(1), path: '/' + ERouterName.DEVICES } |
||||||
|
] |
||||||
|
|
||||||
|
const selected = ref<string>(root.$route.path) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
getPlatformInfo().then(res => { |
||||||
|
workspaceName.value = res.data.workspace_name |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
function selectedRoute (path: string) { |
||||||
|
selected.value = path |
||||||
|
} |
||||||
|
|
||||||
|
const logout = () => { |
||||||
|
localStorage.clear() |
||||||
|
root.$router.push(ERouterName.PROJECT) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@import '/@/styles/index.scss'; |
||||||
|
|
||||||
|
.fontBold { |
||||||
|
font-weight: 500; |
||||||
|
font-size: 18px; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,198 @@ |
|||||||
|
import { EDeviceTypeName } from "."; |
||||||
|
|
||||||
|
export interface Device { |
||||||
|
device_name: string, |
||||||
|
device_sn: string, |
||||||
|
nickname: string, |
||||||
|
firmware_version: string, |
||||||
|
status: string, |
||||||
|
workspace_name: string, |
||||||
|
bound_time: string, |
||||||
|
login_time: string, |
||||||
|
children?: Device[] |
||||||
|
domain: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface DeviceStatus { |
||||||
|
sn: string, |
||||||
|
online_status: boolean, |
||||||
|
device_callsign: string, |
||||||
|
user_id: string, |
||||||
|
user_callsign: string |
||||||
|
bound_status: boolean, |
||||||
|
model: string, |
||||||
|
gateway_sn: string, |
||||||
|
domain: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface OSDVisible { |
||||||
|
sn: string, |
||||||
|
model: string, |
||||||
|
callsign: string, |
||||||
|
visible: boolean, |
||||||
|
is_dock: boolean, |
||||||
|
gateway_sn: string, |
||||||
|
gateway_callsign: string, |
||||||
|
} |
||||||
|
|
||||||
|
export interface GatewayOsd { |
||||||
|
capacity_percent: string, |
||||||
|
transmission_signal_quality: string, |
||||||
|
longitude: number, |
||||||
|
latitude: number, |
||||||
|
} |
||||||
|
export interface DeviceOsd { |
||||||
|
longitude: number, |
||||||
|
latitude: number, |
||||||
|
gear: number, |
||||||
|
mode_code: number, |
||||||
|
height: string, |
||||||
|
home_distance: string, |
||||||
|
horizontal_speed: string, |
||||||
|
vertical_speed: string, |
||||||
|
wind_speed: string, |
||||||
|
wind_direction: string, |
||||||
|
elevation: string, |
||||||
|
position_state: { |
||||||
|
gps_number: string, |
||||||
|
is_fixed: number, |
||||||
|
rtk_number: string |
||||||
|
}, |
||||||
|
battery: { |
||||||
|
capacity_percent: string, |
||||||
|
landing_power: string, |
||||||
|
remain_flight_time: number, |
||||||
|
return_home_power: string, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface DockOsd { |
||||||
|
media_file_detail: { |
||||||
|
remain_upload: number |
||||||
|
}, |
||||||
|
sdr: { |
||||||
|
up_quality: string, |
||||||
|
down_quality: string, |
||||||
|
frequency_band: number, |
||||||
|
}, |
||||||
|
network_state: { |
||||||
|
type: number, |
||||||
|
quality: number, |
||||||
|
rate: number, |
||||||
|
}, |
||||||
|
drone_in_dock: number, |
||||||
|
drone_charge_state: { |
||||||
|
state: number, |
||||||
|
capacity_percent: string, |
||||||
|
}, |
||||||
|
rainfall: string, |
||||||
|
wind_speed: string, |
||||||
|
environment_temperature: string, |
||||||
|
environment_humidity: string |
||||||
|
temperature: string, |
||||||
|
humidity: string, |
||||||
|
latitude: number, |
||||||
|
longitude: number, |
||||||
|
height: number, |
||||||
|
job_number: number, |
||||||
|
acc_time: number, |
||||||
|
first_power_on: number, |
||||||
|
positionState: { |
||||||
|
gps_number: string, |
||||||
|
is_fixed: number, |
||||||
|
rtk_number: string, |
||||||
|
is_calibration: number, |
||||||
|
quality: number, |
||||||
|
}, |
||||||
|
storage: { |
||||||
|
total: number, |
||||||
|
used: number, |
||||||
|
}, |
||||||
|
electric_supply_voltage: number, |
||||||
|
working_voltage: string, |
||||||
|
working_current: string, |
||||||
|
backup_battery_voltage: number, |
||||||
|
mode_code: number, |
||||||
|
cover_state: number, |
||||||
|
supplement_light_state: number, |
||||||
|
putter_state: number, |
||||||
|
sub_device: { |
||||||
|
device_sn: string, |
||||||
|
device_model_key: string, |
||||||
|
device_online_status: number, |
||||||
|
device_paired: number, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export enum EModeCode { |
||||||
|
Standby, |
||||||
|
Preparing, |
||||||
|
Ready, |
||||||
|
Manual, |
||||||
|
Automatic, |
||||||
|
Waypoint, |
||||||
|
Panoramic, |
||||||
|
Active_Track, |
||||||
|
ADS_B, |
||||||
|
Return_To_Home, |
||||||
|
Landing, |
||||||
|
Forced_Landing, |
||||||
|
Three_Blades_Landing, |
||||||
|
Upgrading, |
||||||
|
Disconnected, |
||||||
|
} |
||||||
|
|
||||||
|
export enum EGear { |
||||||
|
A, |
||||||
|
P, |
||||||
|
NAV, |
||||||
|
FPV, |
||||||
|
FARM, |
||||||
|
S, |
||||||
|
F, |
||||||
|
M, |
||||||
|
G, |
||||||
|
T |
||||||
|
} |
||||||
|
|
||||||
|
export enum EDeviceType { |
||||||
|
M30 = '0-67-0' as any, |
||||||
|
M30T = '0-67-1' as any, |
||||||
|
M300 = '0-60-0' as any, |
||||||
|
Z30 = '1-20-0' as any, |
||||||
|
XT2 = '1-26-0' as any, |
||||||
|
FPV = '1-39-0' as any, |
||||||
|
XTS = '1-41-0' as any, |
||||||
|
H20 = '1-42-0' as any, |
||||||
|
H20T = '1-43-0' as any, |
||||||
|
P1 = '1-50-65535' as any, |
||||||
|
M30_Camera = '1-52-0' as any, |
||||||
|
M30T_Camera = '1-53-0' as any, |
||||||
|
H20N = '1-61-0' as any, |
||||||
|
DJI_Dock_Camera = '1-165-0' as any, |
||||||
|
L1 = '1-90742-0' as any, |
||||||
|
} |
||||||
|
|
||||||
|
export enum EDockModeCode { |
||||||
|
Disconnected = -1, |
||||||
|
Idle, |
||||||
|
Debugging, |
||||||
|
Remote_Debugging, |
||||||
|
Upgrading, |
||||||
|
Working, |
||||||
|
} |
||||||
|
|
||||||
|
export interface DeviceHms { |
||||||
|
hms_id: string, |
||||||
|
tid: string, |
||||||
|
bid: string, |
||||||
|
sn: string, |
||||||
|
level: number, |
||||||
|
module: number, |
||||||
|
key: string, |
||||||
|
message_en: string, |
||||||
|
message_zh: string, |
||||||
|
create_time: string, |
||||||
|
update_time: string, |
||||||
|
domain: string |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
|
||||||
|
export interface LiveStreamStatus { |
||||||
|
audioBitRate: number, |
||||||
|
dropRate: number, |
||||||
|
fps: number, |
||||||
|
jitter: number, |
||||||
|
quality: number, |
||||||
|
rtt: number, |
||||||
|
status: number, |
||||||
|
type: number, |
||||||
|
videoBitRate: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface GB28181Param { |
||||||
|
serverIp: string, |
||||||
|
serverPort: string, |
||||||
|
serverId: string, |
||||||
|
agentId: string, |
||||||
|
password: string, |
||||||
|
agentPort: string, |
||||||
|
agentChannel: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface RTSPParam { |
||||||
|
userName: string, |
||||||
|
password: string, |
||||||
|
port: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface LiveConfigParam { |
||||||
|
params: number, |
||||||
|
type: any |
||||||
|
} |
||||||
|
|
||||||
|
export enum EVideoPublishType { |
||||||
|
VideoOnDemand = 'video-on-demand', |
||||||
|
VideoByManual = 'video-by-manual', |
||||||
|
VideoDemandAuxManual = 'video-demand-aux-manual' |
||||||
|
} |
||||||
|
|
||||||
|
export enum ELiveTypeValue { |
||||||
|
Unknown, |
||||||
|
Agora, |
||||||
|
RTMP, |
||||||
|
RTSP, |
||||||
|
GB28181 |
||||||
|
} |
||||||
|
|
||||||
|
export enum ELiveTypeName { |
||||||
|
Unknown = 'Unknown', |
||||||
|
Agora = 'Agora', |
||||||
|
RTMP = 'RTMP', |
||||||
|
RTSP = 'RTSP', |
||||||
|
GB28181 = 'GB28181' |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
export interface WaylineFile { |
||||||
|
id: string, |
||||||
|
name: string, |
||||||
|
drone_model_key: any, |
||||||
|
payload_model_keys: string[], |
||||||
|
template_types: number[], |
||||||
|
update_time: number, |
||||||
|
user_name: string, |
||||||
|
} |
||||||
|
|
||||||
|
export interface TaskExt { |
||||||
|
current_waypoint_index: number, |
||||||
|
media_count: number, |
||||||
|
} |
||||||
|
|
||||||
|
export interface TaskProgress { |
||||||
|
current_step: number, |
||||||
|
percent: number, |
||||||
|
} |
||||||
|
|
||||||
|
export interface TaskInfo { |
||||||
|
status: string, |
||||||
|
progress: TaskProgress, |
||||||
|
ext: TaskExt, |
||||||
|
} |
||||||
|
|
||||||
|
export enum ETaskStatus { |
||||||
|
OK = 'ok', |
||||||
|
FAILED = 'failed' |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
|
||||||
|
export function downloadFile (data: Blob, fileName: string) { |
||||||
|
const lable = document.createElement('a') |
||||||
|
lable.href = window.URL.createObjectURL(data) |
||||||
|
lable.download = fileName |
||||||
|
lable.click() |
||||||
|
URL.revokeObjectURL(lable.href) |
||||||
|
} |