Browse Source

坐标生成空域、空域上锁

mh
menghao 6 days ago
parent
commit
69fc744994
  1. 36
      ruoyi-ui/src/lang/en.js
  2. 36
      ruoyi-ui/src/lang/zh.js
  3. 34
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  4. 92
      ruoyi-ui/src/views/cesiumMap/index.vue
  5. 5
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  6. 33
      ruoyi-ui/src/views/childRoom/index.vue
  7. 402
      ruoyi-ui/src/views/dialogs/GenerateAirspaceDialog.vue

36
ruoyi-ui/src/lang/en.js

@ -51,7 +51,8 @@ export default {
},
airspace: {
powerZone: 'Power Zone',
threatZone: 'Threat Zone'
threatZone: 'Threat Zone',
generateAirspace: 'Generate Airspace'
},
options: {
@ -164,5 +165,38 @@ export default {
pleaseInputMessage: 'Please enter message content',
operationRollbackSuccess: 'Operation rollback successful',
noLogs: 'No operation logs'
},
generateAirspace: {
title: 'Generate Airspace',
shapeType: 'Shape',
polygon: 'Polygon',
rectangle: 'Rectangle',
circle: 'Circle',
sector: 'Sector',
name: 'Name',
namePlaceholder: 'Label on map (optional)',
color: 'Fill color',
borderWidth: 'Outline width',
vertices: 'Vertices',
polygonPlaceholder: 'At least 3 vertices, decimal degrees. One pair per line, or one line: (121.47,31.23), (120.15,30.28) separated by comma/semicolon',
rectangleSwCorner: 'SW corner (lon, lat)',
rectangleNeCorner: 'NE corner (lon, lat)',
cornerLonLatPlaceholder: '(longitude, latitude) e.g. (116.39, 39.90)',
centerLonLat: 'Center (lon, lat)',
radiusM: 'Radius',
radiusUnit: 'km',
startBearing: 'Start bearing (°)',
endBearing: 'End bearing (°)',
cancel: 'Cancel',
confirm: 'Create',
defaultLabel: 'Airspace',
errPolygonPoints: 'Polygon needs at least 3 valid lng,lat vertices',
errRectNumbers: 'Enter SW and NE corners as (longitude, latitude)',
errCircle: 'Enter center as (longitude, latitude) and radius in km',
errSector: 'Enter center as (longitude, latitude) and radius in km',
errBearing: 'Enter valid bearings',
needRoom: 'Enter a mission room first',
successMsg: 'Airspace created; it will be saved with the room',
errImport: 'Failed to create; check coordinates and parameters'
}
}

36
ruoyi-ui/src/lang/zh.js

@ -51,7 +51,8 @@ export default {
},
airspace: {
powerZone: '威力区',
threatZone: '威胁区'
threatZone: '威胁区',
generateAirspace: '生成空域'
},
options: {
@ -164,5 +165,38 @@ export default {
pleaseInputMessage: '请输入消息内容',
operationRollbackSuccess: '操作回滚成功',
noLogs: '暂无操作日志'
},
generateAirspace: {
title: '生成空域',
shapeType: '形状类型',
polygon: '多边形',
rectangle: '矩形',
circle: '圆形',
sector: '扇形',
name: '名称',
namePlaceholder: '地图上显示的名称(可选)',
color: '填充颜色',
borderWidth: '边线宽度',
vertices: '顶点坐标',
polygonPlaceholder: '至少 3 个顶点,十进制度。可每行一对「经度,纬度」;或一行写 (121.47,31.23)、(120.15,30.28) 用顿号分隔',
rectangleSwCorner: '西南角经纬度',
rectangleNeCorner: '东北角经纬度',
cornerLonLatPlaceholder: '(经度,纬度)例如 (116.39, 39.90)',
centerLonLat: '圆心经纬度',
radiusM: '半径',
radiusUnit: '千米',
startBearing: '起始方位角(°)',
endBearing: '终止方位角(°)',
cancel: '取消',
confirm: '生成',
defaultLabel: '空域',
errPolygonPoints: '多边形至少需要 3 个有效顶点(经度,纬度)',
errRectNumbers: '请按(经度,纬度)格式填写有效的西南角与东北角',
errCircle: '请按(经度,纬度)填写有效的圆心与半径(千米)',
errSector: '请按(经度,纬度)填写有效的圆心、半径(千米)',
errBearing: '请填写有效的方位角',
needRoom: '请先进入任务房间',
successMsg: '空域已生成,将随房间自动保存',
errImport: '生成失败,请检查坐标与参数'
}
}

34
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue

@ -18,7 +18,7 @@
</div>
</div>
<div class="menu-section" v-if="(!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')) && (!entityData || entityData.type !== 'platformBoxSelection')">
<div class="menu-section" v-if="(!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')) && (!entityData || entityData.type !== 'platformBoxSelection') && !isLockedAirspaceDrawing">
<div class="menu-item" @click="handleDelete">
<span class="menu-icon">🗑</span>
<span>删除</span>
@ -156,6 +156,15 @@
</div>
</div>
<!-- 空域多边形/矩形//扇形上锁后不可删除与修改属性 -->
<div class="menu-section" v-if="entityData && isAirspaceShapeType">
<div class="menu-title">空域</div>
<div class="menu-item" @click="handleToggleAirspaceLock">
<span class="menu-icon">{{ isAirspaceLocked ? '🔓' : '🔒' }}</span>
<span>{{ isAirspaceLocked ? '解锁' : '上锁' }}</span>
</div>
</div>
<!-- 航线上飞机显示/隐藏/编辑标牌编辑平台 -->
<div class="menu-section" v-if="entityData && entityData.type === 'routePlatform'">
<div class="menu-title">飞机标牌</div>
@ -302,8 +311,8 @@
</div>
</div>
<!-- 空域图形调整位置 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'auxiliaryLine' || entityData.type === 'arrow'">
<!-- 空域图形调整位置已上锁的多边形/矩形//扇形不可移动 -->
<div class="menu-section" v-if="entityData.type === 'auxiliaryLine' || entityData.type === 'arrow' || ((entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector') && !entityData.locked)">
<div class="menu-title">位置</div>
<div class="menu-item" @click="handleAdjustPosition">
<span class="menu-icon">📍</span>
@ -312,7 +321,7 @@
</div>
<!-- 空域图形命名多边形矩形圆形扇形 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector'">
<div class="menu-section" v-if="(entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector') && !entityData.locked">
<div class="menu-title">命名</div>
<div class="menu-item" @click="handleEditAirspaceName">
<span class="menu-icon">📝</span>
@ -322,7 +331,7 @@
</div>
<!-- 多边形特有选项 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector' || entityData.type === 'powerZone'">
<div class="menu-section" v-if="((entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector') && !entityData.locked) || entityData.type === 'powerZone'">
<div class="menu-title">填充属性</div>
<div class="menu-item" @click="editName" v-if="entityData.type === 'powerZone'">
<span class="menu-icon">📝</span>
@ -739,6 +748,17 @@ export default {
if (!this.entityData || this.entityData.routeId == null) return false
return !!this.routeLocked[this.entityData.routeId]
},
isAirspaceShapeType() {
const t = this.entityData && this.entityData.type
return ['polygon', 'rectangle', 'circle', 'sector'].includes(t)
},
isAirspaceLocked() {
return !!(this.entityData && this.entityData.locked)
},
/** 已上锁的空域:隐藏顶层「删除」 */
isLockedAirspaceDrawing() {
return this.isAirspaceShapeType && this.isAirspaceLocked
},
isHoldWaypoint() {
const ed = this.entityData || {}
if (!ed || ed.type !== 'routeWaypoint') return false
@ -766,6 +786,10 @@ export default {
this.$emit('adjust-airspace-position')
},
handleToggleAirspaceLock() {
this.$emit('toggle-airspace-lock')
},
handleEditAirspaceName() {
const name = prompt('请输入图形名称:', this.entityData.name || '')
if (name !== null) {

92
ruoyi-ui/src/views/cesiumMap/index.vue

@ -67,6 +67,7 @@
@edit-hold-speed="handleEditHoldSpeed"
@launch-missile="openLaunchMissileDialog"
@adjust-airspace-position="startAirspacePositionEdit"
@toggle-airspace-lock="toggleAirspaceLock"
@apply-whiteboard-platform-style="applyWhiteboardPlatformStyleFromMenu"
@close-menu="contextMenu.visible = false"
@delete-box-selected-platforms="deleteBoxSelectedPlatformsFromMenu"
@ -9001,6 +9002,11 @@ export default {
this.contextMenu.visible = false;
return;
}
if (this.isAirspaceDrawingLocked(entityData)) {
this.contextMenu.visible = false;
this.$message && this.$message.warning('该空域已上锁,无法调整位置');
return;
}
this.contextMenu.visible = false;
entityData.entity.show = false;
const center = this.getAirspaceCenter(entityData);
@ -9460,6 +9466,11 @@ export default {
const ctx = this.airspacePositionEditContext;
if (!ctx || !ctx.entityData) return;
const entityData = ctx.entityData;
if (this.isAirspaceDrawingLocked(entityData)) {
this.clearAirspacePositionEdit();
this.$message && this.$message.warning('该空域已上锁,无法调整位置');
return;
}
// 穿/
if (this.airspacePositionEditPreviewEntity) {
this.viewer.entities.remove(this.airspacePositionEditPreviewEntity);
@ -9617,6 +9628,28 @@ export default {
this.$emit('route-lock-changed', { routeId, locked: nextLocked });
},
/** 右键空域:上锁/解锁;上锁后不可删除、不可改属性与调整位置 */
toggleAirspaceLock() {
const ed = this.contextMenu.entityData
if (!ed || !['polygon', 'rectangle', 'circle', 'sector'].includes(ed.type)) {
this.contextMenu.visible = false
return
}
const entityData = this.allEntities.find(e => e.id === ed.id && e.type === ed.type)
if (!entityData) {
this.contextMenu.visible = false
return
}
const nextLocked = !entityData.locked
this.$set(entityData, 'locked', nextLocked)
if (this.contextMenu.entityData && this.contextMenu.entityData.id === entityData.id) {
this.$set(this.contextMenu.entityData, 'locked', nextLocked)
}
this.contextMenu.visible = false
this.$message && this.$message.success(nextLocked ? '空域已上锁,不可删除与修改' : '空域已解锁')
this.notifyDrawingEntitiesChanged()
},
/** 右键飞机:切换该航线飞机标牌的显示/隐藏 */
toggleRouteLabelVisibility() {
const ed = this.contextMenu.entityData
@ -11094,6 +11127,11 @@ export default {
deleteEntityFromContextMenu() {
if (this.contextMenu.entityData) {
const entityData = this.contextMenu.entityData
if (this.isAirspaceDrawingLocked(entityData)) {
this.$message && this.$message.warning('该空域已上锁,无法删除')
this.contextMenu.visible = false
return
}
if (entityData.type === 'platformBoxSelection') {
this.contextMenu.visible = false
return
@ -11464,6 +11502,11 @@ export default {
updateEntityProperty(property, value) {
if (this.contextMenu.entityData) {
const entityData = this.contextMenu.entityData
if (this.isAirspaceDrawingLocked(entityData)) {
this.$message && this.$message.warning('该空域已上锁,无法修改')
this.contextMenu.visible = false
return
}
//
entityData[property] = value
// 20%
@ -11628,6 +11671,10 @@ export default {
)
if (index > -1) {
const entity = this.allEntities[index]
if (this.isAirspaceDrawingLocked(entity)) {
this.$message && this.$message.warning('该空域已上锁,无法删除')
return
}
// / Redis
if (entity.type === 'point' && entity.id) {
this.ensureMapScreenDomLabelRegistry()
@ -11725,14 +11772,16 @@ export default {
// /线
if (this.toolMode === 'airspace') {
if (showConfirm) {
this.$confirm('是否清除所有空域图形?(平台与航线将保留)', '提示', {
this.$confirm('是否清除所有空域图形?(平台与航线将保留;已上锁空域将保留)', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const lockedN = (this.allEntities || []).filter(e => this.isAirspaceDrawingLocked(e)).length
this.clearDrawingEntities();
this.notifyDrawingEntitiesChanged();
this.$message({ type: 'success', message: '已清除空域图形' });
const msg = lockedN > 0 ? `已清除未上锁图形(已保留 ${lockedN} 个已上锁空域)` : '已清除空域图形'
this.$message({ type: 'success', message: msg });
}).catch(() => {});
} else {
this.clearDrawingEntities();
@ -11868,6 +11917,13 @@ export default {
getDrawingEntityTypes() {
return ['line', 'polygon', 'rectangle', 'circle', 'sector', 'auxiliaryLine', 'arrow', 'text', 'image', 'powerZone']
},
/** 多边形/矩形/圆/扇形空域是否已上锁 */
isAirspaceDrawingLocked(entity) {
if (!entity) return false
const t = entity.type
if (!['polygon', 'rectangle', 'circle', 'sector'].includes(t)) return false
return !!entity.locked
},
/** 空域/威力区图形增删时通知父组件,用于自动保存到房间(从房间加载时不触发) */
notifyDrawingEntitiesChanged() {
if (this.loadingDrawingsFromRoom) return
@ -11953,7 +12009,8 @@ export default {
opacity: entity.opacity != null ? entity.opacity : 0,
width: entity.width != null ? entity.width : 2,
borderColor: entity.borderColor || entity.color,
name: entity.name || ''
name: entity.name || '',
locked: !!entity.locked
}; break
case 'rectangle':
if (entity.points && entity.points.length >= 2) {
@ -11965,7 +12022,8 @@ export default {
opacity: entity.opacity != null ? entity.opacity : 0,
width: entity.width != null ? entity.width : 2,
borderColor: entity.borderColor || entity.color,
name: entity.name || ''
name: entity.name || '',
locked: !!entity.locked
}
} else {
data = {
@ -11974,7 +12032,8 @@ export default {
opacity: entity.opacity != null ? entity.opacity : 0,
width: entity.width != null ? entity.width : 2,
borderColor: entity.borderColor || entity.color,
name: entity.name || ''
name: entity.name || '',
locked: !!entity.locked
}
}
break
@ -11986,7 +12045,8 @@ export default {
opacity: entity.opacity != null ? entity.opacity : 0,
width: entity.width != null ? entity.width : 2,
borderColor: entity.borderColor || entity.color,
name: entity.name || ''
name: entity.name || '',
locked: !!entity.locked
}; break
case 'ellipse':
case 'hold_ellipse':
@ -12005,7 +12065,8 @@ export default {
opacity: entity.opacity != null ? entity.opacity : 0,
width: entity.width != null ? entity.width : 2,
borderColor: entity.borderColor || entity.color,
name: entity.name || ''
name: entity.name || '',
locked: !!entity.locked
}; break
case 'auxiliaryLine':
data = {
@ -12096,7 +12157,7 @@ export default {
clearDrawingEntities() {
if (!this.allEntities || !this.viewer) return
const types = this.getDrawingEntityTypes()
const toRemove = this.allEntities.filter(item => types.includes(item.type))
const toRemove = this.allEntities.filter(item => types.includes(item.type) && !this.isAirspaceDrawingLocked(item))
toRemove.forEach(item => {
try {
if (item.type === 'text' && item.entity && item.entity.id) {
@ -12119,7 +12180,7 @@ export default {
}
} catch (e) { console.warn('clearDrawingEntities:', e) }
})
this.allEntities = this.allEntities.filter(item => !types.includes(item.type))
this.allEntities = this.allEntities.filter(item => !types.includes(item.type) || this.isAirspaceDrawingLocked(item))
},
/** 从屏幕坐标获取经纬度(用于白板拖放等) */
getLatLngFromScreen(clientX, clientY) {
@ -12783,7 +12844,8 @@ export default {
opacity: polyOpacity,
width: polyWidth,
label: entityData.label || '面',
name: entityData.data.name || ''
name: entityData.data.name || '',
locked: entityData.data.locked === true
}
this.allEntities.push(polyEntityData)
if (polyEntityData.name) this.updateAirspaceEntityLabel(polyEntityData)
@ -12830,10 +12892,12 @@ export default {
opacity: rectOpacity,
width: rectWidth,
label: entityData.label || '矩形',
name: entityData.data.name || ''
name: entityData.data.name || '',
locked: entityData.data.locked === true
}
this.allEntities.push(rectEntityData)
if (rectEntityData.name) this.updateAirspaceEntityLabel(rectEntityData)
this.notifyDrawingEntitiesChanged()
return
}
case 'circle': {
@ -12874,7 +12938,8 @@ export default {
opacity: circleOpacity,
width: circleWidth,
label: entityData.label || '圆形',
name: entityData.data.name || ''
name: entityData.data.name || '',
locked: entityData.data.locked === true
}
this.allEntities.push(circleEntityData)
if (circleEntityData.name) this.updateAirspaceEntityLabel(circleEntityData)
@ -12981,7 +13046,8 @@ export default {
opacity: sectorOpacity,
width: sectorWidth,
label: entityData.label || '扇形',
name: entityData.data.name || ''
name: entityData.data.name || '',
locked: d.locked === true
}
this.allEntities.push(sectorEntityData)
if (sectorEntityData.name) this.updateAirspaceEntityLabel(sectorEntityData)

5
ruoyi-ui/src/views/childRoom/TopHeader.vue

@ -152,6 +152,7 @@
<el-dropdown-menu slot="dropdown" class="file-dropdown-menu">
<el-dropdown-item @click.native="powerZone">{{ $t('topHeader.airspace.powerZone') }}</el-dropdown-item>
<el-dropdown-item @click.native="threatZone">{{ $t('topHeader.airspace.threatZone') }}</el-dropdown-item>
<el-dropdown-item @click.native="generateAirspace">{{ $t('topHeader.airspace.generateAirspace') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@ -596,6 +597,10 @@ export default {
this.$emit('threat-zone')
},
generateAirspace() {
this.$emit('generate-airspace')
},
//
routeCalculation() {
this.$emit('route-calculation')

33
ruoyi-ui/src/views/childRoom/index.vue

@ -147,6 +147,7 @@
@load-aero-chart="loadAeroChart"
@start-power-zone-drawing="startPowerZoneDrawing"
@threat-zone="threatZone"
@generate-airspace="openGenerateAirspaceDialog"
@route-calculation="routeCalculation"
@conflict-display="conflictDisplay"
@data-materials="dataMaterials"
@ -389,6 +390,12 @@
@save="savePageLayout"
/>
<!-- 按坐标生成空域与手绘同一套 frontend_drawings 持久化 -->
<generate-airspace-dialog
v-model="showGenerateAirspaceDialog"
@confirm="handleGenerateAirspaceConfirm"
/>
<!-- 导入平台弹窗 -->
<PlatformImportDialog
:visible.sync="showImportDialog"
@ -520,6 +527,7 @@ import ExternalParamsDialog from '@/views/dialogs/ExternalParamsDialog'
import PageLayoutDialog from '@/views/dialogs/PageLayoutDialog'
import KTimeSetDialog from '@/views/dialogs/KTimeSetDialog'
import UserProfileDialog from '@/views/dialogs/UserProfileDialog'
import GenerateAirspaceDialog from '@/views/dialogs/GenerateAirspaceDialog'
import LeftMenu from './LeftMenu'
import RightPanel from './RightPanel'
import BottomLeftPanel from './BottomLeftPanel'
@ -572,6 +580,7 @@ export default {
PageLayoutDialog,
KTimeSetDialog,
UserProfileDialog,
GenerateAirspaceDialog,
LeftMenu,
RightPanel,
BottomLeftPanel,
@ -613,6 +622,7 @@ export default {
showExternalParamsDialog: false,
currentExternalParams: {},
showPageLayoutDialog: false,
showGenerateAirspaceDialog: false,
menuPosition: 'left',
showNameDialog: false,
newRouteName: '',
@ -4477,6 +4487,29 @@ export default {
this.$message.success('威胁区');
},
openGenerateAirspaceDialog() {
if (!this.currentRoomId) {
this.$message.warning(this.$t('generateAirspace.needRoom'));
return;
}
this.showGenerateAirspaceDialog = true;
},
handleGenerateAirspaceConfirm(payload) {
const map = this.$refs.cesiumMap;
if (!map || typeof map.importEntity !== 'function') {
this.$message.error(this.$t('generateAirspace.errImport'));
return;
}
try {
map.importEntity(payload);
this.$message.success(this.$t('generateAirspace.successMsg'));
} catch (e) {
console.error(e);
this.$message.error(this.$t('generateAirspace.errImport'));
}
},
//
routeCalculation() {
this.$message.success('航线计算');

402
ruoyi-ui/src/views/dialogs/GenerateAirspaceDialog.vue

@ -0,0 +1,402 @@
<template>
<el-dialog
:title="$t('generateAirspace.title')"
:visible.sync="visible"
width="540px"
append-to-body
:close-on-click-modal="false"
@closed="onClosed"
>
<el-form ref="formRef" :model="form" label-width="150px" size="small">
<el-form-item :label="$t('generateAirspace.shapeType')">
<el-select v-model="form.shapeType" style="width: 100%;">
<el-option :label="$t('generateAirspace.polygon')" value="polygon" />
<el-option :label="$t('generateAirspace.rectangle')" value="rectangle" />
<el-option :label="$t('generateAirspace.circle')" value="circle" />
<el-option :label="$t('generateAirspace.sector')" value="sector" />
</el-select>
</el-form-item>
<el-form-item :label="$t('generateAirspace.name')">
<el-input v-model="form.name" :placeholder="$t('generateAirspace.namePlaceholder')" clearable />
</el-form-item>
<el-form-item :label="$t('generateAirspace.color')">
<el-color-picker v-model="form.color" show-alpha />
</el-form-item>
<el-form-item :label="$t('generateAirspace.borderWidth')">
<el-input-number v-model="form.width" :min="1" :max="20" :step="1" controls-position="right" style="width: 100%;" />
</el-form-item>
<template v-if="form.shapeType === 'polygon'">
<el-form-item :label="$t('generateAirspace.vertices')" required>
<el-input
v-model="form.polygonText"
type="textarea"
:rows="6"
:placeholder="$t('generateAirspace.polygonPlaceholder')"
/>
</el-form-item>
</template>
<template v-else-if="form.shapeType === 'rectangle'">
<el-form-item :label="$t('generateAirspace.rectangleSwCorner')" required>
<el-input v-model="form.swCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable />
</el-form-item>
<el-form-item :label="$t('generateAirspace.rectangleNeCorner')" required>
<el-input v-model="form.neCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable />
</el-form-item>
</template>
<template v-else-if="form.shapeType === 'circle'">
<el-form-item :label="$t('generateAirspace.centerLonLat')" required>
<el-input v-model="form.centerCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable />
</el-form-item>
<el-form-item :label="$t('generateAirspace.radiusM')" required>
<div class="input-with-unit">
<el-input-number
v-model="form.radius"
:min="0.1"
:max="2000"
:step="1"
:precision="2"
controls-position="right"
class="input-with-unit__num"
/>
<span class="input-with-unit__suffix">{{ $t('generateAirspace.radiusUnit') }}</span>
</div>
</el-form-item>
</template>
<template v-else-if="form.shapeType === 'sector'">
<el-form-item :label="$t('generateAirspace.centerLonLat')" required>
<el-input v-model="form.centerCorner" :placeholder="$t('generateAirspace.cornerLonLatPlaceholder')" clearable />
</el-form-item>
<el-form-item :label="$t('generateAirspace.radiusM')" required>
<div class="input-with-unit">
<el-input-number
v-model="form.radius"
:min="0.1"
:max="2000"
:step="1"
:precision="2"
controls-position="right"
class="input-with-unit__num"
/>
<span class="input-with-unit__suffix">{{ $t('generateAirspace.radiusUnit') }}</span>
</div>
</el-form-item>
<el-form-item :label="$t('generateAirspace.startBearing')" required>
<el-input-number v-model="form.startBearing" :min="0" :max="360" :precision="2" :step="1" controls-position="right" style="width: 100%;" />
</el-form-item>
<el-form-item :label="$t('generateAirspace.endBearing')" required>
<el-input-number v-model="form.endBearing" :min="0" :max="360" :precision="2" :step="1" controls-position="right" style="width: 100%;" />
</el-form-item>
</template>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="visible = false">{{ $t('generateAirspace.cancel') }}</el-button>
<el-button type="primary" @click="submit">{{ $t('generateAirspace.confirm') }}</el-button>
</span>
</el-dialog>
</template>
<script>
function parseNum(v) {
if (v === null || v === undefined || v === '') return NaN
const n = typeof v === 'number' ? v : parseFloat(String(v).trim().replace(/,/g, ''))
return n
}
/** 从颜色选择器返回值解析填充不透明度(与 Cesium importEntity 的 data.opacity 一致) */
function extractOpacityFromColor(colorStr) {
if (colorStr == null || colorStr === '') return 0
const s = String(colorStr).trim()
const rgba = s.match(
/^rgba\s*\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/i
)
if (rgba) return Math.max(0, Math.min(1, parseFloat(rgba[1])))
if (/^rgb\s*\(/i.test(s) && !/^rgba/i.test(s)) return 1
const hsla = s.match(
/^hsla\s*\(\s*[\d.]+\s*,\s*[\d.]+%\s*,\s*[\d.]+%\s*,\s*([\d.]+)\s*\)/i
)
if (hsla) return Math.max(0, Math.min(1, parseFloat(hsla[1])))
if (s[0] === '#' && s.length === 9) {
const a = parseInt(s.slice(7, 9), 16) / 255
return Number.isFinite(a) ? Math.max(0, Math.min(1, a)) : 0
}
if (s[0] === '#') return 0
return 0
}
/** 解析单个角点:(经度,纬度) 或(经度,纬度),也支持无括号的 经度,纬度 */
function parseLonLatPair(text) {
const raw = String(text == null ? '' : text).trim()
if (!raw) return null
const parenPair =
/[((]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[,,]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[))]/
const m = raw.match(parenPair)
if (m) {
const lng = parseFloat(m[1])
const lat = parseFloat(m[2])
if (Number.isFinite(lng) && Number.isFinite(lat)) return { lng, lat }
}
const cleaned = raw.replace(/^[((]\s*|\s*[))]$/g, '').trim()
const parts = cleaned.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean)
if (parts.length >= 2) {
const lng = parseNum(parts[0])
const lat = parseNum(parts[1])
if (Number.isFinite(lng) && Number.isFinite(lat)) return { lng, lat }
}
return null
}
export default {
name: 'GenerateAirspaceDialog',
props: {
value: {
type: Boolean,
default: false
}
},
data() {
return {
form: {
shapeType: 'polygon',
name: '',
color: 'rgba(0, 138, 255, 0)',
width: 2,
polygonText: '',
swCorner: '',
neCorner: '',
centerCorner: '',
radius: 50,
startBearing: 0,
endBearing: 90
}
}
},
computed: {
visible: {
get() {
return this.value
},
set(v) {
this.$emit('input', v)
}
}
},
methods: {
onClosed() {
this.$emit('closed')
},
resetForm() {
this.form = {
shapeType: 'polygon',
name: '',
color: 'rgba(0, 138, 255, 0)',
width: 2,
polygonText: '',
swCorner: '',
neCorner: '',
centerCorner: '',
radius: 50,
startBearing: 0,
endBearing: 90
}
if (this.$refs.formRef) this.$refs.formRef.clearValidate()
},
/**
* 支持多种写法
* - 每行一对经度,纬度可中英文逗号空格
* - 一行多对用顿号/分号分隔 121.47,31.23120.15,30.28
* - 带括号121.47, 31.23(120.15, 30.28)全角/半角括号与逗号均可
*/
parsePolygonPoints(text) {
const raw = String(text || '').trim()
if (!raw) return []
const points = []
const parenPair =
/[((]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[,,]\s*([+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\s*[))]/g
let m
while ((m = parenPair.exec(raw)) !== null) {
const lng = parseFloat(m[1])
const lat = parseFloat(m[2])
if (Number.isFinite(lng) && Number.isFinite(lat)) points.push({ lng, lat })
}
if (points.length > 0) return points
const lines = raw.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
for (const line of lines) {
const segments = line.split(/[、;;]+/).map(s => s.trim()).filter(Boolean)
const segs = segments.length > 0 ? segments : [line]
for (const seg of segs) {
const cleaned = seg.replace(/^[((]\s*|\s*[))]$/g, '').trim()
const parts = cleaned.split(/[,,\s]+/).map(s => s.trim()).filter(Boolean)
if (parts.length < 2) continue
const lng = parseNum(parts[0])
const lat = parseNum(parts[1])
if (Number.isFinite(lng) && Number.isFinite(lat)) points.push({ lng, lat })
}
}
return points
},
/** 方位角:正北为 0°、顺时针增大,转为 importEntity 扇形所用的数学角(与地图绘制一致) */
bearingDegToSectorAngle(bearingDeg) {
const b = (bearingDeg % 360 + 360) % 360
return Math.PI / 2 - (b * Math.PI) / 180
},
buildPayload() {
const color = this.form.color || 'rgba(0, 138, 255, 0)'
const opacity = extractOpacityFromColor(color)
const width = this.form.width != null ? this.form.width : 2
const borderColor = color
const name = (this.form.name || '').trim()
const labelBase = {
polygon: this.$t('generateAirspace.polygon'),
rectangle: this.$t('generateAirspace.rectangle'),
circle: this.$t('generateAirspace.circle'),
sector: this.$t('generateAirspace.sector')
}
const label = name || labelBase[this.form.shapeType] || this.$t('generateAirspace.defaultLabel')
switch (this.form.shapeType) {
case 'polygon': {
const points = this.parsePolygonPoints(this.form.polygonText)
if (points.length < 3) {
this.$message.error(this.$t('generateAirspace.errPolygonPoints'))
return null
}
return {
type: 'polygon',
label,
color,
data: {
points,
opacity,
width,
borderColor,
name
}
}
}
case 'rectangle': {
const sw = parseLonLatPair(this.form.swCorner)
const ne = parseLonLatPair(this.form.neCorner)
if (!sw || !ne) {
this.$message.error(this.$t('generateAirspace.errRectNumbers'))
return null
}
const w = Math.min(sw.lng, ne.lng)
const e = Math.max(sw.lng, ne.lng)
const s = Math.min(sw.lat, ne.lat)
const n = Math.max(sw.lat, ne.lat)
return {
type: 'rectangle',
label,
color,
data: {
coordinates: { west: w, south: s, east: e, north: n },
points: [
{ lng: w, lat: s },
{ lng: e, lat: n }
],
opacity,
width,
borderColor,
name
}
}
}
case 'circle': {
const c = parseLonLatPair(this.form.centerCorner)
const radiusKm = Number(this.form.radius)
const radius = radiusKm * 1000
if (!c || !Number.isFinite(radiusKm) || radiusKm <= 0) {
this.$message.error(this.$t('generateAirspace.errCircle'))
return null
}
const lng = c.lng
const lat = c.lat
return {
type: 'circle',
label,
color,
data: {
center: { lng, lat },
radius,
opacity,
width,
borderColor,
name
}
}
}
case 'sector': {
const c = parseLonLatPair(this.form.centerCorner)
const radiusKm = Number(this.form.radius)
const radius = radiusKm * 1000
const sb = Number(this.form.startBearing)
const eb = Number(this.form.endBearing)
if (!c || !Number.isFinite(radiusKm) || radiusKm <= 0) {
this.$message.error(this.$t('generateAirspace.errSector'))
return null
}
const lng = c.lng
const lat = c.lat
if (!Number.isFinite(sb) || !Number.isFinite(eb)) {
this.$message.error(this.$t('generateAirspace.errBearing'))
return null
}
const startAngle = this.bearingDegToSectorAngle(sb)
const endAngle = this.bearingDegToSectorAngle(eb)
return {
type: 'sector',
label,
color,
data: {
center: { lng, lat },
radius,
startAngle,
endAngle,
opacity,
width,
borderColor,
name
}
}
}
default:
return null
}
},
submit() {
const payload = this.buildPayload()
if (!payload) return
this.$emit('confirm', payload)
this.visible = false
}
}
}
</script>
<style scoped>
.input-with-unit {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.input-with-unit__num {
flex: 1;
min-width: 0;
}
.input-with-unit__suffix {
flex-shrink: 0;
font-size: 13px;
color: #606266;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>
Loading…
Cancel
Save