空域页面修改及工具集成

This commit is contained in:
zhulongchuan 2025-11-14 17:50:10 +08:00
parent 7cb20acd98
commit 9763668130
14 changed files with 2801 additions and 20 deletions

1
auto-imports.d.ts vendored
View File

@ -2,6 +2,7 @@
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']

View File

@ -1,13 +1,7 @@
<!-- src/App.vue -->
<template>
<div id="app">
<!-- <header class="app-header">
<h1>🌉 重庆三维地图系统</h1>
<p>基于 Vue 3 + Cesium 的三维地理信息系统</p>
</header> -->
<main class="app-main">
<CesiumViewer />
</main>
</div>
</template>

View File

@ -0,0 +1,339 @@
<!-- src/components/CesiumViewer.vue -->
<template>
<div class="cesium-container">
<!-- Cesium 容器 -->
<div id="cesiumContainer" ref="cesiumContainer"></div>
<!-- 绘图工具面板 -->
<!-- <DrawingToolPanel
v-if="isInitialized"
:is-drawing="isDrawing"
:current-drawing-type="currentDrawingType"
:drawings="drawings"
:selected-drawing="selectedDrawing"
:drawing-info="drawingInfo"
:drawing-options="drawingOptions"
:get-drawing-status="getDrawingStatus"
:get-drawing-info-text="getDrawingInfoText"
@start-circle-drawing="startCircleDrawing"
@start-polygon-drawing="startPolygonDrawing"
@cancel-drawing="cancelDrawing"
@select-drawing="selectDrawing"
@deselect-drawing="deselectDrawing"
@remove-drawing="removeDrawing"
@clear-drawings="clearAllDrawings"
@fly-to-drawing="flyToDrawing"
@update-circle-option="updateCircleOption"
@update-polygon-option="updatePolygonOption"
@copy-drawing-info="copyDrawingInfo"
@fly-to-selected-drawing="flyToSelectedDrawing"
@print-selected-drawing-info="handlePrintSelectedDrawingInfo"
@print-all-drawings-info="handlePrintAllDrawingsInfo"
@print-drawing-info="handlePrintDrawingInfo"
@export-drawing-info="handleExportDrawingInfo"
/> -->
<!-- 空域列表 -->
<routeList v-if="showType=='routeList'" @setShowType="setShowType" :id="planId" />
<!-- 空域新增 -->
<routeAdd v-if="showType=='routeAdd'" @setShowType="setShowType" :id="planId" :hxid="hxId" />
<!-- 空域查看 -->
<!-- <routeGet v-if="showType=='routeGet'" @setShowType="setShowType" :id="planId" :hxid="hxId" /> -->
<RouteGet
v-if="showType=='routeGet'"
@setShowType="setShowType" :id="planId" :hxid="hxId"
:import-from-file="importFromFile"
:import-airspace-data="importAirspaceData"
:get-import-statistics="getImportStatistics"
@import-complete="handleImportComplete"
/>
<!-- 导入工具面板 -->
<!-- <ImportToolPanel
v-if="false"
:import-from-file="importFromFile"
:import-airspace-data="importAirspaceData"
:get-import-statistics="getImportStatistics"
@import-complete="handleImportComplete"
/> -->
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useCesium } from './components/useCesium';
import { useCesiumTools } from './components/useCesiumTools';
// import CesiumToolPanel from './CesiumToolPanel.vue';
import { useCoordinatePicker } from './components/useCoordinatePicker';
import { useModelManager } from './components/useModelManager';
import { useDrawingManager } from './components/useDrawingManager';
import ModelControlPanel from './ModelControlPanel.vue';
import CoordinatePickerPanel from './CoordinatePickerPanel.vue';
import DrawingToolPanel from './DrawingToolPanel.vue';
import ImportToolPanel from './ImportToolPanel.vue';
import routeList from "./components/routeList.vue";
import routeAdd from "./components/routeAdd.vue";
// import routeGet from "./components/routeGets.vue";
import RouteGet from "./RouteGet.vue";
// ID
const planId = ref('')
// ID
const hxId = ref('')
//
const showType = ref('routeList');
const setShowType = (val, id, hx) => {
showType.value = val
if (id) {
planId.value = id
}
if (hxId) {
hxId.value = hx
} else {
hxId.value = ""
}
}
const cesiumContainer = ref<HTMLElement>();
// Cesium
const { viewer, viewTool, markerTool, modelTool, coordinatePicker, drawingTool, isInitialized } = useCesium('cesiumContainer');
//
const {
currentView,
cameraInfo,
viewOptions,
landmarkOptions,
setView,
addLandmarks,
clearLandmarks,
startCameraTracking
} = useCesiumTools(viewTool, markerTool);
//
const {
models,
selectedModel,
isModelLoading,
predefinedModels,
loadChongqingLandmarks,
loadPredefinedModel,
selectModel,
deselectModel,
updateModelScale,
updateModelPosition,
removeModel,
clearAllModels,
flyToModel
} = useModelManager(modelTool);
//
const {
isPickerEnabled,
pickHistory,
lastPickResult,
enablePicker,
disablePicker,
togglePicker,
clearHistory,
copyToClipboard,
formatCoordinate
} = useCoordinatePicker(coordinatePicker);
//
const {
isDrawing,
currentDrawingType,
drawings,
selectedDrawing,
drawingInfo,
drawingOptions,
startCircleDrawing,
startPolygonDrawing,
cancelDrawing,
selectDrawing,
deselectDrawing,
removeDrawing,
clearAllDrawings,
flyToDrawing,
updateDrawingOptions,
getDrawingStatus,
getDrawingInfoText,
printSelectedDrawingInfo,
printAllDrawingsInfo,
printDrawingInfo,
exportSelectedDrawingAsText,
importAirspaceData,
importFromFile,
importFromGeoJSON,
getImportStatistics,
isEditing,
editingDrawing,
startEditing,
finishEditing,
cancelEditing,
} = useDrawingManager(drawingTool);
//
const handlePrintSelectedDrawingInfo = () => {
console.log('执行打印选中空域');
printSelectedDrawingInfo();
};
const handlePrintAllDrawingsInfo = () => {
console.log('执行打印所有空域');
printAllDrawingsInfo();
};
const handlePrintDrawingInfo = (id: string) => {
console.log('执行打印指定空域:', id);
printDrawingInfo(id);
};
const handleExportDrawingInfo = () => {
const text = exportSelectedDrawingAsText();
if (text) {
navigator.clipboard.writeText(text).then(() => {
console.log('空域信息已复制到剪贴板');
//
}).catch(err => {
console.error('复制失败:', err);
});
}
};
//
const addPredefinedModel = async (modelType: string, position: any) => {
const modelId = `model_${Date.now()}`;
await loadPredefinedModel(modelId, modelType as any, position);
};
//
const copyCoordinate = (result: any) => {
copyToClipboard(result);
//
console.log('坐标已复制:', formatCoordinate(result));
};
//
const flyToCoordinate = (result: any) => {
if (viewTool.value) {
viewTool.value.flyTo(
result.longitude,
result.latitude,
result.height + 100, // 100
1.5
);
}
};
//
const updateCircleOption = (key: string, value: any) => {
updateDrawingOptions('circle', { [key]: value });
};
//
const updatePolygonOption = (key: string, value: any) => {
updateDrawingOptions('polygon', { [key]: value });
};
//
const copyDrawingInfo = () => {
if (drawingInfo.value) {
const text = getDrawingInfoText();
navigator.clipboard.writeText(text).then(() => {
console.log('绘图信息已复制到剪贴板');
}).catch(err => {
console.error('复制失败:', err);
});
}
};
//
const flyToSelectedDrawing = () => {
if (selectedDrawing.value && drawingTool.value) {
drawingTool.value.flyToDrawing(selectedDrawing.value);
}
};
// const copyDrawingInfo = () => {
// if (drawingInfo.value) {
// const text = getDrawingInfoText();
// navigator.clipboard.writeText(text).then(() => {
// console.log('');
// }).catch(err => {
// console.error(':', err);
// });
// }
// };
// //
// const flyToSelectedDrawing = () => {
// if (selectedDrawing.value && drawingTool.value) {
// drawingTool.value.flyToDrawing(selectedDrawing.value);
// }
// };
//
const handleImportComplete = (result: any) => {
console.log('导入完成:', result);
//
if (result.success) {
showMessage(`成功导入 ${result.importedIds.length} 个空域`, 'success');
} else {
showMessage('导入失败', 'error');
}
};
//
const showMessage = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
// ...
};
onMounted(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
if (isEditing.value) {
cancelEditing();
}
}
};
document.addEventListener('keydown', handleKeyDown);
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
});
//
setTimeout(() => {
startCameraTracking();
}, 2000);
});
</script>
<style scoped>
.cesium-container {
position: relative;
width: 100%;
height: 100vh;
}
#cesiumContainer {
width: 100%;
height: 100%;
}
</style>

View File

@ -328,19 +328,6 @@ import { ref } from 'vue';
import * as Cesium from 'cesium';
import type { DrawingResult } from './components/DrawingTool';
// const props = defineProps<{
// isDrawing: boolean;
// currentDrawingType: string | null;
// drawings: Map<string, DrawingResult>;
// selectedDrawing: string | null;
// drawingInfo: any;
// drawingOptions: any;
// getDrawingStatus: () => string;
// getDrawingInfoText: () => string;
// printSelectedDrawingInfo: () => void;
// printAllDrawingsInfo: () => void;
// exportSelectedDrawingAsText: () => string;
// }>();
// props
const props = defineProps<{

View File

@ -124,7 +124,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { API_GET_list, API_GET_byId, API_DELETE_delPlotting, API_POST_addPlotting, API_PUT_updatePlotting,} from "../../api/uav/plotting";
import { id } from 'element-plus/es/locale/index.js';
const props = defineProps<{
importFromFile: (file: File, options: any) => Promise<string[]>;
@ -204,6 +203,7 @@ const executeJsonImport = () => {
importResult.value = null;
try {
console.log("jsonInput.value:",jsonInput.value)
const data = JSON.parse(jsonInput.value);
console.log("data:",data)
const importedIds = props.importAirspaceData(data, importOptions.value);
@ -235,6 +235,34 @@ const loadData= () => {
const jsonData = item.plottingJson
console.log("此间数据如下111"+jsonData)
jsonInput.value = jsonData;
if (!jsonInput.value.trim()) return;
isImporting.value = true;
importResult.value = null;
try {
console.log("jsonInput.value:",jsonInput.value)
const data = JSON.parse(jsonInput.value);
console.log("data:",data)
const importedIds = props.importAirspaceData(data, importOptions.value);
const statistics = props.getImportStatistics();
importResult.value = {
success: true,
message: `成功导入 ${importedIds.length} 个空域`,
importedIds,
statistics
};
emit('import-complete', importResult.value);
} catch (error) {
importResult.value = {
success: false,
message: `JSON解析失败: ${error}`
};
} finally {
isImporting.value = false;
}
})
} else {

View File

@ -0,0 +1,782 @@
<!-- src/components/ImportToolPanel.vue -->
<template>
<div class="el-container flexD dark">
<div class="el-header">
<div style="display: flex;align-items: center;">
<h2 class="title" style="display: inline-block;margin-left: 8px;">创建空域</h2>
</div>
</div>
<div class="el-main">
<el-form :model="form" :rules="rules" ref="formRef" label-position="top">
<el-form-item label="空域名称" prop="name">
<el-input v-model.trim="form.name" placeholder="请输入空域名称(建议:分局名称+时间+编号)" maxlength="15"></el-input>
</el-form-item>
<el-form-item label="所属战区" prop="militaryAreaName">
<el-select v-model="form.militaryAreaName" placeholder="请选择所属战区" style="width: 100%;">
<el-option v-for="item in slectmilitaryAreaList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="空域类型" prop="airspaceType">
<el-select v-model="form.airspaceType" placeholder="请选择空域类型" style="width: 100%;" >
<el-option v-for="item in slectAirspaceTypeList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="空域性制" prop="airspaceProp">
<el-select v-model="form.airspaceProp" placeholder="请选择空域性制" style="width: 100%;">
<el-option v-for="item in slectAirspacePropList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="最大高度" prop="hightMax">
<el-input v-model.trim="form.hightMax" maxlength="15"></el-input>
</el-form-item>
<el-form-item label="开始时间" prop="planBeg">
<el-date-picker
v-model="form.planBeg"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD hh:mm:ss"
>
</el-date-picker>
</el-form-item>
<el-form-item label="截止时间" prop="planEnd">
<el-date-picker v-model="form.planEnd" type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD hh:mm:ss"
/>
</el-form-item>
</el-form>
</div>
<div class="el-footer">
<el-button type="primary" @click="submitForm" style="width: 100%;">返回</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted,reactive } from 'vue';
import { API_GET_list, API_GET_byId, API_DELETE_delPlotting, API_POST_addPlotting, API_PUT_updatePlotting,} from "../../api/uav/plotting";
import "./components/ele.dark.style.css";
import "@/assets/styles/c_style.scss"
import { eidtAirLine_c, getLineListDetial_c,getPlottingByUuid } from "./api.js";
import type { DrawingResult } from './components/DrawingTool';
const props = defineProps<{
importFromFile: (file: File, options: any) => Promise<string[]>;
importAirspaceData: (data: any[], options: any) => string[];
getImportStatistics: () => { total: number; circles: number; polygons: number };
id:string;
hxid:string;
isDrawing: boolean;
currentDrawingType: string | null;
drawings: Map<string, DrawingResult>;
selectedDrawing: string | null;
drawingInfo: any;
drawingOptions: any;
getDrawingStatus: () => string;
getDrawingInfoText: () => string;
isEditing: boolean;
editingDrawing: any;
startCircleDrawing: () => void;
startPolygonDrawing: () => void;
cancelDrawing: () => void;
startEditing: (id: string) => void;
finishEditing: () => void;
cancelEditing: () => void;
selectDrawing: (id: string) => void;
removeDrawing: (id: string) => void;
flyToDrawing: (id: string) => void;
exportAllToJSON: () => void;
clearAllDrawings: () => void;
}>();
onMounted(() => {
console.log("props:",props)
if (props.hxid) {
getDetial(props.hxid)
}
})
const emit = defineEmits<{
'start-circle-drawing': [];
'start-polygon-drawing': [];
'cancel-drawing': [];
'select-drawing': [id: string];
'deselect-drawing': [];
'remove-drawing': [id: string];
'clear-drawings': [];
'fly-to-drawing': [id: string];
'update-circle-option': [key: string, value: any];
'update-polygon-option': [key: string, value: any];
'copy-drawing-info': [];
'fly-to-selected-drawing': [id: string];
'print-selected-drawing-info': [];
'print-all-drawings-info': [];
'print-drawing-info': [id: string];
'export-drawing-info': [id: string];
'export-drawing': [id: string];
'start-editing': [id: string];
'finish-editing': [];
'cancel-editing': [];
'setShowType':[id: string];
}>();
// const emit = defineEmits(["setShowType"])
//
const formRef = ref(null)
const form = reactive({
// airlineName: '',
airlineType: 0,
waypointCount: '',
length: '',
expectedTime: '',
flyToWaylineMode: 'safely',
alt: 0,
heightType: 'relativeToStartPoint',
waypointHeadingMode: 'followWayline',
gimbalPitchMode: 'manual',
waypointTurnMode: 'coordinateTurn',
executionType: 'goHome',
uavType: '',
uavPTZ: '',
storageType: ['wide', 'zoom', 'ir'],
hightMax: '',
hightMin: '',
lat: '',
lng: '',
militaryAreaName: '',
militaryAreaId: null,
name: '',
flightRule: '',
airspaceType: '',
airspaceProp: '',
radLength: '',
startTime: null,
endTime: null,
planBeg: null,
planEnd: null,
uuid: null
})
//
const rules = reactive({
name: [{ required: true, message: '请输入名称' },],
militaryAreaName: [{ required: true, message: '请选择所属战区' },],
airspaceType: [{ required: true, message: '请选择空域类型' },],
airspaceProp: [{ required: true, message: '请选择空域性质' },],
flightRule: [{ required: true, message: '请选择飞行规则' },],
})
//
const submitForm = () => {
toList()
}
//
const toList = () => {
console.log("重置地图")
emit('setShowType', 'routeList', props.id)
}
//
const getDetial = (id) => {
getLineListDetial_c(id).then((res) => {
if (res.code === 200) {
Object.assign(form, res.data)
console.log("返回的经纬度",res.data.lat)
console.log("返回的UUID",res.data.uuid)
getPlotting(res.data.uuid)
}
});
}
const getPlotting = (id) => {
getPlottingByUuid(id).then((res) => {
if (res.code == 200) {
console.log("返回的数据0:",res)
console.log("返回的数据1:",res.data[0].data)
const jsonData = res.data[0].data
console.log("此间数据如下111"+jsonData)
jsonInput.value = jsonData;
if (!jsonInput.value.trim()) return;
isImporting.value = true;
importResult.value = null;
try {
console.log("jsonInput.value:",jsonInput.value)
const data = JSON.parse(jsonInput.value);
console.log("data:",data)
const importedIds = props.importAirspaceData(data, importOptions.value);
const statistics = props.getImportStatistics();
importResult.value = {
success: true,
message: `成功导入 ${importedIds.length} 个空域`,
importedIds,
statistics
};
// emit('import-complete', importResult.value);
} catch (error) {
importResult.value = {
success: false,
message: `JSON解析失败: ${error}`
};
} finally {
isImporting.value = false;
}
console.log("返回的数据:",res.data)
} else {
// proxy.$message.error(res.msg);
}
});
}
// const emit = defineEmits<{
// 'import-complete': [result: any];
// }>();
const fileInput = ref<HTMLInputElement>();
const selectedFile = ref<File | null>(null);
const jsonInput = ref('');
const isImporting = ref(false);
const importResult = ref<any>(null);
const importOptions = ref({
autoZoom: true,
mergeExisting: false
});
//
const triggerFileInput = () => {
fileInput.value?.click();
};
//
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files[0]) {
selectedFile.value = target.files[0];
}
};
//
const clearFile = () => {
selectedFile.value = null;
if (fileInput.value) {
fileInput.value.value = '';
}
};
//
const executeFileImport = async () => {
if (!selectedFile.value) return;
isImporting.value = true;
importResult.value = null;
try {
const importedIds = await props.importFromFile(selectedFile.value, importOptions.value);
const statistics = props.getImportStatistics();
importResult.value = {
success: true,
message: `成功导入 ${importedIds.length} 个空域`,
importedIds,
statistics
};
// emit('import-complete', importResult.value);
} catch (error) {
importResult.value = {
success: false,
message: `导入失败: ${error}`
};
isImporting.value = false;
}
};
// JSON
const executeJsonImport = () => {
if (!jsonInput.value.trim()) return;
isImporting.value = true;
importResult.value = null;
try {
console.log("jsonInput.value:",jsonInput.value)
const data = JSON.parse(jsonInput.value);
console.log("data:",data)
const importedIds = props.importAirspaceData(data, importOptions.value);
const statistics = props.getImportStatistics();
importResult.value = {
success: true,
message: `成功导入 ${importedIds.length} 个空域`,
importedIds,
statistics
};
// emit('import-complete', importResult.value);
} catch (error) {
importResult.value = {
success: false,
message: `JSON解析失败: ${error}`
};
} finally {
isImporting.value = false;
}
};
const loadData= () => {
API_GET_list().then((response) => {
if (response.code === 200) {
// message.success("");
response.rows.forEach((item) => {
const jsonData = item.plottingJson
console.log("此间数据如下111"+jsonData)
jsonInput.value = jsonData;
if (!jsonInput.value.trim()) return;
isImporting.value = true;
importResult.value = null;
try {
console.log("jsonInput.value:",jsonInput.value)
const data = JSON.parse(jsonInput.value);
console.log("data:",data)
const importedIds = props.importAirspaceData(data, importOptions.value);
const statistics = props.getImportStatistics();
importResult.value = {
success: true,
message: `成功导入 ${importedIds.length} 个空域`,
importedIds,
statistics
};
// emit('import-complete', importResult.value);
} catch (error) {
importResult.value = {
success: false,
message: `JSON解析失败: ${error}`
};
} finally {
isImporting.value = false;
}
})
} else {
// message.error(response.msg);
}
}, error => {
// message.success("");
});
executeJsonImport();
}
//
const loadSampleData = () => {
const sampleData = [
{
id: 'sample_circle_1',
type: 'circle',
coordinates: {
center: {
longitude: 106.5516,
latitude: 29.5630,
height: 500
},
radius: 2000
},
properties: {
name: '示例圆形空域1',
color: '#FF6B6B',
outlineColor: '#C44D4D'
}
},
{
id: 'sample_circle_2',
type: 'circle',
coordinates: {
center: {
longitude: 106.5716,
latitude: 29.5730,
height: 500
},
radius: 1500
},
properties: {
name: '示例圆形空域2',
color: '#4ECDC4',
outlineColor: '#3AA39C'
}
},
{
id: 'sample_polygon_1',
type: 'polygon',
coordinates: [
{ longitude: 106.5400, latitude: 29.5500, height: 300 },
{ longitude: 106.5600, latitude: 29.5500, height: 300 },
{ longitude: 106.5600, latitude: 29.5700, height: 300 },
{ longitude: 106.5400, latitude: 29.5700, height: 300 }
],
properties: {
name: '示例多边形空域1',
color: '#45B7D1',
outlineColor: '#368EA6'
}
}
];
jsonInput.value = JSON.stringify(sampleData, null, 2);
};
//
const formatExample = computed(() => {
return `{
"id": "airspace_001",
"type": "circle",
"coordinates": {
"center": {
"longitude": 106.5516,
"latitude": 29.5630,
"height": 500
},
"radius": 2000
},
"properties": {
"name": "禁飞区",
"color": "#FF0000"
}
}`;
});
</script>
<style lang="scss" scoped>
.import-tool-panel {
position: absolute;
top: 20px;
right: 220px;
background: rgba(42, 42, 42, 0.95);
color: white;
padding: 15px;
border-radius: 10px;
min-width: 320px;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.panel-header h3 {
margin: 0 0 15px 0;
font-size: 16px;
color: #fff;
}
.panel-section {
margin-bottom: 20px;
}
.panel-section h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: #ccc;
}
.file-import {
display: flex;
flex-direction: column;
gap: 10px;
}
.import-btn {
padding: 10px 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.import-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.import-btn:not(:disabled):hover {
transform: translateY(-1px);
}
.file-btn {
background: rgba(76, 175, 80, 0.3);
}
.file-btn:hover:not(:disabled) {
background: rgba(76, 175, 80, 0.5);
}
.json-btn {
background: rgba(156, 39, 176, 0.3);
}
.json-btn:hover:not(:disabled) {
background: rgba(156, 39, 176, 0.5);
}
.execute-btn {
background: rgba(255, 152, 0, 0.3);
}
.execute-btn:hover:not(:disabled) {
background: rgba(255, 152, 0, 0.5);
}
.file-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
font-size: 12px;
}
.clear-file-btn {
background: none;
border: none;
color: #ff4d4f;
cursor: pointer;
font-size: 16px;
padding: 0 4px;
}
.import-options {
display: flex;
flex-direction: column;
gap: 8px;
margin: 10px 0;
}
.option-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #ccc;
cursor: pointer;
}
.option-checkbox input {
margin: 0;
}
.data-import {
display: flex;
flex-direction: column;
gap: 10px;
}
.json-input {
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
font-family: 'Courier New', monospace;
font-size: 11px;
resize: vertical;
}
.json-input::placeholder {
color: #888;
}
.import-result {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
border-radius: 6px;
font-size: 12px;
}
.import-result.success {
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.4);
}
.import-result.error {
background: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.4);
}
.result-icon {
font-size: 16px;
}
.result-content {
flex: 1;
}
.result-message {
font-weight: bold;
margin-bottom: 4px;
}
.result-details,
.result-statistics {
font-size: 11px;
color: #ccc;
margin-top: 2px;
}
.sample-data {
display: flex;
flex-direction: column;
gap: 8px;
}
.sample-btn {
padding: 8px 12px;
background: rgba(33, 150, 243, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
cursor: pointer;
font-size: 12px;
}
.sample-btn:hover {
background: rgba(33, 150, 243, 0.5);
}
.sample-description {
font-size: 11px;
color: #ccc;
line-height: 1.4;
}
.help-section {
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 15px;
}
.format-list {
list-style: none;
padding: 0;
margin: 0 0 15px 0;
}
.format-list li {
font-size: 11px;
margin-bottom: 6px;
color: #ccc;
line-height: 1.4;
}
.format-list strong {
color: #fff;
}
.format-example {
background: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 4px;
font-size: 10px;
color: #fff;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.el-container {
position: absolute;
left: 0;
top: 0;
right: auto;
bottom: 70px;
z-index: 1001;
width: 420px;
height: auto;
background: rgba(29, 37, 50, 0.85);
.el-header,
.el-footer {
background: transparent;
}
.title {
font-size: 16px;
font-weight: bold;
color: white;
}
.addBtn {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.editBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 0 10px;
cursor: pointer;
background: #414243;
&:hover {
opacity: 0.8;
}
}
}
.displayFlex {
border-bottom: 1px solid #414243;
.contentDom {
padding: 15px 0;
}
&:last-child {
border-bottom: none;
.contentDom {
padding-bottom: 0;
}
}
&:first-child {
.contentDom {
padding-top: 0;
}
}
li {
line-height: 22px;
}
}
</style>

161
src/views/cesiums/api.js Normal file
View File

@ -0,0 +1,161 @@
import request from '@/utils/request'
/**
*
* @param {*} options
* 航线规划添加项目
*/
export function addPlaning (options) {
return request({
url: `/item`,
method: 'post',
data: options,
})
}
/**
*
* @param {*} options
* 获取应用列表
*/
export function getApplicationList (data) {
return request({
url: `/app/management/findApplicationList`,
method: 'get',
params: data
})
}
/**
* @param {*} options
* 获取项目列表
*/
export function getPlaningList_c (data) {
return request({
url: `/item/list`,
method: 'get',
params: data
})
}
/**
* 新增航线
*/
// export function addAirLine_c (data) {
// return request({
// url: `/airline`,
// method: 'post',
// data: data
// })
// }
export function addAirLine_c (data) {
return request({
url: `/airspace`,
method: 'post',
data: data
})
}
/**
* 修改航线
*/
// export function eidtAirLine_c (data) {
// return request({
// url: `/airline`,
// method: 'put',
// data: data
// })
// }
export function eidtAirLine_c (data) {
return request({
url: `/airspace`,
method: 'put',
data: data
})
}
/**
* 获取航线详情
*/
// export function getLineListDetial_c (data) {
// return request({
// url: `/airline/` + data,
// method: 'GET',
// })
// }
export function getLineListDetial_c (data) {
return request({
url: `/airspace/` + data,
method: 'GET',
})
}
export function getPlottingByUuid (data) {
return request({
url: '/plot/selectPlotByUuid/'+ data,
method: 'get'
})
}
/**
* 获取无人机类型
*/
export function getSelectUavTypeList_c () {
return request({
url: '/type/selectUavTypeList',
method: 'GET',
})
}
/**
* 获取航线列表
*/
// export function getLineList_c (data) {
// return request({
// url: `/airline/list`,
// method: 'GET',
// params: data
// })
// }
export function getLineList_c (data) {
return request({
url: `/airspace/list`,
method: 'GET',
params: data
})
}
export function API_PUT_archiveAirline (id) {
return request({
url: '/airline/archive/' + id,
method: 'put'
})
}
export function API_PUT_removeAirline (id) {
return request({
// url: '/airline/remove/' + id,
url: '/airspace/' + id,
method: 'DELETE'
})
}
export function API_PUT_removeAspace (id) {
return request({
url: '/airspace/' + id,
method: 'DElETE'
})
}
//查询分组
export function getGroupList () {
return request({
url: `/airline/groups/groups`,
method: 'GET',
})
}

View File

@ -0,0 +1,80 @@
<!-- src/App.vue -->
<template>
<div id="app">
<CesiumViewers />
<div >
<!-- 空域列表 -->
<!-- <routeList v-if="showType=='routeList'" @setShowType="setShowType" :id="planId" /> -->
<!-- 空域新增 -->
<!-- <routeAdd v-if="showType=='routeAdd'" @setShowType="setShowType" :id="planId" :hxid="hxId" /> -->
<!-- 空域查看 -->
<!-- <routeGet v-if="showType=='routeGet'" @setShowType="setShowType" :id="planId" :hxid="hxId" /> -->
</div>
</div>
</template>
<script setup lang="ts">
import CesiumViewers from './CesiumViewers.vue';
import routeList from "./components/routeList.vue";
import routeAdd from "./components/routeAdd.vue";
import routeGet from "./components/routeGet.vue";
import { ref } from 'vue';
const props = defineProps<{
importFromFile: (file: File, options: any) => Promise<string[]>;
importAirspaceData: (data: any[], options: any) => string[];
getImportStatistics: () => { total: number; circles: number; polygons: number };
}>();
// ID
const planId = ref('')
// ID
const hxId = ref('')
//
const showType = ref('routeList');
const setShowType = (val, id, hx) => {
showType.value = val
if (id) {
planId.value = id
}
if (hxId) {
hxId.value = hx
} else {
hxId.value = ""
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.app-header h1 {
margin-bottom: 8px;
font-size: 24px;
}
.app-header p {
opacity: 0.9;
font-size: 14px;
}
.app-main {
height: calc(100vh - 80px);
}
</style>

View File

@ -0,0 +1,92 @@
.dark {
color-scheme: dark;
--el-color-primary: #409eff;
--el-color-primary-light-3: #3375b9;
--el-color-primary-light-5: #2a598a;
--el-color-primary-light-7: #213d5b;
--el-color-primary-light-8: #1d3043;
--el-color-primary-light-9: #18222c;
--el-color-primary-dark-2: #66b1ff;
--el-color-success: #67c23a;
--el-color-success-light-3: #4e8e2f;
--el-color-success-light-5: #3e6b27;
--el-color-success-light-7: #2d481f;
--el-color-success-light-8: #25371c;
--el-color-success-light-9: #1c2518;
--el-color-success-dark-2: #85ce61;
--el-color-warning: #e6a23c;
--el-color-warning-light-3: #a77730;
--el-color-warning-light-5: #7d5b28;
--el-color-warning-light-7: #533f20;
--el-color-warning-light-8: #3e301c;
--el-color-warning-light-9: #292218;
--el-color-warning-dark-2: #ebb563;
--el-color-danger: #f56c6c;
--el-color-danger-light-3: #b25252;
--el-color-danger-light-5: #854040;
--el-color-danger-light-7: #582e2e;
--el-color-danger-light-8: #412626;
--el-color-danger-light-9: #2b1d1d;
--el-color-danger-dark-2: #f78989;
--el-color-error: #f56c6c;
--el-color-error-light-3: #b25252;
--el-color-error-light-5: #854040;
--el-color-error-light-7: #582e2e;
--el-color-error-light-8: #412626;
--el-color-error-light-9: #2b1d1d;
--el-color-error-dark-2: #f78989;
--el-color-info: #909399;
--el-color-info-light-3: #6b6d71;
--el-color-info-light-5: #525457;
--el-color-info-light-7: #393a3c;
--el-color-info-light-8: #2d2d2f;
--el-color-info-light-9: #202121;
--el-color-info-dark-2: #a6a9ad;
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36),
0px 8px 20px rgba(0, 0, 0, 0.72);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72),
0px 12px 32px #000000, 0px 8px 16px -8px #000000;
--el-bg-color-page: #0a0a0a;
--el-bg-color: #141414;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #8d9095;
--el-text-color-disabled: #6c6e72;
--el-border-color-darker: #636466;
--el-border-color-dark: #58585b;
--el-border-color: #d9d9d9;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
--el-fill-color-darker: #424243;
--el-fill-color-dark: #39393a;
--el-fill-color: #303030;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1d1d;
--el-fill-color-extra-light: #191919;
--el-fill-color-blank: transparent;
--el-mask-color: rgba(0, 0, 0, 0.8);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
}
.dark .el-button {
--el-button-disabled-text-color: rgba(255, 255, 255, 0.5);
}
.dark .el-card {
--el-card-bg-color: var(--el-bg-color-overlay);
}
.dark .el-empty {
--el-empty-fill-color-0: var(--el-color-black);
--el-empty-fill-color-1: #4b4b52;
--el-empty-fill-color-2: #36383d;
--el-empty-fill-color-3: #1e1e20;
--el-empty-fill-color-4: #262629;
--el-empty-fill-color-5: #202124;
--el-empty-fill-color-6: #212224;
--el-empty-fill-color-7: #1b1c1f;
--el-empty-fill-color-8: #1c1d1f;
--el-empty-fill-color-9: #18181a;
}

View File

@ -0,0 +1,564 @@
<!--
航线规划-航线列表
crate by churl
2023-3-6
-->
<template>
<div class="el-container flexD dark">
<div class="el-header">
<div style="display: flex;align-items: center;">
<!-- <el-popconfirm title="返回后内容不会保存;确定返回吗?" @confirm="toList()">
<template #reference>
<el-icon size="24px" class="addBtn">
<ArrowLeftBold />
</el-icon>
</template>
</el-popconfirm> -->
<h2 class="title" style="display: inline-block;margin-left: 8px;">创建空域</h2>
</div>
</div>
<div class="el-main">
<el-form :model="form" :rules="rules" ref="formRef" label-position="top">
<el-form-item label="空域名称" prop="name">
<el-input v-model.trim="form.name" placeholder="请输入空域名称(建议:分局名称+时间+编号)"></el-input>
</el-form-item>
<el-form-item label="所属战区" prop="militaryAreaName">
<el-select v-model="form.militaryAreaName" placeholder="请选择所属战区" style="width: 100%;">
<el-option v-for="item in slectmilitaryAreaList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="空域性制" prop="airspaceProp">
<el-select v-model="form.airspaceProp" placeholder="请选择空域性制" style="width: 100%;">
<el-option v-for="item in slectAirspacePropList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="飞行规则" prop="flightRule">
<el-select v-model="form.flightRule" placeholder="请选择飞行规则" style="width: 100%;">
<el-option v-for="item in slectFlightRuleList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item> -->
<!--
<div class="marginBottom18">
<div class="labelBtnDom">
<span class="label">申请空域起点设置 (AGL)</span>
<el-button v-if="mapWork.qfdPosition.value?.lng" size="small" type="danger"
@click="mapWork.deleteQFD">删除起点</el-button>
<el-button v-else size="small" type="success"
@click="() => { pointList_show_handel(false), mapWork.setQFD() }">设置起点</el-button>
</div>
<div class="lnglataltDom" v-if="mapWork.qfdPosition.value?.lng">
<div style="margin-bottom: 8px;">
<CLngLat :lngValue="mapWork.qfdPosition.value.lng"
:latValue="mapWork.qfdPosition.value.lat"
:rules="[{ required: true, message: '请输入经度和纬度', trigger: 'change' }]"
@change="setPosition" />
</div>
</div>
</div>
<div class="marginBottom18" v-if="form.airspaceType==2">
<div class="labelBtnDom">
<span class="label">边界点</span>
<div v-if="mapWork.qfdPosition.value">
<el-button size="small" v-if="pointList_show" type="info"
@click="pointList_show_handel(false)">关闭</el-button>
<el-button size="small" v-else type="warning"
@click="pointList_show_handel(true)">设置</el-button>
</div>
<div v-else>请先设置申请空域起点</div>
</div>
</div> -->
<div>
<el-form-item label="空域类型" prop="airspaceType">
<el-select v-model="form.airspaceType" placeholder="请选择空域类型" style="width: 40%;" >
<el-option v-for="item in slectAirspaceTypeList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
<div style="margin-left: 10px;"></div>
<el-tooltip
class="box-item"
effect="dark"
content="圆"
placement="top-start"
>
<el-button class="vertical-button" v-if="form.airspaceType==1" type="primary" @click="mapWork.drawEllipse(false)">
绘制空域
<svg-icon icon-class="circle"/>
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="多边形"
placement="top-start"
>
<el-button class="vertical-button" v-if="form.airspaceType==2" type="primary" @click="mapWork.drawPolygon(false)">
绘制空域
<svg-icon icon-class="polygon"/>
</el-button>
</el-tooltip>
<el-button type="primary" v-if="form.airspaceType==1 || form.airspaceType==2 " @click="clear">清除空域</el-button>
<!-- <el-tooltip
class="box-item"
effect="dark"
content="取消绘制"
placement="top-start"
>
<el-button class="vertical-button" type="primary" @click="mapWork.endDraw()">
<el-icon>
<Close />
</el-icon>
</el-button>
</el-tooltip> -->
</el-form-item>
</div>
<!-- <el-form-item label="半径长度" prop="radLength" v-if="form.airspaceType==1">
<el-input v-model.trim="form.radLength" placeholder="请输入半径长度 单位(米)" maxlength="15"></el-input>
</el-form-item> -->
<!-- <el-form-item label="最小高度" prop="hightMin">
<el-input v-model.trim="form.hightMin" maxlength="15"></el-input>
</el-form-item> -->
<el-form-item label="最大高度" prop="hightMax">
<el-input v-model.trim="form.hightMax" maxlength="15"></el-input>
</el-form-item>
<el-form-item label="开始时间" prop="planBeg">
<el-date-picker
v-model="form.planBeg"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD hh:mm:ss"
>
</el-date-picker>
<!-- <el-input v-model.trim="form.hightMin" maxlength="15"></el-input> -->
<!-- <el-date-picker v-model="form.planBeg" value-format="YYYY-MM-DD HH:mm:SS" type="date" style="width: 100%" size="default"
@change="getLineList" /> -->
</el-form-item>
<el-form-item label="截止时间" prop="planEnd">
<!-- <el-input v-model.trim="form.hightMin" maxlength="15"></el-input> -->
<el-date-picker v-model="form.planEnd" type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD hh:mm:ss"
/>
</el-form-item>
</el-form>
</div>
<div class="el-footer">
<!-- <el-popconfirm title="提交后会覆盖之前内容;确定提交吗?" @confirm=" editForm()" v-if="props.hxid">
<template #reference>
<div>
<el-tooltip :content="form.isOriginal === 0 ? '该副本不支持修改后保存' : ''" placement="top">
<el-button :type="form.isOriginal === 0 ? 'info' : 'primary'" :disabled="form.isOriginal === 0">
提交
</el-button>
</el-tooltip>
</div>
</template>
</el-popconfirm> -->
<el-button type="primary" @click="submitForms">返回</el-button>
<el-button type="primary" v-if="props.hxid" @click="editForm">提交</el-button>
<el-button type="primary" v-else @click="submitForm">提交</el-button>
</div>
</div>
<!-- 途径点列表 -->
<!-- <pointList ref="pointListRef" :storageTypeList="storageTypeList" :storageType="form.storageType" v-if="pointList_show"
@close="pointList_show_handel" /> -->
</template>
<script setup>
import "./ele.dark.style.css";
import "@/assets/styles/c_style.scss"
// import * as mapWork from "../map.js";
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue';
import { addAirLine_c, eidtAirLine_c, getLineListDetial_c,getPlottingByUuid } from "../api";
// import pointList from "./pointList.vue";
const { proxy } = getCurrentInstance();
const props = defineProps(["id", 'hxid'])
const emit = defineEmits(["setShowType"])
//
const formRef = ref(null)
const form = reactive({
// airlineName: '',
airlineType: 0,
waypointCount: '',
length: '',
expectedTime: '',
flyToWaylineMode: 'safely',
alt: 0,
heightType: 'relativeToStartPoint',
// height: 0,
// speed: 1,
waypointHeadingMode: 'followWayline',
gimbalPitchMode: 'manual',
waypointTurnMode: 'coordinateTurn',
executionType: 'goHome',
uavType: '',
uavPTZ: '',
storageType: ['wide', 'zoom', 'ir'],
hightMax: '',
hightMin: '',
lat: '',
lng: '',
militaryAreaName: '',
militaryAreaId: null,
name: '',
flightRule: '',
airspaceType: '',
airspaceProp: '',
radLength: '',
startTime: null,
endTime: null,
planBeg: null,
planEnd: null,
uuid: null
})
//
//
const rules = reactive({
name: [{ required: true, message: '请输入名称' },],
militaryAreaName: [{ required: true, message: '请选择所属战区' },],
airspaceType: [{ required: true, message: '请选择空域类型' },],
airspaceProp: [{ required: true, message: '请选择空域性质' },],
flightRule: [{ required: true, message: '请选择飞行规则' },],
})
//
const submitForm = () => {
formRef.value.validate(async (valid) => {
if (valid) {
form.itemId = props.id
// form.uuid =mapWork.kyuuid
// console.log("form.uuID",form.uuid)
addAirLine_c(form).then((res) => {
if (res.code === 200) {
// proxy.$message.success("");
toList()
} else {
// proxy.$message.error(res.message);
}
});
} else {
return false;
}
});
}
const submitForms = () => {
toList()
}
//
const editForm = () => {
formRef.value.validate(async (valid) => {
if (valid) {
if (form.isOriginal == 0) {
return proxy.$message.error("该副本不支持修改后保存");
}
// form.uuid =mapWork.kyuuid
// console.log("form.uuID",form.uuid)
eidtAirLine_c(form).then((res) => {
if (res.code === 200) {
// proxy.$message.success("");
toList()
} else {
// proxy.$message.error(res.message);
}
});
} else {
return false;
}
});
}
//
const setPosition = (val) => {
if (val.lng && val.lat) {
mapWork.setQFDposition(val)
}
if (typeof val === 'number') {
mapWork.setQFDposition({ alt: val })
}
}
//
const changeHeight = (val) => {
form.height = val
// form.ellipsoidHeight = val.egmAlt
mapWork.commonData.height = val
}
const clear = () => {
mapWork.resethangxian()
console.log("清除地图")
}
//
const toList = () => {
// mapWork.resethangxian()
console.log("重置地图")
emit('setShowType', 'routeList', props.id)
}
//
const slectUavTypeList = [{ name: 'M30 系列', value: '67' }, { name: 'Mavic 3 行业系列', value: '77' }]
const slectmilitaryAreaList = [{ name: '东部战区', value: '1' }, { name: '南部战区', value: '2' }, { name: '西部战区', value: '3' }, { name: '北部战区', value: '4' }, { name: '中部战区', value: '5' }]
const slectAirspaceTypeList = [{ name: '圆形空域', value: '1' }, { name: '多边形空域', value: '2' }]
const slectFlightRuleList = [{ name: 'VFR', value: '1' }, { name: 'IFR', value: '2' }]
const slectAirspacePropList = [{ name: '固定空域', value: '1' }, { name: '临时空域', value: '2' }]
// const getSelectUavTypeList = (val) => {
// getSelectUavTypeList_c().then((res) => {
// slectUavTypeList.value = res.data
// });
// }
//
const payloadTypeList = computed(() => {
let arr = []
if (form.uavType == '77') {
arr = [{ name: 'Mavic 3E 相机', value: '66' }, { name: 'Mavic 3T 相机', value: '67' }]
} else if (form.uavType == '67') {
arr = [{ name: 'M30 相机', value: '52' }, { name: 'M30T 相机', value: '53' }]
} else {
arr = [{ name: '请选择无人机后选择云台', value: '' }]
}
return arr
})
//
const storageTypeList = computed(() => {
let arr = []
const arrMap = [
[() => form.uavType == '77' && form.payloadType == '66', () => arr = []],
[() => form.uavType == '77' && form.payloadType == '67', () => arr = []],
[() => form.uavType == '67' && form.payloadType == '52', () => arr = [{ value: 'wide', name: '广角照片(W)', jc: 'W' }, { value: 'zoom', name: '变焦照片(Z)', jc: 'Z' },]],
[() => form.uavType == '67' && form.payloadType == '53', () => arr = [{ value: 'wide', name: '广角照片(W)', jc: 'W' }, { value: 'zoom', name: '变焦照片(Z)', jc: 'Z' }, { value: 'ir', name: '红外照片(IR)', jc: 'IR' },]],
]
const target = arrMap.find((m) => m[0]())
if (target) {
target[1]()
}
return arr
})
watch(() => payloadTypeList.value, (val) => {
//form.payloadType = ''
}, { deep: true })
watch(() => storageTypeList.value, (val) => {
if (val && val.length) {
form.storageType = val.map(obj => obj.value)
} else {
form.storageType = []
}
}, { deep: true })
const pointList_show = ref(false)
const pointList_show_handel = (val) => {
console.log("这个val的值为",val)
pointList_show.value = val
if (!val) {
mapWork?.pointEndDraw()
}
}
//
const getDetial = (id) => {
// mapWork.loadingDomShow(true)
// console.log("",mapWork)
getLineListDetial_c(id).then((res) => {
if (res.code === 200) {
Object.assign(form, res.data)
console.log("返回的经纬度",res.data.lat)
console.log("返回的UUID",res.data.uuid)
getPlotting(res.data.uuid)
} else {
proxy.$message.error(res.message);
}
});
// mapWork.loadingDomShow(false)
}
const getPlotting = (id) => {
getPlottingByUuid(id).then((res) => {
if (res.code == 200) {
mapWork.groupLayerTreeArr.value = []
mapWork.graphicLayer;
console.log("mapWork.graphicLayer:",mapWork.graphicLayer)
mapWork.graphicLayer.getGraphicById(id)
console.log("mapWork.graphicLayer根据id获取:",mapWork.graphicLayer.getGraphicById(id))
console.log("返回的数据:",res.data)
let dirList = res.data;
let plot = [];
dirList.forEach((item, index) => {
plot = plot.concat(item.plottings)
// item.plottings.forEach((m,n) =>{
// plot.push(m)
// })
// if(item.init === 1 && item.source === 2){
// item.name = 'DroneFly'
// item.plottings.forEach((plot,inde) =>{
// plot.isShare = true
// })
// }
// if(item.init === 1 && item.source === 1){
// item.name = ''
// }
mapWork.groupLayerTreeArr.value
.push({
id: item.id,
name: item.name,
type: item.type,
appId: item.appId,
orgId: item.orgId,
// parentId: item.parentId,
// folderId: item.parentId,
// lockStatus: item.lockStatus,
// shareStatus: item.shareStatus,
showStatusWeb: item.showStatusWeb,
// source: item.source,
// init: item.init,
// couldDel: item.couldDel,
plottingStatus:item.plottingStatus,
children: item.plottings,
});
});
// mapWork.groupLayerTreeArr.value
// .push({
// id: res.data.id,
// name: res.data.name,
// type: res.data.type,
// appId: res.data.appId,
// orgId: res.data.orgId,
// // parentId: item.parentId,
// // folderId: item.parentId,
// // lockStatus: item.lockStatus,
// // shareStatus: item.shareStatus,
// showStatusWeb: res.data.showStatusWeb,
// // source: item.source,
// // init: item.init,
// // couldDel: item.couldDel,
// plottingStatus:item.plottingStatus,
// children: item.plottings,
// });
mapWork.geoJsonLayerList(res.data);
// handleLocal(res.data)
// 使 nextTick el-tree
// nextTick(() => {
// setTreeNodeSelected({ id: 9 });
// });
} else {
proxy.$message.error(res.msg);
}
});
}
//
const closedTJD = () => {
if (proxy.$refs['pointListRef']) {
console.log("🚀 ~ file: routeAdd.vue:377 ~ closedTJD ~ proxy.$refs['pointListRef']:", proxy.$refs['pointListRef'])
}
}
onMounted(() => {
if (props.hxid) {
getDetial(props.hxid)
}
})
</script>
<style lang="scss" scoped>
.el-container {
position: absolute;
left: 0;
top: 0;
right: auto;
bottom: 70px;
z-index: 1001;
width: 420px;
height: auto;
background: rgba(29, 37, 50, 0.85);
.el-header,
.el-footer {
background: transparent;
}
.title {
font-size: 16px;
font-weight: bold;
color: white;
}
.addBtn {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.editBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 0 10px;
cursor: pointer;
background: #414243;
&:hover {
opacity: 0.8;
}
}
}
.displayFlex {
border-bottom: 1px solid #414243;
.contentDom {
padding: 15px 0;
}
&:last-child {
border-bottom: none;
.contentDom {
padding-bottom: 0;
}
}
&:first-child {
.contentDom {
padding-top: 0;
}
}
li {
line-height: 22px;
}
}
</style>

View File

@ -0,0 +1,388 @@
<!--
航线规划-航线列表
crate by churl
2023-3-6
-->
<template>
<div class="el-container flexD dark">
<div class="el-header">
<div style="display: flex;align-items: center;">
<h2 class="title" style="display: inline-block;margin-left: 8px;">创建空域</h2>
</div>
</div>
<div class="el-main">
<el-form :model="form" :rules="rules" ref="formRef" label-position="top">
<el-form-item label="空域名称" prop="name">
<el-input v-model.trim="form.name" placeholder="请输入空域名称(建议:分局名称+时间+编号)" maxlength="15"></el-input>
</el-form-item>
<el-form-item label="所属战区" prop="militaryAreaName">
<el-select v-model="form.militaryAreaName" placeholder="请选择所属战区" style="width: 100%;">
<el-option v-for="item in slectmilitaryAreaList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="空域类型" prop="airspaceType">
<el-select v-model="form.airspaceType" placeholder="请选择空域类型" style="width: 100%;" >
<el-option v-for="item in slectAirspaceTypeList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="空域性制" prop="airspaceProp">
<el-select v-model="form.airspaceProp" placeholder="请选择空域性制" style="width: 100%;">
<el-option v-for="item in slectAirspacePropList" :key="item.value" :label="item.name"
:value="item.value">{{ item.name }}</el-option>
</el-select>
</el-form-item>
<el-form-item label="最大高度" prop="hightMax">
<el-input v-model.trim="form.hightMax" maxlength="15"></el-input>
</el-form-item>
<el-form-item label="开始时间" prop="planBeg">
<el-date-picker
v-model="form.planBeg"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD hh:mm:ss"
>
</el-date-picker>
</el-form-item>
<el-form-item label="截止时间" prop="planEnd">
<el-date-picker v-model="form.planEnd" type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD hh:mm:ss"
/>
</el-form-item>
</el-form>
</div>
<div class="el-footer">
<el-button type="primary" @click="submitForm" style="width: 100%;">返回</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import "./ele.dark.style.css";
import "@/assets/styles/c_style.scss"
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue';
import { eidtAirLine_c, getLineListDetial_c,getPlottingByUuid } from "../api.js";
import { useCesium } from './useCesium.js';
import { useCesiumTools } from './useCesiumTools.js';
import { useDrawingManager } from './useDrawingManager.js';
const cesiumContainer = ref<HTMLElement>();
// Cesium
const { viewer, viewTool, markerTool, modelTool, coordinatePicker, drawingTool, isInitialized } = useCesium('cesiumContainer');
// import { useDrawingManager } from './useDrawingManager'; //
const drawingManager = useDrawingManager();
//
const {
isDrawing,
currentDrawingType,
drawings,
selectedDrawing,
drawingInfo,
drawingOptions,
startCircleDrawing,
startPolygonDrawing,
cancelDrawing,
selectDrawing,
deselectDrawing,
removeDrawing,
clearAllDrawings,
flyToDrawing,
updateDrawingOptions,
getDrawingStatus,
getDrawingInfoText,
printSelectedDrawingInfo,
printAllDrawingsInfo,
printDrawingInfo,
exportSelectedDrawingAsText,
importAirspaceData,
importFromFile,
importFromGeoJSON,
getImportStatistics,
isEditing,
editingDrawing,
startEditing,
finishEditing,
cancelEditing,
} = useDrawingManager(drawingTool);
// const { proxy } = getCurrentInstance();
const props = defineProps<{
importFromFile: (file: File, options: any) => Promise<string[]>;
importAirspaceData: (data: any[], options: any) => string[];
getImportStatistics: () => { total: number; circles: number; polygons: number };
id:string;
hxid:string;
}>();
// const props = defineProps(["id", 'hxid'])
const emit = defineEmits(["setShowType"])
//
const formRef = ref(null)
const form = reactive({
// airlineName: '',
airlineType: 0,
waypointCount: '',
length: '',
expectedTime: '',
flyToWaylineMode: 'safely',
alt: 0,
heightType: 'relativeToStartPoint',
waypointHeadingMode: 'followWayline',
gimbalPitchMode: 'manual',
waypointTurnMode: 'coordinateTurn',
executionType: 'goHome',
uavType: '',
uavPTZ: '',
storageType: ['wide', 'zoom', 'ir'],
hightMax: '',
hightMin: '',
lat: '',
lng: '',
militaryAreaName: '',
militaryAreaId: null,
name: '',
flightRule: '',
airspaceType: '',
airspaceProp: '',
radLength: '',
startTime: null,
endTime: null,
planBeg: null,
planEnd: null,
uuid: null
})
//
const rules = reactive({
name: [{ required: true, message: '请输入名称' },],
militaryAreaName: [{ required: true, message: '请选择所属战区' },],
airspaceType: [{ required: true, message: '请选择空域类型' },],
airspaceProp: [{ required: true, message: '请选择空域性质' },],
flightRule: [{ required: true, message: '请选择飞行规则' },],
})
//
//
const submitForm = () => {
toList()
}
//
const toList = () => {
// mapWork.resethangxian()
// window.location.reload()
console.log("重置地图")
// mapWork.endDraw()
// mapWork.handleResetPlot()
// mapWork.destroyMap()
emit('setShowType', 'routeList', props.id)
}
//
const slectUavTypeList = [{ name: 'M30 系列', value: '67' }, { name: 'Mavic 3 行业系列', value: '77' }]
const slectmilitaryAreaList = [{ name: '东部战区', value: '1' }, { name: '南部战区', value: '2' }, { name: '西部战区', value: '3' }, { name: '北部战区', value: '4' }, { name: '中部战区', value: '5' }]
const slectAirspaceTypeList = [{ name: '圆形空域', value: '1' }, { name: '多边形空域', value: '2' }]
const slectFlightRuleList = [{ name: 'VFR', value: '1' }, { name: 'IFR', value: '2' }]
const slectAirspacePropList = [{ name: '固定空域', value: '1' }, { name: '临时空域', value: '2' }]
const pointList_show = ref(false)
const pointList_show_handel = (val) => {
console.log("这个val的值为",val)
pointList_show.value = val
if (!val) {
// mapWork?.pointEndDraw()
}
}
//
const getDetial = (id) => {
// mapWork.loadingDomShow(true)
// console.log("",mapWork)
getLineListDetial_c(id).then((res) => {
if (res.code === 200) {
Object.assign(form, res.data)
console.log("返回的经纬度",res.data.lat)
console.log("返回的UUID",res.data.uuid)
getPlotting(res.data.uuid)
} else {
message.error(res.message);
}
});
// mapWork.loadingDomShow(false)
}
const getPlotting = (id) => {
getPlottingByUuid(id).then((res) => {
if (res.code == 200) {
console.log("返回的数据0:",res)
console.log("返回的数据1:",res.data[0].data)
const data = JSON.parse(res.data[0].data);
console.log("isInitialized:",isInitialized)
const importedIds = props.importAirspaceData(data, null);
importAirspaceData(data, null);
console.log("返回的数据:",res.data)
// mapWork.groupLayerTreeArr.value = []
console.log("返回的数据:",res.data)
let dirList = res.data;
let plot = [];
dirList.forEach((item, index) => {
plot = plot.concat(item.plottings)
// item.plottings.forEach((m,n) =>{
// plot.push(m)
// })
// if(item.init === 1 && item.source === 2){
// item.name = 'DroneFly'
// item.plottings.forEach((plot,inde) =>{
// plot.isShare = true
// })
// }
// if(item.init === 1 && item.source === 1){
// item.name = ''
// }
// mapWork.groupLayerTreeArr.value
// .push({
// id: item.id,
// name: item.name,
// type: item.type,
// appId: item.appId,
// orgId: item.orgId,
// showStatusWeb: item.showStatusWeb,
// plottingStatus:item.plottingStatus,
// children: item.plottings,
// });
});
// mapWork.groupLayerTreeArr.value
// .push({
// id: res.data.id,
// name: res.data.name,
// type: res.data.type,
// appId: res.data.appId,
// orgId: res.data.orgId,
// // parentId: item.parentId,
// // folderId: item.parentId,
// // lockStatus: item.lockStatus,
// // shareStatus: item.shareStatus,
// showStatusWeb: res.data.showStatusWeb,
// // source: item.source,
// // init: item.init,
// // couldDel: item.couldDel,
// plottingStatus:item.plottingStatus,
// children: item.plottings,
// });
// mapWork.geoJsonLayerList(res.data);
handleLocal(res.data)
// 使 nextTick el-tree
// nextTick(() => {
// setTreeNodeSelected({ id: 9 });
// });
} else {
// proxy.$message.error(res.msg);
}
});
}
const handleLocal = (data) => {
console.log("走到非地图这儿了吗!",data)
// mapWork.geoCurrentGeoJsonLayers(data)
// let vectorObj = JSON.parse(data);
// console.log("vectorObj:",vectorObj)
// let layer = mapWork.graphicLayer.getGraphicById(data.uuid) || mapWork.currentGeoJsonLayer.getGraphicById(data.uuid)
//
// layer.flyTo();
};
onMounted(() => {
console.log("props:",props)
if (props.hxid) {
getDetial(props.hxid)
}
})
</script>
<style lang="scss" scoped>
.el-container {
position: absolute;
left: 0;
top: 0;
right: auto;
bottom: 70px;
z-index: 1001;
width: 420px;
height: auto;
background: rgba(29, 37, 50, 0.85);
.el-header,
.el-footer {
background: transparent;
}
.title {
font-size: 16px;
font-weight: bold;
color: white;
}
.addBtn {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.editBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 0 10px;
cursor: pointer;
background: #414243;
&:hover {
opacity: 0.8;
}
}
}
.displayFlex {
border-bottom: 1px solid #414243;
.contentDom {
padding: 15px 0;
}
&:last-child {
border-bottom: none;
.contentDom {
padding-bottom: 0;
}
}
&:first-child {
.contentDom {
padding-top: 0;
}
}
li {
line-height: 22px;
}
}
</style>

View File

@ -0,0 +1,299 @@
<!--
空域规划-空域列表
crate by churl
2023-3-6
-->
<template>
<div class="el-container flexD dark">
<div class="el-header">
<div style="display: flex;align-items: center;">
<h2 class="title" style="display: inline-block;margin-left: 8px;">空域列表</h2>
</div>
<div class="right-panel">
<el-tooltip class="box-item" effect="dark" content="新增空域" placement="bottom-end">
<el-icon size="24px" class="addBtn" @click="emit('setShowType','routeAdd', props.id)">
<CirclePlus />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="el-header">
<div>
<div>
<el-input style="width: 100%;" placeholder="请输入名称" v-model="queryParams.taskName" clearable @change="getLineList" />
</div>
<div style="margin-top:10px">
<el-date-picker v-model="queryParams.taskTime" value-format="YYYY-MM-DD" type="daterange" style="width: 100%" range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间" size="default"
@change="getLineList" />
</div>
</div>
</div>
<div class="el-main" v-loading="tableData.loading" element-loading-background="rgba(0, 0, 0, 0.3)" element-loading-text="Loading...">
<template v-for="item in tableData.list" :key="item.id">
<div class="displayFlex">
<ul class="contentDom">
<li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);">
<span>空域名称{{item.name?item.name:'--'}}</span>
</li>
<!-- <li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);"> <span>所属战区{{item.militaryAreaName?item.militaryAreaName:'--'}}</span></li> -->
<li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);"> <span>高度范围{{0+"m~"+item.hightMax}}m</span></li>
<li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);">
<span>开始时间 {{item.planBeg}} </span>
</li>
<li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);">
<span>截止时间 {{item.planEnd}} </span>
</li>
<li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);">创建时间{{item.createTime}}</li>
<!-- <li style="margin:5px 0;color: hsla(0deg, 0%, 100%, 0.45);" v-if="item.archiveFlag === '1' ">已归档</li> -->
<!-- <li>
<el-icon size="20px" color="hsla(0,0%,100%,.45)" style="float:left">
<UserFilled />
</el-icon>
<span style="margin-left:8px"> 1511796</span>
</li> -->
</ul>
<!-- <div class="editBtn" @click="getDetial(item.id)" style="padding-left: 10px">-->
<!-- <el-icon size="20px" color="#ffffff" class="enter-style">-->
<!-- <Operation />-->
<!-- </el-icon>-->
<!-- </div>-->
<div class="bottom">
<div class="btn-group">
<el-button type="primary" style="margin-top: 5px;" @click="getDetials(item.id)" >查看</el-button>
<el-button type="primary" style="margin-top: 5px;" @click="getDetial(item.id)">编辑</el-button>
<el-button type="primary" style="margin-top: 5px;" @click="handleDel(item)" >删除</el-button>
</div>
</div>
</div>
</template>
</div>
<div class="el-footer" v-if="tableData.total>0">
<el-pagination background :small="true" layout="total, prev, pager, next" :page-sizes="[10, 30, 50, 100]" :total="tableData.total" v-model:page-size="queryParams.pageSize"
v-model:currentPage="queryParams.pageNum" @size-change="getLineList" @current-change="getLineList"></el-pagination>
</div>
</div>
</template>
<script setup lang="ts">
// import { useRouter } from "vue-router/dist/vue-router";
import "./ele.dark.style.css";
import "@/assets/styles/c_style.scss"
import { ref,reactive, onMounted, getCurrentInstance } from 'vue';
import { API_PUT_archiveAirline, getLineList_c,API_PUT_removeAirline } from "../api";
const { proxy } = getCurrentInstance();
const props = defineProps(["id"])
const emit = defineEmits(["setShowType"])
//
//
const queryParams = reactive({
taskName: '',
taskTime: [],
pageSize: 10,
pageNum: 1
})
//
const tableData = reactive({
loading: true,
list: [],
total: 0,
})
//
const getLineList = () => {
let fromData = {
// itemId: props.id,
pageSize: queryParams.pageSize,
pageNum: queryParams.pageNum,
name: queryParams.taskName,
startTime: queryParams.taskTime[0] ? queryParams.taskTime[0] : '',
endTime: queryParams.taskTime[1] ? queryParams.taskTime[1] : '',
}
console.log("fromData: " , fromData);
getLineList_c(fromData).then((res) => {
tableData.loading = false
if (res.code === 200) {
tableData.list = res.rows;
tableData.total = res.total;
} else {
proxy.$message.error(res.message);
}
});
}
//
const toList = () => {
emit('setShowType', 'planList')
}
//
const getDetial = (id) => {
emit('setShowType', 'routeAdd', props.id, id)
}
//
const getDetials = (id) => {
emit('setShowType', 'routeGet', props.id, id)
}
//
const handleTask = async (row) => { //
const id = row.id;
const idName = row.airlineName
proxy.$confirm('是否确认归档空域"' + idName + '"', "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function () {
return API_PUT_archiveAirline(id);
}).then(() => {
getLineList()
proxy.$message.success(`归档成功`)
}).catch(function () { return false });
}
//
const handleDel = async (row) => { //
const id = row.id;
const idName = row.name
proxy.$confirm('是否确认删除空域"' + idName + '"', "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(function () {
return API_PUT_removeAirline(id);
}).then(() => {
getLineList()
proxy.$message.success(`删除成功`)
}).catch(function () { return false });
}
const router = useRouter()
onMounted(() => {
//
if (router.currentRoute.value.query.id) {
let id = router.currentRoute.value.query.id
emit('setShowType', 'routeAdd', props.id, id)
router.currentRoute.value.query.id = null
}else {
getLineList()
}
})
</script>
<style lang="scss" scoped>
.singleton-tooltip {
transition: transform 0.3s var(--el-transition-function-fast-bezier);
}
.center {
color: #0a0a0a;
cursor: pointer;
padding: 8px 11px;
font-size: 15px;
.el-popper.is-customized {
padding: 6px 12px;
background: linear-gradient(
90deg,
rgb(159, 229, 151),
rgb(204, 229, 129)
);
}
.el-popper.is-customized .el-popper__arrow::before {
background: linear-gradient(45deg, #b2e68d, #bce689);
right: 0;
}
}
.btn-group button {
display: block; /* 使按钮出现在彼此下方 */
margin-left: 20px;
}
.el-container {
position: absolute;
left: 0;
top: 0;
right: auto;
bottom: 10px;
z-index: 1001;
width: 420px;
height: auto;
background: rgba(29, 37, 50, 0.85);
.el-header {
background: transparent;
}
.customer-header {
background: transparent;
display: block !important; /* 使用 display: block 覆盖 flex */
}
.el-footer {
background: transparent;
display: block !important; /* 使用 display: block 覆盖 flex */
}
.title {
font-size: 16px;
font-weight: bold;
color: white; }
.addBtn {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.editBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
margin: 26px 0;
background: #414243;
}
}
.exx {
color: #ecf5ff;
}
.exx:hover {
background-color: #0a4b8e;
color: #ecf5ff;
}
.displayFlex {
border-bottom: 1px solid #414243;
.contentDom {
padding: 15px 0;
}
li {
line-height: 22px;
}
}
.close-button {
margin-left: 800px; /* 调整右侧间距,根据需要进行调整 */
}
.ImportAirlinesButton {
text-align: left; /* 将文本左对齐 */
margin-left: -160px; /* 调整左侧间距,根据第一个字段的宽度进行调整 */
}
.dialog-content {
display: flex;
justify-content: center; //
align-items: center; //
flex-direction: column; //
height: 100%; // 使100%
}
.table-container {
max-height: 550px; /* You can adjust this value based on your requirements */
overflow-y: auto; /* Enable vertical scrolling when the content overflows */
width: 100%; /* Ensure the container takes full width */
}
.el-dropdown-link {
cursor: pointer;
color: #409eff;
}
.el-icon-arrow-down {
font-size: 12px;
}
</style>

View File

@ -366,12 +366,29 @@ export function useDrawingManager(drawingTool: any) { // 修改参数类型为 a
*
*/
const importAirspaceData = (data: any[], options: ImportOptions = {}): string[] => {
console.log("drawingTool:",drawingTool)
watch(() => drawingTool?.value, (newTool) => {
if (newTool) {
newTool.setCallbacks({
onClick: (result: DrawingResult, info: DrawingInfo) => {
handleDrawingClick(result, info);
}
});
}
}, { immediate: true });
const getTool = () => {
console.log("drawingTool1:",drawingTool)
console.log("drawingTool2:",drawingTool?.value)
return drawingTool?.value;
};
const tool = getTool();
console.log("tool:",tool)
if (!tool) {
console.error('❌ 绘图工具未初始化');
return [];
}
console.log('开始导入空域数据:', data);
console.log('开始导入空域数据,数量:', data.length);
return tool.importAirspaceData(data, options);
};

View File

@ -0,0 +1,49 @@
<!-- src/App.vue -->
<template>
<div id="app">
<!-- <header class="app-header">
<h1>🌉 重庆三维地图系统</h1>
<p>基于 Vue 3 + Cesium 的三维地理信息系统</p>
</header> -->
<!-- <main class="app-main"> -->
<CesiumViewer />
<!-- </main> -->
</div>
</template>
<script setup lang="ts">
import CesiumViewer from './CesiumViewer.vue';
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.app-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.app-header h1 {
margin-bottom: 8px;
font-size: 24px;
}
.app-header p {
opacity: 0.9;
font-size: 14px;
}
.app-main {
height: calc(100vh - 80px);
}
</style>