@ -1,20 +1,36 @@
@@ -1,20 +1,36 @@
|
||||
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/'
|
||||
websocketURL: 'Please enter the WebSocket access address.', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
|
||||
|
||||
// livestreaming
|
||||
// RTMP Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
|
||||
rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
|
||||
gb28181Para: |
||||
'serverIP=Please enter the server ip.&serverPort=Please enter the server port.&serverID=Please enter the server id.' + |
||||
'&agentID=Please enter the agent id.&agentPassword=Please enter the agent password' + |
||||
'&localPort=Please enter the local port.&channel=Please enter the channel.', |
||||
rtspPara: 'userName=Please enter the username.&password=Please enter the password&port=Please enter the port.', |
||||
amapKey: 'Please enter the amap key.', |
||||
// GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
|
||||
gbServerIp: 'Please enter the server ip.', |
||||
gbServerPort: 'Please enter the server port.', |
||||
gbServerId: 'Please enter the server id.', |
||||
gbAgentId: 'Please enter the agent id', |
||||
gbPassword: 'Please enter the agent password', |
||||
gbAgentPort: 'Please enter the local port.', |
||||
gbAgentChannel: 'Please enter the channel.', |
||||
// RTSP
|
||||
rtspUserName: 'Please enter the username.', |
||||
rtspPassword: 'Please enter the password.', |
||||
rtspPort: '8554', |
||||
// Agora
|
||||
agoraAPPID: 'Please enter the agora app id.', |
||||
agoraToken: 'Please enter the agora token.', |
||||
agoraToken: 'Please enter the agora temporary token.', |
||||
agoraChannel: 'Please enter the agora channel.', |
||||
|
||||
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.
|
||||
// map
|
||||
// You can apply on the AMap website.
|
||||
amapKey: 'Please enter the amap key.', |
||||
|
||||
} |
||||
|
@ -1,9 +1,18 @@
@@ -1,9 +1,18 @@
|
||||
import request from '/@/api/http/request' |
||||
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request' |
||||
const HTTP_PREFIX = '/media/api/v1' |
||||
|
||||
// Get Media Files
|
||||
export const getMediaFiles = async function (wid: string, body: {}): Promise<any> { |
||||
const url = `${HTTP_PREFIX}/files/${wid}/files` |
||||
const result = await request.get(url, body) |
||||
export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> { |
||||
const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}` |
||||
const result = await request.get(url) |
||||
return result.data |
||||
} |
||||
// Download Media File
|
||||
export const downloadMediaFile = async function (workspaceId: string, fingerprint: string): Promise<any> { |
||||
const url = `${HTTP_PREFIX}/files/${workspaceId}/file/${fingerprint}/url` |
||||
const result = await request.get(url, { responseType: 'blob' }) |
||||
if (result.data.code) { |
||||
return result.data |
||||
} |
||||
return result |
||||
} |
||||
|
@ -1,9 +1,55 @@
@@ -1,9 +1,55 @@
|
||||
import request from '/@/api/http/request' |
||||
import request, { IPage, IWorkspaceResponse } from '/@/api/http/request' |
||||
const HTTP_PREFIX = '/wayline/api/v1' |
||||
|
||||
export interface CreatePlan { |
||||
name: string, |
||||
file_id: string, |
||||
dock_sn: string, |
||||
immediate: boolean, |
||||
type: string, |
||||
} |
||||
|
||||
// Get Wayline Files
|
||||
export const getWaylineFiles = async function (wid: string, body: {}): Promise<any> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?` + 'order_by=' + body.order_by + '&page=' + body.page + '&page_size=' + body.page_size |
||||
export const getWaylineFiles = async function (wid: string, body: {}): Promise<IWorkspaceResponse<any>> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?order_by=${body.order_by}&page=${body.page}&page_size=${body.page_size}` |
||||
const result = await request.get(url) |
||||
return result.data |
||||
} |
||||
|
||||
// Download Wayline File
|
||||
export const downloadWaylineFile = async function (workspaceId: string, waylineId: string): Promise<any> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}/url` |
||||
const result = await request.get(url, { responseType: 'blob' }) |
||||
if (result.data.code) { |
||||
return result.data |
||||
} |
||||
return result |
||||
} |
||||
|
||||
// Delete Wayline File
|
||||
export const deleteWaylineFile = async function (workspaceId: string, waylineId: string): Promise<IWorkspaceResponse<any>> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/waylines/${waylineId}` |
||||
const result = await request.delete(url) |
||||
return result.data |
||||
} |
||||
|
||||
// Create Wayline Job
|
||||
export const createPlan = async function (workspaceId: string, plan: CreatePlan): Promise<IWorkspaceResponse<any>> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/flight-tasks` |
||||
const result = await request.post(url, plan) |
||||
return result.data |
||||
} |
||||
|
||||
// Get Wayline Jobs
|
||||
export const getWaylineJobs = async function (workspaceId: string, page: IPage): Promise<IWorkspaceResponse<any>> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs?page=${page.page}&page_size=${page.page_size}` |
||||
const result = await request.get(url) |
||||
return result.data |
||||
} |
||||
|
||||
// Execute Wayline Job
|
||||
export const executeWaylineJobs = async function (workspaceId: string, plan_id: string): Promise<IWorkspaceResponse<any>> { |
||||
const url = `${HTTP_PREFIX}/workspaces/${workspaceId}/jobs/${plan_id}` |
||||
const result = await request.post(url) |
||||
return result.data |
||||
} |
||||
|
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 @@
@@ -1,92 +1,168 @@
|
||||
<template> |
||||
<div class="media-panel-wrapper"> |
||||
<div class="header">Media</div> |
||||
<a-button type="primary" style="margin-top:20px" @click="onRefresh" |
||||
>Refresh</a-button |
||||
> |
||||
<a-table class="media-table" :columns="columns" :data-source="data"> |
||||
<template #name="{ text, record }"> |
||||
<a :href="record.preview_url">{{ text }}</a> |
||||
</template> |
||||
<template #action> |
||||
<span class="action-area"> |
||||
action |
||||
</span> |
||||
</template> |
||||
</a-table> |
||||
</div> |
||||
<div class="header">Media Files</div> |
||||
<a-spin :spinning="loading" :delay="1000" tip="downloading" size="large"> |
||||
<div class="media-panel-wrapper"> |
||||
<a-table class="media-table" :columns="columns" :data-source="mediaData.data" row-key="fingerprint" |
||||
:pagination="paginationProp" :scroll="{ x: '100%', y: 600 }" @change="refreshData"> |
||||
<template v-for="col in ['name', 'path']" #[col]="{ text }" :key="col"> |
||||
<a-tooltip :title="text"> |
||||
<a v-if="col === 'name'">{{ text }}</a> |
||||
<span v-else>{{ text }}</span> |
||||
</a-tooltip> |
||||
</template> |
||||
<template #original="{ text }"> |
||||
{{ text }} |
||||
</template> |
||||
<template #action="{ record }"> |
||||
<a-tooltip title="download"> |
||||
<a class="fz18" @click="downloadMedia(record)"><DownloadOutlined /></a> |
||||
</a-tooltip> |
||||
</template> |
||||
</a-table> |
||||
</div> |
||||
</a-spin> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref } from '@vue/reactivity' |
||||
import { getMediaFiles } from '/@/api/media' |
||||
import { TableState } from 'ant-design-vue/lib/table/interface' |
||||
import { onMounted, reactive } from 'vue' |
||||
import { IPage } from '../api/http/type' |
||||
import { ELocalStorageKey } from '../types/enums' |
||||
import { downloadFile } from '../utils/common' |
||||
import { downloadMediaFile, getMediaFiles } from '/@/api/media' |
||||
import { DownloadOutlined } from '@ant-design/icons-vue' |
||||
import { Pagination } from 'ant-design-vue' |
||||
import { load } from '@amap/amap-jsapi-loader' |
||||
|
||||
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||
const loading = ref(false) |
||||
|
||||
const columns = [ |
||||
{ |
||||
title: 'FileName', |
||||
dataIndex: 'name', |
||||
key: 'name', |
||||
title: 'File Name', |
||||
dataIndex: 'file_name', |
||||
ellipsis: true, |
||||
slots: { customRender: 'name' } |
||||
}, |
||||
{ |
||||
title: 'FileSize', |
||||
dataIndex: 'size', |
||||
key: 'size' |
||||
title: 'File Path', |
||||
dataIndex: 'file_path', |
||||
ellipsis: true, |
||||
slots: { customRender: 'path' } |
||||
}, |
||||
// { |
||||
// title: 'FileSize', |
||||
// dataIndex: 'size', |
||||
// }, |
||||
{ |
||||
title: 'Drone', |
||||
dataIndex: 'drone' |
||||
}, |
||||
{ |
||||
title: 'Payload Type', |
||||
dataIndex: 'payload' |
||||
}, |
||||
{ |
||||
title: 'Original', |
||||
dataIndex: 'is_original', |
||||
slots: { customRender: 'original' } |
||||
}, |
||||
{ |
||||
title: 'PayloadType', |
||||
dataIndex: 'payload_type', |
||||
key: 'payload_type', |
||||
ellipsis: true |
||||
title: 'Created', |
||||
dataIndex: 'create_time' |
||||
}, |
||||
{ |
||||
title: 'Action', |
||||
key: 'action', |
||||
slots: { customRender: 'action' } |
||||
} |
||||
] |
||||
const data = ref([ |
||||
{ |
||||
key: '1', |
||||
name: 'name1', |
||||
size: 32, |
||||
payload_type: 'PM320_DUAL', |
||||
preview_url: '' |
||||
} |
||||
]) |
||||
const onRefresh = async () => { |
||||
const wid = localStorage.getItem('workspace-id') |
||||
data.value = [] |
||||
const index = 1 |
||||
const res = await getMediaFiles(wid, {}) |
||||
res.data.forEach(ele => { |
||||
data.value.push({ |
||||
key: index.toString(), |
||||
name: ele.file_name |
||||
}) |
||||
const body: IPage = { |
||||
page: 1, |
||||
total: 0, |
||||
page_size: 50 |
||||
} |
||||
const paginationProp = reactive({ |
||||
pageSizeOptions: ['20', '50', '100'], |
||||
showQuickJumper: true, |
||||
showSizeChanger: true, |
||||
pageSize: 50, |
||||
current: 1, |
||||
total: 0 |
||||
}) |
||||
|
||||
type Pagination = TableState['pagination'] |
||||
|
||||
interface MediaFile { |
||||
fingerprint: string, |
||||
drone: string, |
||||
payload: string, |
||||
is_original: string, |
||||
file_name: string, |
||||
file_path: string, |
||||
create_time: string, |
||||
} |
||||
|
||||
const mediaData = reactive({ |
||||
data: [] as MediaFile[] |
||||
}) |
||||
|
||||
onMounted(() => { |
||||
getFiles() |
||||
}) |
||||
|
||||
function getFiles () { |
||||
getMediaFiles(workspaceId, body).then(res => { |
||||
mediaData.data = res.data.list |
||||
paginationProp.total = res.data.pagination.total |
||||
paginationProp.current = res.data.pagination.page |
||||
console.info(mediaData.data[0]) |
||||
}) |
||||
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> |
||||
|
||||
<style lang="scss" scoped> |
||||
.media-panel-wrapper { |
||||
width: 100%; |
||||
padding: 16px; |
||||
.media-table { |
||||
background: #fff; |
||||
margin-top: 32px; |
||||
} |
||||
.header { |
||||
width: 100%; |
||||
height: 60px; |
||||
background: #fff; |
||||
padding: 16px 24px; |
||||
font-size: 20px; |
||||
text-align: start; |
||||
color: #000; |
||||
margin-top: 10px; |
||||
} |
||||
.action-area { |
||||
color: $primary; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
.header { |
||||
width: 100%; |
||||
height: 60px; |
||||
background: #fff; |
||||
padding: 16px; |
||||
font-size: 20px; |
||||
font-weight: bold; |
||||
text-align: start; |
||||
color: #000; |
||||
} |
||||
</style> |
||||
|
@ -0,0 +1,182 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -1,11 +1,463 @@
|
||||
<template> |
||||
<div class="project-tsa-wrapper"> |
||||
TSA |
||||
<div class="project-tsa-wrapper "> |
||||
<div> |
||||
<a-row> |
||||
<a-col :span="1"></a-col> |
||||
<a-col :span="11">My Username</a-col> |
||||
<a-col :span="11" align="right" style="font-weight: 700">{{ username }}</a-col> |
||||
<a-col :span="1"></a-col> |
||||
</a-row> |
||||
</div> |
||||
<div> |
||||
<a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;"> |
||||
<a-collapse-panel :key="EDeviceTypeName.Dock" header="Dock" style="border-bottom: 1px solid #4f4f4f;"> |
||||
<div v-if="onlineDocks.data.length === 0" style="height: 150px; color: white;"> |
||||
<a-empty :image="noData" :image-style="{ height: '60px' }" /> |
||||
</div> |
||||
<div v-else class="fz12" style="color: white;"> |
||||
<div v-for="dock in onlineDocks.data" :key="dock.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;"> |
||||
<div style="border-radius: 2px; height: 100%; width: 100%;" class="flex-row flex-justify-between flex-align-center"> |
||||
<div style="float: left; padding: 0px 5px 8px 8px; width: 88%"> |
||||
<div style="width: 80%; height: 30px; line-height: 30px; font-size: 16px;"> |
||||
<a-tooltip :title="dock.gateway.callsign"> |
||||
<span class="text-hidden" style="max-width: 200px;">{{ dock.gateway.callsign }}</span> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> |
||||
<div> |
||||
<span class="ml5 mr5"><RobotOutlined /></span> |
||||
<span class="font-bold" :style="dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'"> |
||||
{{ dockInfo[dock.gateway.sn] ? EDockModeCode[dockInfo[dock.gateway.sn].mode_code] : EDockModeCode[EDockModeCode.Disconnected] }} |
||||
</span> |
||||
</div> |
||||
<div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;"> |
||||
<div v-if="hmsInfo[dock.gateway.sn]" class="flex-align-center flex-row"> |
||||
<div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' : |
||||
hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;"> |
||||
<span :style="hmsInfo[dock.gateway.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.gateway.sn].length }}</span> |
||||
<span class="fz10">{{ hmsInfo[dock.gateway.sn].length > 99 ? '+' : ''}}</span> |
||||
</div> |
||||
<a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.gateway.sn]" |
||||
@visibleChange="readHms(hmsVisible[dock.gateway.sn], dock.gateway.sn)" |
||||
:overlayStyle="{width: '200px', height: '300px'}"> |
||||
<div :class="hmsInfo[dock.gateway.sn][0].level === EHmsLevel.CAUTION ? 'caution' : |
||||
hmsInfo[dock.gateway.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;"> |
||||
<span class="word-loop">{{ hmsInfo[dock.gateway.sn][0].message_en }}</span> |
||||
</div> |
||||
<template #content> |
||||
<a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true"> |
||||
<a-collapse-panel v-for="hms in hmsInfo[dock.gateway.sn]" :key="hms.hms_id" :showArrow="false" |
||||
style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px" |
||||
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||
> |
||||
<template #header="{ isActive }"> |
||||
<div class="flex-row flex-align-center" style="width: 130px;"> |
||||
<div style="width: 110px;"> |
||||
<span class="word-loop">{{ hms.message_en }}</span> |
||||
</div> |
||||
<div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center" |
||||
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||
> |
||||
<DoubleRightOutlined :rotate="isActive ? 90 : 0" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<a-tooltip :title="hms.create_time"> |
||||
<div style="color: white;" class="text-hidden">{{ hms.create_time }}</div> |
||||
</a-tooltip> |
||||
</a-collapse-panel> |
||||
</a-collapse> |
||||
</template> |
||||
</a-popover> |
||||
</div> |
||||
<div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div> |
||||
</div> |
||||
</div> |
||||
<div class="mt5 flex-align-center flex-row flex-justify-between" style="background: #595959;"> |
||||
<div> |
||||
<span class="ml5 mr5"><RocketOutlined /></span> |
||||
<span class="font-bold" :style="deviceInfo[dock.sn] && deviceInfo[dock.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'"> |
||||
{{ deviceInfo[dock.sn] ? EModeCode[deviceInfo[dock.sn].mode_code] : EModeCode[EModeCode.Disconnected] }} |
||||
</span> |
||||
</div> |
||||
<div class="mr5 flex-align-center flex-row" style="width: 85px; margin-right: 0; height: 18px;"> |
||||
<div v-if="hmsInfo[dock.sn]" class="flex-align-center flex-row"> |
||||
<div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution-blink' : |
||||
hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn-blink' : 'notice-blink'" style="width: 18px; height: 16px; text-align: center;"> |
||||
<span :style="hmsInfo[dock.sn].length > 99 ? 'font-size: 11px' : 'font-size: 12px'">{{ hmsInfo[dock.sn].length }}</span> |
||||
<span class="fz10">{{ hmsInfo[dock.sn].length > 99 ? '+' : ''}}</span> |
||||
</div> |
||||
<a-popover trigger="click" placement="bottom" color="black" v-model:visible="hmsVisible[dock.sn]" @visibleChange="readHms(hmsVisible[dock.sn], dock.sn)" |
||||
:overlayStyle="{width: '200px', height: '300px'}"> |
||||
<div :class="hmsInfo[dock.sn][0].level === EHmsLevel.CAUTION ? 'caution' : |
||||
hmsInfo[dock.sn][0].level === EHmsLevel.WARN ? 'warn' : 'notice'" style="margin-left: 3px; width: 62px; height: 16px;"> |
||||
<span class="word-loop">{{ hmsInfo[dock.sn][0].message_en }}</span> |
||||
</div> |
||||
<template #content> |
||||
<a-collapse style="background: black; height: 300px; overflow-y: auto;" :bordered="false" expand-icon-position="right" :accordion="true"> |
||||
<a-collapse-panel v-for="hms in hmsInfo[dock.sn]" :key="hms.hms_id" :showArrow="false" |
||||
style=" margin: 0 auto 3px auto; border: 0; width: 140px; border-radius: 3px" |
||||
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||
> |
||||
<template #header="{ isActive }"> |
||||
<div class="flex-row flex-align-center" style="width: 130px;"> |
||||
<div style="width: 110px;"> |
||||
<span class="word-loop">{{ hms.message_en }}</span> |
||||
</div> |
||||
<div style="width: 20px; height: 15px; font-size: 10px; z-index: 2 " class="flex-row flex-align-center flex-justify-center" |
||||
:class="hms.level === EHmsLevel.CAUTION ? 'caution' : hms.level === EHmsLevel.WARN ? 'warn' : 'notice'" |
||||
> |
||||
<DoubleRightOutlined :rotate="isActive ? 90 : 0" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<a-tooltip :title="hms.create_time"> |
||||
<div style="color: white;" class="text-hidden">{{ hms.create_time }}</div> |
||||
</a-tooltip> |
||||
</a-collapse-panel> |
||||
</a-collapse> |
||||
</template> |
||||
</a-popover> |
||||
</div> |
||||
<div v-else class="width-100" style="height: 90%; background: rgba(0, 0, 0, 0.35)"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div style="float: right; background: #595959; height: 100%; width: 40px;" class="flex-row flex-justify-center flex-align-center"> |
||||
<div class="fz16" @click="switchVisible($event, dock, true, dockInfo[dock.gateway.sn] && dockInfo[dock.gateway.sn].mode_code !== EDockModeCode.Disconnected)"> |
||||
<a v-if="osdVisible.gateway_sn === dock.gateway.sn && osdVisible.visible"><EyeOutlined /></a> |
||||
<a v-else><EyeInvisibleOutlined /></a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</a-collapse-panel> |
||||
</a-collapse> |
||||
<a-collapse :bordered="false" expandIconPosition="right" accordion style="background: #232323;"> |
||||
<a-collapse-panel :key="EDeviceTypeName.Aircraft" header="Online Devices" style="border-bottom: 1px solid #4f4f4f;"> |
||||
<div v-if="onlineDevices.data.length === 0" style="height: 150px; color: white;"> |
||||
<a-empty :image="noData" :image-style="{ height: '60px' }" /> |
||||
</div> |
||||
<div v-else class="fz12" style="color: white;"> |
||||
<div v-for="device in onlineDevices.data" :key="device.sn" style="background: #3c3c3c; height: 90px; width: 250px; margin-bottom: 10px;"> |
||||
<div class="battery-slide" v-if="deviceInfo[device.sn]"> |
||||
<div style="background: #535759; width: 100%;"></div> |
||||
<div class="capacity-percent" :style="{ width: deviceInfo[device.sn].battery.capacity_percent + '%'}"></div> |
||||
<div class="return-home" :style="{ width: deviceInfo[device.sn].battery.return_home_power + '%'}"></div> |
||||
<div class="landing" :style="{ width: deviceInfo[device.sn].battery.landing_power + '%'}"></div> |
||||
<div class="battery" :style="{ left: deviceInfo[device.sn].battery.capacity_percent + '%' }"></div> |
||||
</div> |
||||
<div style="border-bottom: 1px solid #515151; border-radius: 2px; height: 50px; width: 100%;" class="flex-row flex-justify-between flex-align-center"> |
||||
<div style="float: left; padding: 5px 5px 8px 8px; width: 88%"> |
||||
<div style="width: 100%; height: 100%;"> |
||||
<a-tooltip> |
||||
<template #title>{{ device.model }} - {{ device.callsign }}</template> |
||||
<span class="text-hidden" style="max-width: 200px; display: block; height: 20px;">{{ device.model }} - {{ device.callsign }}</span> |
||||
</a-tooltip> |
||||
</div> |
||||
<div class="mt5" style="background: #595959;"> |
||||
<span class="ml5 mr5"><RocketOutlined /></span> |
||||
<span class="font-bold" :style="deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected ? 'color: #00ee8b' : 'color: red;'"> |
||||
{{ deviceInfo[device.sn] ? EModeCode[deviceInfo[device.sn].mode_code] : EModeCode[EModeCode.Disconnected] }} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
<div style="float: right; background: #595959; height: 50px; width: 40px;" class="flex-row flex-justify-center flex-align-center"> |
||||
<div class="fz16" @click="switchVisible($event, device, false, deviceInfo[device.sn] && deviceInfo[device.sn].mode_code !== EModeCode.Disconnected)"> |
||||
<a v-if="osdVisible.sn === device.sn && osdVisible.visible"><EyeOutlined /></a> |
||||
<a v-else><EyeInvisibleOutlined /></a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="flex-row flex-justify-center flex-align-center" style="height: 40px;"> |
||||
<div style="height: 20px; background: #595959; width: 94%;" > |
||||
<span class="mr5"><a-image style="margin-left: 2px; margin-top: -2px; height: 20px; width: 20px;" :src="rc" /></span> |
||||
<a-tooltip> |
||||
<template #title>{{ device.gateway.callsign }} </template> |
||||
<span>{{ device.gateway.callsign }}</span> |
||||
</a-tooltip> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</a-collapse-panel> |
||||
</a-collapse> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
import { computed, onMounted, reactive, ref, watch, WritableComputedRef } from 'vue' |
||||
import { EDeviceTypeName, ELocalStorageKey } from '/@/types' |
||||
import noData from '/@/assets/icons/no-data.png' |
||||
import rc from '/@/assets/icons/rc.png' |
||||
import { DeviceStatus, EModeCode, OSDVisible, EDockModeCode, DeviceOsd } from '/@/types/device' |
||||
import { useMyStore } from '/@/store' |
||||
import { getDeviceTopo, getUnreadDeviceHms, updateDeviceHms } from '/@/api/manage' |
||||
import { message } from 'ant-design-vue' |
||||
import { RocketOutlined, EyeInvisibleOutlined, EyeOutlined, RobotOutlined, DoubleRightOutlined } from '@ant-design/icons-vue' |
||||
import { EHmsLevel } from '/@/types/enums' |
||||
|
||||
const store = useMyStore() |
||||
const username = ref(localStorage.getItem(ELocalStorageKey.Username)) |
||||
const workspaceId = ref(localStorage.getItem(ELocalStorageKey.WorkspaceId)!) |
||||
const osdVisible = ref({} as OSDVisible) |
||||
const hmsVisible = new Map<string, boolean>() |
||||
|
||||
interface OnlineDevice { |
||||
model: string, |
||||
callsign: string, |
||||
sn: string, |
||||
mode: number, |
||||
gateway: { |
||||
model: string, |
||||
callsign: string, |
||||
sn: string, |
||||
domain: string, |
||||
}, |
||||
payload: { |
||||
model: string |
||||
}[] |
||||
} |
||||
|
||||
const onlineDevices = reactive({ |
||||
data: [] as OnlineDevice[] |
||||
}) |
||||
|
||||
const onlineDocks = reactive({ |
||||
data: [] as OnlineDevice[] |
||||
}) |
||||
|
||||
const deviceInfo = computed(() => store.state.deviceState.deviceInfo) |
||||
const dockInfo = computed(() => store.state.deviceState.dockInfo) |
||||
const hmsInfo = computed({ |
||||
get: () => store.state.hmsInfo, |
||||
set: (val) => { |
||||
return val |
||||
} |
||||
}) |
||||
|
||||
onMounted(() => { |
||||
getOnlineTopo() |
||||
setTimeout(() => { |
||||
watch(() => store.state.deviceStatusEvent, |
||||
data => { |
||||
getOnlineTopo() |
||||
if (data.deviceOnline.sn) { |
||||
getUnreadHms(data.deviceOnline.sn) |
||||
} |
||||
}, |
||||
{ |
||||
deep: true |
||||
} |
||||
) |
||||
getOnlineDeviceHms() |
||||
}, 3000) |
||||
}) |
||||
|
||||
function getOnlineTopo () { |
||||
getDeviceTopo(workspaceId.value).then((res) => { |
||||
if (res.code !== 0) { |
||||
return |
||||
} |
||||
onlineDevices.data = [] |
||||
onlineDocks.data = [] |
||||
res.data.forEach((val: any) => { |
||||
const gateway = val.gateways_list.pop() |
||||
const device: OnlineDevice = { |
||||
model: val.device_name, |
||||
callsign: val.nickname, |
||||
sn: val.device_sn, |
||||
mode: EModeCode.Disconnected, |
||||
gateway: { |
||||
model: gateway?.device_name, |
||||
callsign: gateway?.nickname, |
||||
sn: gateway?.device_sn, |
||||
domain: gateway?.domain |
||||
}, |
||||
payload: [] |
||||
} |
||||
val.payloads_list.forEach((payload: any) => { |
||||
device.payload.push({ |
||||
model: payload.payload_name |
||||
}) |
||||
}) |
||||
if (gateway && EDeviceTypeName.Dock === gateway.domain) { |
||||
hmsVisible.set(device.sn, false) |
||||
hmsVisible.set(device.gateway.sn, false) |
||||
onlineDocks.data.push(device) |
||||
} |
||||
if (val.status && EDeviceTypeName.Gateway === gateway.domain) { |
||||
onlineDevices.data.push(device) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
function switchVisible (e: any, device: OnlineDevice, isDock: boolean, isClick: boolean) { |
||||
if (!isClick) { |
||||
e.target.style.cursor = 'not-allowed' |
||||
return |
||||
} |
||||
if (device.sn === osdVisible.value.sn) { |
||||
osdVisible.value.visible = !osdVisible.value.visible |
||||
} else { |
||||
osdVisible.value.sn = device.sn |
||||
osdVisible.value.callsign = device.callsign |
||||
osdVisible.value.model = device.model |
||||
osdVisible.value.visible = true |
||||
osdVisible.value.gateway_sn = device.gateway.sn |
||||
osdVisible.value.is_dock = isDock |
||||
osdVisible.value.gateway_callsign = device.gateway.callsign |
||||
} |
||||
store.commit('SET_OSD_VISIBLE_INFO', osdVisible) |
||||
} |
||||
|
||||
function getUnreadHms (sn: string) { |
||||
getUnreadDeviceHms(workspaceId.value, sn).then(res => { |
||||
if (res.data.length !== 0) { |
||||
hmsInfo.value[sn] = res.data |
||||
} |
||||
}) |
||||
console.info(hmsInfo.value) |
||||
} |
||||
|
||||
function getOnlineDeviceHms () { |
||||
const snList = Object.keys(dockInfo.value) |
||||
if (snList.length === 0) { |
||||
return |
||||
} |
||||
snList.forEach(sn => { |
||||
getUnreadHms(sn) |
||||
}) |
||||
const deviceSnList = Object.keys(deviceInfo.value) |
||||
if (deviceSnList.length === 0) { |
||||
return |
||||
} |
||||
deviceSnList.forEach(sn => { |
||||
getUnreadHms(sn) |
||||
}) |
||||
} |
||||
|
||||
function readHms (visiable: boolean, sn: string) { |
||||
if (!visiable) { |
||||
updateDeviceHms(workspaceId.value, sn).then(res => { |
||||
if (res.code === 0) { |
||||
delete hmsInfo.value[sn] |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
<style lang="scss"> |
||||
.project-tsa-wrapper > :first-child { |
||||
height: 50px; |
||||
line-height: 50px; |
||||
align-items: center; |
||||
border-bottom: 1px solid #4f4f4f; |
||||
} |
||||
.ant-collapse > .ant-collapse-item > .ant-collapse-header { |
||||
color: white; |
||||
border: 0; |
||||
padding-left: 14px; |
||||
} |
||||
|
||||
.text-hidden { |
||||
overflow: hidden !important; |
||||
text-overflow: ellipsis !important; |
||||
white-space: nowrap; |
||||
-o-text-overflow: ellipsis; |
||||
} |
||||
.font-bold { |
||||
font-weight: 700; |
||||
} |
||||
|
||||
.battery-slide { |
||||
width: 100%; |
||||
.capacity-percent { |
||||
background: #00ee8b; |
||||
} |
||||
.return-home { |
||||
background: #ff9f0a; |
||||
} |
||||
.landing { |
||||
background: #f5222d; |
||||
} |
||||
.battery { |
||||
background: white; |
||||
border-radius: 1px; |
||||
width: 8px; |
||||
height: 4px; |
||||
margin-top: -3px; |
||||
} |
||||
} |
||||
.battery-slide > div { |
||||
position: relative; |
||||
margin-top: -2px; |
||||
min-height: 2px; |
||||
border-radius: 2px; |
||||
white-space: nowrap; |
||||
} |
||||
.disable { |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.notice-blink { |
||||
background: $success; |
||||
animation: blink 500ms infinite; |
||||
} |
||||
.caution-blink { |
||||
background: orange; |
||||
animation: blink 500ms infinite; |
||||
} |
||||
.warn-blink { |
||||
background: red; |
||||
animation: blink 500ms infinite; |
||||
} |
||||
.notice { |
||||
background: $success; |
||||
overflow: hidden; |
||||
cursor: pointer; |
||||
} |
||||
.caution { |
||||
background: orange; |
||||
cursor: pointer; |
||||
overflow: hidden; |
||||
} |
||||
.warn { |
||||
background: red; |
||||
cursor: pointer; |
||||
overflow: hidden; |
||||
} |
||||
.word-loop { |
||||
white-space: nowrap; |
||||
display: inline-block; |
||||
animation: 10s loop linear infinite normal; |
||||
} |
||||
@keyframes blink { |
||||
from { |
||||
opacity: 1; |
||||
} |
||||
50% { |
||||
opacity: 0.35; |
||||
} |
||||
to { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
@keyframes loop { |
||||
0% { |
||||
transform: translateX(20px); |
||||
-webkit-transform: translateX(20px); |
||||
} |
||||
100% { |
||||
transform: translateX(-100%); |
||||
-webkit-transform: translateX(-100%); |
||||
} |
||||
} |
||||
|
||||
</style> |
||||
|
@ -1,9 +1,205 @@
@@ -1,9 +1,205 @@
|
||||
<template> |
||||
<div class="project-wayline-wrapper"> |
||||
wayline |
||||
<div class="project-wayline-wrapper height-100"> |
||||
<div style="height: 50px; line-height: 50px; border-bottom: 1px solid #4f4f4f; font-weight: 450;"> |
||||
<a-row> |
||||
<a-col :span="1"></a-col> |
||||
<a-col :span="22">Flight Route Library</a-col> |
||||
<a-col :span="1"></a-col> |
||||
</a-row> |
||||
</div> |
||||
<div class="height-100"> |
||||
<div class="scrollbar uranus-scrollbar" v-if="waylinesData.data.length !== 0" @scroll="onScroll"> |
||||
<div v-for="wayline in waylinesData.data" :key="wayline.id"> |
||||
<div class="wayline-panel" style="padding-top: 5px;" @click="selectRoute(wayline)"> |
||||
<div class="title"> |
||||
<a-tooltip :title="wayline.name"> |
||||
<div class="pr10" style="width: 120px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.name }}</div> |
||||
</a-tooltip> |
||||
<div class="ml10"><UserOutlined /></div> |
||||
<a-tooltip :title="wayline.user_name"> |
||||
<div class="ml5 pr10" style="width: 80px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">{{ wayline.user_name }}</div> |
||||
</a-tooltip> |
||||
<div class="fz20"> |
||||
<a-dropdown> |
||||
<a style="color: white;"> |
||||
<EllipsisOutlined /> |
||||
</a> |
||||
<template #overlay> |
||||
<a-menu theme="dark" class="more" style="background: #3c3c3c;"> |
||||
<a-menu-item @click="downloadWayline(wayline.id, wayline.name)"> |
||||
<span>Download</span> |
||||
</a-menu-item> |
||||
<a-menu-item @click="showWaylineTip(wayline.id)"> |
||||
<span>Delete</span> |
||||
</a-menu-item> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
</div> |
||||
</div> |
||||
<div class="ml10 mt5" style="color: hsla(0,0%,100%,0.65);"> |
||||
<span><RocketOutlined /></span> |
||||
<span class="ml5">{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(wayline.drone_model_key)] }}</span> |
||||
<span class="ml10"><CameraFilled style="border-top: 1px solid; padding-top: -3px;" /></span> |
||||
<span class="ml5" v-for="payload in wayline.payload_model_keys" :key="payload.id"> |
||||
{{ Object.keys(EDeviceType)[Object.values(EDeviceType).indexOf(payload)] }} |
||||
</span> |
||||
</div> |
||||
<div class="mt5 ml10" style="color: hsla(0,0%,100%,0.35);"> |
||||
<span class="mr10">Update at {{ new Date(wayline.update_time).toLocaleString() }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div v-else> |
||||
<a-empty :image-style="{ height: '60px', marginTop: '60px' }" /> |
||||
</div> |
||||
<a-modal v-model:visible="deleteTip" width="450px" :closable="false" :maskClosable="false" centered :okButtonProps="{ danger: true }" @ok="deleteWayline"> |
||||
<p class="pt10 pl20" style="height: 50px;">Wayline file is unrecoverable once deleted. Continue?</p> |
||||
<template #title> |
||||
<div class="flex-row flex-justify-center"> |
||||
<span>Delete</span> |
||||
</div> |
||||
</template> |
||||
</a-modal> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts" setup></script> |
||||
<script lang="ts" setup> |
||||
import { reactive } from '@vue/reactivity' |
||||
import { message } from 'ant-design-vue' |
||||
import { onMounted, onUpdated, ref } from 'vue' |
||||
import { deleteWaylineFile, downloadWaylineFile, getWaylineFiles } from '/@/api/wayline' |
||||
import { ELocalStorageKey } from '/@/types' |
||||
import { EllipsisOutlined, RocketOutlined, CameraFilled, UserOutlined } from '@ant-design/icons-vue' |
||||
import { EDeviceType } from '/@/types/device' |
||||
import { useMyStore } from '/@/store' |
||||
import { WaylineFile } from '/@/types/wayline' |
||||
import { downloadFile } from '/@/utils/common' |
||||
import { IPage } from '/@/api/http/type' |
||||
|
||||
<style lang="scss" scoped></style> |
||||
const store = useMyStore() |
||||
const pagination :IPage = { |
||||
page: 1, |
||||
total: 0, |
||||
page_size: 10 |
||||
} |
||||
|
||||
const waylinesData = reactive({ |
||||
data: [] as WaylineFile[] |
||||
}) |
||||
|
||||
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)! |
||||
const deleteTip = ref(false) |
||||
const deleteWaylineId = ref<string>('') |
||||
const canRefresh = ref(true) |
||||
|
||||
onMounted(() => { |
||||
getWaylines() |
||||
}) |
||||
|
||||
onUpdated(() => { |
||||
const element = document.getElementsByClassName('scrollbar').item(0) as HTMLDivElement |
||||
const parent = element?.parentNode as HTMLDivElement |
||||
setTimeout(() => { |
||||
if (element?.scrollHeight < parent?.clientHeight && pagination.total > waylinesData.data.length) { |
||||
if (canRefresh.value) { |
||||
pagination.page++ |
||||
getWaylines() |
||||
} |
||||
} else if (element && element.className.indexOf('height-100') === -1) { |
||||
element.className = element.className + ' height-100' |
||||
} |
||||
}, 300) |
||||
}) |
||||
|
||||
function getWaylines () { |
||||
if (!canRefresh.value) { |
||||
return |
||||
} |
||||
canRefresh.value = false |
||||
getWaylineFiles(workspaceId, { |
||||
page: pagination.page, |
||||
page_size: pagination.page_size, |
||||
order_by: 'update_time desc' |
||||
}).then(res => { |
||||
if (res.code !== 0) { |
||||
return |
||||
} |
||||
res.data.list.forEach((wayline: WaylineFile) => waylinesData.data.push(wayline)) |
||||
pagination.total = res.data.pagination.total |
||||
pagination.page = res.data.pagination.page |
||||
}).finally(() => { |
||||
canRefresh.value = true |
||||
}) |
||||
} |
||||
|
||||
function showWaylineTip (waylineId: string) { |
||||
deleteWaylineId.value = waylineId |
||||
deleteTip.value = true |
||||
} |
||||
|
||||
function deleteWayline () { |
||||
deleteWaylineFile(workspaceId, deleteWaylineId.value).then(res => { |
||||
if (res.code === 0) { |
||||
message.success('Wayline file deleted') |
||||
} |
||||
deleteWaylineId.value = '' |
||||
deleteTip.value = false |
||||
pagination.total-- |
||||
waylinesData.data = [] |
||||
setTimeout(getWaylines, 500) |
||||
}) |
||||
} |
||||
|
||||
function downloadWayline (waylineId: string, fileName: string) { |
||||
downloadWaylineFile(workspaceId, waylineId).then(res => { |
||||
if (res.code && res.code !== 0) { |
||||
return |
||||
} |
||||
const data = new Blob([res.data], { type: 'application/zip' }) |
||||
downloadFile(data, fileName + '.kmz') |
||||
}) |
||||
} |
||||
|
||||
function selectRoute (wayline: WaylineFile) { |
||||
store.commit('SET_SELECT_WAYLINE_INFO', wayline) |
||||
} |
||||
|
||||
function onScroll (e: any) { |
||||
const element = e.srcElement |
||||
if (element.scrollTop + element.clientHeight === element.scrollHeight && Math.ceil(pagination.total / pagination.page_size) > pagination.page && canRefresh.value) { |
||||
pagination.page++ |
||||
getWaylines() |
||||
} |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.wayline-panel { |
||||
background: #3c3c3c; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
margin-top: 10px; |
||||
height: 90px; |
||||
width: 95%; |
||||
font-size: 13px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
.title { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
height: 30px; |
||||
font-weight: bold; |
||||
margin: 0px 10px 0 10px; |
||||
} |
||||
} |
||||
.uranus-scrollbar { |
||||
overflow: auto; |
||||
scrollbar-width: thin; |
||||
scrollbar-color: #c5c8cc transparent; |
||||
} |
||||
</style> |
||||
|
@ -0,0 +1,140 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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) |
||||
} |