Compare commits

..

11 Commits

Author SHA1 Message Date
Teo
0a2d8b06f8 进度计划添加计划,添加日报 2025-05-28 19:52:30 +08:00
Teo
e2e4550736 桩点支架分批次上传 2025-05-27 19:53:19 +08:00
Teo
933fe4d74a init 2025-05-27 09:16:44 +08:00
Teo
cbb62f2bf0 解决冲突 2025-05-21 11:44:03 +08:00
Teo
95e38df6a5 init:first commit of plus-ui 2025-05-21 11:24:53 +08:00
Teo
19974e261f 合并分支 2025-05-16 18:53:22 +08:00
Teo
eaa4a02790 1 2025-05-16 18:50:17 +08:00
Teo
3e67c83ced 无人机系统 2025-05-14 18:31:25 +08:00
Teo
c92f2091d9 大屏天气列表 2025-05-13 18:29:12 +08:00
Teo
055a87d2c2 echarts滚动效果 2025-05-12 18:31:23 +08:00
lcj
4bc93e68a0 B
Merge branch 'main' of http://192.168.110.199:3000/lcj/xinnengy
2025-04-02 15:22:20 +08:00
277 changed files with 19367 additions and 407 deletions

View File

@ -5,7 +5,11 @@ VITE_APP_TITLE = 新能源项目管理平台
VITE_APP_ENV = 'development'
# 开发环境
VITE_APP_BASE_API = 'http://192.168.110.8:8899'
VITE_APP_BASE_API = 'http://192.168.110.126:8899'
# 无人机接口地址
VITE_APP_BASE_DRONE_API = 'http://192.168.110.8:9136'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'

View File

@ -210,6 +210,7 @@
</div>
</div>
<script src="./src/assets/sdk/YJEarth.min.js"></script>
<script src="./src/utils/reconnecting-websocket.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -20,6 +20,7 @@
"url": "https://gitee.com/JavaLionLi/plus-ui.git"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@element-plus/icons-vue": "2.3.1",
"@highlightjs/vue-plugin": "2.1.0",
"@turf/turf": "^7.2.0",
@ -41,6 +42,9 @@
"js-cookie": "3.0.5",
"js-md5": "^0.8.3",
"jsencrypt": "3.3.2",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"mitt": "^3.0.1",
"nprogress": "0.2.0",
"ol": "^10.5.0",

View File

@ -31,8 +31,10 @@ onMounted(() => {
input {
-webkit-user-select: auto; /*webkit浏览器*/
user-select: auto;
}
textarea {
user-select: auto;
-webkit-user-select: auto; /*webkit浏览器*/
}
</style>

430
src/api/air/index.ts Normal file
View File

@ -0,0 +1,430 @@
import { request } from '../../utils/requset2.js';
// 开关空调
export function airConditionerModeSwitch(data) {
return request({
url: '/dj/remote/debug/airConditionerModeSwitch',
method: 'post',
data
});
}
// 生成航线文件
export function waypoint(data) {
return request({
url: '/dj/router/waypoint',
method: 'post',
data
});
}
// 创建任务 /dj/task/create
export function taskCreate(data) {
return request({
url: '/dj/missions/create',
method: 'post',
data
});
}
// 下发任务 /dj/flightTaskPrepare
export function flightTaskPrepare(data) {
return request({
url: '/dj/flightTaskPrepare',
method: 'post',
data
});
}
// 执行任务
export function flightTaskExecute(data) {
return request({
url: '/dj/flightTaskExecute',
method: 'post',
data
});
}
// 取消任务
export function flightTaskUndo(data) {
return request({
url: '/dj/flightTaskUndo',
method: 'post',
data
});
}
// 恢复航线
export function flightTaskRecovery(data) {
return request({
url: '/dj/flightTaskRecovery',
method: 'post',
data
});
}
// 恢复航线
export function flightTaskPause(data) {
return request({
url: '/dj/flightTaskPause',
method: 'post',
data
});
}
// 一键返航
export function returnHome(data) {
return request({
url: '/dj/returnHome',
method: 'post',
data
});
}
// 无参飞行指令
export function noDataFlight(data) {
return request({
url: '/dj/cmdFly/noDataFlight',
method: 'post',
data
});
}
// 有参飞行指令
export function hasDataFlight(data) {
return request({
url: '/dj/cmdFly/hasDataFlight',
method: 'post',
data
});
}
// 开启指令
export function drcModeEnte(data) {
return request({
url: '/dj/cmdFly/drcModeEnter',
method: 'post',
data
});
}
// 查询航线文件信息列表
export function listPaths(query) {
return request({
url: '/system/paths/list',
method: 'get',
params: query
});
}
// 删除航线文件信息
export function delPaths(id) {
return request({
url: '/system/paths/' + id,
method: 'delete'
});
}
// 查询任务列表
export function listTasks(query) {
return request({
url: '/system/tasks/list',
method: 'get',
params: query
});
}
// 查询无人机任务列表
export function listMissions(query) {
return request({
url: '/system/missions/list',
method: 'get',
params: query
});
}
// 删除无人机任务
export function delMissions(id) {
return request({
url: '/system/missions/' + id,
method: 'delete'
});
}
// 远程调试
export function remoteDebug(data) {
return request({
url: '/dj/remote/debug/service',
method: 'post',
data
});
}
// 增强图传
export function sdrWorkmodeSwitch(data) {
return request({
url: '/dj/remote/debug/sdrWorkmodeSwitch',
method: 'post',
data
});
}
// 查询视频设备列表
export function listInfo(query) {
return request({
url: '/system/info/list',
method: 'get',
params: query
});
}
// 登录
export function login(data) {
return request({
url: '/login',
headers: {
isToken: false,
repeatSubmit: false
},
method: 'post',
data: data
});
}
// 获取文件
export function getFileByflightId(data) {
return request({
url: '/dj/getFileByflightId',
method: 'post',
data
});
}
// 机场列表
export function droneList(data) {
return request({
url: '/system/drone/list',
method: 'get',
data
});
}
// 监听网关
export function gatewaysAdd(data) {
return request({
url: '/dj/gateways/add',
method: 'post',
data
});
}
// 关闭网关
export function gatewaysRemove(data) {
return request({
url: '/dj/gateways/remove',
method: 'delete',
data
});
}
// 添加航线
export function routerAdd(data) {
return request({
url: '/dj/router/add',
method: 'post',
data
});
}
// 复制航线 /dj/router/copy/{id}
export function routerCopy(id) {
return request({
url: '/dj/router/copy/' + id,
method: 'post'
});
}
// 重命名文件
export function routerRename(data) {
return request({
url: '/dj/router/rename',
method: 'post',
data
});
}
// 获取文件
export function takeoffList(data) {
return request({
url: '/system/takeoff/list',
method: 'get',
data
});
}
// 切换直播流
export function liveChange(data) {
return request({
url: '/dj/live/change',
method: 'post',
data
});
}
// 设置清晰度 /dj/live/quality
export function liveChangeQuality(data) {
return request({
url: '/dj/live/quality',
method: 'post',
data
});
}
// 开启直播
export function liveStart(data) {
return request({
url: '/dj/live/start',
method: 'post',
data
});
}
// 关闭直播
export function liveStop(data) {
return request({
url: '/dj/live/stop',
method: 'post',
data
});
}
// 静音模式开关
export function propertySet(data) {
return request({
url: '/property/set',
method: 'post',
data
});
}
// 直播相机切换
export function cameraChange(data) {
return request({
url: '/dj/live/camera/change',
method: 'post',
data
});
}
// 新建计划
export function taskAdd(data) {
return request({
url: '/dj/task/add',
method: 'post',
data
});
}
// 计划列表
export function taskList(data) {
return request({
url: '/dj/task/list',
method: 'get',
params: data
});
}
// 新暂停
export function flightTaskPauseNew(data) {
return request({
url: '/dj/task/flightTaskPause',
method: 'post',
data
});
}
// 新恢复
export function flightTaskRecoveryNew(data) {
return request({
url: '/dj/task/flightTaskRecovery',
method: 'post',
data
});
}
// 取消任务
export function flightTaskUndoNew(data) {
return request({
url: '/dj/task/flightTaskUndo',
method: 'post',
data
});
}
// 一键返航 post
export function returnHomeNew(data) {
return request({
url: '/dj/task/returnHome',
method: 'post',
data
});
}
// 删除
export function delMissionsNew(id) {
return request({
url: '/system/new/missions/' + id,
method: 'delete'
});
}
// drc飞行控制 /dj/cmdFly/droneControl
export function droneControl(data) {
return request({
url: '/dj/cmdFly/droneControl',
method: 'post',
data
});
}
// drc急停 /dj/cmdFly/droneEmergencyStop
export function droneEmergencyStop(data) {
return request({
url: '/dj/cmdFly/droneEmergencyStop',
method: 'post',
data
});
}
// 取消返航 /dj/returnHomeCancel
export function returnHomeCancel(data) {
return request({
url: '/dj/returnHomeCancel',
method: 'post',
data
});
}
// 导入航线
export function returnImport(data) {
return request({
url: '/dj/router/import',
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
});
}
// DRC心跳
export function heartBeat(data) {
return request({
url: '/dj/cmdFly/heartBeat',
method: 'post',
data
});
}
// AI识别
export function aiInfo(data) {
return request({
url: '/minio/ai/info',
method: 'get',
params: data
});
}
// 当前最新任务
export function getLatest(data) {
return request({
url: '/dj/task/getLatest',
method: 'get',
params: data
});
}

View File

@ -0,0 +1,118 @@
import { request } from '../../utils/requset2';
// 获取文件列表
export function getFiles(data) {
return request({
url: '/minio/file',
method: 'get',
data,
params: data
});
}
// 获取ai文件列表
export function getAiFiles(data) {
return request({
url: '/minio/ai/file',
method: 'get',
data,
params: data
});
}
// 获取文件信息
export function getInfo(data) {
return request({
url: '/minio/info',
method: 'get',
data,
params: data,
timeout: 30000
});
}
// 获取ai文件信息
export function getAiInfo(data) {
return request({
url: '/minio/ai/info',
method: 'get',
data,
params: data,
timeout: 30000
});
}
// 下载文件
export function fileDownload(params, onProgress) {
return request({
url: '/minio/download',
method: 'post',
params,
responseType: 'blob',
timeout: 0,
// header: {
// "Content-Length": Buffer.byteLength(JSON.stringify(params)),
// },
onDownloadProgress: (progressEvent) => {
if (onProgress && typeof onProgress === 'function') {
const { loaded, total } = progressEvent;
console.log('progressEvent', progressEvent);
const percentage = Math.round((loaded / total) * 100); // 计算百分比
onProgress(percentage); // 调用传入的 onProgress 回调函数
}
}
});
}
// 删除文件
export function deleteFile(data) {
return request({
url: '/minio/deleteBatch',
method: 'delete',
data,
timeout: 0
});
}
// 删除ai文件
export function deleteAiFile(data) {
return request({
url: '/minio/ai/deleteBatch',
method: 'delete',
data,
timeout: 0
});
}
// 批量下载文件或目录
export function downloadBatch(data, onProgress) {
return request({
url: '/minio/downloadBatch',
method: 'post',
responseType: 'blob',
data,
timeout: 0,
onDownloadProgress: (progressEvent) => {
if (onProgress && typeof onProgress === 'function') {
const { loaded, total } = progressEvent;
console.log('progressEvent', progressEvent);
const percentage = Math.round((loaded / total) * 100); // 计算百分比
onProgress(percentage); // 调用传入的 onProgress 回调函数
}
}
});
}
// 获取某个目录下面的所有文件的URL列表
export function getUrlList(data) {
return request({
url: '/minio/urlList',
method: 'post',
data,
params: data,
timeout: 0
});
}
// 获取某个目录下面的所有文件的URL列表
export function getAiUrlList(data) {
return request({
url: '/minio/ai/urlList',
method: 'post',
data,
params: data,
timeout: 0
});
}

View File

@ -1,6 +1,17 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { QualityVO, Query, ConstructionUserVO, MachineryrVO, MaterialsVO } from './type';
import {
QualityVO,
Query,
ConstructionUserVO,
MachineryrVO,
MaterialsVO,
projectNewsVO,
safetyInspectionVO,
projectNewsDetailVO,
weatherVO,
safetyDayVO
} from './type';
/**
* 查询大屏质量信息
* @param query
@ -15,6 +26,47 @@ export const getQualityList = (query?: Query): AxiosPromise<QualityVO> => {
});
};
/**
* 查询大屏项目新闻列表
* @param query
* @returns {*}
*/
export const getprojectNewsList = (query?: Query): AxiosPromise<projectNewsVO[]> => {
return request({
url: '/project/projectNews/list/gis',
method: 'get',
params: query
});
};
/**
* 获取项目新闻详细信息
* @param query
* @returns {*}
*/
export const getprojectNewsDetailList = (id: number): AxiosPromise<projectNewsDetailVO> => {
return request({
url: '/project/projectNews/' + id,
method: 'get'
});
};
/**
* 查询大屏安全信息
* @param query
* @returns {*}
*/
export const getsafetyInspectionList = (query?: Query): AxiosPromise<safetyInspectionVO> => {
return request({
url: '/safety/safetyInspection/gis',
method: 'get',
params: query
});
};
/**
* 查询施工人员大屏数据
* @param query
@ -56,3 +108,29 @@ export const getMaterialsList = (query?: Query): AxiosPromise<MaterialsVO[]> =>
params: query
});
};
/**
* 查询项目天气
* @param query
* @returns {*}
*/
export const getweatherList = (id?: string): AxiosPromise<weatherVO[]> => {
return request({
url: '/project/project/weather/' + id,
method: 'get'
});
};
/**
* 查询项目安全天数
* @param query
* @returns {*}
*/
export const getSafetyDay = (id?: string): AxiosPromise<safetyDayVO> => {
return request({
url: '/project/project/safetyDay/' + id,
method: 'get'
});
};

View File

@ -41,3 +41,52 @@ export interface MaterialsVO {
outCount: string;
value: number;
}
export interface projectNewsVO {
id: string;
title: string;
show?: boolean;
}
export interface projectNewsDetailVO {
id: string;
title: string;
content: string;
file: string;
}
export interface safetyInspectionVO {
//站班会总数
teamMeetingCount: string;
//安全巡检总数
safetyInspectionCount: string;
//整改情况
correctSituationCount: string;
//站班会列表
teamMeetingList: safetyInspectionlist[];
}
export interface safetyInspectionlist {
id: string;
teamName: string;
name: string;
meetingDate: string;
}
export interface weatherVO {
date: string;
week: string;
tempMax: string;
tempMin: string;
dayStatus: string;
dayIcon: string;
nightStatus: string;
nightIcon: string;
sunRise: string;
sunSet: string;
}
export interface safetyDayVO {
safetyDay: string;
}

View File

@ -0,0 +1,128 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ProgressCategoryVO, ProgressCategoryForm, ProgressCategoryQuery, ProgressPlanForm, lastTimeVo, workScheduleListVO, workScheduleListQuery, progressPlanDetailForm } from '@/api/progress/plan/types';
/**
* 查询进度类别列表
* @param query
* @returns {*}
*/
export const listProgressCategory = (query?: ProgressCategoryQuery): AxiosPromise<ProgressCategoryVO[]> => {
return request({
url: '/progress/progressCategory/list',
method: 'get',
params: query
});
};
/**
* 查询进度类别详细
* @param id
*/
export const getProgressCategory = (id: string | number): AxiosPromise<ProgressCategoryVO> => {
return request({
url: '/progress/progressCategory/' + id,
method: 'get'
});
};
/**
* 新增进度类别
* @param data
*/
export const addProgressCategory = (data: ProgressCategoryForm) => {
return request({
url: '/progress/progressCategory',
method: 'post',
data: data
});
};
/**
* 修改进度类别
* @param data
*/
export const updateProgressCategory = (data: ProgressCategoryForm) => {
return request({
url: '/progress/progressCategory',
method: 'put',
data: data
});
};
/**
* 删除进度类别
* @param id
*/
export const delProgressCategory = (id: string | number | Array<string | number>) => {
return request({
url: '/progress/progressCategory/' + id,
method: 'delete'
});
};
/**
* 查询设施-方阵列表
* @param data
*/
export const getProjectSquare = (projectId: string) => {
return request({
url: '/facility/matrix/list',
method: 'get',
params: {projectId}
});
};
/**
* 获取进度类别最后一次进度信息
* @param id
*/
export const lastTime = (id: string | number ):AxiosPromise<lastTimeVo> => {
return request({
url: '/progress/progressCategory/lastTime/' + id,
method: 'get'
});
};
/**
* 新增进度计划
* @param data
*/
export const workScheduleAddPlan = (data: ProgressPlanForm) => {
return request({
url: '/progress/progressPlan',
method: 'post',
data: data
});
};
/**
* 获取进度计划详细信息
* @param data
*/
export const workScheduleList = (query: workScheduleListQuery):AxiosPromise<workScheduleListVO[]> => {
return request({
url: '/progress/progressPlan/list',
method: 'get',
params: query
});
};
/**
* 新增进度计划详情(百分比设施)
* @param data
*/
export const workScheduleSubmit = (data: progressPlanDetailForm) => {
return request({
url: '/progress/progressPlanDetail/insert/percentage',
method: 'post',
data: data
});
};
export const workScheduleDel=()=>{}
export const pvModuleList=()=>{}
export const addDaily=()=>{}
export const getDailyBook=()=>{}
export const deleteDaily=()=>{}

View File

@ -0,0 +1,137 @@
export interface ProgressCategoryVO {
/**
* 主键id
*/
id: string | number;
/**
* 父类别id
*/
pid: string | number;
/**
* 类别名称
*/
name: string;
/**
* 计量方式1数量 2百分比
*/
unitType: string;
/**
* 项目id0表示共用
*/
projectId: string | number;
matrixId?: string | number;
/**
* 子对象
*/
children: ProgressCategoryVO[];
}
export interface ProgressCategoryForm extends BaseEntity {
/**
* 主键id
*/
id?: string | number;
/**
* 父类别id
*/
pid?: string | number;
/**
* 类别名称
*/
name?: string;
/**
* 计量方式1数量 2百分比
*/
unitType?: string;
/**
* 项目id0表示共用
*/
projectId?: string | number;
matrixId?: string | number;
}
export interface ProgressPlanForm {
projectId?: string | number;
matrixId?: string | number;
progressCategoryId?: string | number;
startDate?: string;
endDate?: string;
planNumber?: number;
detailList: {
date: string;
planNumber: number;
}[];
}
export interface workScheduleListVO {
id: string | number;
progressCategoryId: string | number;
progressCategoryName: string;
startDate: string;
endDate: string;
planNumber: number;
finishedNumber: number;
detailList: {
id: string | number;
date: string;
planNumber: number;
finishedNumber: number;
aiFill: boolean;
}[];
}
export interface progressPlanDetailForm {
id: string | number;
finishedNumber: number;
submitTime?: string;
}
export interface workScheduleListQuery {
progressCategoryId?: string | number;
pageSize?: number;
pageNum?: number;
}
export interface lastTimeVo {
endDate: string;
leftNum: number;
}
export interface ProgressCategoryQuery {
/**
* 父类别id
*/
pid?: string | number;
/**
* 类别名称
*/
name?: string;
/**
* 计量方式1数量 2百分比
*/
unitType?: string;
/**
* 项目id0表示共用
*/
projectId?: string | number;
/**
* 日期范围参数
*/
params?: any;
/**
* 方阵id
*/
matrixId?: string | number;
}

View File

@ -28,7 +28,6 @@ export const listConstructionMonth = (query?: ConstructionMonthQuery): AxiosProm
params: query
});
};
/**
* 查询施工人员列表
* @param query

View File

@ -183,7 +183,6 @@ export interface ConstructionUserVO {
*/
createTime: string;
}
export interface skipType {
/**
* 项目id

View File

@ -93,6 +93,8 @@ export const addProjectFacilities = (data: any) => {
* @param data
*/
export const addProjectPilePoint = (data: any) => {
console.log("🚀 ~ addProjectPilePoint ~ data:", data)
return request({
url: '/facility/photovoltaicPanelPoint/parts/geoJson',
method: 'post',
@ -112,6 +114,32 @@ export const addProjectSquare = (data: any) => {
});
};
/**
* 通过GeoJson新增设施-箱变
* @param data
*/
export const addBoxTransformer = (data: any) => {
return request({
url: '/facility/boxTransformer/geoJson',
method: 'post',
data: data
});
};
/**
* 通过GeoJson新增设施-逆变器
* @param data
*/
export const addInverter = (data: any) => {
return request({
url: '/facility/inverter/geoJson',
method: 'post',
data: data
});
};
/**
* 删除项目
* @param id

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

BIN
src/assets/images/fog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/assets/images/haze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
src/assets/images/sunny.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -201,11 +201,16 @@ const handleUploadError = () => {
// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
if (res.code === 200) {
uploadList.value.push({
name: res.data.fileName,
url: res.data.url,
ossId: res.data.ossId
});
if (res.data) {
uploadList.value.push({
name: res.data.fileName,
url: res.data.url,
ossId: res.data.ossId
});
} else {
uploadList.value.push({});
}
uploadedSuccessfully();
} else {
number.value--;
@ -242,6 +247,8 @@ const uploadedSuccessfully = () => {
proxy?.$modal.msgSuccess('导入成功');
return;
}
console.log(number.value, uploadList.value);
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];

View File

@ -12,16 +12,16 @@
@node-click="isMenuVisible = false"
>
<template #default="{ node, data }">
<span @dblclick="handlePosition(data)">{{ data.name }}</span>
<span @dblclick="handlePosition(data, node)">{{ data.name }}</span>
</template>
</el-tree-v2>
<div>
<div class="ol-map" id="olMap"></div>
<div class="h15" v-if="!selectLayer.length"></div>
<div class="m-0 c-white text-3 flex" v-else>
<div class="h15 mt-2" v-if="!selectLayer.length"></div>
<div class="m-0 c-white text-3 flex w-237.5 mt-2 flex-wrap" v-else>
<p
v-for="(item, index) in selectLayer"
class="pl-xl border-rd pr mt-2 p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between"
class="pl-xl border-rd pr p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between"
@click="delLayer(index)"
>
{{ item.location.name + '被选中为' + item.option }}
@ -35,6 +35,8 @@
<el-radio value="1" size="large">光伏板</el-radio>
<el-radio value="2" size="large">桩点/支架</el-radio>
<el-radio value="3" size="large">方阵</el-radio>
<el-radio value="4" size="large">逆变器</el-radio>
<el-radio value="5" size="large">箱变</el-radio>
</el-radio-group>
</el-form-item>
</div>
@ -50,6 +52,12 @@
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('方阵')">
<i class="fa-solid fa-times mr-2"></i>方阵
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('逆变器')">
<i class="fa-solid fa-times mr-2"></i>逆变器
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('箱变')">
<i class="fa-solid fa-times mr-2"></i>箱变
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('名称')">
<i class="fa-solid fa-times mr-2"></i>名称
</li>
@ -78,8 +86,10 @@ import { Circle, Style, Stroke, Fill, Icon, Text } from 'ol/style'; // OpenLayer
import LineString from 'ol/geom/LineString'; // OpenLayers的线几何类用于表示线状的地理数据
import Polygon from 'ol/geom/Polygon'; // OpenLayers的多边形几何类用于表示面状的地理数据
import * as turf from '@turf/turf';
import { FeatureCollection } from 'geojson';
import { TreeInstance } from 'element-plus';
import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject } from '@/api/project/project';
import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject, addInverter, addBoxTransformer } from '@/api/project/project';
import { BatchUploader } from '@/utils/batchUpload';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({
@ -121,15 +131,16 @@ const jsonData = computed(() => {
return arr; // treeData.value;
});
console.log(jsonData);
const handlePosition = (data: any) => {
const handlePosition = (data: any, node) => {
//切换中心点
centerPosition.value = fromLonLat(turf.center(data).geometry.coordinates);
console.log(turf.center(data));
const featureCollection: FeatureCollection = { type: 'FeatureCollection', features: treeData.value[data.index].features } as FeatureCollection;
centerPosition.value = fromLonLat(turf.center(featureCollection).geometry.coordinates);
map.getView().setCenter(centerPosition.value);
};
const handleCheckChange = (data: any, bool) => {
// 处理树形结构的选中变化
let features = treeData.value[data.index].features;
if (isMenuVisible.value) isMenuVisible.value = false;
if (bool) {
@ -308,11 +319,16 @@ const showMenu = (event: MouseEvent, data) => {
// 处理菜单项点击事件的方法
const handleMenuItemClick = (option: string) => {
isMenuVisible.value = false;
if (selectLayer.value.length == 2) {
ElMessage.warning('最多只能选择两个图层');
return;
if (selectLayer.value.some((item) => item.location.name === contextMenu.value.name)) {
return proxy?.$modal.msgError('已选择该图层,请勿重复选择');
}
if (selectLayer.value.some((item) => item.option !== '名称' && item.option !== '箱变')) {
if (option !== '名称' && option !== '箱变') return proxy?.$modal.msgError('只能选择一个类型');
}
selectLayer.value.push({ location: contextMenu.value, option });
console.log('selectLayer.value', selectLayer.value);
emit('handleCheckChange', selectLayer.value);
};
@ -350,62 +366,105 @@ const onUnmounted = () => {
window.removeEventListener('click', closeMenuOnClickOutside);
};
const addFacilities = async () => {
console.log(layerType.value);
type LayerConfig = {
optionB: string;
apiFunc: (data: any) => Promise<any>;
};
if (!layerType.value) {
return proxy?.$modal.msgError('请选择图层类型');
}
if (!selectLayer.value.length) {
return proxy?.$modal.msgError('请选择需要上传的图层');
const LAYER_CONFIG: Record<number, LayerConfig> = {
1: { optionB: '光伏板', apiFunc: addProjectFacilities },
3: { optionB: '方阵', apiFunc: addProjectSquare },
4: { optionB: '逆变器', apiFunc: addInverter },
5: { optionB: '箱变', apiFunc: addBoxTransformer }
};
const showError = (msg: string) => proxy?.$modal.msgError(msg);
const showSuccess = (msg: string) => proxy?.$modal.msgSuccess(msg);
const getGeoJsonData = (nameOption = '名称', secondOption: string): { nameGeoJson: any[]; locationGeoJson: any | null } | null => {
const nameLayers = selectLayer.value.filter((item) => item.option === nameOption);
const secondLayer = selectLayer.value.filter((item) => item.option === secondOption);
if (!nameLayers.length || !secondLayer) {
showError(`请选择${nameOption}${secondOption}`);
return null;
}
const nameGeoJson = nameLayers.map((item) => treeData.value[item.location.index]);
const locationGeoJson = secondLayer.map((item) => treeData.value[item.location.index]);
return { nameGeoJson, locationGeoJson };
};
const handleTwoLayerUpload = async (optionB: string, apiFunc: (data: any) => Promise<any>) => {
const geoJson = getGeoJsonData('名称', optionB);
if (!geoJson) return;
const data = {
projectId: props.projectId,
nameGeoJson: null,
locationGeoJson: null,
nameGeoJson: geoJson.nameGeoJson,
locationGeoJson: geoJson.locationGeoJson,
pointGeoJson: null
};
if (layerType.value == 1) {
if (!checkOptions(selectLayer.value, '光伏板')) {
return proxy?.$modal.msgError('请选择名称和光伏板');
}
loading.value = true;
if (selectLayer.value[0].option == '名称') {
data.nameGeoJson = treeData.value[selectLayer.value[0].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[1].location.index];
} else {
data.nameGeoJson = treeData.value[selectLayer.value[1].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[0].location.index];
}
loading.value = true;
await apiFunc(data);
await showSuccess('添加成功');
};
await addProjectFacilities(data);
await proxy?.$modal.msgSuccess('添加成功');
} else if (layerType.value == 2) {
if (selectLayer.value.length > 1) return proxy?.$modal.msgError('最多选择一个桩点/支架');
if (selectLayer.value[0].option != '桩点/支架') return proxy?.$modal.msgError('请选择类型为桩点/支架');
loading.value = true;
data.pointGeoJson = treeData.value[selectLayer.value[0].location.index];
await addProjectPilePoint(data);
await proxy?.$modal.msgSuccess('添加成功');
} else if (layerType.value == 3) {
if (!checkOptions(selectLayer.value, '方阵')) {
return proxy?.$modal.msgError('请选择名称和方阵');
}
loading.value = true;
if (selectLayer.value[0].option == '名称') {
data.nameGeoJson = treeData.value[selectLayer.value[0].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[1].location.index];
} else {
data.nameGeoJson = treeData.value[selectLayer.value[1].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[0].location.index];
}
const handlePointUpload = async () => {
if (selectLayer.value.length > 1) return showError('最多选择一个桩点/支架');
if (selectLayer.value[0].option !== '桩点/支架') return showError('请选择类型为桩点/支架');
await addProjectSquare(data);
await proxy?.$modal.msgSuccess('添加成功');
const features = treeData.value[selectLayer.value[0].location.index]?.features || [];
if (!features.length) return showError('桩点数据为空');
loading.value = true;
const sessionId = new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 15);
const uploader = new BatchUploader({
dataList: features,
chunkSize: 15000,
delay: 200,
uploadFunc: async (chunk, batchNum, totalBatch) => {
await addProjectPilePoint({
projectId: props.projectId,
locationGeoJson: {
type: 'FeatureCollection',
features: chunk
},
sessionId,
totalBatch,
batchNum
});
},
onComplete: () => {
showSuccess('桩点上传完成');
reset();
loading.value = false;
}
});
await uploader.start();
};
const addFacilities = async () => {
if (!layerType.value) return showError('请选择图层类型');
if (!selectLayer.value.length) return showError('请选择需要上传的图层');
const config = LAYER_CONFIG[layerType.value];
try {
if (layerType.value == 2) {
await handlePointUpload();
} else if (config) {
await handleTwoLayerUpload(config.optionB, config.apiFunc);
} else {
showError('不支持的图层类型');
}
} finally {
reset();
loading.value = false;
}
reset();
loading.value = false;
};
const reset = () => {
@ -416,21 +475,6 @@ const reset = () => {
}
layerType.value = null;
};
//校验
function checkOptions(arr, type) {
let hasName = false;
let hasJson = false;
for (let i = 0; i < arr.length; i++) {
if (arr[i].option === '名称') {
hasName = true;
} else if (arr[i].option === type) {
hasJson = true;
}
}
return hasName && hasJson;
}
watch(
() => props.designId,
@ -446,7 +490,20 @@ watch(
onMounted(() => {
// 地图初始化
initOLMap();
console.log(props.designId);
// creatPoint(
// [
// [
// [107.13205125908726, 23.806785824010216],
// [107.13218187963494, 23.806867960389773],
// [107.13215698891558, 23.806902336258318],
// [107.13202636835067, 23.8068201998575],
// [107.13205125908726, 23.806785824010216]
// ]
// ],
// 'Polygon',
// '1',
// '测试方阵'
// );
});
</script>
<style scoped lang="scss">

View File

@ -0,0 +1,57 @@
<template>
<video class="rtc_media_player" width="100%" height="100%" autoplay muted playsinline ref="rtcMediaPlayer"></video>
</template>
<script>
export default {
name: "SrsPlayer",
data() {
return {
webrtc: null, // Instance of SRS SDK
sessionId: null,
simulatorUrl: null,
playerVisible: false,
url: ""
};
},
methods: {
hideInfo() {
document.querySelector(".alert").style.display = "none";
},
async startPlay(url) {
this.url = url;
this.playerVisible = true;
if (this.webrtc) {
this.webrtc.close();
}
this.webrtc = new SrsRtcWhipWhepAsync();
this.$refs.rtcMediaPlayer.srcObject = this.webrtc.stream;
console.log('stream tracks:', this.webrtc.stream.getTracks());
try {
const session = await this.webrtc.play(url);
console.log('after play, stream tracks:', this.webrtc.stream.getTracks());
this.sessionId = session.sessionid;
this.simulatorUrl = `${session.simulator}?drop=1&username=${session.sessionid}`;
} catch (error) {
console.error("Error playing stream:", error);
this.webrtc.close();
this.playerVisible = false;
}
},
},
beforeDestroy() {
// Cleanup the SDK instance on component destroy
if (window[this.url]) {
window[this.url].close();
window[this.url] = null
}
},
};
</script>
<style scoped>
.rtc_media_player {
width: 100%;
height: 100%;
}
</style>

View File

@ -46,11 +46,22 @@ VXETable.config({
zIndex: 999999
});
//本地保存飞机配置
import { setLocal } from './utils';
setLocal('dockAir', 'http://192.168.110.24:9136');
setLocal('aiUrl', 'http://192.168.110.23:8000');
setLocal('host', '192.168.110.199');
setLocal('rtmpPort', '1935');
setLocal('rtcPort', '1985');
setLocal('dockSocketUrl', 'ws://192.168.110.8:9136/websocket');
// 修改 el-dialog 默认点击遮照为不关闭
/*import { ElDialog } from 'element-plus';
ElDialog.props.closeOnClickModal.default = false;*/
// **main.js**
import { vue3ScrollSeamless } from 'vue3-scroll-seamless';
import bus from './utils/bus';
import $message from '@/plugins/modal';
const app = createApp(App);
@ -62,6 +73,7 @@ app.use(print);
app.use(i18n);
app.use(VXETable);
app.use(plugins);
app.use(bus);
app.component('vue3ScrollSeamless', vue3ScrollSeamless);
// 自定义指令
directive(app);
@ -69,3 +81,4 @@ directive(app);
app.mount('#app');
app.config.globalProperties.mittBus = mitt();
app.config.globalProperties.$message = $message;

View File

@ -18,6 +18,18 @@ export default {
msgWarning(content: any) {
ElMessage.warning(content);
},
// 警告消息
warning(content: any) {
ElMessage.warning(content);
},
// 错误消息
error(content: any) {
ElMessage.error(content);
},
// 成功消息
success(content: any) {
ElMessage.success(content);
},
// 弹出提示
alert(content: any) {
ElMessageBox.alert(content, '系统提示');
@ -34,6 +46,7 @@ export default {
alertWarning(content: any) {
ElMessageBox.alert(content, '系统提示', { type: 'warning' });
},
// 通知提示
notify(content: any) {
ElNotification.info(content);

View File

@ -0,0 +1,75 @@
import * as echarts from 'echarts';
/**
* 为 ECharts 实例添加自动滚动功能(精确固定窗口项数 + 用户操作暂停)
* @param chartInstance ECharts 实例
* @param totalItems x轴总项数
* @param windowSize 显示窗口项数,默认 6
* @param interval 滚动间隔,默认 1500ms
* @returns 停止函数
*/
export function enableEchartsAutoScroll(
chartInstance: echarts.ECharts,
totalItems: number,
windowSize: number = 6,
interval: number = 1500
): () => void {
let index = 0;
let isUserInteracting = false;
let lastInteractionTime = Date.now();
let currentWindowSize = windowSize;
// 主动初始化窗口大小(如果设置了 dataZoom
const option = chartInstance.getOption();
const zoom = option?.dataZoom?.[0];
if (zoom && zoom.start != null && zoom.end != null) {
const startIndex = Math.round((zoom.start / 100) * totalItems);
const endIndex = Math.round((zoom.end / 100) * totalItems);
currentWindowSize = endIndex - startIndex;
}
// 监听用户操作
const dataZoomHandler = (params: any) => {
const zoom = params.batch ? params.batch[0] : params;
const startIndex = Math.round((zoom.start / 100) * totalItems);
const endIndex = Math.round((zoom.end / 100) * totalItems);
currentWindowSize = endIndex - startIndex;
isUserInteracting = true;
lastInteractionTime = Date.now();
};
chartInstance.on('dataZoom', dataZoomHandler);
// 自动滚动定时器
const timer = setInterval(() => {
const now = Date.now();
if (isUserInteracting && now - lastInteractionTime < 1000) return;
isUserInteracting = false;
if (index + currentWindowSize > totalItems) {
index = 0;
}
const startPercent = (index / totalItems) * 100;
const endPercent = ((index + currentWindowSize) / totalItems) * 100;
chartInstance.setOption({
dataZoom: [
{
start: startPercent,
end: endPercent
}
]
});
index++;
}, interval);
// 返回停止方法
return () => {
clearInterval(timer);
chartInstance.off('dataZoom', dataZoomHandler);
};
}

View File

@ -98,6 +98,11 @@ export const constantRoutes: RouteRecordRaw[] = [
path: '/gisHome',
component: () => import('@/views/gisHome/index.vue'),
hidden: true
},
{
path: '/drone',
component: () => import('@/views/drone/index.vue'),
hidden: true
}
];

View File

@ -0,0 +1,42 @@
// stores/useAirStore.js
import { getLocal } from '@/utils';
import { defineStore } from 'pinia';
export const useAirStore = defineStore('air', {
state: () => ({
debugValue: false, // 远程调试模式
HatchCoverValue: false, // 舱盖
AerocraftValue: false, // 飞行器
gateWay: JSON.parse(getLocal('airGateway')) || null, //
queryParams: {
target_height: 100, //目标点高度
rth_altitude: 100, //返航高度
commander_flight_height: 100, //指点飞行高度
rc_lost_action: 2, //遥控器失控动作
rth_mode: 1, //返航模式设置值
commander_mode_lost_action: 1, //指点飞行失控动作
commander_flight_mode: 1, //指点飞行模式设置值
max_speed: 12, //一键起飞的飞行过程中能达到的最大速度
security_takeoff_height: 100, //安全起飞高度
target_latitude: 0, //目标点纬度
target_longitude: 0, //目标点经度
height: 100
}
}),
actions: {
UP_debugValue(payload) {
localStorage.setItem('debugValue', JSON.stringify(payload));
},
UP_HatchCoverValue(payload) {
localStorage.setItem('HatchCoverValue', JSON.stringify(payload));
},
UP_AerocraftValue(payload) {
localStorage.setItem('AerocraftValue', JSON.stringify(payload));
},
SET_GATEWAY(gateWay) {
this.gateWay = gateWay;
localStorage.setItem('airGateway', JSON.stringify(gateWay));
}
}
});

View File

@ -1,3 +1,5 @@
import { getLocal } from '.';
const TokenKey = 'Admin-Token';
const tokenStorage = useStorage<null | string>(TokenKey, null);
@ -7,3 +9,27 @@ export const getToken = () => tokenStorage.value;
export const setToken = (access_token: string) => (tokenStorage.value = access_token);
export const removeToken = () => (tokenStorage.value = null);
export const getDockSocketUrl = () => {
return getLocal('dockSocketUrl');
};
export const getHost = () => {
return getLocal('host');
};
export const getRtmpPort = () => {
return getLocal('rtmpPort');
};
export const getRtcPort = () => {
return getLocal('rtcPort');
};
export const getDockAir = () => {
return getLocal('dockAir');
};
export const getAiUrl = () => {
return getLocal('aiUrl');
};

73
src/utils/batchUpload.ts Normal file
View File

@ -0,0 +1,73 @@
// 文件路径utils/BatchUploader.ts
/**
* BatchUploader 批量上传工具类
* 用于将大量数据分批发送到后端,避免单次请求数据过大导致失败。
* 支持设置每批数据大小、批次间延迟、上传函数和完成回调。
*/
export class BatchUploader<T> {
private dataList: T[]; // 需要上传的数据列表
private chunkSize: number; // 每批上传的条数
private delay: number; // 每批上传之间的延迟时间(单位:毫秒)
private uploadFunc: (chunk: T[], batchIndex: number, totalBatches: number) => Promise<void>; // 每一批次的上传函数
private onComplete?: () => void; // 所有批次完成后的回调函数(可选)
/**
* 构造函数,初始化批量上传器
* @param options 配置参数
*/
constructor(options: {
dataList: T[]; // 待上传的数据
chunkSize?: number; // 每批数据大小(默认 8000
delay?: number; // 每批之间的延迟(默认 200ms
uploadFunc: (chunk: T[], batchIndex: number, totalBatches: number) => Promise<void>; // 上传逻辑函数
onComplete?: () => void; // 所有批次完成后的回调函数(可选)
}) {
const { dataList, chunkSize = 8000, delay = 200, uploadFunc, onComplete } = options;
this.dataList = dataList;
this.chunkSize = chunkSize;
this.delay = delay;
this.uploadFunc = uploadFunc;
this.onComplete = onComplete;
}
/**
* 启动批量上传任务
*/
public async start() {
const total = Math.ceil(this.dataList.length / this.chunkSize); // 计算总批次数
// 循环执行每一批上传任务
for (let i = 0; i < total; i++) {
// 截取当前批次的数据
const chunk = this.dataList.slice(i * this.chunkSize, (i + 1) * this.chunkSize);
// 调用传入的上传函数处理该批次数据
await this.uploadFunc(chunk, i + 1, total);
// 如果不是最后一批,添加延迟
if (this.delay > 0 && i + 1 < total) {
await new Promise((res) => setTimeout(res, this.delay));
}
}
// 所有批次上传完成后,执行完成回调(如果有的话)
this.onComplete?.();
}
}
//使用示例
// const uploader = new BatchUploader({
// dataList: features,
// chunkSize: 15000,
// delay: 200,
// uploadFunc: async (chunk, batchNum, totalBatch) => {
// await addProjectPilePoint();//异步方法
// },
// onComplete: () => {
// console.log('桩点上传完成');
// }
// });
// await uploader.start();

22
src/utils/bus.ts Normal file
View File

@ -0,0 +1,22 @@
import mitt from 'mitt';
const emitter = mitt();
export default {
install(app) {
// 发送事件
app.config.globalProperties.$sendChanel = (event, payload) => {
emitter.emit(event, payload);
};
// 监听事件(返回取消函数)
app.config.globalProperties.$recvChanel = (event, handler) => {
emitter.on(event, handler);
// 可选:返回解绑函数,方便使用
return () => {
emitter.off(event, handler);
};
};
}
};

View File

@ -1,5 +1,5 @@
import { parseTime } from '@/utils/ruoyi';
import $cache from '@/plugins/cache';
/**
* 表格时间格式化
*/
@ -15,6 +15,14 @@ export const formatDate = (cellValue: string) => {
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
};
export const setLocal = (key, value) => {
return $cache.local.set(key, value);
};
export const getLocal = (key) => {
return $cache.local.get(key);
};
/**
* @param {number} time
* @param {string} option
@ -316,3 +324,5 @@ export const removeClass = (ele: HTMLElement, cls: string) => {
export const isExternal = (path: string) => {
return /^(https?:|http?:|mailto:|tel:)/.test(path);
};
export { parseTime };

40
src/utils/moveDiv.ts Normal file
View File

@ -0,0 +1,40 @@
export function setMove(downId, moveID) {
let moveDiv = document.getElementById(downId)
moveDiv.style.cssText += ';cursor:move;'
let informationBox = document.getElementById(moveID)
const sty = (() => {
if (window.document.currentStyle) {
return (dom, attr) => dom.currentStyle[attr];
} else {
return (dom, attr) => getComputedStyle(dom, false)[attr];
}
})()
moveDiv.onmousedown = (e) => {
// 鼠标按下,计算当前元素距离可视区的距离
const disX = e.clientX - moveDiv.offsetLeft;
const disY = e.clientY - moveDiv.offsetTop;
// 获取到的值带px 正则匹配替换
let styL = sty(informationBox, 'left');
let styT = sty(informationBox, 'top');
// 第一次获取到的值为组件自带50% 移动之后赋值为px
if (styL.includes('%')) {
styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
} else {
styL = +styL.replace(/\px/g, '');
styT = +styT.replace(/\px/g, '');
}
document.onmousemove = function (e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX;
let top = e.clientY - disY;
// 移动当前元素
informationBox.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
};
}
}

View File

@ -0,0 +1,382 @@
// MIT License:
//
// Copyright (c) 2010-2012, Joe Walnes
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
/**
* This behaves like a WebSocket in every way, except if it fails to connect,
* or it gets disconnected, it will repeatedly poll until it successfully connects
* again.
*
* It is API compatible, so when you have:
* ws = new WebSocket('ws://....');
* you can replace with:
* ws = new ReconnectingWebSocket('ws://....');
*
* The event stream will typically look like:
* onconnecting
* onopen
* onmessage
* onmessage
* onclose // lost connection
* onconnecting
* onopen // sometime later...
* onmessage
* onmessage
* etc...
*
* It is API compatible with the standard WebSocket API, apart from the following members:
*
* - `bufferedAmount`
* - `extensions`
* - `binaryType`
*
* Latest version: https://github.com/joewalnes/reconnecting-websocket/
* - Joe Walnes
*
* Syntax
* ======
* var socket = new ReconnectingWebSocket(url, protocols, options);
*
* Parameters
* ==========
* url - The url you are connecting to.
* protocols - Optional string or array of protocols.
* options - See below
*
* Options
* =======
* Options can either be passed upon instantiation or set after instantiation:
*
* var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 });
*
* or
*
* var socket = new ReconnectingWebSocket(url);
* socket.debug = true;
* socket.reconnectInterval = 4000;
*
* debug
* - Whether this instance should log debug messages. Accepts true or false. Default: false.
*
* automaticOpen
* - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close().
*
* reconnectInterval
* - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000.
*
* maxReconnectInterval
* - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000.
*
* reconnectDecay
* - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5.
*
* timeoutInterval
* - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000.
*
*/
// (function (global, factory) {
// if (typeof define === 'function' && define.amd) {
// define([], factory);
// } else if (typeof module !== 'undefined' && module.exports){
// module.exports = factory();
// } else {
// global.ReconnectingWebSocket = factory();
// }
// })(this, function () {
//
// if (!('WebSocket' in window)) {
// return;
// }
function ReconnectingWebSocket(url, protocols, options) {
// Default settings
var settings = {
/** Whether this instance should log debug messages. */
debug: false,
/** Whether or not the websocket should attempt to connect immediately upon instantiation. */
automaticOpen: true,
/** The number of milliseconds to delay before attempting to reconnect. */
reconnectInterval: 1000,
/** The maximum number of milliseconds to delay a reconnection attempt. */
maxReconnectInterval: 30000,
/** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
reconnectDecay: 1.5,
/** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
timeoutInterval: 2000,
/** The maximum number of reconnection attempts to make. Unlimited if null. */
maxReconnectAttempts: null,
/** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
binaryType: 'blob'
}
if (!options) {
options = {};
}
// Overwrite and define settings with options if they exist.
for (var key in settings) {
if (typeof options[key] !== 'undefined') {
this[key] = options[key];
} else {
this[key] = settings[key];
}
}
// These should be treated as read-only properties
/** The URL as resolved by the constructor. This is always an absolute URL. Read only. */
this.url = url;
/** The number of attempted reconnects since starting, or the last successful connection. Read only. */
this.reconnectAttempts = 0;
/**
* The current state of the connection.
* Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
* Read only.
*/
this.readyState = WebSocket.CONNECTING;
/**
* A string indicating the name of the sub-protocol the server selected; this will be one of
* the strings specified in the protocols parameter when creating the WebSocket object.
* Read only.
*/
this.protocol = null;
// Private state variables
var self = this;
var ws;
var forcedClose = false;
var timedOut = false;
var eventTarget = document.createElement('div');
// Wire up "on*" properties as event handlers
eventTarget.addEventListener('open', function (event) {
self.onopen(event);
});
eventTarget.addEventListener('close', function (event) {
self.onclose(event);
});
eventTarget.addEventListener('connecting', function (event) {
self.onconnecting(event);
});
eventTarget.addEventListener('message', function (event) {
self.onmessage(event);
});
eventTarget.addEventListener('error', function (event) {
self.onerror(event);
});
// Expose the API required by EventTarget
this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
/**
* This function generates an event that is compatible with standard
* compliant browsers and IE9 - IE11
*
* This will prevent the error:
* Object doesn't support this action
*
* http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563
* @param s String The name that the event should use
* @param args Object an optional object that the event will use
*/
function generateEvent(s, args) {
var evt = document.createEvent("CustomEvent");
evt.initCustomEvent(s, false, false, args);
return evt;
};
this.open = function (reconnectAttempt) {
ws = new WebSocket(self.url, protocols || []);
ws.binaryType = this.binaryType;
if (reconnectAttempt) {
if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
return;
}
} else {
eventTarget.dispatchEvent(generateEvent('connecting'));
this.reconnectAttempts = 0;
}
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
}
var localWs = ws;
var timeout = setTimeout(function () {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
}
timedOut = true;
localWs.close();
timedOut = false;
}, self.timeoutInterval);
ws.onopen = function (event) {
clearTimeout(timeout);
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onopen', self.url);
}
self.protocol = ws.protocol;
self.readyState = WebSocket.OPEN;
self.reconnectAttempts = 0;
var e = generateEvent('open');
e.isReconnect = reconnectAttempt;
reconnectAttempt = false;
eventTarget.dispatchEvent(e);
};
ws.onclose = function (event) {
clearTimeout(timeout);
ws = null;
if (forcedClose) {
self.readyState = WebSocket.CLOSED;
eventTarget.dispatchEvent(generateEvent('close'));
} else {
self.readyState = WebSocket.CONNECTING;
var e = generateEvent('connecting');
e.code = event.code;
e.reason = event.reason;
e.wasClean = event.wasClean;
eventTarget.dispatchEvent(e);
if (!reconnectAttempt && !timedOut) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onclose', self.url);
}
eventTarget.dispatchEvent(generateEvent('close'));
}
var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
setTimeout(function () {
self.reconnectAttempts++;
self.open(true);
}, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
}
};
ws.onmessage = function (event) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
}
var e = generateEvent('message');
e.data = event.data;
eventTarget.dispatchEvent(e);
};
ws.onerror = function (event) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
}
eventTarget.dispatchEvent(generateEvent('error'));
};
}
// Whether or not to create a websocket upon instantiation
if (this.automaticOpen == true) {
this.open(false);
}
/**
* Transmits data to the server over the WebSocket connection.
*
* @param data a text string, ArrayBuffer or Blob to send to the server.
*/
this.send = function (data) {
if (ws) {
if (self.debug || ReconnectingWebSocket.debugAll) {
console.debug('ReconnectingWebSocket', 'send', self.url, data);
}
return ws.send(data);
} else {
throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
}
};
/**
* Closes the WebSocket connection or connection attempt, if any.
* If the connection is already CLOSED, this method does nothing.
*/
this.close = function (code, reason) {
// Default CLOSE_NORMAL code
if (typeof code == 'undefined') {
code = 1000;
}
forcedClose = true;
if (ws) {
ws.close(code, reason);
}
};
/**
* Additional public API method to refresh the connection if still open (close, re-open).
* For example, if the app suspects bad data / missed heart beats, it can try to refresh.
*/
this.refresh = function () {
if (ws) {
ws.close();
}
};
}
/**
* An event listener to be called when the WebSocket connection's readyState changes to OPEN;
* this indicates that the connection is ready to send and receive data.
*/
ReconnectingWebSocket.prototype.onopen = function (event) {
};
/** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */
ReconnectingWebSocket.prototype.onclose = function (event) {
};
/** An event listener to be called when a connection begins being attempted. */
ReconnectingWebSocket.prototype.onconnecting = function (event) {
};
/** An event listener to be called when a message is received from the server. */
ReconnectingWebSocket.prototype.onmessage = function (event) {
};
/** An event listener to be called when an error occurs. */
ReconnectingWebSocket.prototype.onerror = function (event) {
};
/**
* Whether all instances of ReconnectingWebSocket should log debug messages.
* Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true.
*/
ReconnectingWebSocket.debugAll = false;
ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
ReconnectingWebSocket.OPEN = WebSocket.OPEN;
ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
// return ReconnectingWebSocket;
// });

55
src/utils/requset2.ts Normal file
View File

@ -0,0 +1,55 @@
import $modal from '@/plugins/modal';
import axios from 'axios';
// 创建一个 Axios 实例并设置默认配置
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_DRONE_API,
timeout: 5000
});
// 请求拦截器,添加 token 到每个请求头
axiosInstance.interceptors.request.use(
(config) => {
let air = localStorage.getItem('airToken');
let token = '';
if (air) {
token = JSON.parse(air).access_token; // 假设 token 存储在 localStorage 中
}
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 处理消息显示
function showMessage(message, type = 'error') {
$modal.msg({
message: message,
type: type,
duration: 5 * 1000
});
}
export function request(params) {
// 如果有取消请求的需求,可以在此处添加 CancelToken
// const source = axios.CancelToken.source();
// params.cancelToken = source.token;
// 确保 URL 是完整的
// if (!params.url.startsWith(process.env.AIR)) {
// params.url = process.env.AIR;
// }
return axiosInstance(params).then((res) => {
if (res.data.code === 200) {
return res.data;
} else {
if (!res.data.code) return res.data;
showMessage(res.data.message || res.data.msg);
return Promise.reject(res.data);
}
});
}

View File

@ -0,0 +1,365 @@
<template>
<div class="air_advancedSet uav_box">
<div class="header">
<div class="logo">
<img class="title" src="../../images/airsetup.png" alt="" />
<div class="switch" @click="save" title="返回航线设置"></div>
</div>
</div>
<div class="content">
<!-- 拍照设置 -->
<div class="photograph">
<div class="content_header">拍照设置</div>
<div class="btns flex space_between">
<el-checkbox-group v-model="checkboxGroup1" :min="1" class="flex space_between">
<el-checkbox-button v-for="city in cities" :label="city.label" :key="city.value">{{ city.label }}</el-checkbox-button>
</el-checkbox-group>
</div>
<div class="flex space_between">
<div>智能低光</div>
<el-switch
v-model="value"
active-color="rgba(0, 255, 255, 1)"
inactive-color="rgba(0, 255, 255, 0.2)"
:active-value="true"
:inactive-value="false"
>
</el-switch>
</div>
</div>
<!-- 爬升模式 -->
<div class="climb">
<el-radio-group v-model="radio1" class="flex space_between">
<el-radio-button v-for="item in climbs" :label="item"></el-radio-button>
</el-radio-group>
<div class="flex space_between">
<div class="left_img">
<svg-icon icon-class="climb" style="width: 240px; height: 160px; vertical-align: middle"></svg-icon>
</div>
<div class="right_value">
<plusResduce :column="true" unit="m" :max="10000" :min="10"></plusResduce>
</div>
</div>
</div>
<!-- 航线高度模式 -->
<div class="routeAltitude">
<div class="content_header">拍照设置</div>
<el-radio-group v-model="radio2" class="flex space_between">
<el-radio-button v-for="item in routeAltitude" :label="item"></el-radio-button>
</el-radio-group>
<div class="flex space_between">
<div class="left_img">
<svg-icon icon-class="height1" style="width: 240px; height: 160px; vertical-align: middle"></svg-icon>
</div>
<div class="right_value">
<plusResduce :column="true" unit="m"></plusResduce>
</div>
</div>
</div>
<!-- 全局航线速度 -->
<div class="routeSpeed">
<div class="content_header">全局航线速度</div>
<myProgress progressType="num" :max="15" :min="1"></myProgress>
</div>
<!-- 高级设置 -->
<div class="setting">
<el-collapse v-model="activeNames" @change="handleChange">
<el-collapse-item name="1">
<template #title> 高级设置 </template>
<el-divider></el-divider>
<myProgress progressType="num" :max="15" :min="1"></myProgress>
<!-- 航点类型 -->
<div class="subtitle">
<div class="f14 fw500">航点类型</div>
<el-select v-model="value2" placeholder="请选择">
<el-option v-for="item in waypointType" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</div>
<!-- 飞行器偏航角模式 -->
<div class="subtitle">
<div class="f14 fw500">飞行器偏航角模式</div>
<el-select v-model="value3" placeholder="请选择">
<el-option v-for="item in yawAngleMode" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</div>
<!-- 完成动作 -->
<div class="subtitle">
<div class="f14 fw500">完成动作</div>
<el-select v-model="value4" placeholder="请选择">
<el-option v-for="item in completeAction" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</div>
</template>
<script>
import plusResduce from '../component/plusReduce/index.vue';
import myProgress from '../component/progress/index.vue';
import { waypointType, yawAngleMode, completeAction } from '../../utils/options';
export default {
name: 'air_advancedSet',
components: { plusResduce, myProgress },
data() {
return {
value: false,
cities: [
{
label: '广角照片',
value: 'wide'
},
{
label: '变焦照片',
value: 'zoom'
},
{
label: '红外照片',
value: 'ir'
}
],
checkboxGroup1: ['广角照片', '变焦照片', '红外照片'],
climbs: ['垂直爬升', '倾斜爬升'],
radio1: '垂直爬升',
routeAltitude: ['绝对高度', '相对起飞点高度', '相对地形高度'],
radio2: '绝对高度',
activeNames: [],
waypointType: waypointType,
yawAngleMode: yawAngleMode,
completeAction: completeAction,
value2: '2', //航点类型
value3: '1', //偏航角模式
value4: '1' //完成动作
};
},
mounted() {},
methods: {
handleChange(val) {
// console.log(val);
},
// 保存
save() {
this.$emit('save');
}
}
};
</script>
<style lang="scss">
.air_advancedSet {
left: 20px;
user-select: none;
.header {
margin-bottom: 10px;
.logo {
position: relative;
text-align: center;
.title {
width: 380px;
height: 32px;
}
}
.switch {
position: absolute;
top: 5px;
right: 10px;
width: 16px;
height: 16px;
z-index: 20;
background: url('../../images/switch.png');
background-size: cover;
cursor: pointer;
}
.switch:hover {
background: url('../../images/switchh.png');
background-size: cover;
}
}
.content {
height: calc(100% - 36px);
color: #fff;
overflow: auto;
padding-right: 5px;
.content_header {
font-family: 'alimamashuheiti';
font-size: 14px;
font-weight: 700;
margin-bottom: 10px;
}
.photograph {
height: 120px;
padding: 15px 15px 0 15px;
background-color: rgba(0, 0, 0, 0.5);
}
.btns {
margin: 10px 0;
.el-checkbox-group {
width: 100%;
}
.el-checkbox-button:last-child .el-checkbox-button__inner,
.el-checkbox-button:first-child .el-checkbox-button__inner {
border-radius: 5px;
box-shadow: none;
}
.el-checkbox-button.is-checked .el-checkbox-button__inner {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 1);
}
.el-checkbox-button__inner {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 0.8);
border: 1px solid rgba(0, 255, 255, 0.5);
margin-right: 8px;
border-radius: 5px;
padding: 8px 15px;
box-shadow: none;
}
}
.climb,
.routeAltitude,
.routeSpeed {
margin-top: 15px;
padding: 15px 15px 0 15px;
background-color: rgba(0, 0, 0, 0.5);
.el-radio-group {
width: 100%;
}
.el-radio-button {
flex: 1;
}
.el-radio-button__inner {
padding: 8px 15px;
width: 100%;
background: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 0, 0, 0);
color: #fff;
}
.el-radio-button__orig-radio:checked + .el-radio-button__inner {
box-shadow: none;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
}
.el-radio-button:first-child .el-radio-button__inner,
.el-radio-button:last-child .el-radio-button__inner {
border-radius: 0;
}
.left_img {
padding: 15px 0;
}
}
.routeSpeed {
padding-bottom: 5px;
}
.setting {
margin-top: 15px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.5);
position: relative;
z-index: 10;
.el-select {
width: 100%;
margin: 10px 0;
}
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 255, 255, 0.5);
color: #fff;
}
.el-select .el-input__inner:focus {
border-color: rgba(0, 255, 255, 0.5);
}
// .el-select-dropdown__item.hover,
// .el-select-dropdown__item:hover,
.el-divider--horizontal {
margin: 10px 0;
}
.el-divider {
background: rgba(204, 204, 204, 0.2);
}
.el-collapse-item__header {
background-color: transparent !important;
font-family: 'alimamashuheiti';
font-size: 14px;
}
.el-collapse-item__content {
color: #fff;
padding-bottom: 0;
}
.el-collapse-item__wrap {
border-bottom: none;
}
}
}
/* 修改垂直滚动条 */
.content::-webkit-scrollbar {
width: 8px;
/* 修改宽度 */
}
/* 修改滚动条轨道背景色 */
.content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.1);
border-radius: 10px;
}
/* 修改滚动条滑块颜色 */
.content::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.3);
border-radius: 10px;
}
}
.el-select-dropdown {
background-color: rgba(0, 0, 0, 1);
border-color: rgba(0, 255, 255, 0.5);
}
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background-color: rgba(0, 255, 255, 0.5);
}
.el-select-dropdown__item {
color: #fff;
}
.el-select-dropdown__item.selected {
color: rgba(0, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,750 @@
import { waypoint } from "@/api/air";
export default {
data() {
return {
leftShow: false,
pzDisabled: false,
keywordShow: false,
setupShow: false,
selectId: null,
inputShow: false,
selectObj: {
label: "",
params: {},
type: "",
}, // 选中的动作
selectIndexF: null,
selectIndexS: null,
checkboxGroup1: ["visable", "ir"],
checkboxGroup2: ["wide", "ir", "zoom"],
startPoint: null,
pohoto: [
{
label: "可见光",
value: "visable",
},
{
label: "红外照片",
value: "ir",
},
],
pohoto1: [
{
label: "广角照片",
value: "wide",
},
{
label: "红外照片",
value: "ir",
},
{
label: "变焦照片",
value: "zoom",
},
],
dropdownList: [
{
label: "新增航点(前)",
value: 1,
type: "before",
},
{
label: "新增航点(后)",
value: 2,
type: "after",
},
{
label: "删除",
value: 3,
},
],
controls: [
{
url: require("../../../images/control_start.png"),
label: "开始录像",
value: "startRecord",
svg: "start_record",
params: {
fileSuffix: "123456",
payloadPositionIndex: 0,
useGlobalPayloadLensIndex: 1, //是否使用全局 1为使用全局 0不使用全局
payloadLensIndex: "visable,ir",
},
},
{
url: require("../../../images/control_suspend.png"),
label: "停止录像",
value: "stopRecord",
svg: "stop_record",
params: {
payloadPositionIndex: 0,
},
},
// {
// url: require("../../../images/control_Isochronous.png"),
// label: "开始等时间隔拍照",
// value: "takePhoto",
// svg: "interval_time",
// params: {
// payloadPositionIndex: 0,
// useGlobalPayloadLensIndex: 1,
// payloadLensIndex: "visable,ir",
// },
// actionTrigger: {
// actionTriggerType: "multipleTiming",
// actionTriggerParam: 10,
// },
// },
// {
// url: require("../../../images/control_equidistant.png"),
// label: "开始等距间隔拍照",
// value: "takePhoto",
// svg: "interval_distance",
// params: {
// payloadPositionIndex: 0,
// useGlobalPayloadLensIndex: 1,
// payloadLensIndex: "visable,ir",
// },
// actionTrigger: {
// actionTriggerType: "multipleDistance",
// actionTriggerParam: 10,
// },
// },
// {
// url: require("../../../images/control_interval.png"),
// label: "结束间隔拍照",
// value: "",
// svg: "interval_stop",
// },
{
url: require("../../../images/control_hover.png"),
label: "悬停",
value: "hover",
svg: "hover",
params: {
hoverTime: 10,
},
},
{
url: require("../../../images/control_aircraft.png"),
label: "飞行器偏航角",
value: "rotateYaw",
svg: "drone_yaw",
params: {
aircraftHeading: 0,
aircraftPathMode: "counterClockwise",
},
},
// {
// url: require("../../../images/control_yaw.png"),
// label: "云台偏航角",
// value: "gimbalRotate",
// svg: "action_gimbal_pitch",
// params: {
// gimbalHeadingYawBase: "north",
// gimbalRotateMode: "absoluteAngle",
// gimbalPitchRotateEnable: 1,
// gimbalPitchRotateAngle: 0,
// gimbalRollRotateEnable: 0,
// gimbalRollRotateAngle: 0,
// gimbalYawRotateEnable: 0,
// gimbalYawRotateAngle: 0,
// gimbalRotateTimeEnable: 0,
// gimbalRotateTime: 0,
// payloadPositionIndex: 0,
// },
// },
{
url: require("../../../images/control_pitch.png"),
label: "云台俯仰角",
value: "gimbalRotate",
svg: "action_gimbal_pitch",
params: {
gimbalHeadingYawBase: "north",
gimbalRotateMode: "absoluteAngle",
gimbalPitchRotateEnable: 1,
gimbalPitchRotateAngle: 0, // 变化的值
gimbalRollRotateEnable: 0,
gimbalRollRotateAngle: 0,
gimbalYawRotateEnable: 0,
gimbalYawRotateAngle: 0,
gimbalRotateTimeEnable: 0,
gimbalRotateTime: 0,
payloadPositionIndex: 0,
},
},
{
url: require("../../../images/control_photograph.png"),
label: "拍照",
value: "takePhoto",
svg: "take_photo",
params: {
payloadPositionIndex: 0,
useGlobalPayloadLensIndex: 1,
payloadLensIndex: "wide,zoom,ir",
},
},
{
url: require("../../../images/control_zoom.png"),
label: "相机变焦",
value: "zoom",
svg: "camera_zoom",
params: {
focalLength: 24,
isUseFocalFactor: 0,
payloadPositionIndex: 0,
},
},
{
url: require("../../../images/control_panorama.png"),
label: "全景拍照",
value: "panoShot",
svg: "pano_shot",
params: {
payloadPositionIndex: 0,
useGlobalPayloadLensIndex: 0,
payloadLensIndex: "visable,ir",
panoShotSubMode: "panoShot_360",
},
},
],
points: [],
obj: {
type: 0, // 航线类型
imageFormat: "wide,zoom,ir,narrow_band,visable", // 拍照配置
estimateTime: "", // 预计执行时间 sdk
photoNum: 0, // 航点拍照数量 sdk
waylineLen: "", // 航线长度 sdk
flag: "",
// 0-91-1
droneInfo: {
// 无人机的型号及其子类型
droneEnumValue: "91",
droneSubEnumValue: "1",
},
// 3-2-0
payloadInfo: {
// 负载类型和子类型以及负载的位置
payloadEnumValue: "81",
payloadPositionIndex: "0",
payloadSubEnumValue: "0",
},
placemarkList: [], // 经纬度
globalHeight: 120, // 相对起飞点高度
autoFlightSpeed: 10, // 全局航线速度
globalShootHeight: 60,
// remark: undefined,
// imageFormat: '',
takeOffSecurityHeight: 100, //安全起飞高度
globalTransitionalSpeed: 10, //飞向首航点速度
takeOffRefPoint: {
lng: undefined,
lat: undefined,
alt: undefined,
},
},
networkState: {},
length: 0,
time: 0,
gensui: true,
inputDiv: false,
};
},
mounted() {
this.createTongBu();
// let points = JSON.parse(localStorage.getItem("routePoints"));
let pointsAndTakeOff = JSON.parse(this.routeLine.points);
console.log(
"pointsAndTakeOff",
this.routeLine,
this.routeLine.normalHeight
);
// console.log("points", points);
if (pointsAndTakeOff) {
this.points = pointsAndTakeOff.points;
console.log(".takeOffRefPoint", pointsAndTakeOff);
// return;
let takeOff = pointsAndTakeOff.takeOffRefPoint.split(",");
this.obj.takeOffRefPoint = {
lng: takeOff[1] - 0,
lat: takeOff[0] - 0,
alt: takeOff[2] - 0,
};
console.log("this.obj.takeOffRefPoint", this.obj.takeOffRefPoint);
if (!window.airLine) {
console.log("this.points", this.routeLine.normalHeight);
this.renderRouter(this.points);
}
if (!window.PointEntity) {
this.renderStart(this.obj.takeOffRefPoint);
}
} else {
this.drawStart();
}
},
computed: {
getPhotoNum() {
let photoNum = 0;
this.points.forEach((item) => {
if (item.actions.length > 0) {
item.actions.forEach((item1) => {
if (item1.type == "takePhoto" || item1.type == "panoShot") {
photoNum++;
}
});
}
});
return photoNum;
},
},
methods: {
// 创建地球
createTongBu() {
window.EarthTongbu = new YJ.YJEarth("tongbuEarth");
YJ.Global.setKeyboardEventActive(window.EarthTongbu, false);
const obj = {
compass: false, //罗盘
legend: false, //比例尺
info: false, //信息栏
frame: false, //刷新率
};
let viewer = window.EarthTongbu.viewer;
YJ.Global.CesiumContainer(window.EarthTongbu, obj);
viewer.terrainProvider = new Cesium.createWorldTerrain();
let imageryProvider = new Cesium.ArcGisMapServerImageryProvider({
url:
"https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer",
});
viewer.imageryLayers.addImageryProvider(imageryProvider);
viewer.scene.screenSpaceCameraController.enableRotate = false;
viewer.scene.screenSpaceCameraController.enableZoom = false;
viewer.scene.screenSpaceCameraController.enableTilt = false;
},
// 跟随航线
genliuhangxian() {
this.gensui = !this.gensui;
if (this.gensui) {
this.selectObj.params.useGlobalPayloadLensIndex = 1;
} else {
this.selectObj.params.useGlobalPayloadLensIndex = 0;
}
},
// genliuhangxian
genliuhangxian2() {
this.selectObj.params.useGlobalPayloadLensIndex = 1;
},
// 保存成功
saveSuucess() {
this.inputDiv = false;
this.$message.success("保存成功");
},
// 删除成功
deleteSuucess() {
this.$message.success("删除成功");
this.inputShow = false;
this.selectObj.params.fileSuffix = "";
},
changeGroup() {
this.selectObj.params.payloadLensIndex = this.checkboxGroup1.join();
},
changeGroup1() {
this.selectObj.params.payloadLensIndex = this.checkboxGroup2.join();
},
interval_distance(value) {
let svg = this.selectObj.svg;
if (svg == "interval_time" || svg == "interval_distance") {
this.selectObj.actionTrigger.actionTriggerParam = value;
}
if (svg == "hover") {
console.log("hoverTime", this.selectObj.params.hoverTime);
this.selectObj.params.hoverTime = value;
}
},
//
hpr(value) {
let svg = this.selectObj.svg;
if (svg == "drone_yaw") {
this.selectObj.params.aircraftHeading = value;
let h = value == 0 ? 0 : value;
window.airLine.frustum.updateFrustumHPR(
h,
window.airLine.frustum.pitch,
window.airLine.frustum.roll,
true,
"alone"
);
}
if (svg == "action_gimbal_pitch") {
function mapValue(x, a1, b1, a2, b2) {
// 线性映射公式
return a2 + ((x - a1) / (b1 - a1)) * (b2 - a2);
}
// 原始区间和目标区间
const range1 = [35, -90]; // 原始区间
const range2 = [60, 180]; // 目标区间
let mappedValue = mapValue(
value,
range1[0],
range1[1],
range2[0],
range2[1]
);
this.selectObj.params.gimbalPitchRotateAngle = value;
let p = mappedValue == 0 ? 90 : mappedValue;
// return;
let hh = Cesium.Math.toDegrees(window.airLine.frustum.hpr.heading);
window.airLine.frustum.updateFrustumHPR(hh, p, 0, true, "alone");
}
if (svg == "camera_zoom") {
this.selectObj.params.focalLength = value * 24;
}
},
closeSetup() {
this.setupShow = false;
this.selectIndexS = null;
},
beforeHandleDropMenu(dropId, airLine) {
return {
dropId: dropId,
airLine: airLine,
};
},
handleDropMenu(command) {
const { dropId, airLine } = command;
if (dropId == 1 || dropId == 2) {
let draw = new YJ.Draw.DrawPoint(window.Earth1);
draw.start((err, params) => {
if (params != undefined) {
let position = {
...params,
actions: [],
};
if (dropId == 1) {
if (window.airLine) {
this.points.splice(this.selectId, 0, position);
window.airLine.addPoint(this.points);
}
}
if (dropId == 2) {
if (window.airLine) {
if (this.selectId == this.points.length - 1) {
this.points.push(position);
} else {
this.points.splice(this.selectId + 1, 0, position);
}
window.airLine.addPoint(this.points);
}
}
}
});
}
if (dropId == 3) {
const index = this.points.findIndex((obj) => obj.lng == airLine.lng);
this.points = this.points.filter((obj) => obj.lng !== airLine.lng);
if (window.airLine) {
window.airLine.delPosition(index, this);
}
}
},
// 新增航点
addPoint() {
let draw = new YJ.Draw.DrawPoint(window.Earth1);
draw.start((err, params) => {
if (params != undefined) {
let position = {
...params,
actions: [],
};
this.points.push(position);
window.airLine.addPoint(this.points);
this.save(() => {}, false);
}
});
},
// 选中
select(index) {
this.selectId = index;
let item = this.points[index];
let obj = item.actions[0];
if (obj) {
this.openSet(index, 0, obj);
} else {
this.setupShow = false;
this.selectIndexS = null;
}
if (window.airLine) {
//判断动作组中有value为rotateYaw、gimbalRotate时就不执行window.airLine.updateFrustumPosition(index);
let flag = item.actions.some(
(obj) => obj.value == "rotateYaw" || obj.value == "gimbalRotate"
);
if (!flag) {
window.airLine.updateFrustumPosition(index);
}
}
},
add() {},
more() {
this.leftShow = !this.leftShow;
},
// 打开高级设置
senior() {
this.save(() => {
this.$emit("seniorSet");
});
},
// 返回
back() {
if (this.points.length > 0) {
this.save(() => {
if (airLine) {
window.airLine.remove();
window.airLine = null;
}
if (window.PointEntity) {
window.PointEntity.remove();
window.PointEntity = null;
}
this.$emit("back");
}, false);
} else {
this.$emit("back");
}
this.$changeComponentShow(".bottomTabs", true);
},
// 绘制航线
draw() {
let draw = new YJ.Draw.DrawPolyline(window.Earth1, {
tipText: "请选择航点(左键确定,右键结束)",
});
draw.start((err, positions) => {
if (positions.length == 0) {
return;
}
positions.forEach((el) => {
el.actions = [];
return el;
});
this.points = [...positions];
this.renderRouter(positions);
});
},
// 绘制起飞点
drawStart() {
let draw = new YJ.Draw.DrawTakeOff(window.Earth1, {
tipText: "选择参考起飞点(左键确定)",
});
draw.start((err, params, flag) => {
if (params != undefined) {
if (!flag) {
this.renderStart(params);
this.obj.takeOffRefPoint = window.PointEntity.options.positions;
} else {
this.obj.takeOffRefPoint = window.airportEntity.options.positions;
}
this.draw();
}
});
},
// 渲染参考起飞点
renderStart(positions) {
let option = {
positions,
label: {
text: "参考起飞点",
fontSize: 20,
},
billboard: {
image: "/static/sdk/img/start.png",
},
};
window.PointEntity = new YJ.Obj.BillboardObject(window.Earth1, option);
window.PointEntity.picking = false;
window.PointEntity.onClick = () => {
setTimeout(() => {
window.PointEntity.positionEditing = true;
}, 100);
};
},
// 渲染航线
renderRouter(positions) {
console.log("globalPointHeight", this.routeLine.globalPointHeight);
let airLine = new YJ.Obj.newAirLine(
{
positions,
image: "/static/img/定位.png",
saveFun: this.save,
selectFun: this.select,
normalHeight: this.routeLine.globalPointHeight,
airHeight: this.routeLine.height,
},
window.Earth1.viewer,
window.EarthTongbu.viewer
);
window.airLine = airLine;
window.airLine.flyTo();
this.length = airLine.countLength();
this.time = airLine.countTime();
// console.log("airLine", airLine);
},
// 添加动作到航点
onClick(item) {
console.log("item", item);
// 添加动作时,
// console.log("this.selectId, item", this.selectId, item);
if (this.selectId === null) {
this.$message.warning("请选择航点,再进行动作添加");
return;
}
let selectItem = this.points[this.selectId];
selectItem.actions.push({
label: item.label,
type: item.value,
params: item.params,
svg: item.svg,
actionTrigger: item.actionTrigger,
});
// 打开当前动作设置
// this.openSet(this.selectId, selectItem.actions.length - 1, item);
// console.log("this.points", this.points);
},
// 点击动作图标
openSet(indexF, indexS, item) {
this.selectIndexF = indexF;
this.selectIndexS = indexS;
this.selectObj = item;
console.log("this.selectObj", this.selectObj);
if (
this.selectObj.type == "takePhoto" ||
this.selectObj.type == "startRecord"
) {
this.checkboxGroup2 = this.selectObj.params.payloadLensIndex.split(",");
if (this.selectObj.params.fileSuffix) {
this.inputDiv = false;
this.inputShow = true;
} else {
this.inputShow = false;
this.inputDiv = true;
}
}
this.setupShow = true;
},
// 删除动作
delAction() {
let curAction = this.points[this.selectIndexF];
curAction.actions.splice(this.selectIndexS, 1);
this.setupShow = false;
// console.log(
// "curActioncurActioncurActioncurAction",
// curAction,
// this.selectIndexS
// );
},
// 在保存之前判断每个点中得动作组,如果前一个动作组中有开始录像,后面的动作组中有全景拍照的动作,就提示用户该航线因为全景拍照的动作处于录像过程中,无法执行,并返回当前有问题的航点的索引
saveBefore() {
let flag = true;
let indexf = null;
this.points.forEach((item, index) => {
if (item.actions.length > 0) {
let startRecord = item.actions.find(
(obj) => obj.type == "startRecord"
);
if (startRecord) {
let nextAction = this.points[index + 1];
if (nextAction) {
let panoShot = nextAction.actions.find(
(obj) => obj.type == "panoShot"
);
if (panoShot) {
flag = false;
indexf = index + 1;
}
}
}
}
});
return {
flag: !flag,
index: indexf,
};
},
// 保存操作
save(cb = null, flag = true) {
let before = this.saveBefore();
if (before.flag) {
this.$message.warning(
`${before.index +
1}#航点的全景拍照动作处于录像过程中,无法执行。请删除该动作后再保存`
);
return;
}
let positions = window.airLine.getNewPositions();
let newPoints = [];
this.points.forEach((item1, index1) => {
let obj = {
actions: item1.actions,
};
positions.forEach((item, index) => {
if (index == index1) {
obj.lng = item.lng;
obj.lat = item.lat;
obj.alt = item.alt - this.routeLine.globalPointHeight;
obj.longitude = item.lng;
obj.latitude = item.lat;
obj.altitude =
item.alt -
this.routeLine.height +
this.routeLine.globalPointHeight;
}
});
newPoints.push(obj);
});
this.points = newPoints;
this.obj.placemarkList = newPoints;
this.obj.id = this.routeLine.id;
if (typeof this.obj.takeOffRefPoint == "object") {
this.obj.takeOffRefPoint =
this.obj.takeOffRefPoint.lat +
"," +
this.obj.takeOffRefPoint.lng +
"," +
this.obj.takeOffRefPoint.alt;
} else {
this.obj.takeOffRefPoint = this.obj.takeOffRefPoint;
}
let obj = {
points: newPoints,
takeOffRefPoint: this.obj.takeOffRefPoint,
};
if (!obj.points[0].lng) {
this.obj.points = [];
this.takeOffRefPoint = "";
window.PointEntity.remove();
window.PointEntity = null;
}
this.obj.points = JSON.stringify(obj);
waypoint(this.obj).then((res) => {
if (flag) {
this.$message.success("操作成功");
}
if (typeof cb == "function") {
cb();
}
});
},
// 航线航点移动
towardsLeft() {},
up() {},
towardsRight() {},
left() {},
after() {},
right() {},
},
};

View File

@ -0,0 +1,627 @@
<template>
<div class="air_list uav_box">
<div class="header">
<div class="logo">
<img class="title" src="../../images/airlist.png" alt="" />
<el-tooltip class="item" effect="dark" content="导入航线" placement="top">
<div class="importPath" @click="importPath"></div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="添加" placement="top">
<div class="add" @click="add"></div>
</el-tooltip>
</div>
<div class="search">
<el-row>
<el-col :span="17">
<el-input placeholder="请输入内容" v-model="queryParam.fileName">
<i slot="suffix" class="el-input__icon el-icon-search" @click="search"></i>
</el-input>
</el-col>
<el-col :span="7">
<div class="date flex">
<span style="margin-right: 5px">时间排序</span>
<div class="flex arrow">
<i class="el-icon-caret-top" @click="order(0)"></i>
<i class="el-icon-caret-bottom" @click="order(1)"></i>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
<el-divider></el-divider>
<div class="content">
<div v-for="item in airList" :key="item.id" class="air_item">
<div class="top flex">
<el-tooltip effect="dark" :content="item.fileName" placement="top-start">
<div class="text">
{{ item.fileName }}
</div>
</el-tooltip>
<div class="flex" style="align-items: center">
<div v-if="item.isImport == 'false'" class="edit" @click="edit(item)"></div>
<el-dropdown @command="handleDropMenu">
<div class="more_icon"></div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="beforeHandleDropMenu(drop.value, item)" v-for="drop in dropdownList" :key="drop.value">{{
drop.label
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="middle m10">
<img src="../../images/wurenji.png" alt="" />
<span>{{ item.deviceType }}</span>
</div>
<div class="bottom m10">
<span class="f14">更新时间</span>
<span class="f12">{{ parseTime(item.updatedAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</div>
</div>
</div>
<div class="pagination"></div>
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="1"
:pager-count="5"
:page-sizes="[10, 20, 30, 40]"
:page-size="10"
layout="total, sizes, prev, pager, next"
:total="total"
>
</el-pagination>
<myDialog ref="createHX" class="createHX" :title="title" @close="close">
<el-divider></el-divider>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="航线名称" prop="value1" style="margin-top: 10px; margin-bottom: 20px">
<el-input placeholder="请输入名称" v-model.trim="form.value1" clearable></el-input>
</el-form-item>
<el-form-item v-if="globalPointHeightShow" label="全局航线高度" prop="value2" style="margin-top: 10px; margin-bottom: 20px">
<el-input placeholder="请输入全局航线高度(2~3000)" v-model.trim.number="form.value2" clearable></el-input>
</el-form-item>
</el-form>
<div class="btns">
<el-button size="small" @click="submit">确定</el-button>
<el-button size="small" @click="cancel">取消</el-button>
</div>
</myDialog>
<input type="file" id="fileInput" style="display: none" accept=".kmz" @change="uploadFile" />
</div>
</template>
<script>
import myDialog from '../component/dialog/index.vue';
import { listPaths, routerAdd, routerRename, routerCopy, delPaths, listInfo, returnImport } from '@/api/air';
import { useAirStore } from '@/store/modules/drone';
import { parseTime } from '@/utils';
export default {
name: 'airList',
components: {
myDialog
},
data() {
return {
value: '',
form: {
value1: '',
value2: ''
},
rules: {
value1: [{ required: true, message: '请输入名称', trigger: 'blur' }],
value2: [
{ required: true, message: '请输入全局航线高度', trigger: 'blur', type: 'number' },
{ min: 2, max: 3000, message: '请输入2~3000之间的数字', trigger: 'blur', type: 'number' }
]
},
airList: [],
curId: '',
title: '创建新航线',
parseTime: parseTime,
queryParam: {
pageNum: 1,
pageSize: 10,
fileName: '',
order: 1
},
dropdownList: [
{
label: '重命名',
value: 1
},
{
label: '复制',
value: 2
},
{
label: '下载',
value: 3
},
{
label: '删除',
value: 4
}
],
gateWay: useAirStore().gateWay,
info: {},
networkState: {},
total: 0,
height: 100,
globalPointHeightShow: true
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getAirRouteList();
this.getList();
},
deep: true
}
},
mounted() {
this.getAirRouteList();
this.getList();
this.$recvChanel('websocketBus', (data) => {
if (data.businessType == 'osd3') {
this.networkState = { ...data.data };
}
});
},
methods: {
handleSizeChange(val) {
this.queryParam.pageSize = val;
this.getAirRouteList();
},
handleCurrentChange(val) {
this.queryParam.pageNum = val;
this.getAirRouteList();
},
uploadFile(event) {
let files = event.target.files;
if (files && files[0]) {
const file = files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('flag', this.gateWay.gateway);
returnImport(formData).then((res) => {
if (res.code == 200) {
this.$message.success('上传成功');
this.getAirRouteList();
}
});
}
},
// 导入航线
importPath() {
document.getElementById('fileInput').click();
},
// 判断是否有地形
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway,
type: '飞机'
}).then((res) => {
this.info = res.rows[0];
// console.log("this.info", this.info);
});
},
// 搜索
search() {
this.getAirRouteList();
},
// 获取航线列表数据
getAirRouteList() {
this.queryParam.flag = this.gateWay.gateway;
listPaths(this.queryParam).then((res) => {
let list = res.rows || [];
this.airList = list;
this.total = res.total;
});
},
beforeHandleDropMenu(dropId, airLine) {
return {
dropId: dropId,
airLine: airLine
};
},
handleDropMenu(command) {
// console.log("command", command);
const { dropId, airLine } = command;
if (dropId == 1) {
this.title = '重命名';
this.form.value1 = airLine.fileName;
this.curId = airLine.id;
this.globalPointHeightShow = false;
this.$refs.createHX.open();
}
if (dropId == 2) {
routerCopy(airLine.id).then((res) => {
if (res.code == 200) {
this.$message.success('复制成功');
this.getAirRouteList();
}
});
}
if (dropId == 3) {
if (airLine.fileUrl) {
let link = document.createElement('a');
link.style.display = 'none';
link.href = airLine.fileUrl;
// decodeURIComponent
link.setAttribute('download', airLine.fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link); //下载完成移除元素
} else {
this.$message.warning('请编辑后再下载');
}
}
if (dropId == 4) {
this.$confirm('此操作将永久删除航线数据,是否继续?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
delPaths(airLine.id).then(() => {
this.$message.success('删除成功');
if (window.airLine) {
window.airLine.remove();
window.airLine = null;
}
// 若当前项之前为选中状态清除
this.getAirRouteList();
});
})
.catch(() => {});
}
},
// 创建航线
add() {
this.title = '创建新航线';
this.globalPointHeightShow = true;
this.$refs.createHX.open();
},
// 升降序
order(order) {
this.queryParam.order = order;
this.getAirRouteList();
},
// 确认
submit() {
this.$refs['form'].validate((valid) => {
if (valid) {
if (this.title == '创建新航线') {
routerAdd({
filename: this.form.value1,
remark: this.info.remark,
flag: this.gateWay.gateway,
globalPointHeight: this.form.value2
}).then((res) => {
// console.log(res);
if (res.code == 200) {
this.$message.success('创建成功');
this.getAirRouteList();
}
});
}
if (this.title == '重命名') {
routerRename({
id: this.curId,
newFileName: this.form.value1
}).then((res) => {
if (res.code == 200) {
this.$message.success('重命名成功');
this.getAirRouteList();
}
});
}
this.form.value1 = '';
this.$refs.createHX.close();
} else {
console.log('error submit!!');
return false;
}
});
},
close() {
this.form.value1 = '';
this.form.value2 = '';
// this.$refs.createHX.close();
},
// 取消
cancel() {
this.form.value1 = '';
this.form.value2 = '';
this.$refs.createHX.close();
},
hasTerrain() {
return window.Earth1.viewer.terrainProvider.availability;
},
// 编辑
edit(item) {
let hasTerrain = window.Earth1.viewer.terrainProvider.availability;
if (!hasTerrain) {
this.$message.warning('请先添加地形文件(.pnk,再进行操作');
return;
}
this.$emit('routeEdit', item, this.networkState);
}
}
};
</script>
<style lang="scss">
.air_list {
left: 20px;
.el-pager li {
min-width: 18px;
}
.el-pager li.btn-quicknext,
.el-pager li.btn-quickprev {
color: #fff;
}
.header {
.logo {
position: relative;
text-align: center;
.title {
width: 380px;
height: 32px;
}
.add {
position: absolute;
top: 5px;
right: 10px;
width: 16px;
height: 16px;
z-index: 20;
background: url('../../images/add.png');
background-size: cover;
cursor: pointer;
}
.add:hover {
background: url('../../images/addh.png');
background-size: cover;
}
.importPath {
position: absolute;
top: 5px;
right: 40px;
width: 16px;
height: 16px;
z-index: 20;
background: url('../../images/importpath.png');
background-size: cover;
cursor: pointer;
}
.importPath:hover {
background: url('../../images/importpathh.png');
background-size: cover;
}
}
.search {
margin-top: 16px;
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
height: 32px;
line-height: 32px;
border-color: rgba(0, 255, 255, 0.5);
}
.el-input__prefix,
.el-input__suffix {
top: -2px;
}
.date {
height: 32px;
color: #fff;
align-items: center;
justify-content: center;
}
.arrow {
flex-direction: column;
font-size: 12px;
i {
cursor: pointer;
}
}
}
}
.el-divider--horizontal {
margin: 10px 0;
}
.el-divider {
background: rgba(204, 204, 204, 0.2);
}
.content {
height: calc(100% - 130px);
color: #fff;
overflow: auto;
padding-right: 5px;
.air_item {
background: rgba(0, 0, 0, 0.5);
height: 113px;
border-radius: 2px;
padding: 16px;
margin-bottom: 10px;
.top {
justify-content: space-between;
.text {
font-family: 'alimamashuheiti';
width: 200px;
overflow: hidden;
white-space: nowrap;
/* 确保文本在一行内显示允许设置overflow属性 */
text-overflow: ellipsis;
/* 使用省略符号表示文本溢出 */
}
}
.middle {
img {
width: 14px;
height: 14px;
}
}
.more_icon {
height: 20px;
width: 20px;
margin: 0 6px 0 10px;
cursor: pointer;
background: url('../../images/more1.png');
background-size: cover;
}
.more_icon:hover {
background: url('../../images/more2.png');
background-size: cover;
}
.edit {
width: 18px;
height: 18px;
cursor: pointer;
background: url('../../images/edit.png');
background-size: cover;
}
.edit:hover {
background: url('../../images/edith.png');
background-size: cover;
}
.middle,
.bottom {
font-family: sans-serif;
color: rgba(230, 247, 255, 1);
}
}
}
.pagination {
.el-pager li {
width: 20px;
}
}
/* 修改垂直滚动条 */
.content::-webkit-scrollbar {
width: 8px;
/* 修改宽度 */
}
/* 修改滚动条轨道背景色 */
.content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.1);
border-radius: 10px;
}
/* 修改滚动条滑块颜色 */
.content::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.3);
border-radius: 10px;
}
.createHX {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
height: 32px;
border-color: rgba(0, 255, 255, 0.5);
width: 100%;
color: #fff;
}
}
.btns {
text-align: center;
.el-button {
background: rgba(0, 255, 255, 0.2);
color: #fff;
border-color: rgba(0, 255, 255, 0.5);
}
.el-button:focus,
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
}
.el-dropdown-menu {
background: rgba(0, 0, 0, 1);
border: 1px solid rgba(0, 255, 255, 0.5);
color: #fff;
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: rgba(0, 255, 255, 1);
background-color: transparent;
}
.el-dropdown-menu__item {
color: #fff;
}
}
.el-popper .popper__arrow,
.el-popper .popper__arrow::after {
display: none;
}
.el-message-box {
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.3);
border: none;
.el-message-box__content,
.el-message-box__title {
color: #fff;
}
.el-button {
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
color: #fff;
border: 1px solid rgba(0, 255, 255, 0.5);
}
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
.el-form-item__label {
color: #fff;
}
</style>

View File

@ -0,0 +1,881 @@
<template>
<div class="airPointSetup uav_box">
<div class="header">
<div class="logo">
<img class="title" src="../../images/airsetup.png" alt="" />
<div class="item">
<div class="back com" @click="back" title="返回"></div>
</div>
<div class="item">
<div class="save com" @click="save" title="保存"></div>
</div>
<div class="item">
<div class="switch com" @click="senior" title="高级设置"></div>
</div>
<!-- <el-tooltip class="item" effect="dark" content="返回" placement="top">
</el-tooltip>
<el-tooltip class="item" effect="dark" content="保存" placement="top">
</el-tooltip>
<el-tooltip class="item" effect="dark" content="高级设置" placement="top">
</el-tooltip> -->
</div>
</div>
<div class="title">
<img src="../../images/title.png" alt="" />
<span>航点列表</span>
</div>
<el-divider></el-divider>
<!-- 航线信息 -->
<div class="info">
<el-row>
<el-col :span="10">
<div class="info_item">
<div>航线长度</div>
<div>{{ length }} m</div>
</div>
</el-col>
<el-col :span="7">
<div class="info_item">
<div>航点</div>
<div>{{ points.length }}</div>
</div>
</el-col>
<el-col :span="7">
<div class="info_item">
<div>照片</div>
<div>{{ getPhotoNum }}</div>
</div>
</el-col>
</el-row>
</div>
<el-divider></el-divider>
<!-- 航点列表 -->
<div class="content">
<div v-for="(item, index) in points" :key="index" @click="select(index, item)" class="point_item"
:class="{ active: selectId == index }">
<div class="left">
<img src="../../images/point.png" alt="" />
<span style="margin: 0 5px;">{{ index + 1 }}</span>
</div>
<div v-if="item.actions.length > 0" class="middle">
<div v-for="(item1, index1) in item.actions" :key="index1" @click.stop="openSet(index, index1, item1)"
style="display: inline-block;margin: 0 3px;cursor: pointer;">
<svg-icon :icon-class="item1.svg" style="width: 16px;height: 16px;" :style="selectIndexS == index1 && selectIndexF == index
? 'fill:rgba(0, 255, 255, 1)'
: 'fill:#fff'
"></svg-icon>
</div>
</div>
<div class="right">
<el-dropdown @command="handleDropMenu" trigger="click">
<div class="more_icon"></div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="beforeHandleDropMenu(drop.value, item)" v-for="drop in dropdownList"
:key="drop.value">{{ drop.label }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- 操作仓 -->
<div v-show="keywordShow" class="keyword flex">
<div class="left mr20">
<div class="top">
<div class="flex space_between">
<div class="img_box">
<img src="../../images/left.png" alt="" />
</div>
<div class="img_box">
<img style="width: 16px;height: 20px;" src="../../images/top.png" alt="" />
</div>
<div class="img_box">
<img src="../../images/right.png" alt="" />
</div>
</div>
<div class="flex space_between ">
<div class="item_bottom" @mousedown="towardsLeft">Q</div>
<div class="item_bottom" @mousedown="up">W</div>
<div class="item_bottom" @mousedown="towardsRight">E</div>
</div>
</div>
<div class="bottom">
<div class="flex space_between ">
<div class="img_box">
<img src="../../images/left_f.png" alt="" />
</div>
<div class="img_box">
<img src="../../images/after_f.png" alt="" />
</div>
<div class="img_box">
<img src="../../images/right_f.png" alt="" />
</div>
</div>
<div class="flex space_between ">
<div class="item_bottom" @mousedown="left">A</div>
<div class="item_bottom" @mousedown="after">S</div>
<div class="item_bottom" @mousedown="right">D</div>
</div>
</div>
</div>
<div class="middle mr20">
<div class="middle_item">
<div class="titel">经度</div>
<div class="value">106.25467890 °</div>
</div>
<div class="middle_item">
<div class="titel">维度</div>
<div class="value">36.25467890 °</div>
</div>
<div class="middle_item">
<div class="titel">高度</div>
<div class="value">90 m</div>
</div>
</div>
<div class="right flex">
<img src="../../images/top_r.png" alt="" />
<div class="item_right">C</div>
<div class="alt flex space_between">
<div class="f30">96.3</div>
<div class="f14 text_algin_right">
<div>ALT</div>
<div>m</div>
</div>
</div>
<div class="f12 asl text_algin_right">581 ASL</div>
<img src="../../images/bottom_r.png" alt="" />
<div class="item_right">Z</div>
</div>
</div>
<!-- 动作 -->
<div class="control">
<div class="right">
<div v-if="points.length > 0" class="control_item" @click="addPoint">
<span>新增航点</span>
<img src="../../images/control_add.png" alt="" />
</div>
<div class="control_item" @click="more">
<span>更多</span>
<img src="../../images/control_more.png" alt="" />
</div>
</div>
<div v-show="leftShow" class="left">
<div class="control_item" v-for="item in controls" :key="item.label" @click="onClick(item)">
<span>{{ item.label }}</span>
<img :src="item.url" alt="" />
</div>
</div>
</div>
<!-- 动作设置 -->
<div v-show="setupShow" class="setup">
<div class="setup_header flex space_between">
<div class="setup_header_left">
<svg-icon icon-class="camera" style="width: 20px;height: 16px;vertical-align: middle;"></svg-icon>
<span class="f14 header_title">{{ selectObj.label }}</span>
</div>
<div class="setup_header_right flex ai_center">
<svg-icon icon-class="left" style="width: 18px;height: 10px;"></svg-icon>
{{ selectIndexF + 1 }}-{{ selectIndexS + 1 }}
<svg-icon icon-class="right" style="width: 18px;height: 10px;color: red;"></svg-icon>
<div class="del" @click="delAction"></div>
</div>
<div class="close" @click="closeSetup"></div>
</div>
<el-divider></el-divider>
<!-- 开始录像 -->
<div v-if="selectObj.svg == 'start_record'" class="setup_content">
<div class="flex space_between">
<div>DJI_YYYYMMDDhhmm_XXX_</div>
<div v-if="!selectObj.params.fileSuffix" class="edit" @click="inputShow = true, inputDiv = true"></div>
</div>
<div v-if="inputShow" class="flex wuyongBox">
<el-input v-if="inputDiv" v-model="selectObj.params.fileSuffix"></el-input>
<div style="flex: 1;" v-if="!inputDiv">{{ selectObj.params.fileSuffix }}</div>
<i class="el-icon-edit wuyong-icon" v-if="!inputDiv" @click="inputDiv = true"></i>
<i class="el-icon-check wuyong-icon" v-if="inputDiv" style="color: green;" @click="saveSuucess"></i>
<i class="el-icon-close wuyong-icon" v-if="inputDiv" style="color: red;" @click="deleteSuucess"></i>
</div>
<div class="flex mt15 space_between">
<div class="btns">
<el-checkbox-group :disabled="!selectObj.params.useGlobalPayloadLensIndex" v-model="checkboxGroup1" :min="1"
:max="2" @change="changeGroup">
<el-checkbox-button v-for="item in pohoto" :label="item.value" :key="item.value">{{ item.label
}}</el-checkbox-button>
</el-checkbox-group>
</div>
<el-button size="small" :class="{ bugen: selectObj.params.useGlobalPayloadLensIndex == 1 }"
@click="genliuhangxian">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 停止录像 -->
<!-- 开始等时隔拍照 -->
<div v-if="selectObj.svg == 'interval_time'" class="setup_content">
<div class="flex space_between">
<div>间隔时间</div>
</div>
<div>
<plusReduce :defValue="selectObj.actionTrigger.actionTriggerParam" @value="interval_distance" :max="30"
:min="1">
</plusReduce>
</div>
<div class="flex mt15 space_between">
<div>
<el-button size="small" :disabled="pzDisabled">可见光</el-button>
<el-button size="small" :disabled="pzDisabled">红外照片</el-button>
</div>
<el-button size="small" :class="{ bugen: selectObj.params.useGlobalPayloadLensIndex == 1 }">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 拍照 -->
<div v-if="selectObj.svg == 'take_photo'" class="setup_content">
<div class="flex space_between">
<div>DJI_YYYYMMDDhhmm_XXX_</div>
<div v-if="!selectObj.params.fileSuffix" class="edit" @click="inputShow = true, inputDiv = true"></div>
</div>
<div v-if="inputShow" class="flex wuyongBox">
<el-input v-if="inputDiv" v-model="selectObj.params.fileSuffix"></el-input>
<div style="flex: 1;" v-if="!inputDiv">{{ selectObj.params.fileSuffix }}</div>
<i class="el-icon-edit wuyong-icon" v-if="!inputDiv" @click="inputDiv = true"></i>
<i class="el-icon-check wuyong-icon" v-if="inputDiv" style="color: green;" @click="saveSuucess"></i>
<i class="el-icon-close wuyong-icon" v-if="inputDiv" style="color: red;" @click="deleteSuucess"></i>
</div>
<div class="flex mt15 space_between">
<div class="btnss">
<el-checkbox-group :disabled="!selectObj.params.useGlobalPayloadLensIndex" v-model="checkboxGroup2" :min="1"
@change="changeGroup1">
<el-checkbox-button v-for="item in pohoto1" :label="item.value" :key="item.value">{{ item.label
}}</el-checkbox-button>
</el-checkbox-group>
<!-- <el-button size="small" :disabled="pzDisabled">可见光</el-button>
<el-button size="small" :disabled="pzDisabled">红外照片</el-button> -->
</div>
<el-button size="small" :class="{ bugen: selectObj.params.useGlobalPayloadLensIndex == 1 }"
@click="genliuhangxian">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 开始等距隔拍照 -->
<div v-if="selectObj.svg == 'interval_distance'" class="setup_content">
<div class="flex space_between">
<div>间隔时间</div>
</div>
<div>
<plusReduce @value="interval_distance" :defValue="selectObj.actionTrigger.actionTriggerParam" unit="m"
:max="100" :min="1"></plusReduce>
</div>
<div class="flex mt15 space_between">
<div>
<el-button size="small" :disabled="pzDisabled">可见光</el-button>
<el-button size="small" :disabled="pzDisabled">红外照片</el-button>
</div>
<el-button size="small" @click="pzDisabled = !pzDisabled">
<svg-icon icon-class="fly" style="width: 18px;height: 10px;"></svg-icon>跟随航线</el-button>
</div>
</div>
<!-- 悬停 -->
<div v-if="selectObj.svg == 'hover'" class="setup_content">
<div class="flex space_between">
<div>间隔时间</div>
</div>
<div>
<plusReduce @value="interval_distance" :defValue="Number(selectObj.params.hoverTime)" :max="1800" :min="1">
</plusReduce>
</div>
</div>
<!-- 飞行器偏航角 -->
<div v-if="selectObj.svg == 'drone_yaw'" class="setup_content">
<div class="flex space_between">
<div>飞行器偏航角</div>
<div class="lan">{{ selectObj.params.aircraftHeading }}°</div>
</div>
<div>
<myProgress :defValue="Number(selectObj.params.aircraftHeading)" @value="hpr" :sliderMax="180"
:sliderMin="-180">
</myProgress>
</div>
<div class="flex mt15 space_between"></div>
</div>
<!-- 云台俯仰角 -->
<div v-if="selectObj.svg == 'action_gimbal_pitch'" class="setup_content">
<div class="flex space_between">
<div>云台俯仰角</div>
<div class="lan">{{ selectObj.params.gimbalPitchRotateAngle }}°</div>
</div>
<div>
<myProgress :defValue="Number(selectObj.params.gimbalPitchRotateAngle)" @value="hpr" :sliderMax="30"
:sliderMin="-90"></myProgress>
</div>
<div class="flex mt15 space_between"></div>
</div>
<!-- 变焦 camera_zoom -->
<div v-if="selectObj.svg == 'camera_zoom'" class="setup_content">
<div class="flex space_between">
<div>变焦</div>
<div class="lan">
{{ Number(selectObj.params.focalLength) / 24 }}
<span style="font-size: 24px;color: #fff;">X</span>
</div>
</div>
<div>
<myProgress :defValue="Number(selectObj.params.focalLength) / 24" @value="hpr" :sliderMax="56" :sliderMin="1"
unit="X"></myProgress>
</div>
<div class="flex mt15 space_between"></div>
</div>
</div>
<div id="tongbuEarth"></div>
</div>
</template>
<script>
import plusReduce from "../component/plusReduce/index.vue";
import myProgress from "../component/progress/index.vue";
import mixin from "./js/pointSetup";
export default {
name: "airSetup",
components: { plusReduce, myProgress },
props: {
routeLine: {
type: Object,
default: () => { },
},
},
mixins: [mixin],
};
</script>
<style lang="scss">
.airPointSetup {
color: #fff;
left: 20px;
.header {
.logo {
position: relative;
text-align: center;
.title {
width: 380px;
height: 32px;
}
.com {
position: absolute;
top: 5px;
width: 16px;
height: 16px;
z-index: 20;
cursor: pointer;
}
.back {
right: 70px;
background: url("../../images/back.png");
background-size: cover;
}
.back:hover {
background: url("../../images/back_h.png");
background-size: cover;
}
.save {
right: 40px;
background: url("../../images/save.png");
background-size: cover;
}
.save:hover {
background: url("../../images/saveh.png");
background-size: cover;
}
.switch {
right: 10px;
background: url("../../images/switch.png");
background-size: cover;
}
.switch:hover {
background: url("../../images/switchh.png");
background-size: cover;
}
}
.search {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
height: 32px;
border-color: rgba(0, 255, 255, 0.5);
}
.date {
height: 32px;
color: #fff;
align-items: center;
justify-content: center;
}
.arrow {
flex-direction: column;
font-size: 12px;
}
}
}
.title {
height: 24px;
color: #fff;
display: flex;
align-items: center;
margin-top: 8px;
img {
width: 20px;
height: 16px;
margin-right: 10px;
}
}
.info {
.info_item {
div {
text-align: center;
line-height: 24px;
}
div:last-child {
font-family: "ddin";
}
}
}
.content {
height: calc(100% - 154px);
color: #fff;
overflow: auto;
padding-right: 5px;
.point_item {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 40px;
// margin-bottom: 10px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
background: transparent;
.left {
min-width: 50px;
margin-left: 15px;
img {
width: 12px;
height: 16px;
}
}
.middle {
flex: 1;
align-items: center;
img {
width: 12px;
height: 16px;
margin: 0 5px;
}
}
.right {
.more_icon {
height: 20px;
width: 20px;
margin: 0 6px 0 10px;
cursor: pointer;
background: url("../../images/more1.png");
background-size: cover;
}
.more_icon:hover {
background: url("../../images/more2.png");
background-size: cover;
}
}
}
.active {
background: linear-gradient(180deg,
rgba(0, 255, 255, 0) 0%,
rgba(0, 255, 255, 0.5) 100%);
}
.point_item:hover {
background: linear-gradient(180deg,
rgba(0, 255, 255, 0) 0%,
rgba(0, 255, 255, 0.5) 100%);
}
}
/* 修改垂直滚动条 */
.content::-webkit-scrollbar {
width: 8px;
/* 修改宽度 */
}
/* 修改滚动条轨道背景色 */
.content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.1);
border-radius: 10px;
}
/* 修改滚动条滑块颜色 */
.content::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.3);
border-radius: 10px;
}
.el-divider--horizontal {
margin: 10px 0;
}
.el-divider {
background: rgba(204, 204, 204, 0.2);
}
.text_align_center {
text-align: center;
}
.keyword {
position: fixed;
left: 50%;
bottom: 6%;
transform: translate(-50%, -8%);
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.top,
.bottom {
width: 157px;
height: 80px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
padding: 10px;
.img_box {
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
img {
width: 20px;
height: 18px;
}
}
.item_bottom {
width: 30px;
height: 30px;
line-height: 30px;
border-radius: 2px;
background: rgba(60, 60, 60, 1);
text-align: center;
}
}
.bottom {
.img_box:nth-child(1) {
img {
width: 12px;
height: 18px;
}
}
.img_box:nth-child(2) {
img {
width: 18px;
height: 12px;
}
}
.img_box:nth-child(3) {
img {
width: 12px;
height: 18px;
}
}
}
.middle {
min-width: 145px;
border-radius: 8px;
padding: 16px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
justify-content: space-between;
.middle_item {
.title {
font-size: 16px;
font-weight: 400;
color: rgba(230, 247, 255, 1);
}
.value {
font-family: "ddin";
font-size: 18px;
font-weight: 700;
color: rgba(255, 255, 255, 1);
}
}
}
.right {
min-width: 120px;
border-radius: 8px;
padding: 10px 16px;
background-color: rgba(0, 0, 0, 0.5);
flex-direction: column;
align-items: center;
justify-content: space-between;
img {
width: 16px;
height: 20px;
}
.item_right {
width: 30px;
height: 30px;
line-height: 30px;
border-radius: 2px;
background: rgba(60, 60, 60, 1);
text-align: center;
}
.alt,
.asl {
font-family: "ddin";
}
.alt {
color: rgba(0, 255, 255, 1);
}
.asl {
width: 100%;
}
}
.middle,
.right {
height: 200px;
}
}
.control {
position: fixed;
right: 2%;
top: 35%;
// background-color: transparent;
.control_item {
text-align: right;
margin-bottom: 20px;
span {
font-size: 14px;
font-weight: 700;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000,
1px 1px 0 #000;
/* 文字阴影,模拟边框效果 */
}
img {
width: 30px;
height: 30px;
vertical-align: middle;
}
}
.left {
position: absolute;
right: 120px;
top: 0;
min-width: 200px;
}
}
// 设置
.setup {
position: fixed;
top: 10%;
right: 5%;
min-width: 400px;
min-height: 100px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px;
border: 1px solid rgba(0, 255, 255, 0.5);
padding: 30px 12px 12px 16px;
.close {
width: 15px;
height: 15px;
position: absolute;
top: 8px;
right: 12px;
cursor: pointer;
background: url("../../images/close1.png");
background-size: cover;
}
.setup_header_left {
.header_title {
font-weight: 500;
color: rgba(0, 255, 255, 1);
}
}
.setup_header_right {
.del {
width: 14px;
height: 14px;
background: url("../../images/del.png");
background-size: cover;
cursor: pointer;
margin: 0 0 0 8px;
}
.del:hover {
background: url("../../images/delh.png");
background-size: cover;
}
}
.setup_content {
margin: 10px 0;
.wuyongBox {
align-items: center;
margin-top: 10px;
.wuyong-icon {
font-weight: bold;
margin: 0 5px;
cursor: pointer;
}
}
.bugen {
// background: rgba(0, 255, 255, 0.2) !important;
// color: rgba(0, 255, 255, 0.8) !important;
// border: 1px solid rgba(0, 255, 255, 0.5) !important;
background: rgb(255 255 255 / 20%) !important;
color: rgb(255 255 255 / 80%) !important;
border: 1px solid rgb(107 107 107 / 50%) !important;
}
.el-checkbox-group {
width: 100%;
}
.el-checkbox-button:last-child .el-checkbox-button__inner,
.el-checkbox-button:first-child .el-checkbox-button__inner {
border-radius: 5px;
box-shadow: none;
}
.el-checkbox-button.is-checked .el-checkbox-button__inner {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 1);
}
.el-checkbox-button__inner {
// background: rgba(0, 255, 255, 0.2);
// color: rgba(0, 255, 255, 0.8);
// border: 1px solid rgba(0, 255, 255, 0.5);
background: rgb(255 255 255 / 20%);
color: rgb(255 255 255 / 80%);
border: 1px solid rgb(107 107 107 / 50%);
margin-right: 8px;
border-radius: 5px;
padding: 8px 15px;
box-shadow: none;
}
.el-input__inner {
background-color: transparent;
border-color: aqua;
height: 30px;
line-height: 30px;
color: #fff;
}
.edit {
width: 14px;
height: 14px;
background: url("../../images/edit.png");
background-size: cover;
cursor: pointer;
}
.el-button.is-disabled,
.el-button.is-disabled:focus,
.el-button.is-disabled:hover {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 0.5);
color: rgba(255, 255, 255, 0.8);
}
.el-button {
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
color: rgba(0, 255, 255, 1);
}
.lan {
color: rgba(0, 255, 255, 1);
font-size: 30px;
font-family: "ddin";
}
}
}
}
.el-dropdown-menu {
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 255, 255, 0.5);
color: #fff;
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: rgba(0, 255, 255, 1);
background-color: transparent;
}
}
.el-popper .popper__arrow,
.el-popper .popper__arrow::after {
display: none;
}
#tongbuEarth {
width: 300px;
height: 200px;
position: fixed;
right: 0;
bottom: 0;
background: aqua;
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="bottom_tabs flex ai_center center">
<div v-for="item in tabs" class="item" :class="{ active: item.id == selectId }" @click="select(item)">
<div class="text" :class="{ active: item.id == selectId }">
{{ item.label }}
</div>
</div>
</div>
</template>
<script>
import $modal from '@/plugins/modal';
export default {
data() {
return {
tabs: [
{
label: '无人机',
id: 1
},
{
label: '航线库',
id: 2
},
{
label: '计划库',
id: 3
},
{
label: '媒体库',
id: 4
}
],
selectId: 1
};
},
created() {},
mounted() {},
methods: {
select(item) {
let airGateway = JSON.parse(localStorage.getItem('airGateway'));
if (!airGateway) {
$modal.msgWarning('请先选择无人机');
return;
}
this.selectId = item.id;
this.$emit('select', item);
}
}
};
</script>
<style lang="scss" scoped>
.bottom_tabs {
width: 50%;
height: 80px;
position: absolute;
z-index: 10;
left: 25%;
bottom: 2%;
// background: rgba(0, 0, 0, 0.5);
// border: 1px solid rgba(0, 255, 255, 0.5);
.item {
position: relative;
width: 136px;
height: 40px;
line-height: 45px;
background: url('../../images/tab.png');
background-size: cover;
font-family: 'alimamashuheiti';
text-align: center;
color: aliceblue;
cursor: pointer;
.text {
background: linear-gradient(180deg, #ffffff, #efffff, #00ffff);
/* 标准语法 */
-webkit-background-clip: text;
/* Chrome, Safari */
background-clip: text;
-webkit-text-fill-color: transparent;
/* Chrome, Safari */
color: transparent;
/* 其他浏览器 */
}
.text:hover,
.active {
background: linear-gradient(180deg, #ffffff, #fff8eb, #ffa600);
/* 标准语法 */
-webkit-background-clip: text;
/* Chrome, Safari */
background-clip: text;
-webkit-text-fill-color: transparent;
/* Chrome, Safari */
color: transparent;
/* 其他浏览器 */
}
}
.item:hover,
.active {
background: url('../../images/tab_h.png');
background-size: cover;
}
.item::after {
// content: " 🔗";
position: absolute;
left: 50%;
bottom: -16px;
/* 初始设置为不可见 */
opacity: 0;
transition: opacity 0.3s ease;
transform: translate(-50%);
content: '';
display: block;
width: 22px;
/* 设置图片宽度 */
height: 16px;
/* 设置图片高度 */
background-image: url('../../images/hover.png');
/* 设置图片路径 */
background-size: cover;
/* 背景图片覆盖整个元素 */
background-position: center;
/* 背景图片居中 */
}
.item:hover::after {
opacity: 1;
}
.active::after {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<div class="dialog" id="my_dialog" v-if="dialogShow">
<div class="header" id="my_dialog_header">
<span class="title">{{ title }}</span>
<div class="close" @click="close">
<div class="sector"></div>
<img src="../../../images/close.png" alt="" />
</div>
</div>
<slot></slot>
</div>
</template>
<script>
import { setMove } from '@/utils/moveDiv.ts';
export default {
name: 'dialog',
props: {
width: {
type: String,
default: ''
},
title: {
type: String,
default: '默认'
}
},
data() {
return {
dialogShow: false
};
},
mounted() {},
methods: {
close() {
this.$emit('close', false);
this.dialogShow = false;
},
open() {
this.dialogShow = true;
this.$nextTick(() => {
setMove('my_dialog_header', 'my_dialog');
});
}
}
};
</script>
<style lang="scss" scoped>
.dialog::before {
content: '';
width: 100px;
height: 6px;
background: aqua;
position: absolute;
top: -6px;
left: -2px;
clip-path: polygon(0% 0%, 90% 0%, 100% 100%, 0% 100%);
}
.dialog {
position: fixed;
top: 50%;
left: 50%;
z-index: 100;
width: 515px;
transform: translate(-50%, -50%);
padding: 10px;
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
border: 1.5px solid rgba(0, 255, 255, 1);
color: #fff;
.header {
height: 40px;
padding: 10px;
.title {
font-family: 'alimamashuheiti';
font-size: 18px;
font-weight: 700;
text-shadow: 0px 0px 9px rgba(20, 118, 255, 1);
}
.close {
cursor: pointer;
}
.sector {
position: absolute;
top: -30px;
right: -30px;
width: 0;
height: 0;
border: 30px solid transparent;
/* 边框宽度和颜色可以调整 */
border-bottom-color: rgba(0, 255, 255, 0.5);
/* 底边的颜色 */
border-radius: 50%;
/* 将边框变为圆形 */
transform: rotate(45deg);
/* 旋转45度可根据需要调整角度 */
}
img {
position: absolute;
top: 5px;
right: 5px;
width: 14px;
height: 14px;
}
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="plusReduce flex" :class="{ flex_column: column }">
<div class="left flex" :class="{ flex_column: column }" @click="calculate">
<span :class="{ disabled: active }">-100</span>
<span :class="{ disabled: active }">-10</span>
<span v-show="!column" :class="{ disabled: active }">-1</span>
</div>
<div class="middle">{{ value }} {{ unit }}</div>
<div class="right flex" :class="{ flex_column: column }" @click="calculate">
<span v-show="!column">+1</span>
<span>+10</span>
<span>+100</span>
</div>
</div>
</template>
<script>
export default {
name: "plusReduce",
props: {
defValue: {
type: Number || String,
default: 0,
},
unit: {
type: String,
default: "s",
},
max: {
type: Number,
default: 10,
},
min: {
type: Number,
default: 1,
},
column: {
type: Boolean,
default: false,
},
},
data() {
return {
active: false,
value: 1,
};
},
watch: {
value(newVal) {
if (newVal == this.min) {
this.active = true;
} else {
this.active = false;
}
},
defValue(newVal) {
this.value = newVal;
},
},
mounted() {
this.value = this.defValue;
console.log('this.value', this.value);
},
methods: {
calculate(e) {
console.log(e.target.innerHTML);
if (this.value >= 0 && e.target.innerHTML.length < 10) {
this.value += Number(e.target.innerText);
if (this.value <= this.min) {
this.value = this.min;
}
if (this.value >= this.max) {
this.value = this.max;
}
}
this.$emit("value", this.value);
},
},
};
</script>
<style lang="scss" scoped>
.plusReduce {
align-items: center;
margin: 10px 0;
span {
display: inline-block;
padding: 6px 7px;
background: rgba(60, 60, 60, 1);
border-radius: 3px;
margin: 5px;
cursor: pointer;
}
.disabled {
background: rgba(60, 60, 60, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
pointer-events: none;
}
// :not(.disabled)
span:hover {
background: rgba(0, 255, 255, 0.5);
}
.middle {
flex: 1;
text-align: center;
}
}
.flex_column {
flex-direction: column;
}
.flex {
display: flex;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="plusReduce flex">
<div class="left" @click="calculate(-1)">
<svg-icon icon-class="jian" style="width: 20px;height: 20px;cursor: pointer;"></svg-icon>
</div>
<div class="middle">
<el-slider v-if="progressType == 'progress'" style="width: 90%;" v-model="value" :max="sliderMax" :min="sliderMin"
:show-tooltip="false" @input="change"></el-slider>
<div v-if="progressType == 'num'">
<span class="num">{{ value2 }}</span>
m/s
</div>
</div>
<div class="right" @click="calculate(1)">
<svg-icon icon-class="jia" style="width: 20px;height: 20px;cursor: pointer;"></svg-icon>
</div>
</div>
</template>
<script>
export default {
name: "plusReduce",
props: {
defValue: {
type: Number,
default: 0,
},
unit: {
type: String,
default: "s",
},
sliderMax: {
type: Number,
default: 0,
},
sliderMin: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 10,
},
min: {
type: Number,
default: 1,
},
progressType: {
type: String,
default: "progress",
},
},
data() {
return {
active: false,
value: 1,
value2: 10,
};
},
watch: {
value(newVal) {
if (newVal == this.min) {
this.active = true;
} else {
this.active = false;
}
},
defValue(newVal) {
this.value = newVal;
this.value2 = newVal;
},
},
mounted() {
this.value = this.defValue;
this.value2 = this.defValue;
},
methods: {
calculate(num) {
if (this.progressType == "progress") {
if (this.value >= this.sliderMin || this.value < this.sliderMax) {
this.value += Number(num);
if (this.value < this.sliderMin) {
this.value = this.sliderMin;
}
if (this.value > this.sliderMax) {
this.value = this.sliderMax;
}
}
this.$emit("value", this.value);
} else {
if (this.value2 >= this.min || this.value2 < this.max) {
this.value2 += num;
if (this.value2 < this.min) {
this.value2 = this.min;
}
if (this.value2 > this.max) {
this.value2 = this.max;
}
this.$emit("value", this.value2);
}
}
},
change(value) {
this.$emit("value", value);
},
},
};
</script>
<style lang="scss">
.plusReduce {
align-items: center;
margin: 10px 0;
.middle {
flex: 1;
display: flex;
justify-content: center;
.el-slider__bar {
background-color: rgba(0, 255, 255, 1);
}
.el-slider__button {
border: none;
}
.num {
font-family: "ddin";
font-size: 30px;
font-weight: 700;
color: rgba(0, 255, 255, 1);
}
}
.disabled {
background: rgba(60, 60, 60, 0.5);
color: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
pointer-events: none;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -0,0 +1,37 @@
<template>
<div class="flyerimg">
<img style="width: 100%;height: 100%;" src="./1.jpg" alt="" />
</div>
</template>
<script>
export default {
name: 'flyer',
data() {
return {
};
},
mounted() {
},
methods: {
},
};
</script>
<style>
.flyerimg {
position: absolute;
left: 50%;
top: 50%;
z-index: 20;
transform: translate(-50%, -50%);
width: 80%;
height: 75%;
color: #fff;
}
</style>

View File

@ -0,0 +1,446 @@
<template>
<div class="airport-information-wrapper">
<ModuleItem :title="'飞行器信息详情'" :subTitle="'Aircraft Information Details'">
<div class="airport-information-content">
<div class="airport-info-wrapper">
<div style="display: flex">
<div class="airport-left">
<img src="../../../../images/home/aircraft.png" alt="" />
</div>
<div class="airport-center">
<div>设备型号 {{ fxqinfo?.deviceType || '----' }}</div>
<div>
设备项目呼号 {{ fxqinfo?.businessName || '----' }}
<img @click="onview" src="../../../../images/home/local-point.png" alt="" />
</div>
</div>
</div>
<!--
<div class="airport-right">
{{ specialHandling("drone_in_dock")
}}{{ specialHandling("device_online_status") }}
</div> -->
</div>
<div class="airport-list-wrapper">
<div v-for="item in airportsList" :key="item.name" class="airport-list-item">
<div class="airport-list-top">
<img v-if="item.img" :src="item.img" alt="" />
<div>{{ item.data }}{{ item.unit }}</div>
</div>
<div class="airport-list-bottom">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { listInfo } from '@/api/air';
import { debounce } from '@/utils/index';
import satellite from '../../../../images/home/satellite.png';
import paizhao from '../../../../images/home/paizhao.png';
import pzsyl from '../../../../images/home/pzsyl.png';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'AirportInformationDetails',
components: { ModuleItem },
props: {},
data() {
return {
airportsList: [
{
name: 'RTK',
data: 0,
unit: '',
key: 'rtk_number',
img: satellite
},
{
name: '拍照状态',
data: '空闲',
unit: '',
key: 'photo_state',
img: paizhao
},
{
name: '拍照剩余数量',
data: '0',
unit: '张',
key: 'remain_photo_num',
img: pzsyl
},
{
name: '电池剩余总量',
data: '0',
unit: '%',
key: 'capacity_percent',
img: null
},
{
name: '剩余飞行时间',
data: '00:00:00',
key: 'remain_flight_time',
img: null
},
{
name: '返航所需电量',
data: '0',
unit: '%',
key: 'return_home_power',
img: null
},
{
name: '内存总容量',
data: '0',
unit: 'GB',
key: 'total',
img: null
},
{
name: '内存已使用容量',
data: '0',
unit: 'GB',
key: 'used',
img: null
},
{
name: '飞行高度',
data: '0',
unit: 'm',
key: 'elevation',
img: null
}
],
osd4: null,
osd4Map: new Map(),
dataMap: new Map(),
dataAll: {},
info: {},
fxqinfo: {},
gateWay: useAirStore().gateWay,
flag: true,
timer: null,
time: ''
};
},
// computed: {
// // 映射 Vuex 中的状态到当前组件的计算属性
// ...mapState({
// gateWay: (state) => state.gateWay,
// }),
// },
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getList();
},
deep: true
}
},
methods: {
// 机场位置
onview: debounce(function () {
this.$sendChanel('onViewUAV');
}, 500),
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway
}).then((res) => {
// console.log("list-------", res);
// 暂时写死
this.info = res.rows[0];
this.fxqinfo = res.rows[1];
});
},
dataTreat(info) {
// console.log(456);
if (info.businessType !== 'osd4') {
this[info.businessType] = info.data;
Object.assign(this.dataAll, info.data);
// this.osd3.
} else {
this.osd4 = info.data;
// console.log("this.dataAll--------", this.dataAll);
}
function flattenObjectToMap(obj, map = new Map()) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenObjectToMap(value, map);
} else {
map.set(key, value);
}
}
}
return map;
}
this.osd4Map = flattenObjectToMap(this.osd4, this.osd4Map);
this.dataMap = flattenObjectToMap(this.dataAll, this.dataMap);
// console.log("this.dataAll--------", this.dataAll);
// 需要特殊处理的字段
const spcial = ['remain_flight_time', 'photo_state', 'remain_photo_num', 'total', 'used'];
// 特殊处理
this.airportsList.forEach((item) => {
if (!spcial.includes(item.key)) {
item.data = this.osd4Map.get(item.key) || 0;
} else {
item.data = this.specialHandling(item.key);
}
});
},
specialHandling(key) {
function kbToGb(kb) {
return kb / (1024 * 1024);
}
switch (key) {
case 'remain_flight_time':
const val = this.osd4Map.get(key);
if (!this.osd4Map.get(key)) {
return 0 + '时' + 0 + '分' + 0 + '秒';
}
//返回分钟
let h = parseInt(val / 60 / 60);
let miu = parseInt(h % 60);
let s = val % 60;
return h + '时' + miu + '分' + s + '秒';
case 'photo_state':
if (!this.osd4Map.get('cameras')) {
return '空闲';
}
const photoState = this.osd4Map.get('cameras')[0][key] == 0 ? '空闲' : '拍照中';
return photoState ? photoState : '空闲';
case 'remain_photo_num':
if (!this.osd4Map.get('cameras')) {
return 0;
}
const remain_photo_num = this.osd4Map.get('cameras')[0][key];
return remain_photo_num ? remain_photo_num : 0;
case 'total':
if (!this.osd4Map.get(key)) {
return 0;
}
let b = kbToGb(this.osd4Map.get(key));
const total = b.toFixed(2);
return total ? total : 0;
case 'used':
if (!this.osd4Map.get(key)) {
return 0;
}
let a = kbToGb(this.osd4Map.get(key));
const used = a.toFixed(2);
return used ? used : 0;
case 'device_online_status':
return String(this.dataMap.get(key)) === '1' ? '开机' : '关机';
case 'drone_in_dock':
return String(this.dataMap.get(key)) === '1' ? '舱内' : '舱外';
default:
break;
}
},
restet() {
// console.log(123);
this.airportsList = [
{
name: 'RTK',
data: 0,
unit: '',
key: 'rtk_number',
img: satellite
},
{
name: '拍照状态',
data: '空闲',
unit: '',
key: 'photo_state',
img: paizhao
},
{
name: '拍照剩余数量',
data: '0',
unit: '张',
key: 'remain_photo_num',
img: pzsyl
},
{
name: '电池剩余总量',
data: '0',
unit: '%',
key: 'capacity_percent',
img: null
},
{
name: '剩余飞行时间',
data: '00:00:00',
key: 'remain_flight_time',
img: null
},
{
name: '返航所需电量',
data: '0',
unit: '%',
key: 'return_home_power',
img: null
},
{
name: '内存总容量',
data: '0',
unit: 'GB',
key: 'total',
img: null
},
{
name: '内存已使用容量',
data: '0',
unit: 'GB',
key: 'used',
img: null
},
{
name: '飞行高度',
data: '0',
unit: 'm',
key: 'elevation',
img: null
}
];
},
onReset() {
let that = this;
that.timer = setInterval(() => {
const currentTimestamp = Date.now(); // 获取当前时间戳
const elapsed = currentTimestamp - that.time; // 计算差值
if (elapsed > 2000) {
that.restet(); // 如果大于2000毫秒则执行方法
}
}, 2000);
}
},
// 定时器
created() {
this.$recvChanel('liveBus', (bool) => {
setTimeout(() => {
this.restet();
}, 2000);
});
this.$recvChanel('airInfoBus', (bool) => {
this.flag = bool;
});
this.$recvChanel('websocketBus', (airdata) => {
// if (airdata.businessType == 'osd3') {
// this.dataTreat(airdata);
// }
if (airdata.businessType == 'osd4' && this.flag) {
this.dataTreat(airdata);
this.time = new Date().getTime();
}
if (airdata.businessType == 'osd4' && !this.flag) {
this.restet();
}
});
},
mounted() {
if (this.gateWay) {
this.getList();
}
this.onReset();
}
};
</script>
<style lang="scss" scoped>
.airport-information-wrapper {
width: 100%;
position: relative;
.airport-information-content {
height: 100%;
width: 100%;
// background-color: blueviolet;
.airport-info-wrapper {
padding: 0 2%;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(0, 255, 255, 0.2);
background-color: rgba(0, 0, 0, 0.5);
margin: 5px 0;
.airport-left {
transform: translateY(5px);
margin-right: 10px;
}
.airport-center {
font-size: 12px;
color: rgba(207, 255, 255, 1);
display: flex;
flex-direction: column;
justify-content: center;
> div:nth-of-type(2) {
display: flex;
align-items: center;
margin-top: 5px;
img {
margin-left: 5px;
cursor: pointer;
}
}
}
.airport-right {
display: flex;
align-items: center;
color: rgba(241, 108, 85, 1);
}
}
.airport-list-wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* 创建3列每列等宽 */
gap: 15px;
/* 设置列和行之间的间距 */
padding: 10px 0;
.airport-list-item {
font-size: 14px;
.airport-list-top {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
img {
width: 14px;
height: 14px;
margin-right: 4px;
}
}
.airport-list-bottom {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,486 @@
<template>
<div class="airport-information-wrapper">
<ModuleItem :title="'无人机机场信息详情'" :subTitle="'Airport Information Details'">
<div class="airport-information-content">
<div class="airport-info-wrapper">
<div style="display: flex">
<div class="airport-left">
<img src="../../../../images/home/cabin.png" alt="" />
</div>
<div class="airport-center">
<!-- {{ gateway.deviceType }} -->
<div>机舱型号{{ info.deviceType || '----' }}</div>
<div>
<!-- {{ gateway.businessName }} -->
机舱项目呼号{{ info.businessName || '----' }}
<!-- 防止重复点击,使用once修饰符 -->
<img @click="onview" src="../../../../images/home/local-point.png" alt="" />
</div>
</div>
</div>
<div class="airport-right">
{{ specialHandling('flighttask_step_code') }}
</div>
</div>
<div class="airport-list-wrapper">
<div v-for="item in airportsList" :key="item.name" class="airport-list-item">
<div class="airport-list-top">
<img v-if="item.img" :src="item.img" alt="" />
<div>{{ item.data }}{{ item.unit }}</div>
</div>
<div class="airport-list-bottom">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import dayjs from 'dayjs';
import { listInfo } from '@/api/air';
import { debounce } from '@/utils/index';
import satellite from '../../../../images/home/satellite.png';
import calibration from '../../../../images/home/calibration.png';
import recharge from '../../../../images/home/recharge.png';
import file from '../../../../images/home/file.png';
import uav from '../../../../images/home/uav.png';
import uavLocal from '../../../../images/home/uav-local.png';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'AirportInformationDetails',
components: { ModuleItem },
data() {
return {
airportsList: [
{
name: '机场搜星',
key: 'gps_number',
data: 0,
unit: '',
img: satellite
},
{
name: '标定状态',
data: '已标定',
key: 'is_calibration',
unit: '',
img: calibration
},
{
name: '机场存储',
data: '0/0',
unit: 'GB',
key: 'storage',
img: null
},
{
name: '以太网网络',
data: '0',
unit: 'KB/s',
key: 'rate',
img: null
},
{
name: '风速',
data: '0',
unit: 'm/s',
key: 'wind_speed',
img: null
},
{
name: '降雨量',
data: '0',
unit: 'mm',
key: 'rainfall',
img: null
},
{
name: '充电状态',
data: '空闲',
unit: '',
key: 'state',
img: recharge
},
{
name: '待上传数',
data: '0',
unit: '',
img: file,
key: 'remain_upload'
},
{
name: '舱内温度',
data: '0',
unit: '℃',
key: 'temperature',
img: null
},
{
name: '舱外温度',
data: '0',
unit: '℃',
key: 'environment_temperature',
img: null
},
{
name: '舱内湿度',
data: '0',
unit: '%RH',
key: 'humidity',
img: null
},
{
name: '运行时长',
data: '0天0时0分',
unit: '',
key: 'acc_time',
img: null
},
{
name: '飞行器状态',
data: '关机',
unit: '',
key: 'device_online_status',
img: uav
},
{
name: '飞行器位置',
data: '舱内',
unit: '',
key: 'drone_in_dock',
img: uavLocal
}
],
osd1: null,
osd2: null,
osd3: null,
dataMap: new Map(),
dataAll: {},
info: {},
gateway: useAirStore().gateWay
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateway = newValue;
this.getList();
},
deep: true
}
},
methods: {
dataTreat(info) {
if (['osd1', 'osd2', 'osd3'].includes(info.businessType)) {
this[info.businessType] = info.data;
Object.assign(this.dataAll, info.data);
}
// console.log("osd1", this.osd1);
// console.log("osd2", this.osd2);
// console.log("osd3", this.osd3);
// console.log("数据", this.dataAll);
function flattenObjectToMap(obj, map = new Map()) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenObjectToMap(value, map);
} else {
map.set(key, value);
}
}
}
return map;
}
this.dataMap = flattenObjectToMap(this.dataAll, this.dataMap);
// console.log("dataMap", this.dataMap);
// 需要特殊处理的字段
const spcial = ['is_calibration', 'storage', 'rate', 'state', 'acc_time', 'device_online_status', 'drone_in_dock'];
// 特殊处理
this.airportsList.forEach((item) => {
if (!spcial.includes(item.key)) {
item.data = this.dataMap.get(item.key);
} else {
item.data = this.specialHandling(item.key);
}
});
},
specialHandling(key) {
switch (key) {
case 'flighttask_step_code':
const flighttaskObj = {
'0': '作业准备中',
'1': '飞行作业中',
'2': '作业后状态恢复',
'3': '自定义飞行区更新中',
'4': '地形障碍物更新中',
'5': '任务空闲',
'255': '飞行器异常',
'256': '未知状态'
};
return flighttaskObj[String(this.dataMap.get(key))];
case 'is_calibration':
return String(this.dataMap.get(key)) === '1' ? '已标定' : '未标定';
case 'storage':
function kbToGb(kb) {
return kb / (1024 * 1024);
}
let a = kbToGb(this.dataMap.get('used')) || 0;
let b = kbToGb(this.dataMap.get('total')) || 0;
const used = a.toFixed(2);
const total = b.toFixed(2);
return `${used}/${total}`;
case 'rate':
let c = this.dataMap.get(key) || 0;
return c.toFixed(2);
case 'state':
return String(this.dataMap.get(key)) === '1' ? '充电中' : '空闲';
case 'acc_time':
function formatTime(seconds) {
const days = Math.floor(seconds / (3600 * 24));
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}${hours}${minutes}`;
}
const timeFormatter = formatTime(dayjs(this.dataMap.get(key)));
return timeFormatter;
case 'device_online_status':
return String(this.dataMap.get(key)) === '1' ? '开机' : '关机';
case 'drone_in_dock':
return String(this.dataMap.get(key)) === '1' ? '舱内' : '舱外';
default:
break;
}
},
// 获取设备列表
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateway.gateway
}).then((res) => {
this.number = res.rows.length;
this.info = res.rows[0];
console.log('this.infothis.infothis.infothis.info', this.info);
});
},
// 机场位置
// 使用防抖处理点击事件,避免短时间内重复触发
onview: debounce(function () {
this.$sendChanel('onView');
}, 500), // 500ms内只执行一次
restet() {
this.airportsList = [
{
name: '机场搜星',
key: 'gps_number',
data: 0,
unit: '',
img: satellite
},
{
name: '标定状态',
data: '已标定',
key: 'is_calibration',
unit: '',
img: calibration
},
{
name: '机场存储',
data: '0/0',
unit: 'GB',
key: 'storage',
img: null
},
{
name: '以太网网络',
data: '0',
unit: 'KB/s',
key: 'rate',
img: null
},
{
name: '风速',
data: '0',
unit: 'm/s',
key: 'wind_speed',
img: null
},
{
name: '降雨量',
data: '0',
unit: 'mm',
key: 'rainfall',
img: null
},
{
name: '充电状态',
data: '空闲',
unit: '',
key: 'state',
img: recharge
},
{
name: '待上传数',
data: '0',
unit: '',
img: file,
key: 'remain_upload'
},
{
name: '舱内温度',
data: '0',
unit: '℃',
key: 'temperature',
img: null
},
{
name: '舱外温度',
data: '0',
unit: '℃',
key: 'environment_temperature',
img: null
},
{
name: '舱内湿度',
data: '0',
unit: '%RH',
key: 'humidity',
img: null
},
{
name: '运行时长',
data: '0天0时0分',
unit: '',
key: 'acc_time',
img: null
},
{
name: '飞行器状态',
data: '关机',
unit: '',
key: 'device_online_status',
img: uav
},
{
name: '飞行器位置',
data: '舱内',
unit: '',
key: 'drone_in_dock',
img: uavLocal
}
];
}
},
mounted() {
console.log(useAirStore().gateWay);
if (this.gateway) {
this.getList();
}
this.$recvChanel('liveBus', (bool) => {
this.restet();
});
this.$recvChanel('websocketBus', (data) => {
this.dataTreat(data);
});
}
};
</script>
<style lang="scss" scoped>
.airport-information-wrapper {
width: 100%;
height: 36%;
position: relative;
margin-bottom: 15px;
.airport-information-content {
height: 100%;
width: 100%;
// background-color: blueviolet;
.airport-info-wrapper {
padding: 0 2%;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(0, 255, 255, 0.2);
background-color: rgba(0, 0, 0, 0.5);
margin: 5px 0;
.airport-left {
margin-right: 10px;
}
.airport-center {
// transform: translateX(-40px);
font-size: 12px;
color: rgba(207, 255, 255, 1);
display: flex;
flex-direction: column;
// justify-content: space-between;
justify-content: center;
> div:nth-of-type(2) {
display: flex;
align-items: center;
margin-top: 5px;
img {
margin-left: 5px;
cursor: pointer;
}
}
}
.airport-right {
display: flex;
align-items: center;
color: rgba(27, 248, 195, 1);
}
}
.airport-list-wrapper {
display: grid;
grid-template-columns: repeat(4, 1fr);
/* 创建4列每列等宽 */
// gap: 6px; /* 设置列和行之间的间距 */
grid-gap: 10px 6px;
.airport-list-item {
font-size: 14px;
.airport-list-top {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
img {
width: 14px;
height: 14px;
margin-right: 4px;
}
div:first-child {
font-family: 'ddin';
}
}
.airport-list-bottom {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div class="airport-debug-list-item" :style="{ width: cusWidth ? cusWidth : '100%' }">
<div class="debug-top" :style="{ minHeight: showBottom ? '70%' : '100%' }">
<div class="debug-l">
<div class="debug-l-img">
<img :src="imgUrl" alt="" />
</div>
<div class="debug-l-text">
<div>{{ name }}</div>
<el-tooltip :content="status" placement="bottom">
<div class="debug-l-status">{{ status }}</div>
</el-tooltip>
</div>
</div>
<div class="debug-r">
<slot name="btn"></slot>
</div>
</div>
<div v-if="showBottom" class="debug-bottom">
<div class="debug-bottom-wrapper">
<slot name="progress"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "DebugItem",
props: {
imgUrl: {
type: String,
},
status: {
type: String,
},
name: {
type: String,
},
cusWidth: {
type: String,
default: `100%`,
},
showBottom: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
methods: {},
created() { },
mounted() { },
};
</script>
<style lang="scss" scoped>
.airport-debug-list-item {
font-size: 14px;
border: 1px solid rgba(0, 255, 255, 0.2);
background-color: rgba(0, 0, 0, 0.5);
// height: 10vh;
padding: 20px 0;
width: 100%;
.debug-top {
padding: 0 3%;
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
min-height: 70%;
.debug-l {
display: flex;
align-items: center;
width: 70%;
.debug-l-img {
width: 24px;
height: 24px;
margin-right: 10px;
img {
width: 24px;
height: 24px;
}
}
.debug-l-text {
>div {
margin: 4px 0;
}
.debug-l-status {
width: 2.8vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.debug-r {
// width: 20%;
}
}
.debug-bottom {
padding: 0 2%;
transform: translateY(10px);
width: 100%;
display: flex;
align-items: center;
.debug-bottom-wrapper {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,811 @@
<template>
<div class="airport-debug-wrapper">
<ModuleItem :title="'机场远程调试'" :subTitle="'Airport Remote Debugging'">
<template #titleOption>
<el-switch v-model="debugSwitch" active-color="#00FFFFFF" inactive-color="#CCCCCC33" @change="changeSwitch"> </el-switch>
</template>
<div class="airport-debug-content">
<div class="airport-debug-list-wrapper">
<div class="debug-item-box">
<DebugItem :name="jcxt.name" :imgUrl="jcxt.img" :status="jcxt.data" :showBottom="true">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(jcxt.key)" size="small">
{{ jcxt.btnStatus }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="jcxt.progress" :show-text="false" :color="'#00FFFFFF'" :define-back-color="'#CCCCCC33'"></el-progress>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="cg.name" :imgUrl="cg.img" :status="cg.data" :showBottom="true">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(cg.key)" size="small">
{{ specialHandling(cg.key) !== '打开' ? '打开' : '关闭' }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="cg.progress" :show-text="false" :color="'#00FFFFFF'" style="background-color: #000"></el-progress>
</template>
</DebugItem>
</div>
<div v-if="number == 2" class="debug-item-box">
<DebugItem :name="tg.name" :imgUrl="tg.img" :status="tg.data" :showBottom="true">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(tg.key)" size="small">
{{ specialHandling(tg.key) !== '打开' ? '打开' : '关闭' }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="tg.progress" :show-text="false" :color="'#00FFFFFF'" :define-back-color="'#CCCCCC33'"></el-progress>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="jyms.name" :imgUrl="jyms.img" :status="jyms.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(jyms.key)" size="small">
{{ jyms.btnStatus }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="jccc.name" :imgUrl="jccc.img" :status="jccc.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(jccc.key)" size="small">
{{ jccc.btnStatus }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="zqtc.name" :imgUrl="zqtc.img" :status="zqtc.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(zqtc.key)" size="small">
{{ dataMap.get(zqtc.key) == 0 ? '开启' : '关闭' }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<!-- 我需要页面刚进来时status显示开机或者关机而不是开机中或者关机中这个状态来自dataTreat -->
<DebugItem
:name="fxq.name"
:imgUrl="fxq.img"
:status="
fxqLoading ? (fxqMethod == 'drone_open' ? '开机中' : '关机中') : fxqMethod ? (fxqMethod == 'drone_open' ? '开机' : '关机') : fxq.data
"
:showBottom="true"
>
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(fxq.key)" size="small">
<span v-show="!fxqLoading">{{ fxq.data == '开机' ? '关机' : '开机' }}</span>
<img v-show="fxqLoading" class="myLoading" src="../../../../images/loading.png" alt="" />
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="kt.name" :imgUrl="kt.img" :status="kt.data" class="custom-width">
<template #btn>
<div class="debug-btn-groups">
<el-button v-for="item in kt.btns" :key="item.id" @click="handleBtnClick(item.id, true)" size="mini">
{{ item.name }}
</el-button>
</div>
</template>
</DebugItem>
</div>
<div class="debug-item-box">
<DebugItem :name="cd.name" :imgUrl="cd.img" :status="cd.data">
<template #btn>
<div class="debug-btn-group">
<el-button @click="handleBtnClick(cd.key)" size="small">
{{ dataAll.drone_charge_state.state == 0 ? '开启' : '关闭' }}
</el-button>
</div>
</template>
<template #progress>
<el-progress :percentage="cd.progress" :show-text="false" :color="'#00FFFFFF'" :define-back-color="'#CCCCCC33'"></el-progress>
</template>
</DebugItem>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import { useAirStore } from '@/store/modules/drone';
import ModuleItem from '../ModuleItem/index.vue';
import DebugItem from './DebugItem.vue';
import { remoteDebug, listInfo, propertySet, sdrWorkmodeSwitch, airConditionerModeSwitch } from '@/api/air';
import debugJcxt from '../../../../images/home/debug-jcxt.png';
import debugCg from '../../../../images/home/debug-cg.png';
import debugTg from '../../../../images/home/debug-tg.png';
import debugJyms from '../../../../images/home/debug-jyms.png';
import debugJccc from '../../../../images/home/debug-jccc.png';
import debugZqtc from '../../../../images/home/debug-zqtc.png';
import debugFxq from '../../../../images/home/debug-fxq.png';
import debugKt from '../../../../images/home/debug-kt.png';
export default {
name: 'AirportRemoteDebugging',
components: { ModuleItem, DebugItem },
data() {
return {
debugSwitch: false,
// 机场系统
jcxt: {
name: '机场系统',
data: '工作中',
img: debugJcxt,
key: 'flighttask_step_code',
btnStatus: '重启',
progress: 0,
progressMethod: ['device_reboot']
},
cg: {
name: '舱盖',
data: '关',
img: debugCg,
key: 'cover_state',
btnStatus: '开启',
progress: 0,
progressMethod: ['cover_open', 'cover_close']
},
tg: {
name: '推杆',
data: '闭合',
img: debugTg,
key: 'putter_state',
btnStatus: '关闭',
progress: 0,
progressMethod: ['putter_open', 'putter_close']
},
jyms: {
name: '静音模式',
data: '未开启',
img: debugJyms,
key: 'silent_mode',
btnStatus: '开启'
},
jccc: {
name: '机场存储',
data: '0/0 GB',
img: debugJccc,
key: 'storage',
btnStatus: '格式化'
},
zqtc: {
name: '增强图传',
data: '未开启',
img: debugZqtc,
key: 'link_workmode',
btnStatus: '开启'
},
fxq: {
name: '飞行器',
data: '关机',
img: debugFxq,
key: 'device_online_status',
progress: 0,
progressMethod: ['drone_close'],
btnStatus: '开启'
},
kt: {
name: '空调',
data: '空闲中',
img: debugKt,
key: 'air_conditioner_state',
btns: [
{ name: '空闲', id: '0' },
{ name: '制冷', id: '1' },
{ name: '制热', id: '2' },
{ name: '除湿', id: '3' }
]
},
cd: {
name: '充电',
data: '未充电',
img: debugTg,
key: 'drone_charge_state',
btnStatus: '开启',
progress: 0,
progressMethod: ['charge_open', 'charge_close']
},
osd1: null,
osd2: null,
osd3: null,
osd4: null,
dataMap: new Map(),
dataAll: {
drone_charge_state: {
state: 0
}
},
info: {},
gateWay: useAirStore().gateWay,
number: 0,
fxqLoading: false,
fxqMethod: ''
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getDeviceNumber();
if (newValue) {
// 重置飞行器状态相关数据
this.fxqLoading = false;
this.fxqMethod = '';
this.fxq.data = '关机'; // 设置默认状态为关机
// 清空数据映射
this.dataMap = new Map();
this.dataAll = {
drone_charge_state: {
state: 0
}
};
}
},
deep: true
}
},
methods: {
// 获取设备列表
dataTreat(info) {
if (info.businessType !== 'osd4') {
if (['osd1', 'osd2', 'osd3'].includes(info.businessType)) {
this[info.businessType] = info.data;
Object.assign(this.dataAll, info.data);
} else {
// console.log("this[info.businessType]", info);
this.debuggingProgress(info.businessType, info.data);
}
} else {
this.osd4 = info.data;
}
//
if (this.dataMap.get('device_online_status') == '1' && this.fxqMethod == 'drone_open') {
this.fxqLoading = false;
this.fxqMethod = 'drone_open';
} else if (this.dataMap.get('device_online_status') == '0' && this.fxqMethod == 'drone_close') {
this.fxqLoading = false;
this.fxqMethod = 'drone_close';
if (window.airModel) {
window.airModel.remove();
window.airModel = null;
return;
}
} else if (this.dataMap.get('device_online_status') == '0') {
if (window.airModel) {
window.airModel.remove();
window.airModel = null;
return;
}
}
function flattenObjectToMap(obj, map = new Map()) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
flattenObjectToMap(value, map);
} else {
map.set(key, value);
}
}
}
return map;
}
this.dataMap = flattenObjectToMap(this.dataAll, this.dataMap);
this.jcxt.data = this.specialHandling(this.jcxt.key);
this.cg.data = this.specialHandling(this.cg.key);
this.jyms.data = this.specialHandling(this.jyms.key);
this.tg.data = this.specialHandling(this.tg.key);
this.jccc.data = this.specialHandling(this.jccc.key);
this.zqtc.data = this.specialHandling(this.zqtc.key);
this.fxq.data = this.specialHandling(this.fxq.key);
this.kt.data = this.specialHandling(this.kt.key);
this.cd.data = this.dataAll.drone_charge_state.state == 0 ? '未充电' : '充电中';
// console.log("this.dataMap", this.dataMap);
// console.log("this.jccc.data", this.dataMap.get(this.jccc.key));
if (this.dataMap.get('mode_code') && this.dataMap.get('mode_code') === 2) {
this.debugSwitch = true;
} else {
this.debugSwitch = false;
}
// console.log("mode_code", this.dataMap.get("mode_code"));
},
specialHandling(key) {
switch (key) {
case 'flighttask_step_code':
const flighttaskObj = {
'0': '作业准备中',
'1': '飞行作业中',
'2': '作业后状态恢复',
'3': '自定义飞行区更新中',
'4': '地形障碍物更新中',
'5': '任务空闲',
'255': '飞行器异常',
'256': '未知状态'
};
// console.log("this.dataMap.get(key)", this.dataMap.get(key));
return flighttaskObj[String(this.dataMap.get(key))];
case 'storage':
function kbToGb(kb) {
return kb / (1024 * 1024);
}
let a = kbToGb(this.dataMap.get('used')) || 0;
let b = kbToGb(this.dataMap.get('total')) || 0;
const used = a.toFixed(2);
const total = b.toFixed(2);
return `${used}/${total}GB`;
case 'cover_state':
const coverStateObj = {
'0': '关闭',
'1': '打开',
'2': '半开',
'3': '舱盖状态异常'
};
return coverStateObj[String(this.dataMap.get(key))];
case 'silent_mode':
const silentModeObj = {
'0': '非静音模式',
'1': '静音模式'
};
return silentModeObj[String(this.dataMap.get(key))];
case 'putter_state':
const putterStateObj = {
'0': '关闭',
'1': '打开',
'2': '半开',
'3': '推杆状态异常'
};
return putterStateObj[String(this.dataMap.get(key))];
case 'link_workmode':
const linkWorkmodeObj = {
'0': 'SDR 模式',
'1': '4G 融合模式'
};
return linkWorkmodeObj[String(this.dataMap.get(key))];
case 'device_online_status':
const deviceOnlineObj = {
'0': '关机',
'1': '开机'
};
return deviceOnlineObj[String(this.dataMap.get(key))];
case 'air_conditioner_state':
const airConditionerObj = {
'0': '空闲模式',
'1': '制冷模式',
'2': '制热模式',
'3': '除湿模式',
'4': '制冷退出模式',
'5': '制热退出模式',
'6': '除湿退出模式',
'7': '制冷准备模式',
'8': '制热准备模式',
'9': '除湿准备模式'
};
return airConditionerObj[String(this.dataMap.get(key))];
case 'drone_charge_state':
const chargeObj = {
'0': '空闲',
'1': '充电中'
};
return chargeObj[String(this.dataMap.get(key).state)];
default:
break;
}
},
handleBtnClick(key, isAirConditioner = false) {
// console.log("key", key);
switch (key) {
case 'flighttask_step_code':
this.airportRestart();
break;
case 'cover_state':
this.hatch();
break;
case 'putter_state':
this.putterStateChange();
break;
case 'silent_mode':
this.silentModeChange();
break;
case 'storage':
this.storageFormat();
break;
case 'link_workmode':
this.linkWorkmodeChange();
break;
case 'device_online_status':
this.AircraftBoot();
break;
case 'drone_charge_state':
this.chargeState();
break;
default:
// 空调
if (isAirConditioner) {
this.airConditioner(key);
}
break;
}
},
// 远程调试开关
changeSwitch(status) {
let air = localStorage.getItem('airGateway');
if (!air) {
this.$message.error('请先选择机场');
this.debugSwitch = false;
return;
}
this.debugSwitch = status;
const { gateway } = this.gateWay;
const params = { gateway, method: '' };
if (this.debugSwitch) {
params.method = 'debug_mode_open';
} else {
params.method = 'debug_mode_close';
}
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
// console.log("this.debugSwitch", this.debugSwitch);
},
// 机场重启
airportRestart() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = 'device_reboot';
const params = { gateway, method };
this.$sendChanel('liveBus', true);
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 飞行器开机
AircraftBoot() {
if (!this.debugSwitch) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataMap.get('device_online_status')) === 0 ? 'drone_open' : 'drone_close';
const params = { gateway, method };
this.fxqLoading = true;
this.fxqMethod = method;
if (method == 'drone_close') {
this.$sendChanel('liveBus', false);
}
this.$sendChanel('airInfoBus', method == 'drone_open' ? true : false);
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 舱盖开关
hatch() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataMap.get('cover_state')) === 0 ? 'cover_open' : 'cover_close';
const params = { gateway, method };
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 推杆开关
putterStateChange() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataMap.get('putter_state')) === 0 ? 'putter_open' : 'putter_close';
const params = { gateway, method };
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 静音模式开关
silentModeChange() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const status = Number(this.dataMap.get('silent_mode')) === 0 ? 1 : 0;
if (status === 0) {
this.jyms.btnStatus = '开启';
} else {
this.jyms.btnStatus = '关闭';
}
const params = { gateway, params: { silent_mode: status } };
propertySet(params).then((res) => {
this.$message.success(res.msg);
});
},
// 增强图传
linkWorkmodeChange() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
// const method = "sdr_workmode_switch";
// const params = { gateway, method };
// remoteDebug(params).then((res) => {});
const action = this.dataMap.get('link_workmode') == 1 ? 0 : 1;
const params = { gateway, action };
sdrWorkmodeSwitch(params).then((res) => {
const { code, msg } = res;
if (code == 200) {
this.$message.success('操作成功');
} else {
this.$message.error(msg);
}
});
},
// 空调
airConditioner(key) {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const params = { gateway, action: key };
airConditionerModeSwitch(params).then((res) => {
// console.log("空调-----------", res);
this.$message.success(res.msg);
});
},
// 机场存储格式化
storageFormat() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const params = { gateway, method: 'device_format' };
this.$confirm('此操作将清除机场存储数据, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'customMessageBox',
confirmButtonClass: 'customConfirm',
cancelButtonClass: 'customCancel'
}).then(() => {
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
});
},
chargeState() {
if (!this.gateWay) {
this.$message.error('机场已退出远程调试模式,无法执行当前操作');
return;
}
const { gateway } = this.gateWay;
const method = Number(this.dataAll.drone_charge_state.state) === 0 ? 'charge_open' : 'charge_close';
const params = { gateway, method };
remoteDebug(params).then((res) => {
this.$message.success(res.msg);
});
},
// 调试进度
debuggingProgress(key, data) {
if (this.cg.progressMethod.includes(key)) {
this.cg.progress = data.output.progress.percent;
} else if (this.jcxt.progressMethod.includes(key)) {
this.jcxt.progress = data.output.progress.percent;
} else if (this.fxq.progressMethod.includes(key)) {
this.fxq.progress = data.output.progress.percent;
} else if (this.tg.progressMethod.includes(key)) {
this.tg.progress = data.output.progress.percent;
} else if (this.cd.progressMethod.includes(key)) {
this.cd.progress = data.output.progress.percent;
}
},
// 获取数量
getDeviceNumber() {
const gateway = this.gateWay.gateway;
// 获取首位数字
const firstDigit = parseInt(gateway.charAt(0));
// 判断是4还是7
if (firstDigit === 4) {
// 是4返回4
this.number = 2;
} else {
this.number = 1;
}
}
},
created() {
this.$recvChanel('websocketBus', (data) => {
// console.log("收到的数据222", data);
this.dataTreat(data);
});
if (this.gateWay) {
this.getDeviceNumber();
}
},
mounted() {}
};
</script>
<style lang="scss">
@keyframes identifier {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.airport-debug-wrapper {
width: 100%;
height: 32%;
margin-bottom: 15px;
position: relative;
.airport-debug-content {
width: 100%;
height: 100%;
padding: 0 2%;
.airport-debug-list-wrapper {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
/* 创建2列每列等宽 */
gap: 12px;
/* 设置列和行之间的间距 */
overflow-y: auto;
.debug-item-box {
.myLoading {
width: 10px;
height: 10px;
animation: identifier 1s linear infinite;
}
.custom-width {
// width: 130% !important;
// display: flex;
// justify-content: space-around;
// background-color: aquamarine;
.debug-l {
width: 50%;
}
.debug-r {
width: 50%;
}
}
.debug-btn-group {
display: flex;
justify-content: flex-end;
.el-button--mini {
padding: 5px !important;
}
}
.debug-btn-groups {
display: grid;
grid-template-columns: repeat(2, 1fr);
/* 创建2列每列等宽 */
gap: 2px;
/* 设置列和行之间的间距 */
// padding: 2%;
.el-button--mini {
padding: 3px !important;
}
}
.el-button {
pointer-events: all;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 0.5);
color: #fff;
margin: 0 !important;
}
}
}
}
}
.customMessageBox {
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
border: 1.5px solid rgba(0, 255, 255, 1);
.el-message-box__message,
.el-message-box__title,
.el-message-box__close {
color: #fff;
&:hover {
color: #fff;
}
}
}
.customConfirm,
.customCancel {
border-radius: 4px !important;
background: rgba(0, 255, 255, 0.2) !important;
border: 1px solid rgba(0, 255, 255, 1) !important;
color: #fff;
&:hover {
color: #fff;
}
}
/* 整个滚动条 */
::-webkit-scrollbar {
width: 8px;
/* 滚动条的宽度 */
height: 8px;
/* 滚动条的高度(横向滚动条) */
}
/* 滑块部分 */
::-webkit-scrollbar-thumb {
background-color: rgba(0, 255, 255, 0.5);
/* 滑块的背景颜色 */
border-radius: 10px;
/* 滑块的圆角 */
border: none;
/* 滑块的边框 */
background-clip: content-box;
/* 背景裁剪 */
}
/* 滑块在悬停状态下的样式 */
::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 255, 255, 0.7);
/* 悬停时滑块的颜色 */
}
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background-color: transparent;
/* 轨道的背景颜色 */
border-radius: 10px;
/* 轨道的圆角 */
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="module-item-container">
<div class="module-item-title">
<div class="module-item-title-left">
<div class="module-item-title-left-main">{{ title }}</div>
<div class="module-item-title-left-sub">{{ subTitle }}</div>
</div>
<div class="module-item-title-right">
<slot name="titleOption"></slot>
</div>
</div>
<div class="module-item-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "ModuleItem",
props: ["title", "subTitle"],
computed: {},
data() {
return {};
},
created() { },
mounted() { },
};
</script>
<style lang="scss" scoped>
.module-item-container {
width: 100%;
height: 100%;
color: #fff;
.module-item-title {
height: 40px;
width: 100%;
margin-bottom: 15px;
background: url("../../../../images/home/module-item-title.png") no-repeat;
background-size: 100% 100%;
display: flex;
padding-left: 8%;
justify-content: space-between;
align-items: center;
.module-item-title-left {
display: flex;
align-items: flex-end;
&-main {
font-size: 18px;
font-weight: 700;
background: linear-gradient(180deg,
rgba(255, 255, 255, 1) 40%,
rgba(0, 255, 255, 1) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-right: 10px;
font-family: "alimamashuheiti";
}
&-sub {
font-size: 14px;
font-weight: 500;
}
}
.module-item-title-right {
// background-color: aqua;
}
}
.module-item-content {
width: 100%;
height: calc(100% - 50px);
}
}
</style>

View File

@ -0,0 +1,751 @@
<template>
<div class="cabin-live-wrapper">
<ModuleItem :title="'无人机机场监控直播'" :subTitle="'Monitor live streaming'">
<!-- <button @click="startRecording" :disabled="isRecording">开始录制</button>
<button @click="stopRecording" :disabled="!isRecording">停止录制</button>
<a ref="downloadLink" style="display: none;">下载视频</a> -->
<div
:id="'cabin-live-video_' + info.cameraIndex"
ref="monitorLiveVideo"
:class="[screen.currentStatus ? 'cabin-live-content-full' : 'cabin-live-content']"
>
<div class="cabin-live-title">
<div class="cabin-live-title-left">机舱SN:{{ info.sn || '----' }}</div>
<div class="cabin-live-title-right">
<!-- <el-tooltip :content="backlight.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="backlight.img" @click="changeBacklight" :title="backlight.tips" />
</div>
<!-- </el-tooltip> -->
<el-dropdown v-if="number == 1" size="mini" placement="bottom" trigger="click" :hide-on-click="false" @visible-change="visibleDropdown">
<!-- <el-tooltip :content="monitor.tips" placement="bottom"> -->
<div class="monitor cabin-live-title-right-wrapper">
<img :src="monitor.img" :title="monitor.tips" />
</div>
<!-- </el-tooltip> -->
<el-dropdown-menu #dropdown>
<el-dropdown-item v-for="item in monitor.list" :key="item.name" :class="{ active: monitor.current === item.index }">
<span @click="monitorCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown size="mini" trigger="click" placement="bottom" :hide-on-click="false" :teleported="false">
<!-- <el-tooltip :content="set.tips"> -->
<div class="monitor cabin-live-title-right-wrapper">
<img :src="set.img" :title="set.tips" />
</div>
<!-- </el-tooltip> -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in set.list" :key="item.name" :class="{ active: set.current === item.index }">
<span @click="setCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-tooltip :content="reset.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="reset.img" @click="handleReset" :title="reset.tips" />
</div>
<!-- </el-tooltip> -->
<!-- <el-tooltip :content="screen.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="screen.img" @click="makeFullScreen(`cabin-live-video_${info.cameraIndex}`)" :title="screen.tips" />
</div>
<!-- </el-tooltip> -->
<!-- <el-tooltip :content="switchs.tips" placement="bottom"> -->
<div class="bgd cabin-live-title-right-wrapper">
<img :src="switchs.img" @click="handleOpen" :title="switchs.tips" />
</div>
<!-- </el-tooltip> -->
</div>
</div>
<div class="cabin-live-video">
<!-- <video ref="sourceVideo" v-show="switchs.currentStatus && !switchs.loading" class="cabin-live-video-item"
style="position: absolute;width: 100%;height: 100%;object-fit: fill;"
:id="'video-webrtc_' + info.cameraIndex" autoplay muted width="100%" height="100%"></video> -->
<div v-show="switchs.currentStatus && !switchs.loading" class="cabin-live-video-item">
<webrtc :ref="'video-webrtc_' + info.cameraIndex"></webrtc>
</div>
<div v-show="!switchs.currentStatus && !switchs.loading" class="live-stop">
<img src="../../../../images/home/live-stop.png" alt="" />
<span>机舱已经停止直播</span>
</div>
<div v-show="switchs.loading" class="live-loading">
<img src="../../../../images/home/loading.png" alt="" />
<span>加载中请稍侯</span>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { listInfo, liveStart, liveStop, remoteDebug, liveChangeQuality, cameraChange } from '@/api/air';
import webrtc from '@/components/webrtc';
import { useAirStore } from '@/store/modules/drone';
import { getHost, getRtmpPort, getRtcPort } from '@/utils/auth';
import uavBgd from '../../../../images/home/uav-bgd.png';
import uavMonitor from '../../../../images/home/uav-monitor.png';
import uavSet from '../../../../images/home/uav-set.png';
import uavReset from '../../../../images/home/uav-reset.png';
import uavScreen from '../../../../images/home/uav-screen.png';
import uavClose from '../../../../images/home/uav-close.png';
import uavBgdOpen from '../../../../images/home/uav-bgd-open.png';
import uavScreenEsc from '../../../../images/home/uav-screen-esc.png';
import _ from 'lodash';
export default {
name: 'MonitorLiveStreaming',
components: { ModuleItem, webrtc },
data() {
return {
videoUrl: '',
flvPlayer: null,
info: {},
gateWay: useAirStore().gateWay,
time1: null,
time2: null,
//背光灯
backlight: {
light: false, // 是否开启
img: uavBgd,
tips: '打开背光灯',
key: 'supplement_light_open'
},
// 摄像头
monitor: {
img: uavMonitor,
list: [
{ name: '舱内', active: true, index: 0 },
{ name: '舱外', active: false, index: 1 }
],
tips: '摄像头',
key: 'supplement_light_open',
current: null
},
// 设置
set: {
img: uavSet,
list: [
{ name: '自适应', active: true, index: 0 },
{ name: '流畅', active: false, index: 1 },
{ name: '标清', active: false, index: 2 },
{ name: '高清', active: false, index: 3 },
{ name: '超清', active: false, index: 4 }
],
tips: '设置',
current: null
},
// 重置
reset: {
img: uavReset,
tips: '刷新',
key: 'device_reboot'
},
// 全屏切换
screen: {
img: uavScreen,
tips: '全屏切换',
currentStatus: false
},
//开关
switchs: {
img: uavClose,
tips: '开启直播',
currentStatus: false,
loading: false
},
number: 0,
isRecording: false,
mediaRecorder: null,
recordedChunks: [],
canvas: null,
canvasStream: null,
drawInterval: null
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getList();
this.getDeviceNumber();
},
deep: true
}
},
methods: {
// 背光灯
changeBacklight() {
this.backlight.light = !this.backlight.light;
if (this.backlight.light) {
this.backlight.img = uavBgdOpen;
this.backlight.key = 'supplement_light_open';
this.backlight.tips = '关闭背光灯';
this.backlight.msg = '已为你开启背光灯';
} else {
this.backlight.img = uavBgd;
this.backlight.key = 'supplement_light_close';
this.backlight.tips = '打开背光灯';
this.backlight.msg = '已为关闭背光灯';
}
const { gateway } = this.info;
let method = this.backlight.key;
const params = { gateway, method };
remoteDebug(params)
.then((res) => {
this.$message.success(this.backlight.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 摄像头
monitorCheck(data) {
console.log('data', data);
this.monitor.current = data.index;
this.monitor.list.forEach((item) => {
if (this.monitor.current === item.index) {
item.active = true;
} else {
item.active = false;
}
});
const { gateway } = this.info;
const params = {
gateway,
cameraIndex: this.gateWay.cameraIndex,
cameraPosition: data.index,
sn: this.gateWay.sn,
videoIndex: this.gateWay.videoIndex
};
cameraChange(params)
.then((res) => {
this.$message.success(res.data.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
console.log('this.monitor', this.monitor.list);
},
// // 开始录制
// startRecording() {
// this.isRecording = true;
// this.recordedChunks = [];
// // 获取 video 元素
// const sourceVideo = this.$refs.sourceVideo;
// console.log('sourceVideo', sourceVideo);
// // 创建 canvas 并设置大小
// this.canvas = document.createElement("canvas");
// this.canvas.width = sourceVideo.videoWidth;
// this.canvas.height = sourceVideo.videoHeight;
// const ctx = this.canvas.getContext("2d");
// // 定时将 video 内容绘制到 canvas
// const drawToCanvas = () => {
// if (!sourceVideo.paused && !sourceVideo.ended) {
// ctx.drawImage(sourceVideo, 0, 0, this.canvas.width, this.canvas.height);
// this.drawInterval = requestAnimationFrame(drawToCanvas);
// }
// };
// drawToCanvas();
// // 获取 canvas 的 MediaStream 并创建 MediaRecorder
// this.canvasStream = this.canvas.captureStream();
// this.mediaRecorder = new MediaRecorder(this.canvasStream, { mimeType: "video/webm" });
// // 处理录制的数据块
// this.mediaRecorder.ondataavailable = (event) => {
// if (event.data.size > 0) {
// this.recordedChunks.push(event.data);
// }
// };
// // 录制完成后生成下载链接
// this.mediaRecorder.onstop = () => {
// const blob = new Blob(this.recordedChunks, { type: "video/webm" });
// console.log('blobblobblobblob', blob);
// const url = URL.createObjectURL(blob);
// const downloadLink = document.createElement("a");
// downloadLink.href = url;
// downloadLink.download = String(new Date().getTime()) + ".webm";
// downloadLink.click();
// };
// // 开始录制
// this.mediaRecorder.start();
// },
// stopRecording() {
// this.isRecording = false;
// // 停止录制和绘制
// if (this.mediaRecorder) {
// this.mediaRecorder.stop();
// }
// if (this.drawInterval) {
// cancelAnimationFrame(this.drawInterval);
// }
// },
// 设置
setCheck(data) {
console.log('data', data);
this.set.current = data.index;
const { gateway, sn, cameraIndex, videoIndex } = this.info;
const params = {
gateway,
sn,
cameraIndex,
videoIndex,
videoQuality: this.set.current
};
liveChangeQuality(params)
.then((res) => {
this.$message.success(res.data.msg);
this.set.list.forEach((item) => {
if (this.set.current === item.index) {
item.active = true;
} else {
item.active = false;
}
});
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 刷新
handleReset() {
if (this.switchs.currentStatus) {
this.switchs.loading = true;
this.debouncehandleReset();
}
},
debouncehandleReset: _.debounce(async function () {
{
if (this.switchs.currentStatus) {
console.log(123123);
await this.stopLive();
const res = await this.startLive();
this.$message.success(res.msg);
this.switchs.currentStatus = true;
this.switchs.loading = false;
this.handleFullscreenChange();
}
}
}, 600),
// 电源
handleOpen() {
this.switchs.loading = true;
// 关闭就不显示加载动画
this.switchs.currentStatus && (this.switchs.loading = false);
this.debounceHandleOpen();
},
// 加个防抖不准频繁请求解决频繁请求关不了的问题
debounceHandleOpen: _.debounce(function () {
if (this.switchs.currentStatus) {
this.stopLive()
.then((res) => {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
console.log('err', err);
this.switchs.loading = false;
})
.finally(() => {
this.switchs.loading = false;
this.handleFullscreenChange();
});
} else {
this.startLive()
.then((res) => {
console.log('res', res);
this.switchs.currentStatus = true;
this.switchs.tips = '关闭直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
console.log('err', err);
this.switchs.loading = false;
})
.finally(() => {
console.log('finally');
this.switchs.loading = false;
this.handleFullscreenChange();
});
}
}, 600),
makeFullScreen(id) {
const videoElement = document.getElementById(id);
console.log('videoElement', videoElement);
if (!this.screen.currentStatus) {
this.screen.img = uavScreenEsc;
this.screen.currentStatus = true;
if (videoElement.requestFullscreen) {
videoElement.requestFullscreen();
} else if (videoElement.mozRequestFullScreen) {
// Firefox
videoElement.mozRequestFullScreen();
} else if (videoElement.webkitRequestFullscreen) {
// Chrome, Safari and Opera
videoElement.webkitRequestFullscreen();
} else if (videoElement.msRequestFullscreen) {
// IE/Edge
videoElement.msRequestFullscreen();
}
} else {
this.screen.img = uavScreen;
if (document.fullscreenElement) {
document
.exitFullscreen()
.then(() => {
this.screen.currentStatus = false;
clearInterval(this.time1);
clearInterval(this.time2);
})
.catch((err) => {
console.error(`Failed to exit full screen: ${err}`);
});
}
}
},
// 获取设备列表
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway,
type: '机场'
}).then((res) => {
console.log('list-------', res);
this.info = res.rows[0];
});
},
// 开启直播
startLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStart({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex,
host: getHost(),
rtmpPort: getRtmpPort(),
rtcPort: getRtcPort()
})
.then((res) => {
// this.playFlv(
// this.videoUrl,
// "video-webrtc_" + this.info.cameraIndex,
// this.info.cameraIndex
// );
// webrtc
if (res.code == 200) {
this.videoUrl = res.data.url;
this.$refs['video-webrtc_' + cameraIndex].startPlay(res.data.url);
resovle(res);
}
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 关闭直播
stopLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStop({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex
})
.then((res) => {
console.log('res-------', res);
this.flvDestroy();
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 播放flv视频
playFlv(flv, videoId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById(videoId);
this.flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: flv,
enableStashBuffer: true, //
enableWorker: true,
autoCleanupSourceBuffer: true //自动清除缓存
});
this.$nextTick(() => {
this.flvPlayer.attachMediaElement(videoElement);
this.flvPlayer.load();
this.flvPlayer.play();
});
}
},
// 关闭flv
flvDestroy() {
if (this.flvPlayer) {
try {
this.flvPlayer.pause(); // 暂停播放数据流
this.flvPlayer.unload(); // 取消数据流加载
this.flvPlayer.destroy(); // 销毁播放实例
// flvPlayer.off("error"); // 移除错误处理程序
this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出
this.flvPlayer = null;
} catch (error) {}
}
},
// 判断全屏添加消息提示
handleFullscreenChange() {
console.log('全屏---------');
// 检查是否处于全屏模式
if (document.fullscreenElement) {
console.log('进入全屏模式');
// 执行你的代码
if (document.querySelector('.el-message')) {
this.$refs.monitorLiveVideo.appendChild(document.querySelectorAll('.el-message')[0]);
}
}
},
// 我需要通过获取this.gateWay.gateway(7CTDM4S00B459L)中的首位数字来判断当前设备的数量
getDeviceNumber() {
const gateway = this.gateWay.gateway;
// 获取首位数字
const firstDigit = parseInt(gateway.charAt(0));
// 判断是4还是7
if (firstDigit === 4) {
// 是4返回4
this.number = 2;
} else {
this.number = 1;
}
}
},
mounted() {
if (this.gateWay) {
this.getList();
this.getDeviceNumber();
}
this.$recvChanel('liveBus', (flag) => {
if (flag && this.switchs.currentStatus) {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
}
});
this.$nextTick(() => {
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = uavScreen;
}
});
});
},
beforeDestroy() {
// 在组件销毁前移除事件监听
document.removeEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = uavScreen;
}
});
}
};
</script>
<style lang="scss">
.cabin-live-wrapper {
width: 100%;
height: 30%;
margin-bottom: 15px;
position: relative;
.cabin-live-content {
width: 100%;
height: 100%;
padding: 10px;
background: url('../../../../images/home/UAV-bg.png') no-repeat;
background-size: 100% 100%;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
clip-path: polygon(0 0, 97% 0, 100% 7%, 100% 100%, 3% 100%, 0 93%);
border: 1px solid rgba(0, 255, 255, 1);
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
// 全屏
.cabin-live-content-full {
width: 100%;
height: 100%;
padding: 10px;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
}
.active {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,784 @@
<template>
<div class="cabin-live-wrapper">
<ModuleItem :title="'无人机机场监控直播'" :subTitle="'Monitor live streaming'">
<div
:id="'cabin-live-video_' + info.cameraIndex"
ref="monitorLiveVideo"
:class="[screen.currentStatus ? 'cabin-live-content-full' : 'cabin-live-content']"
>
<div class="cabin-live-title">
<div class="cabin-live-title-left">机舱SN:{{ info.sn || '----' }}</div>
<div class="cabin-live-title-right">
<el-tooltip :content="backlight.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="backlight.img" @click="changeBacklight" />
</div>
</el-tooltip>
<!-- -->
<el-dropdown v-if="number == 2" size="mini" trigger="click" :hide-on-click="false" @visible-change="visibleDropdown">
<el-tooltip :content="monitor.tips" placement="bottom">
<div class="monitor cabin-live-title-right-wrapper">
<img :src="monitor.img" />
</div>
</el-tooltip>
<el-dropdown-menu #dropdown>
<el-dropdown-item v-for="item in monitor.list" :key="item.name" :class="{ active: monitor.current === item.index }">
<span @click="monitorCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown size="mini" trigger="click" :hide-on-click="false" @visible-change="visibleDropdown">
<el-tooltip :content="set.tips" placement="bottom">
<div class="monitor cabin-live-title-right-wrapper">
<img :src="set.img" />
</div>
</el-tooltip>
<el-dropdown-menu #dropdown>
<el-dropdown-item v-for="item in set.list" :key="item.name" :class="{ active: set.current === item.index }">
<span @click="setCheck(item)">{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- <div class="monitor cabin-live-title-right-wrapper" :ref="'screenFull'">
<el-tooltip :content="set.tips" placement="bottom">
<img :src="set.img" @click="showDropDown()" />
</el-tooltip>
<div v-show="set.show" class="monitor-live-dropdown-menu">
<span v-for="itm in set.list" :key="itm.value" class="dropdown-item"
:class="{ 'dropdown-active': itm.active }" @click="setCheck(itm)">
{{ itm.name }}
</span>
</div>
</div> -->
<el-tooltip :content="reset.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="reset.img" @click="handleReset" />
</div>
</el-tooltip>
<el-tooltip :content="screen.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="screen.img" @click="makeFullScreen(`cabin-live-video_${info.cameraIndex}`)" />
</div>
</el-tooltip>
<el-tooltip :content="switchs.tips" placement="bottom">
<div class="bgd cabin-live-title-right-wrapper">
<img :src="switchs.img" @click="handleOpen" />
</div>
</el-tooltip>
</div>
</div>
<div class="cabin-live-video">
<video
v-show="switchs.currentStatus && !switchs.loading"
class="cabin-live-video-item"
style="position: absolute; width: 100%; height: 100%; object-fit: fill"
:id="'video-webrtc_' + info.cameraIndex"
autoplay
muted
width="100%"
height="100%"
></video>
<div v-show="!switchs.currentStatus && !switchs.loading" class="live-stop">
<img :src="require('../../../../images/home/live-stop.png')" alt="" />
<span>机舱已经停止直播</span>
</div>
<div v-show="switchs.loading" class="live-loading">
<img :src="require('../../../../images/home/loading.png')" alt="" />
<span>加载中请稍侯</span>
</div>
</div>
</div>
</ModuleItem>
</div>
</template>
<script>
import ModuleItem from '../ModuleItem/index.vue';
import { listInfo, liveStart, liveStop, remoteDebug, liveChangeQuality, cameraChange } from '@/api/air';
import _ from 'lodash';
import { getHost, getRtmpPort, getRtcPort } from '@/utils/auth';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'MonitorLiveStreaming',
components: { ModuleItem },
data() {
return {
videoUrl: '',
flvPlayer: null,
info: {},
gateWay: useAirStore().gateWay,
time1: null,
time2: null,
number: 0,
//背光灯
backlight: {
light: false, // 是否开启
img: require('../../../../images/home/uav-bgd.png'),
tips: '打开补光灯',
key: 'supplement_light_open',
msg: '补光灯已开启'
},
// 摄像头
monitor: {
img: require('../../../../images/home/uav-monitor.png'),
list: [
{ name: '舱外', active: false, index: 1 },
{ name: '舱内', active: true, index: 0 }
],
tips: '摄像头',
key: 'supplement_light_open',
current: 0
},
// 设置
set: {
show: false,
img: require('../../../../images/home/uav-set.png'),
list: [
{ name: '自适应', active: true, index: 0 },
{ name: '流畅', active: false, index: 1 },
{ name: '标清', active: false, index: 2 },
{ name: '高清', active: false, index: 3 },
{ name: '超清', active: false, index: 4 }
],
tips: '设置',
current: 0
},
// 重置
reset: {
img: require('../../../../images/home/uav-reset.png'),
tips: '刷新',
key: 'device_reboot'
},
// 全屏切换
screen: {
img: require('../../../../images/home/uav-screen.png'),
tips: '全屏切换',
currentStatus: false
},
//开关
switchs: {
img: require('../../../../images/home/uav-close.png'),
tips: '开启直播',
currentStatus: false,
loading: false
}
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getList();
},
deep: true
}
},
methods: {
showDropDown() {
this.set.show = true;
},
// 关闭直播
stopLives() {
if (this.flvPlayer) {
this.stopLive();
}
},
// 背光灯
changeBacklight() {
this.backlight.light = !this.backlight.light;
if (this.backlight.light) {
this.backlight.img = require('../../../../images/home/uav-bgd-open.png');
this.backlight.key = 'supplement_light_open';
this.backlight.tips = '关闭补光灯';
this.backlight.msg = '补光灯已开启';
} else {
this.backlight.img = require('../../../../images/home/uav-bgd.png');
this.backlight.key = 'supplement_light_close';
this.backlight.tips = '打开补光灯';
this.backlight.msg = '补光灯已关闭';
}
const { gateway } = this.info;
let method = this.backlight.key;
const params = { gateway, method };
remoteDebug(params)
.then((res) => {
console.log(res);
console.log(this.backlight.msg);
this.$message.success(this.backlight.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 摄像头
monitorCheck(data) {
// console.log("data", data);
this.monitor.current = data.index;
this.monitor.list.forEach((item) => {
if (this.monitor.current === item.index) {
item.active = true;
} else {
item.active = false;
}
});
const { gateway } = this.info;
const params = {
gateway,
cameraIndex: this.gateWay.cameraIndex,
cameraPosition: data.index,
sn: this.gateWay.sn,
videoIndex: this.gateWay.videoIndex
};
cameraChange(params)
.then((res) => {
this.$message.success(res.msg);
})
.finally(() => {
this.handleFullscreenChange();
});
// console.log("this.monitor", this.monitor.list);
},
// 设置
setCheck(data) {
// console.log("data", data);
this.set.show = false;
this.set.current = data.index;
const { gateway, sn, cameraIndex, videoIndex } = this.info;
const params = {
gateway,
sn,
cameraIndex,
videoIndex,
videoQuality: this.set.current
};
liveChangeQuality(params)
.then((res) => {
this.set.list.forEach((item) => {
if (this.set.current === item.index) {
item.active = true;
this.$message.success(res.msg);
} else {
item.active = false;
}
});
})
.finally(() => {
this.handleFullscreenChange();
});
},
// 刷新
handleReset() {
// if (this.switchs.currentStatus) {
this.switchs.loading = true;
this.debouncehandleReset();
// }
},
debouncehandleReset: _.debounce(async function () {
{
if (this.switchs.currentStatus) {
// await this.stopLive();
const res = await this.startLive();
this.$message.success(res.msg);
this.switchs.currentStatus = true;
this.switchs.loading = false;
this.handleFullscreenChange();
}
}
}, 600),
// 电源
handleOpen() {
this.switchs.loading = true;
// 关闭就不显示加载动画
this.switchs.currentStatus && (this.switchs.loading = false);
this.debounceHandleOpen();
},
// 加个防抖不准频繁请求解决频繁请求关不了的问题
debounceHandleOpen: _.debounce(function () {
if (this.switchs.currentStatus) {
this.stopLive()
.then((res) => {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
// console.log("err", err);
this.switchs.loading = false;
})
.finally(() => {
this.switchs.loading = false;
this.handleFullscreenChange();
});
} else {
this.startLive()
.then((res) => {
// console.log("res", res);
this.switchs.currentStatus = true;
this.switchs.tips = '关闭直播';
this.$message.success(res.msg);
this.switchs.loading = false;
})
.catch((err) => {
// console.log("err", err);
this.switchs.loading = false;
})
.finally(() => {
// console.log("finally");
this.switchs.loading = false;
this.handleFullscreenChange();
});
}
}, 600),
makeFullScreen(id) {
this.$nextTick(() => {
const videoElement = document.getElementById(id);
if (!this.screen.currentStatus) {
this.screen.img = require('../../../../images/home/uav-screen-esc.png');
this.screen.currentStatus = true;
if (videoElement.requestFullscreen) {
videoElement.requestFullscreen();
} else if (videoElement.mozRequestFullScreen) {
// Firefox
videoElement.mozRequestFullScreen();
} else if (videoElement.webkitRequestFullscreen) {
// Chrome, Safari and Opera
videoElement.webkitRequestFullscreen();
} else if (videoElement.msRequestFullscreen) {
// IE/Edge
videoElement.msRequestFullscreen();
}
} else {
this.screen.img = require('../../../../images/home/uav-screen.png');
if (document.fullscreenElement) {
document
.exitFullscreen()
.then(() => {
this.screen.currentStatus = false;
clearInterval(this.time1);
clearInterval(this.time2);
})
.catch((err) => {
console.error(`Failed to exit full screen: ${err}`);
});
}
}
});
},
// 用于解决全屏下下拉菜单不显示的问题
visibleDropdown() {
// this.time1 = setTimeout(() => {
// // console.log("有执行吗", this.$refs.monitorLiveVideo);
// // console.log("el-dropdown", document.querySelector(".el-dropdown-menu"));
// if (document.querySelector(".el-dropdown-menu")) {
// this.$refs.monitorLiveVideo.appendChild(
// document.querySelectorAll(".el-dropdown-menu")[0]
// );
// this.$refs.monitorLiveVideo.appendChild(
// document.querySelectorAll(".el-dropdown-menu")[1]
// );
// }
// }, 200);
},
// 获取设备列表
getList() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway,
type: '机场'
}).then((res) => {
// console.log("list-------", res);
this.info = res.rows[0];
});
},
// 获取数量
getNumber() {
listInfo({
pageNum: 1,
pageSize: 10,
gateway: this.gateWay.gateway
}).then((res) => {
// console.log("list-------", res);
this.number = res.rows.length;
});
},
// 开启直播
startLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStart({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex,
host: getHost(),
rtmpPort: getRtmpPort(),
rtcPort: getRtcPort()
})
.then((res) => {
this.videoUrl = res.data.url;
this.playFlv(this.videoUrl, 'video-webrtc_' + this.info.cameraIndex, this.info.cameraIndex);
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 关闭直播
stopLive() {
return new Promise((resovle, reject) => {
if (this.info) {
const { cameraIndex, gateway, sn, videoIndex } = this.info;
liveStop({
cameraIndex,
definition: 0,
gateway,
sn,
videoIndex
})
.then((res) => {
// console.log("res-------", res);
this.flvDestroy();
resovle(res);
this.handleFullscreenChange();
})
.catch((err) => {
reject(err);
this.handleFullscreenChange();
});
}
});
},
// 播放flv视频
playFlv(flv, videoId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById(videoId);
this.flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: flv,
enableStashBuffer: true, //
enableWorker: true,
autoCleanupSourceBuffer: true //自动清除缓存
});
this.$nextTick(() => {
this.flvPlayer.attachMediaElement(videoElement);
this.flvPlayer.load();
this.flvPlayer.play();
});
}
},
// 关闭flv
flvDestroy() {
if (this.flvPlayer) {
try {
this.flvPlayer.pause(); // 暂停播放数据流
this.flvPlayer.unload(); // 取消数据流加载
this.flvPlayer.destroy(); // 销毁播放实例
// flvPlayer.off("error"); // 移除错误处理程序
this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出
this.flvPlayer = null;
} catch (error) {}
}
},
// 判断全屏添加消息提示
handleFullscreenChange() {
// console.log("全屏---------");
// 检查是否处于全屏模式
if (document.fullscreenElement) {
// console.log("进入全屏模式");
// 执行你的代码
if (document.querySelector('.el-message')) {
this.$refs.monitorLiveVideo.appendChild(document.querySelectorAll('.el-message')[0]);
}
}
},
handleClickOutside(event) {
const dropdown = this.$refs.screenFull;
// console.log("dropdown", dropdown);
setTimeout(() => {
if (!dropdown.contains(event.target)) {
this.set.show = false; // 点击外部区域时隐藏下拉框
}
}, 200);
}
},
mounted() {
if (this.gateWay) {
this.getList();
this.getNumber();
}
this.$recvChanel('liveBus', (flag) => {
if (flag && this.switchs.currentStatus) {
this.switchs.currentStatus = false;
this.switchs.tips = '开启直播';
}
});
// this.$recvChanel("flyerChange", () => {
// this.stopLives()
// });
// document.addEventListener("click", this.handleClickOutside);
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = require('../../../../images/home/uav-screen.png');
}
});
},
beforeDestroy() {
// 在组件销毁前移除事件监听
document.removeEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.screen.currentStatus = false;
this.screen.img = require('../../../../images/home/uav-screen.png');
}
});
}
};
</script>
<style lang="scss">
.cabin-live-wrapper {
width: 100%;
height: 30%;
margin-bottom: 15px;
position: relative;
.cabin-live-content {
width: 100%;
height: 100%;
padding: 10px;
background: url('../../../../images/home/UAV-bg.png') no-repeat;
background-size: 100% 100%;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 10;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
// position: relative;
img {
height: 100%;
width: 100%;
}
}
.monitor-live-dropdown-menu {
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.5);
border: 1.5px solid rgba(0, 255, 255, 1);
backdrop-filter: blur(2px);
border-radius: 5px;
margin: 0;
width: 60px;
.dropdown-item {
padding: 5px;
display: inline-block;
cursor: pointer;
width: 100%;
text-align: center;
line-height: 24px;
font-size: 12px;
&:hover {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
}
.dropdown-active {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
}
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
// clip-path: polygon(0 0, 97% 0, 100% 7%, 100% 100%, 3% 100%, 0 93%);
border: 1px solid rgba(0, 255, 255, 1);
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
// 全屏
.cabin-live-content-full {
width: 100%;
height: 100%;
padding: 10px;
.cabin-live-title {
height: 24px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
top: 0;
left: 0;
&-left {
font-size: 14px;
}
&-right {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
&-wrapper {
width: 16px;
height: 16px;
margin: 0 3px;
cursor: pointer;
position: relative;
img {
height: 100%;
width: 100%;
}
}
// .monitor-live-dropdown-menu {
// display: flex;
// flex-direction: column;
// background: rgba(0, 0, 0, 0.5);
// border: 1.5px solid rgba(0, 255, 255, 1);
// backdrop-filter: blur(2px);
// border-radius: 5px;
// margin: 0;
// width: 60px;
// .dropdown-item {
// padding: 5px;
// display: inline-block;
// cursor: pointer;
// width: 100%;
// text-align: center;
// line-height: 24px;
// font-size: 12px;
// &:hover {
// background: rgba(0, 255, 255, 0.2);
// color: rgba(0, 255, 255, 1);
// }
// }
// .dropdown-active {
// background: rgba(0, 255, 255, 0.2);
// color: rgba(0, 255, 255, 1);
// }
// }
}
}
.cabin-live-video {
margin-top: 10px;
height: calc(100% - 34px);
width: 100%;
position: relative;
.cabin-live-video-item {
height: 100%;
width: 100%;
}
.live-stop {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #000;
flex-direction: column;
size: 12px;
}
.live-loading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
size: 12px;
> img {
height: 50px;
height: 50px;
}
}
}
}
}
.active {
background: rgba(0, 255, 255, 0.2);
color: rgba(0, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="home_left uav_box">
<MonitorLiveStreaming></MonitorLiveStreaming>
<AirportInformationDetails></AirportInformationDetails>
<AirportRemoteDebugging></AirportRemoteDebugging>
<!-- <div v-for="(url, index) in urls" :key="index">
<webrtc :ref="`webrtc-${index}`"></webrtc>
<button @click="startPlay(url, index)">播放</button>
</div> -->
</div>
</template>
<script>
import MonitorLiveStreaming from './components/MonitorLiveStreaming/index.vue';
import AirportInformationDetails from './components/AirportInformationDetails/index.vue';
import AirportRemoteDebugging from './components/AirportRemoteDebugging/index.vue';
// import webrtc from "@/components/webrtc";
export default {
components: {
MonitorLiveStreaming,
AirportInformationDetails,
AirportRemoteDebugging
// webrtc,
},
data() {
return {
urls: [
'http://121.37.237.116:28453/rtc/v1/whep/?app=live&stream=4SEDL9C001X8GE4SEDL9C001X8GE165-0-7normal-0',
'http://121.37.237.116:28453/rtc/v1/whep/?app=live&stream=4SEDL9C001X8GE1581F5BMD238V00172JR39-0-7normal-0',
'http://121.37.237.116:28453/rtc/v1/whep/?app=live&stream=4SEDL9C001X8GE1581F5BMD238V00172JR53-0-0normal-0'
]
};
},
methods: {
startPlay(url, index) {
// console.log(this.$refs[`webrtc-${index}`][0]);
this.$refs[`webrtc-${index}`][0].startPlay(url);
}
},
created() {},
mounted() {}
};
</script>
<style lang="scss" scoped>
.home_left {
left: 20px;
top: 6%;
width: 450px;
height: 90%;
position: absolute;
z-index: 2;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div class="home_right uav_box">
<!-- <el-button @click="chongdian">充电</el-button> -->
<AircraftMonitoringLiveBroadcast></AircraftMonitoringLiveBroadcast>
<AircraftInformationDetails></AircraftInformationDetails>
</div>
</template>
<script>
import AircraftMonitoringLiveBroadcast from './components/AircraftMonitoringLiveBroadcast/index.vue';
import AircraftInformationDetails from './components/AircraftInformationDetails/index.vue';
import { useAirStore } from '@/store/modules/drone';
// import { remoteDebug } from "@/api/air";
export default {
components: {
AircraftMonitoringLiveBroadcast,
AircraftInformationDetails
},
data() {
return {
gateWay: useAirStore().gateWay
};
},
watch: {
// // 监听 Vuex 中的状态变化
// "useAirStore().gateWay": {
// handler(newValue, oldVal) {
// this.gateWay = newValue;
// },
// deep: true,
// },
},
created() {},
mounted() {},
methods: {
// chongdian() {
// let obj = {
// gateway: this.gateWay.gateway,
// method: "charge_open",
// };
// // this.$store.commit("UP_debugValue", val);
// this.droneDebug(obj);
// },
// // 调试模式
// droneDebug(obj, val) {
// remoteDebug(obj).then((res) => {
// const { code, data, msg } = res;
// if (code == 200) {
// this.$message.success("操作成功");
// } else {
// this.$message.error(msg);
// this[val] = !this[val];
// }
// });
// },
}
};
</script>
<style lang="scss">
.home_right {
position: absolute;
right: 20px;
width: 450px;
height: 90%;
z-index: 2;
top: 6%;
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="home-container" ref="monitorLiveVideo">
<homeLeft></homeLeft>
<homeRight></homeRight>
</div>
</template>
<script>
import homeLeft from './homeLeft.vue';
import homeRight from './homeRight.vue';
export default {
components: { homeLeft, homeRight },
data() {
return {};
},
methods: {},
created() {},
mounted() {}
};
</script>
<style lang="scss" scoped>
.home-container {
height: 100%;
width: 100%;
}
.uav_box {
// border-radius: 8px;
// background: rgba(0, 0, 0, 0.5);
// border: 1px solid rgba(0, 255, 255, 0.5);
// padding: 15px 10px;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="medium-container">
<div class="medium-wrapper">
<div class="medium-menu-container">
<div class="medium-title">媒体库</div>
<div class="medium-info">
<div class="medium-info-title">项目空间使用情况</div>
<div v-for="item in mediumInfoList" :key="item.name" class="medium-info-item">
<div class="medium-info-item-left">
<div class="medium-img-box">
<img :src="item.img" alt="" />
</div>
<div class="medium-num-box">
<div>{{ item.name }}</div>
<div>{{ item.count }} 文件</div>
</div>
</div>
<div class="medium-info-item-right">
{{ item.storage }}
</div>
</div>
<div v-for="item in aiInfoList" :key="item.name" class="medium-info-item">
<div class="medium-info-item-left">
<div class="medium-img-box">
<img :src="item.img" alt="" />
</div>
<div class="medium-num-box">
<div>{{ item.name }}</div>
<div>{{ item.count }} 文件</div>
</div>
</div>
<div class="medium-info-item-right">
{{ item.storage }}
</div>
</div>
</div>
<!-- <div class="medium-info medium-storage">
<div class="medium-info-item">
<div class="medium-info-item-left">
<div class="medium-img-box">
<img src="../../images/medium/medium-storage.png" alt="" />
</div>
<div class="medium-num-box">
剩余可用空间
</div>
</div>
<div class="medium-info-item-right">
50 G
</div>
</div>
</div> -->
</div>
<div class="medium-main-container">
<el-tabs class="myTABS" v-model="activeName">
<el-tab-pane label="无人机" name="first">
<MediumHome></MediumHome>
</el-tab-pane>
<el-tab-pane label="AI识别" name="second">
<mediumHomeAi></mediumHomeAi>
</el-tab-pane>
</el-tabs>
</div>
</div>
<Preview></Preview>
</div>
</template>
<script>
import MediumHome from './mediumHome.vue';
import mediumHomeAi from './mediumHomeAi.vue';
import { getInfo } from '@/api/fileMangement';
import { aiInfo } from '@/api/air';
import Preview from './preview.vue';
import { useAirStore } from '@/store/modules/drone';
import mediumImg from '../../images/medium/medium-img.png';
import mediumVideo from '../../images/medium/medium-video.png';
export default {
components: { MediumHome, Preview, mediumHomeAi },
data() {
return {
info: {},
gateway: {},
mediumHomeVisible: false,
previewVisible: false,
previewImage: '',
previewTitle: '',
activeName: 'first',
mediumInfoList: [
{
img: mediumImg,
name: '照片',
count: 0,
storage: '0'
},
{
img: mediumVideo,
name: '视频',
count: 0,
storage: '0'
}
],
aiInfoList: [
{
img: mediumVideo,
name: 'AI',
count: 0,
storage: '0'
}
],
gateWay: useAirStore().gateWay
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getFileInfo();
},
deep: true
}
},
methods: {
handleClick(tab, event) {
console.log(tab, event);
},
// 文件信息概况
async getFileInfo() {
const { gateway } = this.gateWay;
const res = await getInfo({ gateway });
// console.log("文件信息概况", res);
this.mediumInfoList[0].count = res.data.totalImageCount;
this.mediumInfoList[0].storage = res.data.totalImageSize;
this.mediumInfoList[1].count = res.data.totalVideoCount;
this.mediumInfoList[1].storage = res.data.totalVideoSize;
},
// ai信息
async getAiInfo() {
const { gateway } = this.gateWay;
const res = await aiInfo({ gateway });
this.aiInfoList[0].count = res.data.totalImageCount;
this.aiInfoList[0].storage = res.data.totalImageSize;
}
},
created() {},
mounted() {
this.getFileInfo();
this.getAiInfo();
this.$recvChanel('mediumDel', (bool) => {
this.getFileInfo();
this.getAiInfo();
});
}
};
</script>
<style lang="scss">
.medium-container {
// height: 100%;
// width: 100%;
color: #fff;
// background-color: rgb(224, 137, 22);
// position: absolute;
display: flex;
justify-content: center;
align-items: center;
.medium-wrapper {
position: absolute;
top: 15%;
margin: auto;
width: 80%;
height: 75%;
background-color: antiquewhite;
display: flex;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
z-index: 2;
.medium-menu-container {
width: 20%;
height: 100%;
.medium-title {
font-size: 18px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
line-height: 60px;
padding-left: 20px;
}
.medium-info {
padding: 0 20px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
.medium-info-title {
line-height: 60px;
font-size: 16px;
font-weight: 400;
color: rgba(255, 255, 255, 1);
}
.medium-info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.medium-info-item-left {
display: flex;
.medium-img-box {
width: 40px;
height: 40px;
border-radius: 8px;
background: rgba(0, 0, 0, 1);
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin-right: 10px;
img {
width: 24px;
height: 20px;
}
}
.medium-num-box {
font-size: 12px;
display: flex;
flex-direction: column;
justify-content: space-around;
}
}
.medium-info-item-right {
height: 40px;
display: flex;
align-items: center;
font-size: 14px;
}
}
}
.medium-storage {
border-bottom: none;
padding-top: 20px;
img {
width: 20px !important;
height: 20px;
}
}
}
.medium-main-container {
width: 80%;
height: 100%;
background-color: #f6f8fa;
overflow: hidden;
.el-tabs__item {
padding-left: 20px !important;
}
}
}
}
</style>

View File

@ -0,0 +1,623 @@
<template>
<div class="medium-home-container">
<div class="medium-home-main">
<div class="medium-home-main-top">
<div class="medium-breadcrumb">
<div v-for="item in paths" :key="item" class="breadcrumb-item" @click="changePath(item)">
{{ item || '全部文件' }}
</div>
</div>
</div>
<div class="medium-home-top-btn">
<div>
<el-button size="small" @click="handleDelete(false)" type="danger"> 删除 </el-button>
<el-button size="small" @click="handleDownload(false)" type="success"> 下载 </el-button>
</div>
<div class="medium-check">
<div class="check-item">已选/全部</div>
<div class="check-item">{{ selectNum }}/{{ fileList.length }}</div>
</div>
</div>
<div class="medium-home-main-table">
<el-table
ref="multipleTable"
:data="fileList"
tooltip-effect="dark"
:default-sort="{ prop: 'creatTime' }"
stripe
size="small"
height="600"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="fileName" label="文件名称">
<template #default="scope">
<div
class="file-name-wrapper"
style="display: flex; align-items: center; cursor: pointer"
@click="handleSelectFile(scope.row.path, scope.row)"
>
<img :src="scope.row.img" alt="" style="width: 16px; height: 16px; transform: translateY(-1px); margin-right: 2px" />
<div class="file-name">{{ scope.row.name ? scope.row.name : scope.row.fileName }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="80" show-overflow-tooltip> </el-table-column>
<el-table-column prop="creatTime" label="创建时间" sortable="custom">
<template #default="scope">{{ scope.row.creatTime }}</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<div class="opration-btns">
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="下载" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDownload(scope.row)">
<img src="../../images/medium/down-load.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="预览" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handlePreview(scope.row)">
<img src="../../images/medium/prview-icon.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip class="item" content="删除" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDelete(scope.row)">
<img src="../../images/medium/delete.png" srcset="" />
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="shade" v-if="loading">
<el-progress type="circle" :percentage="percentage" :format="percentageFun"></el-progress>
<!-- <div
style="white-space: pre-line;text-align: center;color: aqua;color: aqua; display: flex; flex-direction: column;position: relative;">
<img src="../../images/loadingdow.gif" alt="">
<span style="display: inline-block;position: absolute;left: 50%;top:50%;transform: translate(-50%, -50%);">{{
this.text }}</span>
</div> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
import JSZip from 'jszip';
import { listInfo } from '@/api/air';
import { getFiles, fileDownload, deleteFile, downloadBatch, getUrlList } from '@/api/fileMangement';
import Preview from './preview.vue';
import { useAirStore } from '@/store/modules/drone';
import videoIcon from '../../images/medium/video-icon.png';
import filesIcon from '../../images/medium/files-icon.png';
export default {
components: { Preview },
data() {
return {
percentage: 0,
info: {},
paths: [''],
fileList: [],
totle: 0,
selectNum: 0,
length: 0,
sn: '',
currentPath: '',
searchName: '', // 搜索关键字
dateTime: [new Date(), new Date()], // 初始时间范围为今日至今日
typeOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
typeValue: '',
loadOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
loadValue: '',
selectList: [],
loading: false,
gateWay: useAirStore().gateWay,
text: ''
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getFileList(this.gateWay.gateway);
},
deep: true
}
},
methods: {
percentageFun(percentage) {
let text = '';
if (percentage == 99) {
text = `${percentage}%\n 正在打包中`;
} else {
text = `${percentage}%\n 下载中`;
}
return text;
},
handleSortChange({ column, prop, order }) {
this.fileList.sort((a, b) => {
if (a.excludeSort || b.excludeSort) {
// 如果有数据被标记为排除排序,则直接返回原始顺序
return 0;
}
if (order === 'ascending') {
return a[prop] > b[prop] ? 1 : -1;
} else if (order === 'descending') {
return a[prop] < b[prop] ? 1 : -1;
}
return 0;
});
},
handleSelectionChange(datas) {
// console.log("datas: ", datas);
this.selectList = datas;
this.selectNum = datas.length;
},
// 获取文件列表
async getFileList(gateway, path = '') {
this.fileList = [];
const list = await getFiles({ gateway, itemPath: path });
// console.log("文件列表1-------", list);
this.paths = [
'',
...Object.keys(list)[0]
.split('/')
.filter((path) => path !== '')
.slice(0, -1)
];
this.fileList = Object.keys(list).map((item) => {
const pathList = item.split('/').filter((path) => path !== '');
let img = filesIcon;
let type = 'file';
// 文件缩略图
if (list[item].size !== 'N/A') {
type = item.split('.')[1];
if (item.split('.')[1] !== 'mp4') {
img = list[item].download_url;
} else {
img = videoIcon;
}
}
return {
name: list[item].missionName,
fileName: pathList[pathList.length - 1],
size: list[item].size,
creatTime: list[item].creation_time,
path: item,
img,
type,
url: list[item].download_url,
fileLength: list[item].length,
excludeSort: pathList[pathList.length - 1] == '视频录制' ? true : false
};
});
console.log(this.fileList);
this.totle = this.fileList.length;
this.length = this.fileList.fileLength;
},
// 文件夹点击进入文件夹
handleSelectFile(path, data) {
console.log('path11111111111', path);
if (data && data.size !== 'N/A') {
// console.log("文件------------");
} else {
this.currentPath = path;
const { gateway } = this.gateWay;
this.getFileList(gateway, path);
}
},
// 路径变化
changePath(path) {
if (path) {
// console.log("currentPath", this.currentPath);
this.currentPath = this.currentPath.split(path)[0] + path;
// console.log("path", path);
// 跳转到该路径
this.handleSelectFile(this.currentPath);
} else {
this.handleSelectFile('');
}
},
// 删除
handleDelete(row) {
// console.log("删除---------", row);
let params = { itemPaths: [row.path], gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要删除的文件');
this.loading = false;
return;
}
const pathList = this.selectList.map((item) => item.path);
params = { itemPaths: pathList, gateway: this.gateWay.gateway };
}
this.$confirm('此操作将删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'customMessageBox',
confirmButtonClass: 'customConfirm',
cancelButtonClass: 'customCancel'
}).then(() => {
deleteFile(params).then((res) => {
this.$message.success(res.msg);
this.$sendChanel('mediumDel');
// 过滤掉删除的文件
this.fileList = this.fileList.filter((file) => {
return !params.itemPaths.includes(file.path);
});
if (this.fileList.length === 0) {
// 如果列表为空跳转到根路径
this.handleSelectFile('');
}
});
});
},
// 下载
handleDownload(row) {
this.loading = true;
// console.log("下载---------", row);
let params = { itemPath: row.path, gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要下载的文件');
this.loading = false;
return;
}
let pathList = [];
// 定义是否是文件夹
let isFile = false;
// console.log("this.selectList", this.selectList);
this.selectList.forEach((item) => {
if (item.type === 'file') {
isFile = true;
}
});
if (isFile) {
const { gateway } = this.gateWay;
const promises = this.selectList.map((item) => {
const params = { gateway, itemPath: item.path };
return getUrlList(params).then((res) => {
console.log('res', res);
pathList.push(...res); // 将结果合并到 pathList
});
});
// 等待所有异步操作完成后,再执行下载操作
Promise.all(promises)
.then(() => {
// console.log("pathList1", pathList);
// 确保 pathList 有值后再调用下载方法
console.log('pathList', pathList);
this.downloadAndZipFiles(pathList);
})
.catch((error) => {
console.error('获取文件路径列表时出错:', error);
});
} else {
pathList = this.selectList.map((item) => item.url);
// console.log("pathList2", pathList);
// 并行下载文件并压缩为 zip
this.downloadAndZipFiles(pathList);
}
} else {
this.downloadAndZipFiles([row.url], row);
}
},
async downloadAndZipFiles(fileUrls, file) {
try {
const zip = new JSZip();
const fileSizeMap = new Map();
let fileTotal = 0;
if (file) {
fileTotal = file.fileLength;
} else {
this.selectList.forEach((item) => {
fileTotal += item.fileLength;
});
}
// console.log("file-----", fileTotal);
// 并行下载所有文件
const downloadPromises = fileUrls.map((url) =>
axios({
url: url,
method: 'get',
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
fileSizeMap.set(url, loaded);
const totalFileSize = [...fileSizeMap.values()].reduce((acc, value) => acc + value, 0);
const progress = Math.round((totalFileSize / fileTotal) * 100); // 计算下载进度百分比
// console.log("progressEvent", progressEvent);
// console.log("overallProgress", progress);
this.percentage = progress;
if (progress == 100) {
this.percentage = progress * 0.99;
}
// if (progress == 100) {
// this.text = `${99}%\n 正在打包中`;
// } else {
// this.text = `${progress}%\n 下载中`
// }
}
}).then((response) => {
// console.log("Response:", response); // 在这里打印 response 对象
// console.log("url:", url); // 在这里打印 response 对象
const regex = /\/(DJI_[^\/]+\/DJI_[^\/]+\.[^\/?]+)(?=\?)/;
// 提取匹配的部分
const match = url.match(regex);
let extractedString = 'undefined';
if (match) {
extractedString = match[1]; // 捕获的字符串
}
// 打印文件名和文件数据
const fileName = file ? file.fileName : extractedString; // 获取文件名
const fileData = response.data; // 保存文件数据
// 获取文件大小:使用 Blob 的 size 属性
const fileSize = fileData.size; // 单位为字节bytes
// console.log("File size:", fileSize);
// 返回一个对象,继续后续操作
return { fileName, fileData };
})
);
// 等待所有文件下载完成
const downloadedFiles = await Promise.all(downloadPromises);
if (file) {
this.percentage = 100;
this.selectList = [];
// console.log("Downloading", downloadedFiles);
const url = window.URL.createObjectURL(downloadedFiles[0].fileData);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', downloadedFiles[0].fileName); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
return;
}
// 将每个文件添加到 zip 中
downloadedFiles.forEach(({ fileName, fileData }) => {
// console.log("File Name:", fileName);
zip.file(fileName, fileData); // 添加文件到 zip
});
// 生成 zip 文件并触发下载
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = window.URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
const temp = new Date().getTime();
link.setAttribute('download', `文件${temp}.zip`); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
} catch (error) {
// console.error("Error downloading or zipping files:", error);
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
}
},
// 预览
handlePreview(row) {
const list = this.fileList;
// console.log("预览---------", row);
// console.log("list---------", list);
const index = list.findIndex((item) => item === row);
const data = { list, detail: row, index };
this.$sendChanel('previewBus', data);
}
},
created() {},
mounted() {
this.getFileList(this.gateWay.gateway);
}
};
</script>
<style lang="scss">
.medium-home-container {
height: 100%;
width: 100%;
position: relative;
background-color: antiquewhite;
.medium-home-search-wrapper {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
> .el-date-editor {
width: 38%;
}
> .el-select {
width: 18%;
}
> .el-input {
width: 18%;
}
> .el-button {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
img {
width: 20px;
height: 20px;
}
}
}
.medium-home-main {
height: calc(100%);
background: #fff;
.medium-home-main-top {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 16px;
font-size: 12px;
width: 100%;
.medium-breadcrumb {
display: flex;
.breadcrumb-item {
// margin-right: 20px;
color: #909399;
cursor: pointer;
&::after {
content: '/';
display: inline-block;
margin: 0 3px;
}
}
.breadcrumb-item:last-child {
color: #000;
&::after {
content: '';
display: inline-block;
}
}
}
.file-name-wrapper {
display: flex;
.file-img-wrapper {
height: 20px !important;
width: 20px !important;
img {
width: 100%;
height: 100%;
}
}
}
.el-table {
.el-checkbox {
margin-left: 8px !important;
}
}
}
.medium-home-top-btn {
width: 100%;
height: 40px;
padding: 0 16px;
display: flex;
align-items: center;
font-size: 12px;
justify-content: space-between;
.medium-check {
display: flex;
align-items: center;
color: #000;
}
}
.medium-home-main-table {
overflow-y: auto;
height: calc(100% - 80px);
width: 100%;
}
.el-checkbox__input {
margin-left: 8px !important;
}
}
.shade {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.el-progress__text {
color: aqua !important;
white-space: pre-line;
line-height: 20px;
}
}
.el-picker-panel {
background: rgba(255, 255, 255, 1) !important;
color: #606266 !important;
filter: drop-shadow(2px 4px 6px black);
}
.opration-btns {
display: flex;
justify-content: start;
align-items: center;
// background-color: #606266;
.opration-btn {
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
cursor: pointer;
img {
width: 12px;
height: 12px;
}
&:hover {
// background: #e6e6e6;
}
}
}
}
</style>

View File

@ -0,0 +1,611 @@
<template>
<div class="medium-home-container">
<div class="medium-home-main">
<div class="medium-home-main-top">
<div class="medium-breadcrumb">
<div v-for="item in paths" :key="item" class="breadcrumb-item" @click="changePath(item)">
{{ item || '全部文件' }}
</div>
</div>
</div>
<div class="medium-home-top-btn">
<div>
<el-button size="small" @click="handleDelete(false)" type="danger"> 删除 </el-button>
<el-button size="small" @click="handleDownload(false)" type="success"> 下载 </el-button>
</div>
<div class="medium-check">
<div class="check-item">已选/全部</div>
<div class="check-item">{{ selectNum }}/{{ totle }}</div>
</div>
</div>
<div class="medium-home-main-table">
<el-table
ref="multipleTable"
:data="fileList"
tooltip-effect="dark"
stripe
size="small"
height="600"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="fileName" label="文件名称">
<template #default="scope">
<div
class="file-name-wrapper"
style="display: flex; align-items: center; cursor: pointer"
@click="handleSelectFile(scope.row.path, scope.row)"
>
<img :src="scope.row.img" alt="" style="width: 16px; height: 16px; transform: translateY(-1px); margin-right: 2px" />
<div class="file-name">{{ scope.row.name ? scope.row.name : scope.row.fileName }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="size" label="大小" width="80" show-overflow-tooltip> </el-table-column>
<el-table-column prop="creatTime" label="创建时间">
<template #default="scope">{{ scope.row.creatTime }}</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<div class="opration-btns">
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="下载" effect="dark">
<!-- v-if="scope.row.size !== 'N/A'" -->
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDownload(scope.row)">
<img src="../../images/medium/down-load.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip v-if="scope.row.size !== 'N/A'" class="item" content="预览" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handlePreview(scope.row)">
<img src="../../images/medium/prview-icon.png" srcset="" />
</span>
</el-tooltip>
<el-tooltip class="item" content="删除" effect="dark">
<span class="opration-btn" style="display: inline-block; cursor: pointer" @click="handleDelete(scope.row)">
<img src="../../images/medium/delete.png" srcset="" />
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="shade" v-if="loading">
<el-progress type="circle" :percentage="percentage"></el-progress>
<!-- <div
style="white-space: pre-line;text-align: center;color: aqua;color: aqua; display: flex; flex-direction: column;">
<img src="../../images/loadingdow.gif" alt="">
<span style="display: inline-block;position: absolute;left: 50%;top:50%;transform: translate(-50%, -50%);">{{
this.text }}</span>
</div> -->
</div>
</div>
</template>
<script>
import axios from 'axios';
import JSZip from 'jszip';
import { getAiFiles, deleteAiFile, getAiUrlList } from '@/api/fileMangement';
import Preview from './preview.vue';
import { useAirStore } from '@/store/modules/drone';
import filesIcon from '../../images/medium/files-icon.png';
import videoIcon from '../../images/medium/video-icon.png';
export default {
components: { Preview },
data() {
return {
percentage: 0,
info: {},
paths: [''],
fileList: [
// {
// fileName: item,
// size: list[item].size,
// creatTime: list[item].creation_time,
// path:''
// img: ''
// },
],
totle: 0,
selectNum: 0,
length: 0,
sn: '',
currentPath: '',
searchName: '', // 搜索关键字
dateTime: [new Date(), new Date()], // 初始时间范围为今日至今日
typeOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
typeValue: '',
loadOptions: [
{
value: '选项1',
label: '选项1'
},
{
value: '选项2',
label: '选项2'
}
],
loadValue: '',
selectList: [],
loading: false,
gateWay: useAirStore().gateWay,
text: ''
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateWay = newValue;
this.getFileList(this.gateWay.gateway);
},
deep: true
}
},
methods: {
handleSelectionChange(datas) {
// console.log("datas: ", datas);
this.selectList = datas;
this.selectNum = datas.length;
},
isEmptyObject(obj) {
return Object.keys(obj).length === 0;
},
// 获取文件列表
async getFileList(gateway, path = '') {
this.fileList = [];
const list = await getAiFiles({ gateway, itemPath: path });
// console.log("文件列表1-------", list);
if (Object.keys(list).length === 0) {
return;
}
this.paths = [
'',
...Object.keys(list)[0]
.split('/')
.filter((path) => path !== '')
.slice(0, -1)
];
this.fileList = Object.keys(list).map((item) => {
const pathList = item.split('/').filter((path) => path !== '');
let img = filesIcon;
let type = 'file';
// console.log("item----------", item);
// console.log("list----------", list[item]);
// 文件缩略图
if (list[item].size !== 'N/A') {
type = item.split('.')[1];
if (item.split('.')[1] !== 'mp4') {
img = list[item].download_url;
} else {
img = videoIcon;
}
}
return {
name: list[item].missionName,
fileName: pathList[pathList.length - 1],
size: list[item].size,
creatTime: list[item].creation_time,
path: item,
img,
type,
url: list[item].download_url,
fileLength: list[item].length
};
});
this.totle = this.fileList.length;
this.length = this.fileList.fileLength;
// console.log("文件列表2-------", this.fileList);
},
// 文件夹点击进入文件夹
handleSelectFile(path, data) {
console.log('path11111111111', path);
if (data && data.size !== 'N/A') {
// console.log("文件------------");
} else {
this.currentPath = path;
const { gateway } = this.gateWay;
this.getFileList(gateway, path);
}
},
// 路径变化
changePath(path) {
if (path) {
// console.log("currentPath", this.currentPath);
this.currentPath = this.currentPath.split(path)[0] + path;
// console.log("path", path);
// 跳转到该路径
this.handleSelectFile(this.currentPath);
} else {
this.handleSelectFile('');
}
},
// 删除
handleDelete(row) {
// console.log("删除---------", row);
let params = { itemPaths: [row.path], gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要删除的文件');
this.loading = false;
return;
}
const pathList = this.selectList.map((item) => item.path);
params = { itemPaths: pathList, gateway: this.gateWay.gateway };
}
this.$confirm('此操作将删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
customClass: 'customMessageBox',
confirmButtonClass: 'customConfirm',
cancelButtonClass: 'customCancel'
}).then(() => {
deleteAiFile(params).then((res) => {
this.$message.success(res.msg);
this.$sendChanel('mediumDel');
// 过滤掉删除的文件
this.fileList = this.fileList.filter((file) => {
return !params.itemPaths.includes(file.path);
});
if (this.fileList.length === 0) {
// 如果列表为空跳转到根路径
this.handleSelectFile('');
}
});
});
},
// 下载
handleDownload(row) {
this.loading = true;
// console.log("下载---------", row);
let params = { itemPath: row.path, gateway: this.gateWay.gateway };
if (!row) {
if (this.selectList.length === 0) {
this.$message.warning('请先选择要下载的文件');
this.loading = false;
return;
}
let pathList = [];
// 定义是否是文件夹
let isFile = false;
// console.log("this.selectList", this.selectList);
this.selectList.forEach((item) => {
if (item.type === 'file') {
isFile = true;
}
});
if (isFile) {
const { gateway } = this.gateWay;
// this.selectList.forEach((item) => {
// const params = { gateway, itemPath: item.path };
// getUrlList(params).then((res) => {
// pathList.push(...res);
// });
// });
// console.log("pathList1", pathList);
// // 并行下载文件并压缩为 zip
// this.downloadAndZipFiles(pathList);
const promises = this.selectList.map((item) => {
const params = { gateway, itemPath: item.path };
return getAiUrlList(params).then((res) => {
pathList.push(...res); // 将结果合并到 pathList
});
});
// 等待所有异步操作完成后,再执行下载操作
Promise.all(promises)
.then(() => {
// console.log("pathList1", pathList);
// 确保 pathList 有值后再调用下载方法
this.downloadAndZipFiles(pathList);
})
.catch((error) => {
console.error('获取文件路径列表时出错:', error);
});
} else {
pathList = this.selectList.map((item) => item.url);
// console.log("pathList2", pathList);
// 并行下载文件并压缩为 zip
this.downloadAndZipFiles(pathList);
}
} else {
this.downloadAndZipFiles([row.url], row);
}
},
async downloadAndZipFiles(fileUrls, file) {
try {
const zip = new JSZip();
const fileSizeMap = new Map();
let fileTotal = 0;
if (file) {
fileTotal = file.fileLength;
} else {
this.selectList.forEach((item) => {
fileTotal += item.fileLength;
});
}
// console.log("file-----", fileTotal);l
// 并行下载所有文件
const downloadPromises = fileUrls.map((url) =>
axios({
url: url,
method: 'get',
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
fileSizeMap.set(url, loaded);
const totalFileSize = [...fileSizeMap.values()].reduce((acc, value) => acc + value, 0);
const progress = Math.round((totalFileSize / fileTotal) * 100); // 计算下载进度百分比
// console.log("progressEvent", progressEvent);
// console.log("overallProgress", progress);
// this.percentage = progress; // 更新进度百分比
if (progress == 100) {
this.text = `${99}%\n 正在打包中`;
} else {
this.text = `${progress}%\n 下载中`;
}
}
}).then((response) => {
console.log('Response:', response); // 在这里打印 response 对象
console.log('url:', url); // 在这里打印 response 对象
console.log('flie:', file); // 在这里打印 response 对象
const regex = /\/(DJI_[^\/]+\/DJI_[^\/]+\.[^\/?]+)(?=\?)/;
// 提取匹配的部分
const match = url.match(regex);
let extractedString = response.data.size + '.jpg';
if (match) {
extractedString = match[1]; // 捕获的字符串
}
// 打印文件名和文件数据
const fileName = file ? file.fileName : extractedString; // 获取文件名
const fileData = response.data; // 保存文件数据
// 获取文件大小:使用 Blob 的 size 属性
const fileSize = fileData.size; // 单位为字节bytes
// console.log("File size:", fileSize);
// 返回一个对象,继续后续操作
return { fileName, fileData };
})
);
// 等待所有文件下载完成
const downloadedFiles = await Promise.all(downloadPromises);
if (file) {
// console.log("Downloading", downloadedFiles);
const url = window.URL.createObjectURL(downloadedFiles[0].fileData);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', downloadedFiles[0].fileName); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
return;
}
// 将每个文件添加到 zip 中
downloadedFiles.forEach(({ fileName, fileData }) => {
// console.log("File Name:", fileName);
zip.file(fileName, fileData); // 添加文件到 zip
});
// 生成 zip 文件并触发下载
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = window.URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
const temp = new Date().getTime();
link.setAttribute('download', `文件${temp}.zip`); // 设置下载的文件名
document.body.appendChild(link);
link.click();
link.remove();
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
} catch (error) {
// console.error("Error downloading or zipping files:", error);
this.loading = false;
this.percentage = 0; // 下载完进度百分比清零
}
},
// 预览
handlePreview(row) {
const list = this.fileList;
// console.log("预览---------", row);
// console.log("list---------", list);
const index = list.findIndex((item) => item === row);
const data = { list, detail: row, index };
this.$sendChanel('previewBus', data);
}
},
created() {},
mounted() {
this.getFileList(this.gateWay.gateway);
}
};
</script>
<style lang="scss">
.medium-home-container {
height: 100%;
width: 100%;
position: relative;
background-color: antiquewhite;
.medium-home-search-wrapper {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
> .el-date-editor {
width: 38%;
}
> .el-select {
width: 18%;
}
> .el-input {
width: 18%;
}
> .el-button {
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
img {
width: 20px;
height: 20px;
}
}
}
.medium-home-main {
height: calc(100%);
background: #fff;
.medium-home-main-top {
display: flex;
justify-content: space-between;
line-height: 40px;
padding: 0 16px;
font-size: 12px;
width: 100%;
.medium-breadcrumb {
display: flex;
.breadcrumb-item {
// margin-right: 20px;
color: #909399;
cursor: pointer;
&::after {
content: '/';
display: inline-block;
margin: 0 3px;
}
}
.breadcrumb-item:last-child {
color: #000;
&::after {
content: '';
display: inline-block;
}
}
}
.file-name-wrapper {
display: flex;
.file-img-wrapper {
height: 20px !important;
width: 20px !important;
img {
width: 100%;
height: 100%;
}
}
}
.el-table {
.el-checkbox {
margin-left: 8px !important;
}
}
}
.medium-home-top-btn {
width: 100%;
height: 40px;
padding: 0 16px;
display: flex;
align-items: center;
font-size: 12px;
justify-content: space-between;
.medium-check {
display: flex;
align-items: center;
color: #000;
}
}
.medium-home-main-table {
overflow-y: auto;
height: calc(100% - 80px);
width: 100%;
}
.el-checkbox__input {
margin-left: 8px !important;
}
}
.shade {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
.el-progress__text {
color: aqua !important;
white-space: pre-line;
line-height: 20px;
}
}
.el-picker-panel {
background: rgba(255, 255, 255, 1) !important;
color: #606266 !important;
filter: drop-shadow(2px 4px 6px black);
}
.opration-btns {
display: flex;
justify-content: start;
align-items: center;
// background-color: #606266;
.opration-btn {
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
cursor: pointer;
img {
width: 12px;
height: 12px;
}
&:hover {
// background: #e6e6e6;
}
}
}
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<div v-if="show" class="preview-container">
<div class="preview-top">
<div class="preview-t-l">
<div class="preview-item-wrapper">
<img v-if="detail.type !== 'mp4'" :src="detail.url" alt="" class="preview-item" />
<video v-else :src="detail.url" controls crossorigin="anonymous" class="preview-item"></video>
<div class="arrows arrow-l" @click="check(current - 1)">
<img src="../../images/medium/arrow.png" />
</div>
<div class="arrows arrow-r" @click="check(current + 1)">
<img src="../../images/medium/arrow.png" />
</div>
</div>
</div>
<div class="preview-t-r">
<div class="preview-close" @click="show = false">
<img src="../../images/medium/close.png" />
</div>
<div class="preview-t-infos">
<div class="preview-t-info">
<span class="preview-t-info-text label">文件名</span>
<span class="preview-t-info-text">{{ detail.fileName }}</span>
</div>
<div class="preview-t-info">
<span class="preview-t-info-text label">文件大小</span>
<span class="preview-t-info-text">{{ detail.size }}</span>
</div>
<div class="preview-t-info">
<span class="preview-t-info-text label">创建时间</span>
<span class="preview-t-info-text">{{ detail.creatTime }}</span>
</div>
</div>
</div>
</div>
<div class="preview-bottom">
<div class="preview-list-wrapper scroll-container">
<div v-for="(item, index) in list" :key="item.url" class="preview-list-item" :class="{ active: current === index }" @click="check(index)">
<img :src="item.img" alt="" srcset="" />
</div>
</div>
</div>
</div>
</template>
<script>
import mediumVideo from '../../images/medium/medium-video.png';
import mediumImg from '../../images/medium/medium-img.png';
export default {
components: {},
data() {
return {
show: false,
list: [
{
fileName: '测试1',
size: '3M',
creatTime: '2024',
path: 'ua/',
img: mediumVideo,
type: 'mp4',
url: 'https://vdept3.bdstatic.com/mda-qh0g1z9fvz28wwe9/cae_h264/1722574521989748719/mda-qh0g1z9fvz28wwe9.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1726726486-0-0-bdef95510c480bcaa1f3c311508a59de&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=3&logid=0886315216&vid=10817113278196649776&klogid=0886315216&abtest='
},
{
fileName: '测试2',
size: '3M',
creatTime: '2024',
path: 'ua/',
img: mediumImg,
url: '//lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg',
type: 'img'
}
],
detail: {
fileName: '测试1',
size: '3M',
creatTime: '2024',
path: 'ua/',
img: mediumVideo,
type: 'mp4',
url: 'https://vdept3.bdstatic.com/mda-qh0g1z9fvz28wwe9/cae_h264/1722574521989748719/mda-qh0g1z9fvz28wwe9.mp4?v_from_s=hkapp-haokan-hbf&auth_key=1726726486-0-0-bdef95510c480bcaa1f3c311508a59de&bcevod_channel=searchbox_feed&pd=1&cr=0&cd=0&pt=3&logid=0886315216&vid=10817113278196649776&klogid=0886315216&abtest='
},
current: 0
};
},
methods: {
check(idx) {
// const index = this.list.findIndex((item) => item.url === this.detail.url);
// console.log("index", index);
// console.log("list", this.list);
if (idx < 0) {
this.detail = this.list[this.list.length - 1];
this.current = this.list.length - 1;
} else if (idx > this.list.length - 1) {
this.detail = this.list[0];
this.current = 0;
} else {
this.detail = this.list[idx];
this.current = idx;
}
}
},
created() {
this.$recvChanel('previewBus', (data) => {
// console.log("收到的数据222", data);
this.list = data.list;
this.detail = data.detail;
this.current = data.index;
this.show = true;
});
},
mounted() {
// this.detail = this.list[0];
// const container = document.querySelector(".scroll-container");
// container.addEventListener("wheel", (event) => {
// event.preventDefault(); // 阻止默认纵向滚动
// // 横向滚动父容器中的内容
// container.scrollLeft += event.deltaY;
// });
}
};
</script>
<style lang="scss">
.preview-container {
height: 100vh;
width: 100vw;
position: fixed;
// top: -20%;
// left: -12%;
top: 0;
left: 0;
z-index: 111;
background-color: rgba($color: #000000, $alpha: 0.5);
.preview-top {
width: 100%;
height: 85%;
display: flex;
background-color: rgba(240, 248, 255, 0.281);
.preview-t-l {
width: 80%;
height: 100%;
background-color: #000000;
.preview-item-wrapper {
width: 100%;
height: 100%;
position: relative;
.arrows {
position: absolute;
top: 50%;
width: 50px;
height: 50px;
background-color: rgba(0, 0, 0, 0.4);
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
img {
width: 30px;
height: 30px;
}
}
.arrow-l {
left: 40px;
}
.arrow-r {
right: 40px;
transform: rotate(180deg);
}
.preview-item {
width: 100%;
height: 100%;
}
}
}
.preview-t-r {
width: 20%;
height: 100%;
background-color: #1c1c1c;
position: relative;
.preview-close {
position: absolute;
top: 10px;
right: 10px;
> img {
height: 20px;
width: 20px;
cursor: pointer;
}
}
.preview-t-infos {
height: 100%;
width: 100%;
padding: 15% 0 0 5%;
font-size: 12px;
.preview-t-info {
width: 100%;
display: flex;
// &:nth-child(1) {
// width: 25%;
// text-align: justify;
// background-color: aqua;
// }
.preview-t-info-text {
margin-right: 15px;
margin-bottom: 15px;
}
.label {
width: 18%;
text-align: justify;
}
}
}
}
}
.preview-bottom {
width: 100%;
height: 15%;
background-color: #000000;
display: flex;
align-items: center;
justify-content: center;
margin: auto;
.preview-list-wrapper {
height: 80px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow-x: auto;
.preview-list-item {
display: inline-block;
height: 60px;
width: 100px;
border: 2px solid rgb(255, 255, 255);
margin: 0 5px;
cursor: pointer;
img {
height: 100%;
width: 100%;
}
}
.active {
border: 2px solid rgb(0, 140, 255);
}
}
}
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<div class="flyHistroy">
<span class="flyHistroy_name" @click="back">返回</span>
<span class="flyHistroy_name">{{ data.missionName }}</span>
<div class="flyHistroy_bottom">
<div class="bottom_item">
<div class="title">飞行器</div>
<div class="content">M3TD-庆福广场</div>
</div>
<div class="bottom_item">
<div class="title">任务开始时间</div>
<div class="content">{{ data.createdAt }}</div>
</div>
<div class="bottom_item">
<div class="title">任务结束时间</div>
<div class="content">
{{ parseTime(data.lastTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</div>
</div>
<div class="bottom_item">
<div class="title">轨迹最后更新时间</div>
<div class="content">
{{ parseTime(data.lastTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</div>
</div>
<div class="bottom_item">
<div class="title">实际飞行距离</div>
<div class="content">{{ length }}m</div>
</div>
<div class="bottom_item">
<div class="title">实际飞行时长</div>
<div class="content">{{ time }}</div>
</div>
</div>
</div>
</template>
<script>
import { parseTime } from '@/utils/index.ts';
export default {
name: 'flyHistroy',
props: {
data: {
type: Object,
default: () => {}
}
},
data() {
return {
points: [],
length: '',
time: ''
};
},
// 监听data
// watch: {
// data: function handle(newVal, oldVal) {
// let pointss = JSON.parse(newVal);
// this.renderRouter(pointss.points);
// }
// },
mounted() {
let pointss = JSON.parse(this.data.points);
this.renderRouter(pointss.points);
},
methods: {
parseTime(val) {
return parseTime(val);
},
renderRouter(positions) {
let airLine = new YJ.Obj.newAirLine(
{
positions,
frustumShow: false,
keyboard: false
},
window.Earth1.viewer
);
window.airLine = airLine;
this.length = airLine.countLength();
this.time = airLine.countTime();
window.airLine.flyTo();
// console.log("airLine", airLine);
},
// 返回计划库
back() {
this.$emit('back');
}
}
};
</script>
<style lang="scss">
.flyHistroy {
position: absolute;
top: 8%;
left: 20px;
z-index: 20;
color: #fff;
&_name {
border-radius: 8px;
padding: 8px 16px;
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.6);
backdrop-filter: blur(2px);
}
&_bottom {
position: fixed;
bottom: 10%;
left: 30px;
height: 80px;
width: 60%;
display: flex;
justify-content: space-between;
.title {
font-size: 16px;
margin-bottom: 10px;
font-weight: 400;
}
.content {
font-size: 18px;
color: rgba(0, 255, 255, 1);
font-weight: 600;
}
}
}
</style>

View File

@ -0,0 +1,837 @@
<template>
<div class="planLank flex">
<div class="planLank_left">
<div class="header flex space_between ai_center">
<span class="text">计划库</span>
<div class="add" @click="add">
<img src="../../images/add_plan.png" alt="" />
<span class="f16">新建</span>
</div>
</div>
<div class="content">
<div class="title">
<span>{{ gateway.deviceType }}</span>
<span>{{ gateway.businessName }}</span>
</div>
<div class="card">
<div class="card_top flex">
<div class="card_top_left flex ai_center">
<div>
<img src="../../images/time.png" alt="" />
<span>待执行</span>
</div>
<div>
{{ listObj.timer ? listObj.timer : '' }}
</div>
</div>
<div class="card_top_right flex">
<span>{{ listObj.missionName ? listObj.missionName : '暂无' }}</span>
</div>
</div>
<div class="card_bottom flex">
<!-- <div class="card_bottom_top">
<span>{{ gateway.deviceType }}</span>
<span>{{ gateway.businessName }}</span>
</div> -->
<div class="card_bottom_bottom">
<div class="flex ai_center mr10 mb10">
<img src="../../images/eq.png" alt="" />
<span class="status">设备{{ filterModeCode(mode.flighttask_step_code) }}</span>
</div>
<div class="flex ai_center mb10">
<img src="../../images/air.png" alt="" />
<span
>{{ filterDroneInDock(networkState.drone_in_dock) }}
{{ filterDeviceOnlineStatus(networkState.sub_device.device_online_status) }}</span
>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="planLank_right">
<div class="header_form">
<el-form :inline="true" size="small" class="demo-form-inline">
<el-form-item>
<el-date-picker
v-model="time"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="changeTime"
>
</el-date-picker>
</el-form-item>
<el-form-item>
<el-select v-model="tableForm.type" @change="getQuestRecordList" placeholder="所有类型">
<el-option v-for="item in options" :label="item.label" :value="item.value"></el-option>
</el-select>
<template #label></template>
</el-form-item>
<el-form-item>
<el-select v-model="tableForm.state" @change="getQuestRecordList" placeholder="全部状态">
<el-option v-for="item in states" :label="item" :value="item"></el-option>
</el-select>
<template #label></template>
</el-form-item>
<el-form-item>
<el-input v-model.trim="tableForm.name" @change="getQuestRecordList" clearable placeholder="按航线或者计划名称搜索">
<i style="cursor: pointer" slot="suffix" class="el-input__icon el-icon-search" @click="search"></i>
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="refresh" icon="el-icon-refresh-right"></el-button>
</el-form-item>
</el-form>
</div>
<div class="table">
<el-table :data="tableData.data" style="width: 100%">
<el-table-column prop="createdAt" label="创建时间" width="180" align="center"> </el-table-column>
<el-table-column prop="state" label="执行状态" width="180" align="center"> </el-table-column>
<el-table-column prop="type" label="类型" align="center">
<template slot-scope="scope">
<span>{{ scope.row.type == 0 ? '立即' : '定时' }}</span>
</template>
</el-table-column>
<el-table-column prop="missionName" label="计划名称" align="center"> </el-table-column>
<el-table-column prop="fileName" label="航线名称" align="center"> </el-table-column>
<!-- <el-table-column prop="realPhotoNum" label="实际上传" align="center">
</el-table-column> -->
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<div>
<el-tooltip v-if="['执行中', '暂停'].includes(scope.row.state)" class="item" content="航线暂停" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(1, scope.row)">
<img src="../../images/zanting.png" alt="" />
</span>
</el-tooltip>
<el-tooltip v-if="['执行中', '暂停'].includes(scope.row.state)" class="item" content="航线恢复" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(2, scope.row)">
<img src="../../images/huifu.png" alt="" />
</span>
</el-tooltip>
<el-tooltip v-if="['执行中', '暂停'].includes(scope.row.state)" class="item" content="一键返航" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(3, scope.row)">
<img src="../../images/fanhang.png" alt="" />
</span>
</el-tooltip>
<el-tooltip v-if="['执行成功'].includes(scope.row.state)" class="item" content="飞行记录" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(4, scope.row)">
<img src="../../images/flyh.png" alt="" />
</span>
</el-tooltip>
<el-tooltip class="item" content="删除" effect="dark">
<span style="display: inline-block; cursor: pointer" @click="handlerClick(5, scope.row)">
<img src="../../images/shanchu.png" alt="" />
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="progress">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="tableForm.pageNum"
:page-sizes="[10, 20, 30, 40]"
:page-size="tableForm.pageSize"
layout="prev, pager, next,sizes"
:total="tableData.total"
>
</el-pagination>
</div>
</div>
<myDialog ref="createPL" class="createPL" :title="title">
<div class="form">
<el-form :model="form" :rules="rules" ref="ruleForm" label-width="110px" class="demo-ruleForm">
<el-form-item label="计划名称" prop="missionName">
<el-input v-model="form.missionName" placeholder="请输入计划名称" clearable></el-input>
</el-form-item>
<el-form-item label="执行航线" prop="fileId">
<el-select style="width: 100%" v-model="form.fileId" filterable placeholder="请选择活动区域">
<el-option v-for="item in airList" :label="item.fileName" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="任务精度" prop="waylinePrecisionType">
<el-radio-group style="width: 100%" v-model="form.waylinePrecisionType">
<el-radio-button v-for="dict in waylinePrecisionTypeOptions" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="任务策略" prop="type">
<el-radio-group style="width: 100%" v-model="form.type">
<el-radio-button :label="0">立即</el-radio-button>
<el-radio-button :label="1">单次定时</el-radio-button>
<!-- <el-radio-button :label="2">重复定时</el-radio-button> -->
</el-radio-group>
</el-form-item>
<el-form-item label="返航高度模式" prop="returnAltitudeMode">
<el-radio-group style="width: 50%" v-model="form.returnAltitudeMode">
<el-radio-button v-for="dict in rthModeOptions" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.type == 1" label="执行时间" prop="timer">
<el-date-picker value-format="yyyy-MM-dd HH:mm:ss" v-model="form.timer" type="datetime" placeholder="选择日期时间"> </el-date-picker>
</el-form-item>
<el-form-item v-if="form.type == 2" label="执行时间" prop="execTime">
<el-date-picker v-model="form.execTime" type="datetime" placeholder="选择日期时间"> </el-date-picker>
</el-form-item>
<el-form-item label="航线飞行中失联" prop="outOfControlAction">
<el-radio-group style="width: 100%" v-model="form.outOfControlAction">
<el-radio-button v-for="dict in exitWaylineOptions" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
<div>
<div>返航高度(相对机场返航高度)</div>
<div style="display: flex; justify-content: center">
<plusReduce :defValue="form.returnAltitude" @value="height" style="width: 80%" :max="1500" :min="20" unit="m"> </plusReduce>
</div>
</div>
<el-form-item label="失联动作" prop="flightControlAction">
<!-- 失控动作类型 -->
<el-radio-group style="width: 100%" v-model="form.flightControlAction">
<el-radio-button v-for="dict in outOfControlOptions" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<div class="btns">
<el-button size="small" @click="submit">确定</el-button>
<el-button size="small" @click="cancel">取消</el-button>
</div>
</myDialog>
</div>
</template>
<script>
import myDialog from '../component/dialog/index.vue';
import plusReduce from '../component/plusReduce/index.vue';
import {
listPaths,
taskList,
taskAdd,
getLatest,
flightTaskPauseNew,
flightTaskRecoveryNew,
flightTaskUndoNew,
delMissionsNew,
returnHomeNew
} from '@/api/air';
import { getLocal, parseTime } from '@/utils/';
import { exitWaylineOptions, waylinePrecisionTypeOptions, outOfControlOptions, rthModeOptions } from '../../utils/options.js';
import { useAirStore } from '@/store/modules/drone';
export default {
name: 'planLank',
components: {
myDialog,
plusReduce
},
data() {
return {
parseTime: parseTime,
rthModeOptions: rthModeOptions,
outOfControlOptions: outOfControlOptions,
exitWaylineOptions: exitWaylineOptions,
waylinePrecisionTypeOptions: waylinePrecisionTypeOptions,
rules: {
missionName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
fileId: [{ required: true, message: '请选择任务航线', trigger: 'blur' }],
timer: [{ required: true, message: '请选择时间', trigger: 'blur' }]
},
formInline: {
region: '',
value: '',
user: ''
},
options: [
{
label: '全部',
value: ''
},
{
label: '立即',
value: 0
},
{
label: '定时',
value: 1
}
],
time: [],
states: ['全部', '取消或终止', '失败', '执行中', '定时待执行', '执行成功', '部分完成', '暂停', '拒绝', '已下发', '超时'],
form: {
missionName: '',
// executionTime: "",
timer: null,
type: 0,
fileId: '',
returnAltitude: 100,
returnAltitudeMode: 0,
outOfControlAction: 1,
flightControlAction: 0,
waylinePrecisionType: 1,
gateway: '',
timeStamp: ''
},
value1: '',
value2: '',
title: '新建计划',
queryParam: {
pageNum: 1,
pageSize: 10000000,
fileName: '',
order: 1,
gateway: ''
},
tableForm: {
pageNum: 1,
pageSize: 20,
gateway: ''
},
airList: [],
networkState: {
mode_code: 0,
drone_in_dock: 1,
sub_device: {
device_online_status: 0
}
},
mode: {},
gateway: useAirStore().gateWay,
tableData: {
data: [],
total: 0
},
time: [],
listObj: {}
};
},
watch: {
// 监听 Vuex 中的状态变化
'useAirStore().gateWay': {
handler(newValue, oldVal) {
this.gateway = newValue;
console.log('newValue', newValue);
this.getQuestRecordList();
this.getNewestTimer();
},
deep: true
}
},
mounted() {
this.allApi();
this.$recvChanel('websocketBus', (data) => {
if (data.businessType == 'osd3') {
this.networkState = { ...data.data };
// console.log(this.networkState);
}
if (data.businessType == 'osd2') {
this.mode = { ...data.data };
}
});
this.form.gateway = this.gateway.gateway;
this.queryParam.flag = this.gateway.gateway;
},
methods: {
allApi() {
this.getAirRouteList();
this.getQuestRecordList();
this.getNewestTimer();
},
search() {
if (this.time.length > 0) {
this.tableForm.startTime = this.time[0];
this.tableForm.endTime = this.time[1];
}
this.getQuestRecordList();
},
changeTime(value) {
this.tableForm.startTime = value[0];
this.tableForm.endTime = value[1];
this.getQuestRecordList();
},
handlerClick(num, item) {
console.log(num, item);
if (num == 1) {
this.pauseAirLine(item);
}
if (num == 2) {
this.recoverAirLine(item);
}
if (num == 3) {
this.onBackHome(item);
}
if (num == 4) {
this.$emit('openHistroy', item);
}
if (num == 5) {
this.onDelQuest(item);
}
},
// delMissions
// 航线暂停
pauseAirLine(row) {
let obj = { gateway: this.gateway.gateway, taskId: row.id };
flightTaskPauseNew(obj).then((res) => {
this.$message.success('暂停成功');
this.getQuestRecordList();
});
},
// 航线恢复
recoverAirLine(row) {
let obj = { gateway: this.gateway.gateway, taskId: row.id };
flightTaskRecoveryNew(obj).then((res) => {
this.$message.success('恢复成功');
this.getQuestRecordList();
});
},
// 一键航航
onBackHome(row) {
let obj = { gateway: this.gateway.gateway, taskId: row.id };
returnHomeNew(obj).then((res) => {
this.$message.success('返航成功');
this.getQuestRecordList();
});
},
// 删除 delMissions
onDelQuest(row) {
this.$confirm('确认删除该任务信息?', '温馨提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delMissionsNew(row.id).then((res) => {
if (res.code == 200) {
this.$message.success('删除成功');
this.getQuestRecordList();
this.getNewestTimer();
}
});
});
},
refresh() {
this.tableForm = {
pageNum: 1,
pageSize: 10,
gateway: this.gateway.gateway
};
this.getQuestRecordList();
},
// 获取任务历史记录数据
getQuestRecordList(flag = false) {
this.loading = true;
this.tableForm.gateway = this.gateway.gateway;
taskList(this.tableForm).then((res) => {
this.loading = false;
if (res.code == 200) {
console.log(res.rows);
let list = res.rows || [];
this.tableData.data = list;
this.tableData.total = res.total;
if (flag) {
this.$sendChanel('TaskIssuedSuccessfully', list[0].flightId);
}
}
});
},
// 最新的定时任务
getNewestTimer() {
let obj = { gateway: this.gateway.gateway };
getLatest(obj).then((res) => {
if (res.code == 200) {
this.listObj = res.data || {};
}
});
},
getAirRouteList() {
listPaths(this.queryParam).then((res) => {
let list = res.rows || [];
this.airList = list;
});
},
filterDeviceOnlineStatus(val) {
let obj = { '0': '关机', '1': '开机' };
return obj[val];
},
filterDroneInDock(val) {
let obj = { '0': '舱外', '1': '舱内' };
return obj[val];
},
filterModeCode(val) {
let obj = {
'0': '作业准备中',
'1': '飞行作业中',
'2': '作业后状态恢复',
'3': '自定义飞行区更新中',
'4': '地形障碍物更新中',
'5': '任务空闲',
'255': '飞行器异常',
'256': '未知状态'
};
return obj[val];
},
height(value) {
this.form.height = value;
},
add() {
this.$refs.createPL.open();
},
submit() {
// console.log("this.form", this.form);
this.form.timeStamp = Date.now();
const { mode_code } = this.networkState;
if (mode_code == 2) {
this.$message.warning('请退出调试模式');
return;
}
// 判断returnAltitude是否有值
if (this.form.returnAltitude == '') {
return this.$message.warning('请设置返航高度');
}
// 判断this.mode.flighttask_step_code是否为2
if (this.mode.flighttask_step_code !== 5) {
let text = this.filterModeCode(this.mode.flighttask_step_code);
this.$message.warning(text);
return;
}
this.$refs['ruleForm'].validate((valid) => {
if (valid) {
taskAdd(this.form).then((res) => {
// console.log(res);
if (res.code == 200) {
this.$message.success('新建成功');
this.getQuestRecordList(true);
this.getNewestTimer();
this.cancel();
this.reset();
}
});
} else {
return false;
}
});
},
cancel() {
this.$refs.createPL.close();
this.reset();
},
handleSizeChange(val) {
this.tableForm.pageSize = val;
this.getQuestRecordList();
},
handleCurrentChange(val) {
this.tableForm.pageNum = val;
this.getQuestRecordList();
},
reset() {
this.form = {
missionName: '',
timer: null,
type: 0,
fileId: '',
returnAltitude: 100,
returnAltitudeMode: 0,
outOfControlAction: 1,
flightControlAction: 0,
waylinePrecisionType: 0,
gateway: this.gateway.gateway,
timeStamp: ''
};
}
}
};
</script>
<style lang="scss">
.planLank {
position: absolute;
left: 50%;
top: 50%;
z-index: 20;
transform: translate(-50%, -50%);
width: 80%;
height: 75%;
color: #fff;
&_left {
width: 320px;
background-color: rgba(0, 0, 0, 0.6);
.header {
box-sizing: border-box;
height: 60px;
padding: 0 20px;
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
.text {
font-size: 18px;
font-family: 'alimamashuheiti';
}
.add {
cursor: pointer;
img {
width: 13px;
height: 12px;
}
}
}
.content {
padding: 0 20px;
.title {
margin: 20px 0;
font-weight: 400;
}
.card {
border: 1px solid rgb(0, 255, 255, 0.5);
border-radius: 4px;
&_top {
border-bottom: 1px solid rgba(204, 204, 204, 0.2);
margin-bottom: 15px;
&_left {
flex-direction: column;
justify-content: space-evenly;
align-items: center;
font-size: 12px;
height: 44px;
background-color: rgba(0, 255, 255, 0.5);
padding: 0 5px;
img {
width: 12px;
height: 12px;
}
}
&_right {
padding: 0 20px;
align-items: center;
font-family: 'alimamashuheiti';
}
}
&_bottom {
flex-direction: column;
padding: 0 15px;
&_top {
margin-bottom: 15px;
}
&_bottom {
color: #1bf8c3;
}
img {
width: 16px;
height: 16px;
margin-right: 5px;
}
}
}
}
}
&_right {
flex: 1;
padding: 10px;
background-color: #f7f9fa;
overflow: hidden;
.table {
height: 650px;
overflow-y: auto;
background-color: #fff;
padding: 10px;
width: 100%;
.el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
.el-table__header-wrapper table,
.el-table__body-wrapper table {
width: 100% !important;
}
.el-table__body,
.el-table__footer,
.el-table__header {
table-layout: auto;
}
.el-table__empty-block,
.el-table__body {
width: 100% !important;
}
.el-pagination .btn-next,
.el-pagination .btn-prev {
background: center center no-repeat #fff !important;
background-size: 16px !important;
cursor: pointer;
margin: 0;
color: #303133;
}
.el-pagination .el-pagination__total,
.el-pagination .el-pagination__jump,
.el-pagination .number {
color: #000 !important;
}
.el-pagination .el-pager li.active {
color: #409eff !important;
}
img {
width: 14px;
height: 14px;
vertical-align: middle;
}
}
.progress {
padding-top: 5px;
text-align: right;
background-color: #fff;
.el-pagination .el-pagination__total,
.el-pagination .el-pagination__jump,
.el-pagination .number {
color: #606266 !important;
}
.el-pagination .el-pager li.active {
color: #267aff !important;
}
}
}
.createPL {
.el-input__inner {
background-color: rgba(0, 0, 0, 0.5);
border-color: rgba(0, 255, 255, 0.5);
width: 100%;
color: #fff;
}
.el-form-item__label {
color: #fff;
}
.el-row {
line-height: 40px;
margin: 10px 0;
}
.el-radio-group {
display: flex;
.el-radio-button {
flex: 1;
.el-radio-button__inner {
width: 100%;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border-color: rgba(0, 255, 255, 0.5);
-webkit-box-shadow: -1px 0 0 0 rgba(0, 255, 255, 0.5);
box-shadow: -1px 0 0 0 rgba(0, 255, 255, 0.5);
}
.el-radio-button__orig-radio:checked + .el-radio-button__inner {
color: rgba(0, 255, 255, 1);
border-color: rgba(0, 255, 255, 1);
}
}
}
}
.btns {
text-align: center;
.el-button {
background: rgba(0, 255, 255, 0.2);
color: #fff;
border-color: rgba(0, 255, 255, 0.5);
}
.el-button:focus,
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
}
.el-picker-panel {
background: #fff !important;
color: #606266 !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
filter: none;
.el-picker-panel__footer .el-button--default {
color: #606266 !important;
}
.el-button {
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: #fff !important;
color: #606266 !important;
font-size: 14px;
border-radius: 4px;
}
.el-button.is-plain:focus,
.el-button.is-plain:hover {
background: #fff !important;
border-color: #409eff !important;
color: #409eff !important;
}
.el-button.is-disabled,
.el-button.is-disabled:focus,
.el-button.is-disabled:hover {
color: #c0c4cc !important;
cursor: not-allowed;
background-image: none;
background-color: #fff !important;
border-color: #ebeef5 !important;
}
}
.el-message-box {
background: linear-gradient(180deg, rgba(0, 255, 255, 0.2) 0%, rgba(0, 255, 255, 0) 100%), rgba(0, 0, 0, 0.3);
border: none;
.el-message-box__content,
.el-message-box__title {
color: #fff;
}
.el-button {
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
color: #fff;
border: 1px solid rgba(0, 255, 255, 0.5);
}
.el-button:hover {
border-color: rgba(0, 255, 255, 1);
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="uavselect">
<el-select v-model="value" @change="onChange" placeholder="请选择" size="mini">
<el-option v-for="item in options" :key="item.gateway" :label="item.businessName || item.gateway" :value="item.gateway"> </el-option>
</el-select>
</div>
</template>
<script>
import { listInfo } from '@/api/air';
import { setLocal, getLocal } from '@/utils';
import { gatewaysRemove, gatewaysAdd } from '@/api/air';
import { useAirStore } from '@/store/modules/drone';
export default {
data() {
return {
options: [],
value: '无人机',
parma: {
pageNum: 1,
pageSize: 100,
type: '机场'
}
};
},
watch: {
value: {
handler(newV, oldV) {
if (newV) {
this.$emit('showAirListAndAttr');
}
}
}
},
mounted() {
this.getDrone();
this.value = JSON.parse(getLocal('airGateway')).gateway;
},
methods: {
// 保存sn gateway
onChange(value) {
// console.log("valuevalue", value);
let obj = this.options.filter((item) => item.gateway == value)[0];
setLocal('airGateway', JSON.stringify(obj));
useAirStore().SET_GATEWAY(obj);
this.$emit('showAirListAndAttr');
},
// 获取机场列表
getDrone() {
listInfo(this.parma).then((res) => {
if (res.code == 200) {
let list = res.rows || [];
this.options = list;
}
});
}
}
};
</script>
<style lang="scss">
.uavselect {
position: fixed;
top: 5%;
left: 1%;
z-index: 1999;
.el-input__inner {
background-color: transparent;
border-color: rgba(0, 255, 255, 0.5);
color: #fff;
}
}
</style>

View File

@ -0,0 +1,26 @@
import { request } from "@/utils/requset2";
//进入指令飞行控制模式
export function drcModeEnter(data) {
return request({
url: "/dj/cmdFly/drcModeEnter",
method: "post",
data,
});
}
//有参数的指令飞行
export function hasDataFlight(data) {
return request({
url: "/dj/cmdFly/hasDataFlight",
method: "post",
data,
});
}
//无参数的指令飞行
export function noDataFlight(data) {
return request({
url: "/dj/cmdFly/noDataFlight",
method: "post",
data,
});
}

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="15.52001953125" viewBox="0 0 16 15.52001953125" fill="none">
<path d="M15.9466 14.3674L11.9464 9.45645C11.8964 9.40634 11.8464 9.35623 11.7964 9.35623C11.7464 9.35623 11.6964 9.30611 11.6964 9.30611L10.4964 9.40634C10.5464 9.10566 10.5464 8.75488 10.4464 8.4041C10.2464 7.85286 9.89637 7.45197 9.39637 7.20141L10.1464 0.386176C10.1964 -0.014721 9.64637 -0.165056 9.49637 0.23584L7.19632 6.14906C7.14632 6.19917 7.14632 6.24928 7.14632 6.3495C7.14632 6.39962 7.14632 6.44973 7.19632 6.44973L7.69633 7.20141C6.84632 7.6023 6.34631 8.50432 6.54631 9.40634L0.196191 12.2126C-0.153816 12.3629 -0.00381303 12.9142 0.396195 12.8641L6.69631 11.8618C6.74632 11.8618 6.79632 11.8117 6.84632 11.8117C6.89632 11.8117 6.89632 11.7616 6.94632 11.7115L7.44633 10.7092C7.94634 11.0099 8.54635 11.1101 9.14636 10.9097C9.34636 10.8596 9.49637 10.7594 9.64637 10.6591L15.3965 14.8685C15.7465 15.1191 16.1466 14.7182 15.9466 14.3674ZM9.44637 9.45645C9.29636 9.70701 9.04636 9.95757 8.74635 10.0077L8.54635 10.0077C8.29634 10.0077 8.09634 9.90746 7.89634 9.80723C7.64633 9.60678 7.54633 9.35623 7.54633 9.00544C7.54633 8.55443 7.89634 8.15354 8.29634 8.05331C8.39635 8.0032 8.44635 8.0032 8.54635 8.0032C8.84635 8.0032 9.09636 8.15354 9.29636 8.35398C9.44637 8.50432 9.54637 8.75488 9.54637 9.00544C9.54637 9.20589 9.49637 9.35623 9.44637 9.45645ZM13.4964 8.50432C13.4964 9.10566 13.3964 9.70701 13.1964 10.2582L14.2465 11.5612C14.6965 10.609 14.9465 9.60678 14.9465 8.50432C14.9465 5.4976 13.0964 2.992 10.4464 1.93965L10.3464 3.49312C12.1964 4.39514 13.4964 6.29939 13.4964 8.50432ZM2.89624 10.5088C2.64624 9.85734 2.49623 9.20589 2.49623 8.50432C2.49623 7.6023 2.74624 6.70029 3.09625 5.94861L1.69622 5.34726C1.24621 6.29939 0.996206 7.40186 0.996206 8.50432C0.996206 9.40634 1.19621 10.3084 1.49622 11.1101L2.89624 10.5088ZM7.89634 2.992L8.49635 1.48864L8.04634 1.48864C6.84632 1.48864 5.7463 1.78931 4.79628 2.29043L4.84628 3.99424C5.6963 3.3929 6.74632 3.04211 7.89634 2.992ZM7.99634 14.0166C6.64631 14.0166 5.44629 13.5155 4.49627 12.7137L2.64624 13.0144C3.94626 14.5178 5.8463 15.52 7.99634 15.52C9.84637 15.52 11.5464 14.7683 12.7964 13.5656L11.5464 12.6636C10.5964 13.5155 9.34636 14.0166 7.99634 14.0166ZM4.19627 1.93965L0.996206 4.49536L4.49627 5.99872L4.19627 1.93965Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M12.16 11.52L12.568 11.112L12.568 15.432C12.568 15.7481 12.8279 16 13.144 16C13.4601 16 13.712 15.7481 13.712 15.432L13.712 11.112L14.128 11.52C14.3512 11.7432 14.7128 11.7432 14.936 11.52C15.0485 11.4075 15.104 11.2664 15.104 11.12C15.104 10.9736 15.0485 10.8227 14.936 10.712L13.544 9.328L13.528 9.304L13.52 9.296L13.504 9.288L13.488 9.28L13.48 9.272L13.464 9.264L13.456 9.256L13.44 9.248L13.432 9.24L13.416 9.232L13.408 9.224L13.392 9.216L13.384 9.216L13.368 9.208L13.36 9.2L13.344 9.2L13.328 9.192L13.32 9.192L13.304 9.184L13.296 9.184L13.28 9.176L13.264 9.176L13.248 9.168L13.24 9.168L13.224 9.168L13.216 9.168L13.192 9.16L13.184 9.16L13.168 9.16L13.112 9.16L13.096 9.16L13.08 9.16L13.064 9.168L13.048 9.168L13.032 9.168L13.024 9.168L13.008 9.176L13 9.176L12.984 9.184L12.968 9.184L12.952 9.192L12.944 9.192L12.928 9.2L12.92 9.2L12.904 9.208L12.888 9.216L12.88 9.216L12.864 9.224L12.856 9.232L12.84 9.24L12.832 9.248L12.816 9.256L12.808 9.264L12.792 9.272L12.784 9.28L12.768 9.288L12.76 9.296L12.752 9.304L12.728 9.328L11.344 10.712C11.1208 10.9352 11.1208 11.2968 11.344 11.52C11.5744 11.7414 11.9368 11.7432 12.16 11.52ZM10.304 3.432C10.304 3.11593 10.0521 2.856 9.736 2.856L2.888 2.856C2.57193 2.856 2.32 3.11593 2.32 3.432C2.32 3.74807 2.57193 4 2.888 4L9.736 4C10.0503 4 10.304 3.74807 10.304 3.432ZM10.304 6.864C10.304 6.54793 10.0521 6.288 9.736 6.288L2.888 6.288C2.57193 6.288 2.32 6.54793 2.32 6.864C2.32 7.18007 2.57193 7.432 2.888 7.432L9.736 7.432C10.0503 7.432 10.304 7.18007 10.304 6.864ZM2.888 9.72C2.57193 9.72 2.32 9.97193 2.32 10.288C2.32 10.6041 2.57193 10.864 2.888 10.864L6.312 10.864C6.62807 10.864 6.88 10.6041 6.88 10.288C6.88 9.97193 6.62807 9.72 6.312 9.72L2.888 9.72ZM8.584 13.16L2.32 13.16C2.00571 13.16 1.64 12.9081 1.64 12.592L1.64 2.176C1.64 1.85993 1.89371 1.6 2.208 1.6L10.4 1.6C10.7143 1.6 10.968 1.85993 10.968 2.176L10.968 7.432C10.968 7.74807 11.6857 8 12 8C12.3143 8 12.6 7.74807 12.6 7.432L12.6 1.144C12.6 0.511857 12.0864 0 11.456 0L1.144 0C0.513643 0 0 0.513643 0 1.144L0 13.72C0 14.3521 0.513643 14.864 1.144 14.864L8.584 14.864C8.89829 14.864 9.136 14.3321 9.136 14.016C9.136 13.7017 8.90007 13.16 8.584 13.16ZM10.856 13.44C10.5399 13.44 10.288 13.6999 10.288 14.016L10.288 15.152C10.288 15.4681 10.5399 15.728 10.856 15.728C11.1721 15.728 11.432 15.4681 11.432 15.152L11.432 14.016C11.432 13.6999 11.1721 13.44 10.856 13.44ZM9.712 10.856C10.0281 10.856 10.28 10.5961 10.28 10.28L10.28 9.712C10.28 9.39593 10.0281 9.144 9.712 9.144C9.39593 9.144 9.136 9.39593 9.136 9.712L9.136 10.28C9.136 10.5961 9.39771 10.856 9.712 10.856ZM14.856 13.16L14.856 14.304C14.856 14.6201 15.1159 14.872 15.432 14.872C15.7481 14.872 16 14.6201 16 14.304L16 13.16C16 12.8439 15.7481 12.584 15.432 12.584C15.1159 12.584 14.856 12.8439 14.856 13.16Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15.919921875" height="16" viewBox="0 0 15.919921875 16" fill="none">
<path d="M15.1112 6.9457C14.7718 6.9457 14.4865 6.71375 14.4053 6.39967L14.4042 6.39988C14.363 6.23191 14.3153 6.06583 14.2609 5.90166C14.2065 5.73748 14.1458 5.57572 14.0786 5.41639C13.7466 4.6337 13.2723 3.93128 12.6703 3.32739C12.0666 2.7235 11.3643 2.24916 10.5817 1.91892C10.3134 1.80506 10.0378 1.71088 9.75837 1.63459C9.39315 1.59066 9.11 1.27987 9.11 0.902734C9.11 0.495495 9.44008 0.165363 9.84724 0.165363C9.95397 0.165363 10.0553 0.188238 10.1469 0.229059C12.9246 0.987503 15.1006 3.19988 15.8076 5.99827C15.8185 6.03353 15.8268 6.06944 15.8324 6.10597C15.838 6.14248 15.8407 6.17922 15.8407 6.21617C15.8408 6.61908 15.5141 6.9457 15.1112 6.9457ZM0 7.97245C0 12.4058 3.59352 16 8.02615 16C11.9404 16 15.1997 13.1977 15.9083 9.49084L15.906 9.49002C15.915 9.43909 15.92 9.38673 15.92 9.33319C15.92 8.87028 15.5633 8.49098 15.11 8.45452L15.1103 8.44802L8.00791 7.99434L8.00791 0.962189C8.0103 0.935711 8.01172 0.908938 8.01172 0.881822C8.01172 0.394809 7.61697 0 7.13004 0C7.0789 5.78452e-05 7.02814 0.00452013 6.97776 0.0133868C3.04116 0.526739 0 3.89402 0 7.97245ZM1.97552 10.5285C1.63259 9.72027 1.45929 8.85913 1.45929 7.97245C1.45929 7.08577 1.63259 6.22828 1.9737 5.42006C2.30387 4.63919 2.77631 3.93678 3.37827 3.33289C3.98023 2.73083 4.6807 2.25647 5.46143 1.92442C5.81349 1.773 6.1783 1.65623 6.54861 1.57048L6.54861 8.45897L6.54961 8.45963L6.55082 8.6708C6.55075 8.6748 6.55052 8.67873 6.55052 8.68273C6.55052 9.08825 6.87784 9.4172 7.28264 9.41997L7.28252 9.42023L7.42858 9.42094L7.42858 9.42091L7.91486 9.45208L14.3175 9.86075C13.9892 10.9591 13.3762 11.957 12.5226 12.7598C11.3005 13.9074 9.70435 14.5405 8.02615 14.5405C7.13962 14.5405 6.27863 14.3671 5.47238 14.026C4.68983 13.6957 3.98753 13.2214 3.38374 12.6175C2.77995 12.0136 2.30569 11.3112 1.97552 10.5285Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14.3203125" height="16" viewBox="0 0 14.3203125 16" fill="none">
<path d="M13.9947 3.10876L10.0045 0.103786C9.91533 0.0362046 9.80931 0 9.69847 0L0.402396 0.00241378C0.197584 0.00241378 0 0.144818 0 0.407905L0 15.3435C0 15.679 0.289147 16 0.657809 16L13.9634 16C14.1754 16 14.32 15.8962 14.32 15.6573L14.32 3.5046C14.3176 3.34289 14.1248 3.20531 13.9947 3.10876ZM10.1563 1.75713L10.1563 4.76211C10.1563 4.88037 10.0985 4.98416 9.96834 4.98416L3.6553 4.98416C3.48181 4.98416 3.45772 4.89244 3.45772 4.75487L3.45772 1.25268L9.44306 1.25268L10.1563 1.75713ZM13.1032 14.3298C13.1032 14.588 12.9466 14.7642 12.696 14.7642L1.5879 14.7642C1.39272 14.7666 1.26261 14.6242 1.26261 14.4142L1.26261 1.53266C1.26261 1.36129 1.42164 1.25509 1.63127 1.25509C1.81199 1.25509 2.0409 1.25509 2.29872 1.25509L2.30113 5.85066C2.30113 6.0172 2.40474 6.09443 2.56136 6.09443L10.9008 6.09202C11.0647 6.09443 11.1587 5.97134 11.1538 5.78549L11.1563 2.52225L13.1032 3.98009L13.1032 14.3298ZM2.66738 10.5307L7.21422 10.5307C7.37566 10.5307 7.48409 10.4269 7.48409 10.2797C7.48409 10.1542 7.48409 9.79454 7.48409 9.65938C7.48409 9.50008 7.31542 9.3987 7.16602 9.3987L2.63365 9.3987C2.4457 9.3987 2.35173 9.51697 2.35173 9.65938C2.35173 9.81385 2.35173 10.1831 2.35173 10.2724C2.35173 10.4173 2.46498 10.5307 2.66738 10.5307ZM10.8189 9.3987L8.52983 9.3987C8.33225 9.3987 8.19009 9.51214 8.19009 9.67144C8.19009 9.78488 8.19009 10.13 8.19009 10.2459C8.19009 10.4414 8.34189 10.5331 8.51779 10.5331L10.79 10.5331C10.9924 10.5331 11.1563 10.3907 11.1563 10.2145C11.1563 10.0649 11.1563 9.9297 11.1563 9.75833C11.1587 9.56524 11.0261 9.3987 10.8189 9.3987ZM5.28175 12.3964L2.6722 12.3964C2.47221 12.3964 2.34932 12.5002 2.34932 12.7175C2.34932 12.8623 2.34932 13.0119 2.34932 13.135C2.34932 13.3498 2.47943 13.4826 2.67943 13.4826L5.22392 13.4826C5.41428 13.4826 5.58776 13.3547 5.58776 13.1447C5.58776 12.983 5.58776 12.8647 5.58776 12.7512C5.58776 12.5533 5.42632 12.3964 5.28175 12.3964ZM10.7996 12.3964L6.57086 12.3964C6.35641 12.3964 6.22871 12.5533 6.22871 12.7368C6.22871 12.843 6.22871 13.0385 6.22871 13.1543C6.22871 13.304 6.36364 13.4826 6.57809 13.4826L10.7707 13.4826C10.9852 13.4826 11.1563 13.3305 11.1563 13.1109C11.1563 13.0119 11.1563 12.9009 11.1563 12.7512C11.1587 12.5485 11.031 12.3964 10.7996 12.3964ZM8.83344 4.23593C9.00934 4.23593 9.1515 4.0887 9.1515 3.96078L9.1515 2.23261C9.1515 2.05401 9.05271 1.95505 8.90332 1.95505C8.78043 1.95505 8.30334 1.95505 8.19732 1.95505C8.04792 1.95505 7.97564 2.06366 7.97564 2.2302L7.97564 3.96561C7.97564 4.12008 8.12744 4.23835 8.26237 4.23835C8.37803 4.23593 8.70332 4.23593 8.83344 4.23593Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="12.6400146484375" viewBox="0 0 16 12.6400146484375" fill="none">
<path d="M13.52 12.64L2.48 12.64C1.12 12.64 0 11.52 0 10.16L0 2.48C0 1.12001 1.12 0 2.48 0L13.52 0C14.88 0 16 1.12001 16 2.48L16 10.16C16 11.52 14.88 12.64 13.52 12.64ZM2.8 1.6C2.16 1.6 1.6 2.16001 1.6 2.8L1.68 9.92C1.68 10.56 2.24 11.12 2.88 11.12L13.2 11.12C13.84 11.12 14.4 10.56 14.4 9.92L14.32 2.8C14.32 2.16001 13.76 1.6 13.12 1.6L2.8 1.6ZM6.4 9.84C4.48 9.84 2.88 8.24 2.88 6.32C2.88 4.4 4.48 2.8 6.4 2.8C8.32 2.8 9.92 4.4 9.92 6.32C10 8.24 8.39999 9.84 6.4 9.84ZM6.4 4C5.11999 4 4.08 5.04 4.08 6.32C4.08 7.6 5.11999 8.64 6.4 8.64C7.67999 8.64 8.72 7.6 8.72 6.32C8.72 5.04 7.67999 4 6.4 4ZM13.04 3.6L11.76 3.6C11.52 3.6 11.28 3.36 11.28 3.12L11.28 2.96C11.28 2.72 11.52 2.48 11.76 2.48L13.04 2.48C13.28 2.48 13.52 2.72 13.52 2.96L13.52 3.12C13.52 3.36 13.28 3.6 13.04 3.6Z" fill="#FFFFFF" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 981 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,199 @@
<template>
<div class="left_detail">
<div>
<el-tooltip class="item" effect="dark" content="搜星数量" placement="top-start">
<div>
<img src="./icons/satellite.svg" alt="" />
<span>{{ info.position_state.rtk_number }}RTK</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="电池剩余电量" placement="top-start">
<div>
<img src="./icons/battery.svg" alt="" />
<span>{{ info.battery.capacity_percent }}%</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="返航所需电量" placement="top-start">
<div>
<img src="./icons/quantity.svg" alt="" />
<span>{{ info.battery.return_home_power }}%</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="内存总容量" placement="top-start">
<div>
<img src="./icons/memory.svg" alt="" />
<span>{{ (info.storage.total / (1024 * 1024)).toFixed(2) }}GB</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="内存已使用容量" placement="top-start">
<div>
<img src="./icons/big.svg" alt="" />
<span>{{ (info.storage.used / (1024 * 1024)).toFixed(2) }}GB</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="飞行高度" placement="top-start">
<div>
<img src="./icons/plane.svg" alt="" />
<span>{{ info.elevation.toFixed(2) }}m</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="拍照状态" placement="top-start">
<div>
<img src="./icons/picture.svg" alt="" />
<span>{{
info.cameras && info.cameras[0]
? (info.cameras[0].photo_state == 0 ? "空闲" : "拍照中")
: "-"
}}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="拍照剩余数量" placement="top-start">
<div>
<img src="./icons/Remaining.svg" alt="" />
<span>{{ info.cameras && info.cameras[0] ? info.cameras[0].remain_photo_num : "-" }}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="剩余飞行时间" placement="top-start">
<div>
<img src="./icons/FlightTime.svg" alt="" />
<span>{{ filterTime(info.battery.remain_flight_time) }}</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="垂直速度" placement="top-start">
<div>
<span>VS {{ info.vertical_speed.toFixed(2) }}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="相对起飞点高度" placement="top-start">
<div>
<span>ALT {{ Number(info.elevation).toFixed(2) }}</span>
</div>
</el-tooltip>
<el-tooltip class="item" effect="dark" content="海拔高度" placement="top-start">
<div>
<span>ASL {{ (Number(info.elevation) + Number(info.height)).toFixed(2) }}</span>
</div>
</el-tooltip>
</div>
<div>
<el-tooltip class="item" effect="dark" content="水平速度" placement="top-start">
<div>
<span>HS {{ info.horizontal_speed.toFixed(2) }}</span>
</div>
</el-tooltip>
<div>&nbsp;</div>
<div>&nbsp;</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
info: {
//飞行器数据信息
cameras: [{ remain_photo_num: 0, photo_state: 0 }],
horizontal_speed: 0,
position_state: { rtk_number: 0 },
vertical_speed: 0,
horizontal_speed: 0,
elevation: 0,
height: 0,
wind_direction: "0",
battery: {
capacity_percent: 0,
remain_flight_time: 0,
return_home_power: 0,
},
wind_speed: 0,
storage: { total: 0, used: 0 },
},
};
},
created() { },
mounted() {
this.$recvChanel("websocketBus", (res) => {
if (res.businessType == "osd4") {
this.info = res.data || {}
this.time = new Date().getTime()
}
});
this.onReset()
},
methods: {
restet() {
this.info = {
cameras: [{ remain_photo_num: 0, photo_state: 0 }],
horizontal_speed: 0,
position_state: { rtk_number: 0 },
vertical_speed: 0,
horizontal_speed: 0,
elevation: 0,
height: 0,
wind_direction: "0",
battery: {
capacity_percent: 0,
remain_flight_time: 0,
return_home_power: 0,
},
wind_speed: 0,
storage: { total: 0, used: 0 },
};
},
onReset() {
let that = this
that.timer = setInterval(() => {
const currentTimestamp = Date.now(); // 获取当前时间戳
const elapsed = currentTimestamp - that.time; // 计算差值
if (elapsed > 2000) {
that.restet();
}
}, 2000);
},
filterTime(val) {
//返回分钟
let miu = parseInt(val / 60);
let s = val % 60;
return miu + "分" + s + "秒";
},
},
};
</script>
<style lang="scss" scoped>
.left_detail {
display: flex;
align-items: center;
padding: 20px 30px 0 80px;
justify-content: space-between;
height: 100%;
>div {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
font-size: 14px;
>div {
display: flex;
// height: 100%;
align-items: center;
color: #fff;
font-weight: 600;
>img {
width: 20px;
margin-right: 6px;
height: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="drone_video_box" id="drone_video_box" @wheel="debouncedWheel" @dblclick="onDetailPost">
<div class="shutter" :id="'shutter' + type"></div>
<!-- <video :class="{ video_drone: true }" :id="'drone_video-webrtc_' + droneObj.cameraIndex" autoplay muted width="100%"
height="100%"></video> -->
<div :id="'drone_video-webrtc_' + droneObj.cameraIndex" :class="{ video_drone: true }" style="width: 100%; height: 100%">
<webrtc :ref="'drone_video-webrtc_' + droneObj.cameraIndex" width="100%" height="100%"></webrtc>
</div>
<myLoading ref="myLoadingRef"></myLoading>
</div>
</template>
<script>
// 加载中
import myLoading from '../myLoading/index.vue';
import { hasDataFlight } from '@/api/air';
import webrtc from '@/components/webrtc';
import { getDockAir } from '@/utils/auth';
export default {
components: {
myLoading,
webrtc
},
props: {
type: {
type: Number,
default: 1 //1、无人机 2、机场
},
typeLabel: {
type: String,
default: '广角' //1、无人机 2、机场
}
},
data() {
return {
droneObj: {},
videoShow: false,
flvPlayer: null,
camera_type: 'wide',
videoUrl: '',
aiUrl: ''
};
},
mounted() {
this.$recvChanel('camera_type', (type) => {
this.camera_type = type;
});
this.$recvChanel('video_width', (height) => {
this.getInfo(this.droneObj);
let div = document.getElementById('drone_video_box');
div.style.height = height + 'px';
let div2 = document.getElementById('drone_video-webrtc_' + this.droneObj.cameraIndex);
div2.style.height = height + 'px';
});
},
beforeDestroy() {
this.getVideoEnd(this.droneObj);
},
methods: {
debouncedWheel(event) {
// 滚轮滚动
// 如果是机场视频和广角类型 不需要滚动
if (!(this.type == 2 || this.typeLabel == '广角')) {
this.$emit('wheelScale', event);
}
},
// 拍照效果
setShutter() {
const shutter = document.getElementById('shutter' + this.type);
shutter.classList.add('active');
// 动画结束后移除类,以便可以重新触发动画
setTimeout(() => {
shutter.classList.remove('active');
}, 300); // 动画时长
},
onDetailPost(e) {
if (this.type == 2) {
// 机场视频不需要点击
return;
}
// 获取坐标
let div = document.getElementById('drone_video_box');
// let div = document.getElementById("drone_video-webrtc_" + this.droneObj.cameraIndex);
let rect = div.getBoundingClientRect();
let width = rect.width;
let height = rect.height;
let x, y;
if (width && height && e.offsetX && e.offsetY) {
x = Number(e.offsetX / width);
y = Number(e.offsetY / height);
let obj = {
gateway: this.droneObj.gateway,
method: 'camera_aim',
params: {
camera_type: this.camera_type,
locked: true,
payload_index: this.droneObj.cameraIndex,
x,
y
}
};
hasDataFlight(obj).then((res) => {
if (res.code == 200) {
this.$message.success('操作成功');
}
});
}
},
// 数据获取并播放
getInfo(data) {
this.$refs.myLoadingRef.statusLoading(true);
this.droneObj = data;
console.log('this.droneObj', this.droneObj);
this.$nextTick(() => {
this.getVideo(data);
});
},
// 视频结束
getVideoEnd(data, cb) {
if (this.flvPlayer) {
axios({
// url: process.env.DOCKAIR + "/dj/live/stop",
url: getDockAir() + '/dj/live/stop',
method: 'post',
data
}).then((res) => {
if (res.data.code !== 200) {
this.$message.error(res.data.msg);
} else {
this.videoShow = false;
try {
this.destroyVideo();
if (cb) cb();
} catch (error) {}
}
});
} else {
if (cb) cb();
}
},
// 销毁
destroyVideo() {
this.flvPlayer.pause(); // 暂停播放数据流
this.flvPlayer.unload(); // 取消数据流加载
this.flvPlayer.destroy(); // 销毁播放实例
this.flvPlayer.detachMediaElement(); // 将播放实例从节点中取出
this.flvPlayer = null;
},
// 开始视频调用
getVideo(data1) {
if (this.flvPlayer) {
this.destroyVideo();
}
axios({
// url: process.env.DOCKAIR + "/dj/live/start",
url: getDockAir() + '/dj/live/start',
method: 'post',
data: data1
}).then((res) => {
const { code, data, msg } = res.data;
if (code == 200) {
// this.playFlv(data.url, "drone_video-webrtc_" + data1.cameraIndex, data1.cameraIndex)
setTimeout(() => {
this.$refs['drone_video-webrtc_' + data1.cameraIndex].startPlay(data.url);
this.videoUrl = data.url;
this.aiUrl = data.aiUrl;
}, 1000);
this.$refs.myLoadingRef.statusLoading(false);
} else {
this.$refs.myLoadingRef.statusLoading(false);
this.$message.error(msg);
}
});
},
// 播放flv视频
playFlv(flv, videoId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById(videoId);
this.flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
hasAudio: false,
url: flv,
enableStashBuffer: true,
enableWorker: true,
autoCleanupSourceBuffer: true //自动清除缓存
});
this.$nextTick(() => {
this.flvPlayer.attachMediaElement(videoElement);
this.flvPlayer.load();
this.flvPlayer.play();
this.$refs.myLoadingRef.statusLoading(false);
// this.handleDelay(this.flvPlayer);
});
this.flvPlayer.on('error', () => {
console.log('播放错误');
});
// videoElement.addEventListener("error", () => {
// console.log("播放错误");
// // 重新播放
// // flvPlayer.attachMediaElement(videoElement);
// // flvPlayer.load();
// // flvPlayer.play();
// });
}
},
// 重新播放加载
onPlay() {
if (this.flvPlayer) {
this.flvPlayer.load();
this.flvPlayer.play();
}
},
// 处理延迟
handleDelay(flvPlayer) {
let timer = setInterval(() => {
if (!flvPlayer) return;
if (flvPlayer.buffered.length) {
let end = flvPlayer.buffered.end(0); //获取当前buffered值
let diff = end - flvPlayer.currentTime; //获取buffered与currentTime的差值
if (diff >= 2) {
//如果差值大于等于0.5 手动跳帧 这里可根据自身需求来定
flvPlayer.currentTime = flvPlayer.buffered.end(0) - 1.5; //手动跳帧
}
}
}, 1000); //1000毫秒执行一次
return timer;
}
}
};
</script>
<style lang="scss" scoped>
.drone_video_box {
width: 960px;
height: 720px;
position: relative;
overflow: hidden;
.shutter {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
transform: translateY(-100%);
opacity: 0;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.shutter.active {
transform: translateY(0);
opacity: 1;
}
@keyframes shutterAnimation {
0% {
transform: translateY(-100%);
opacity: 0;
}
50% {
transform: translateY(0);
opacity: 1;
}
100% {
transform: translateY(-100%);
opacity: 0;
}
}
.video_drone {
// width: 100%;
background-color: rgba(0, 0, 0, 1);
// object-fit: fill;
// height: 100%;
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

View File

@ -0,0 +1,187 @@
<template>
<div class="flight_setup">
<div class="left_in">
<div>任务剩余距离</div>
<div>~</div>
<div>任务剩余时长</div>
<div>~</div>
<div>飞行作业模式</div>
<!-- <div></div> -->
<!-- display: flex;justify-content: center; -->
<div v-if="taskId" style="" @click="stopLine">
<div class="hangxian">
<span>{{ showPath ? '暂停航线' : '恢复航线' }}</span>
</div>
</div>
</div>
<div class="tool_flight">
<div @click="onFightTool">
<img src="./icons/icon1.svg" alt="" />
<span>飞行设置</span>
</div>
<div @click="onHomewardVoyage">
<img src="./icons/icon2.svg" alt="" />
<span>{{ showCancel ? '返航' : '取消返航' }}</span>
</div>
<div @click="onScram">
<img src="./icons/icon3.svg" alt="" />
<span>急停</span>
</div>
</div>
</div>
</template>
<script>
import { droneControl, droneEmergencyStop, flightTaskPauseNew, flightTaskRecoveryNew, drcModeEnte } from '@/api/air';
import { useAirStore } from '@/store/modules/drone';
export default {
data() {
return {
showCancel: true,
height: 0,
gateway: useAirStore().gateWay,
showPath: true,
taskId: ''
};
},
mounted() {
this.$recvChanel('TaskIssuedSuccessfully', (taskId) => {
this.taskId = taskId;
});
this.$recvChanel('showCancel', (showCancel) => {
this.showCancel = !showCancel;
});
},
methods: {
onHomewardVoyage() {
if (this.showCancel) {
// 飞机返航
this.$emit('showTips', true);
} else {
// 取消返航
this.$emit('showTipss', true);
}
},
// 飞行设置
onFightTool() {
this.$emit('onTakeOff', true);
},
// 暂停航线
stopLine() {
if (this.showPath) {
this.showPath = false;
// 暂停航线
this.pauseAirLine();
} else {
this.showPath = true;
// 恢复航线
this.recoverAirLine();
}
},
// 急停
onScram() {
// 无参数接口调用
droneEmergencyStop({ gateway: this.gateway.gateway }).then((res) => {
if (res.code == 200) {
this.$message.success('操作成功');
}
});
},
// 航线暂停
pauseAirLine() {
let obj = { gateway: this.gateway.gateway, taskId: this.taskId };
flightTaskPauseNew(obj).then((res) => {
this.$message.success('暂停成功');
});
},
// 航线恢复
recoverAirLine() {
let obj = { gateway: this.gateway.gateway, taskId: this.taskId };
flightTaskRecoveryNew(obj).then((res) => {
this.$message.success('恢复成功');
});
}
}
};
</script>
<style lang="scss" scoped>
.flight_setup {
width: 100%;
height: 100%;
position: relative;
padding: 20px;
box-sizing: border-box;
display: flex;
> div {
width: 50%;
}
.left_in {
color: #fff;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
> div {
font-size: 14px;
}
.hangxian {
text-align: center;
border: 1px solid rgb(255, 255, 255, 0.8);
width: 60%;
padding: 6px 0;
color: rgb(255, 255, 255, 0.8);
border-radius: 5px;
}
.hangxian:hover {
color: #fff;
border-color: #fff;
}
}
.tool_flight {
> div {
cursor: pointer;
margin-left: 30px;
border-radius: 4px;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
padding: 8px 0px;
display: flex;
width: 150px;
color: rgba(0, 255, 255, 1);
place-items: center;
justify-content: center;
margin-bottom: 8px;
> img {
width: 18px;
margin-right: 10px;
}
}
& > div:last-child {
background: linear-gradient(180deg, rgba(241, 108, 85, 0.2) 0%, rgba(241, 108, 85, 0) 100%), rgba(0, 0, 0, 0.5);
border: 1px solid rgba(241, 108, 85, 1);
color: rgb(241, 108, 85);
}
}
}
.flight_setup::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 50%;
width: 1px;
height: 100%;
background: url('./icons/line.png') no-repeat;
background-size: 100% 100%;
}
</style>

View File

@ -0,0 +1,203 @@
<template>
<div class="graduated_scale" id="graduated_scale" @wheel="debouncedWheel">
<span class="max">{{ max }}X</span>
<div v-for="(item, i) of labelList" :key="i">
<div class="text">{{ item.count }}X</div>
<div class="boxs">
<div
v-for="(items, index) of item.arr"
:key="index"
:style="'height:' + (1 / item.arr.length) * 100 + '%'"
@click="onSelect(items)"
>
<div :class="valueScale == items ? 'select' : ''"></div>
</div>
</div>
</div>
</div>
</template>
<script>
let timeout;
export default {
props: {
valueScale: {
type: Number,
default: 2,
},
typeLabel: {
type: String,
default: "变焦",
},
},
data() {
return {
list: [
//变焦
{ count: 2, arr: [2, 3, 4, 5, 6].reverse() },
{ count: 7, arr: [7, 8, 9, 10, 11, 12].reverse() },
{
count: 14,
arr: [
14,
16,
18,
20,
22,
24,
26,
28,
30,
32,
34,
38,
43,
46,
50,
53,
56,
].reverse(),
},
].reverse(),
// 红外
listInfrared: [
//变焦
{ count: 2, arr: [2, 3, 4, 5].reverse() },
{ count: 6, arr: [7, 8, 9, 10, 11, 12].reverse() },
{ count: 13, arr: [13, 14, 15, 16, 17, 18, 19, 20].reverse() },
].reverse(),
labelList: [], //当前应该显示的倍速
detailList: [],
max: 56, //获取最大值 默认
index: 0, //鼠标滚轮滚动时 改变下标 获取对应的值(倍数大小)
};
},
watch: {
typeLabel(newlog) {
this.setInfo(newlog);
},
},
mounted() {
this.setInfo(this.typeLabel);
},
methods: {
onSelect(item) {
this.index = this.detailList.findIndex((items) => {
return items == item;
});
this.$emit("getScale", item);
},
handleWheel(event) {
// 处理滚轮事件的逻辑
if (event.deltaY > 0) {
this.index--;
if (this.index <= 0) {
this.index = 0;
}
} else {
this.index++;
if (this.index >= this.detailList.length) {
this.index = this.detailList.length - 1;
}
}
},
// 300 毫秒的防抖时间
debouncedWheel(event) {
// 清除之前的定时器
clearTimeout(timeout);
// 设置一个新的定时器
timeout = setTimeout(() => {
this.$emit("getScale", this.detailList[this.index]);
}, 200); // 200毫秒后判断为停止
this.handleWheel(event);
},
// 数据处理
setInfo(label) {
this.detailList = [];
if (label == "变焦") {
this.labelList = this.list;
this.list.forEach((element) => {
this.detailList.push(...element.arr);
});
} else {
//红外
this.labelList = this.listInfrared;
this.listInfrared.forEach((element) => {
this.detailList.push(...element.arr);
});
}
this.detailList.sort((a, b) => a - b);
this.index = this.detailList.findIndex(
(item) => item === this.valueScale
);
this.max = this.detailList[this.detailList.length - 1];
},
},
};
</script>
<style lang="scss" scoped>
.graduated_scale {
width: 70px;
height: 180px;
background: rgba(0, 0, 0, 0.6);
position: absolute;
color: #fff;
bottom: 70px;
right: 20px;
padding: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
.max {
position: absolute;
top: 6px;
left: 10px;
}
> div {
height: 33%;
color: #fff;
font-size: 14px;
display: flex;
> div {
height: 100%;
}
.text {
display: flex;
justify-content: flex-end;
flex-direction: column;
width: 32px;
}
.boxs {
display: flex;
flex-direction: column;
justify-content: space-around;
height: 100%;
> div {
width: 100%;
display: grid;
place-items: center;
> div {
position: relative;
background-color: #fff;
display: inline-block;
width: 10px;
height: 1px;
}
.select::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #fff;
}
}
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12.96 18.4869C13.2 18.2469 13.44 17.9931 13.44 17.5131C13.44 17.0331 13.2 16.8069 12.96 16.5669C12.72 16.3269 12.48 16.0869 12 16.0869C11.52 16.0869 11.28 16.3269 11.04 16.5669C10.8 16.8069 10.56 17.0331 10.56 17.5131C10.56 17.9931 10.8 18.2469 11.04 18.4869C11.28 18.7269 11.52 18.7269 12 18.9669C12.48 18.7269 12.72 18.7269 12.96 18.4869ZM12 24C8.64 24 5.76686 22.8069 3.60686 20.4069C1.20686 18.2469 0 15.36 0 12C0 8.64 1.20686 5.76686 3.60686 3.60686C5.76686 1.20686 8.64 0 12 0C15.36 0 18.2469 1.20686 20.4069 3.60686C22.8069 5.76686 24 8.64 24 12C24 15.36 22.8069 18.2469 20.4069 20.4069C18.2469 22.8069 15.36 24 12 24ZM10.56 6.95314L11.04 13.6869C11.04 13.9269 11.2869 14.16 11.2869 14.4C11.5269 14.4 11.76 14.6469 12 14.6469C12.24 14.6469 12.4869 14.4 12.7269 14.4C12.7269 14.16 12.96 13.9269 12.96 13.6869L13.44 6.95314C13.68 6.47314 13.4469 6 13.2069 5.76C12.9669 5.28 12.48 5.04686 12 5.04686C11.52 5.04686 11.0469 5.28 10.8069 5.76C10.5669 6 10.32 6.47314 10.56 6.95314Z" fill-rule="evenodd" fill="#FFA145" >
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,126 @@
<template>
<div class="tips" v-if="showTip">
<div class="header_tips">
<span class="title"> 提示 </span>
<img src="./icons/close.png" @click="onClose" alt="" srcset="" />
</div>
<div class="content">
<img src="./icons/warn.svg" alt="" srcset="" />
<span> {{ obj.text }}</span>
</div>
<div class="footer_bottom">
<div class="cl" @click="onSubmit"> </div>
<div @click="onClose"> </div>
</div>
</div>
</template>
<script>
import { returnHome, returnHomeCancel } from '@/api/air';
export default {
props: {
Drone: {
type: Object,
default: () => ({})
},
obj: {
type: Object,
default: () => ({})
}
},
data() {
return { showTip: false };
},
mounted() {},
methods: {
openTip() {
this.showTip = true;
},
onClose() {
this.showTip = false;
},
// 返航
cancelHome(obj) {
returnHomeCancel(obj).then((res) => {
this.onClose();
this.$message.success('取消成功');
});
},
subHome(obj) {
returnHome(obj).then((res) => {
this.onClose();
this.$message.success('返航成功');
});
},
onSubmit() {
let obj = { gateway: this.Drone.gateway };
if (this.obj.flag) {
this.subHome(obj);
} else {
this.cancelHome(obj);
}
this.$sendChanel('showCancel', this.obj.flag);
}
}
};
</script>
<style lang="scss" scoped>
.tips {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 500px;
opacity: 1;
background: linear-gradient(180deg, rgb(33 33 33 / 70%) 0%, rgb(9 156 156 / 55%) 100%), #0000;
padding: 20px 20px 0;
box-sizing: border-box;
.header_tips {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 16px;
color: #fff;
}
img {
width: 12px;
cursor: pointer;
}
}
.content {
height: 60px;
display: flex;
align-items: center;
color: #fff;
font-size: 16px;
> img {
width: 18px;
margin-right: 10px;
}
}
.footer_bottom {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: flex-end;
> div {
border-radius: 4px;
cursor: pointer;
background: rgba(0, 255, 255, 0.2);
border: 1px solid rgba(0, 255, 255, 1);
padding: 10px 16px;
margin-right: 16px;
color: rgba(0, 255, 255, 1);
}
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,98 @@
<template>
<div class="right_detail">
<div class="left_info">
<div>
<span>智能规划最佳飞行路线</span>
<div>
<el-switch v-model="value" inactive-color="#ff4949" active-color="rgba(0, 255, 255, 1)"> </el-switch>
</div>
</div>
<div>
<span>飞行作业高(ALT)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.commander_flight_height += v)"
:valueNumber="queryParams.commander_flight_height"
@change="(b) => (queryParams.commander_flight_height = b)"
>
</inputNumber>
</div>
</div>
<div>
<span>返航高(ALT)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.rth_altitude += v)"
:valueNumber="queryParams.rth_altitude"
@change="(b) => (queryParams.rth_altitude = b)"
>
</inputNumber>
</div>
</div>
<div>
<span>失联动作</span>
<div class="lost_action">
<el-select v-model="queryParams.rc_lost_action" placeholder="请选择">
<el-option v-for="item in lostAction" :key="item.id" :label="item.label" :value="item.id"> </el-option>
</el-select>
</div>
</div>
<div>
<span>目标点高度(AGL)</span>
<div>
<inputNumber
:min="0"
:max="10"
uuit="m"
@updateInput="(v) => (queryParams.height += v)"
:valueNumber="queryParams.height"
@change="(b) => (queryParams.height = b)"
></inputNumber>
</div>
</div>
</div>
<div class="take_off" @click="onTakeOff">
<img src="./icons/fight.svg" alt="" srcset="" />
</div>
</div>
</template>
<script>
// 输入框
import inputNumber from '../inputNumber/index.vue';
import { lostAction } from '../../index.js';
import { useAirStore } from '@/store/modules/drone';
export default {
components: { inputNumber },
data() {
return {
value: true,
queryParams: useAirStore().queryParams,
// queryParams: {
// target_height: 100, //目标点高度
// rth_altitude: 100, //返航高度
// security_takeoff_height: 100, //安全起飞高度
// rc_lost_action: 2, //遥控器失控动作
// commander_flight_height: 100, //指点飞行高度
// },
value4: 1,
lostAction
};
},
methods: {
onTakeOff() {
// 一键起飞
this.$emit('onTakeOff', false);
}
}
};
</script>
<style lang="scss" scoped>
@import '../../style/comm.scss';
</style>

Some files were not shown because too many files have changed in this diff Show More