Browse Source

实时航向,测量海里,框选平台移动,航线拆分

ctw
cuitw 2 weeks ago
parent
commit
dd42f554cc
  1. 80
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  2. 122
      ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue
  3. 1968
      ruoyi-ui/src/views/cesiumMap/index.vue
  4. 9
      ruoyi-ui/src/views/childRoom/LeftMenu.vue
  5. 142
      ruoyi-ui/src/views/childRoom/index.vue

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

@ -7,7 +7,17 @@
<span>切换选择 ({{ (pickIndex || 0) + 1 }}/{{ pickList.length }})</span>
</div>
</div>
<div class="menu-section" v-if="!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')">
<!-- 框选多平台仅批量删除 -->
<div class="menu-section" v-if="entityData && entityData.type === 'platformBoxSelection'">
<div class="menu-title">框选平台{{ entityData.count }} </div>
<div class="menu-item" @click="handleDeleteBoxSelection">
<span class="menu-icon">🗑</span>
<span>删除全部框选平台</span>
</div>
</div>
<div class="menu-section" v-if="(!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')) && (!entityData || entityData.type !== 'platformBoxSelection')">
<div class="menu-item" @click="handleDelete">
<span class="menu-icon">🗑</span>
<span>删除</span>
@ -101,6 +111,14 @@
<span class="menu-icon">📋</span>
<span>复制</span>
</div>
<div v-if="!isRouteLocked" class="menu-item" @click="handleRouteSegmentSplit">
<span class="menu-icon"></span>
<span>拆分航段</span>
</div>
<div v-if="!isRouteLocked" class="menu-item" @click="handleRouteSegmentCopy">
<span class="menu-icon">📄</span>
<span>拆分复制</span>
</div>
<div class="menu-item" @click="handleSingleRouteDeduction">
<span class="menu-icon"></span>
<span>单条航线推演</span>
@ -194,6 +212,21 @@
<span>磁方位</span>
</div>
</div>
<template v-if="toolMode === 'ranging'">
<div class="menu-item" @click="toggleRangingUnitMenu">
<span class="menu-icon">📏</span>
<span>距离单位</span>
<span class="menu-value">{{ rangingDistanceUnit === 'nm' ? '海里' : '公里' }}</span>
</div>
<div class="sub-menu" v-if="showRangingUnitMenu">
<div class="sub-menu-item" @click="selectRangingUnit('km')" :class="{ active: rangingDistanceUnit === 'km' }">
<span>公里km</span>
</div>
<div class="sub-menu-item" @click="selectRangingUnit('nm')" :class="{ active: rangingDistanceUnit === 'nm' }">
<span>海里1 海里 = 1852 </span>
</div>
</div>
</template>
</div>
<!-- 点特有选项 -->
@ -568,6 +601,14 @@ export default {
powerZoneVisible: {
type: Boolean,
default: true
},
toolMode: {
type: String,
default: 'airspace'
},
rangingDistanceUnit: {
type: String,
default: 'km'
}
},
data() {
@ -583,6 +624,7 @@ export default {
showOpacityPicker: false,
showFontSizePicker: false,
showBearingTypeMenu: false,
showRangingUnitMenu: false,
presetColors: [
'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
@ -632,6 +674,10 @@ export default {
this.$emit('delete')
},
handleDeleteBoxSelection() {
this.$emit('delete-box-selected-platforms')
},
handleAdjustPosition() {
this.$emit('adjust-airspace-position')
},
@ -690,6 +736,14 @@ export default {
this.$emit('copy-route')
},
handleRouteSegmentSplit() {
this.$emit('route-segment-split')
},
handleRouteSegmentCopy() {
this.$emit('route-segment-copy')
},
handleSingleRouteDeduction() {
this.$emit('single-route-deduction', this.entityData.routeId)
},
@ -782,6 +836,8 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showBearingTypeMenu = false
this.showRangingUnitMenu = false
this.showColorPickerFor = property
}
},
@ -800,6 +856,8 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showBearingTypeMenu = false
this.showRangingUnitMenu = false
this.showWidthPicker = true
}
},
@ -894,6 +952,7 @@ export default {
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showRangingUnitMenu = false
}
},
@ -901,6 +960,25 @@ export default {
//
this.$emit('update-property', 'bearingType', bearingType)
this.showBearingTypeMenu = false
},
toggleRangingUnitMenu() {
this.showRangingUnitMenu = !this.showRangingUnitMenu
if (this.showRangingUnitMenu) {
this.showColorPickerFor = null
this.showWidthPicker = false
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showBearingTypeMenu = false
}
},
selectRangingUnit(unit) {
if (unit === 'km' || unit === 'nm') {
this.$emit('ranging-distance-unit', unit)
}
this.showRangingUnitMenu = false
}
}
}

122
ruoyi-ui/src/views/cesiumMap/MapScreenDomLabels.vue

@ -0,0 +1,122 @@
<template>
<div class="map-screen-dom-labels-layer">
<div
v-for="item in items"
v-show="item.visible"
:key="item.id"
class="map-screen-dom-label"
:class="item.themeClass"
:style="item.wrapperStyle"
>
<template v-if="item.kind === 'platform'">
<div
class="map-screen-dom-label__platform-name"
:style="{ fontSize: item.titleSizePx + 'px' }"
>
{{ item.name }}
</div>
<div
class="map-screen-dom-label__platform-stats"
:style="{ fontSize: item.statSizePx + 'px' }"
>
h: {{ item.altitude }}m &nbsp; v: {{ item.speed }}km/h &nbsp; s: {{ item.heading }}°
</div>
</template>
<template v-else-if="item.kind === 'waypoint' || item.kind === 'mapText'">
<span class="map-screen-dom-label__transparent-text" :style="item.transparentTextStyle">{{ item.text }}</span>
</template>
<template v-else>
<span
class="map-screen-dom-label__plain"
:style="{ whiteSpace: item.multiline ? 'pre-line' : 'nowrap' }"
>{{ item.text }}</span>
</template>
</div>
</div>
</template>
<script>
export default {
name: 'MapScreenDomLabels',
props: {
items: {
type: Array,
default: () => []
}
}
}
</script>
<style scoped>
.map-screen-dom-labels-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9998;
overflow: hidden;
}
.map-screen-dom-label {
position: absolute;
pointer-events: none;
white-space: nowrap;
}
/* 与 HoverTooltip 一致的深色提示条 */
.map-screen-dom-label--tooltip {
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
line-height: 1.35;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
/* 飞机平台标牌:白底黑字、机名蓝色,尺寸紧凑 */
.map-screen-dom-label--platform-card {
background: rgba(255, 255, 255, 0.96);
color: #1a1a1a;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
line-height: 1.25;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
.map-screen-dom-label--platform {
min-width: 0;
text-align: center;
}
.map-screen-dom-label__platform-name {
font-weight: 600;
line-height: 1.2;
margin-bottom: 2px;
color: #0078ff;
}
.map-screen-dom-label__platform-stats {
font-weight: 400;
color: #1a1a1a;
}
/* 航点 / 插入文字 / 空域威力区命名:透明底,描边式字 */
.map-screen-dom-label--maptext {
background: transparent;
padding: 0 2px;
border-radius: 0;
box-shadow: none;
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
line-height: 1.2;
}
.map-screen-dom-label__transparent-text {
display: inline-block;
}
</style>

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

File diff suppressed because it is too large

9
ruoyi-ui/src/views/childRoom/LeftMenu.vue

@ -29,7 +29,7 @@
v-for="item in localMenuItems"
:key="item.id"
class="menu-item"
:class="{ active: activeMenu === item.id, 'dragging': isDragging, 'edit-mode': isEditMode }"
:class="{ active: isItemActive(item), 'dragging': isDragging, 'edit-mode': isEditMode }"
@click="handleSelectMenu(item)"
@contextmenu.prevent="handleRightClick(item)"
:title="item.name"
@ -143,6 +143,13 @@ export default {
return icon && typeof icon === 'string' && !icon.startsWith('el-icon-')
},
/** 框选等项通过图标编辑添加时 id 为随机字符串,activeMenu 固定为 platformBoxSelect,需按 action 匹配高亮 */
isItemActive(item) {
if (this.activeMenu === item.id) return true
if (this.activeMenu === 'platformBoxSelect' && item.action === 'platformBoxSelect') return true
return false
},
handleHide() {
this.$emit('hide')
},

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

@ -22,6 +22,8 @@
:route-locked-by-other-ids="routeLockedByOtherRouteIds"
:deduction-time-minutes="deductionMinutesFromK"
:room-id="currentRoomId"
:platform-box-select-mode="platformBoxSelectMode"
@request-exit-platform-box-select="onRequestExitPlatformBoxSelect"
@draw-complete="handleMapDrawComplete"
@route-lock-changed="handleRouteLockChanged"
@drawing-points-update="missionDrawingPointsCount = $event"
@ -30,6 +32,8 @@
@open-waypoint-dialog="handleOpenWaypointEdit"
@open-route-dialog="handleOpenRouteEdit"
@copy-route="handleCopyRoute"
@route-segment-pick="handleRouteSegmentPick"
@route-segment-placed="handleRouteSegmentPlaced"
@single-route-deduction="handleSingleRouteDeduction"
@route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt"
@ -39,6 +43,7 @@
@missile-deleted="handleMissileDeleted"
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
@platform-icons-batch-updated="onPlatformIconsBatchUpdated"
@platform-icon-removed="onPlatformIconRemoved"
@viewer-ready="onViewerReady"
@drawing-entities-changed="onDrawingEntitiesChanged"
@ -68,7 +73,7 @@
<div class="red-dot"></div>
<i class="el-icon-s-unfold icon-inside"></i>
</div>
<el-dialog title="保存新航线" :visible.sync="showNameDialog" width="30%" :append-to-body="true" @open="onSaveRouteDialogOpen" @close="tempMapPlatform = null; saveDialogScenarioId = null">
<el-dialog title="保存新航线" :visible.sync="showNameDialog" width="30%" :append-to-body="true" @open="onSaveRouteDialogOpen" @close="onSaveNewRouteDialogClose">
<el-form label-width="80px">
<el-form-item label="航线名称">
<el-input v-model="newRouteName" placeholder="例如:航线一"></el-input>
@ -559,6 +564,8 @@ export default {
return {
drawDom:false,
airspaceDrawDom:false,
/** 左侧菜单「框选平台」:多选房间平台图标并整体平移 */
platformBoxSelectMode: false,
/** 是否允许地图拖动(由顶部小手图标切换,默认关闭) */
mapDragEnabled: false,
// 线
@ -589,6 +596,8 @@ export default {
showNameDialog: false,
newRouteName: '',
tempMapPoints: [],
/** 拆分航段:保存新航线成功后从原航线删除这些航点 id */
pendingSegmentSplitAfterNewRoute: null,
/** 从平台右键「在此之前/在此之后插入航线」完成时传入,保存航线时用作 platformId */
tempMapPlatform: null,
/** 保存新航线弹窗内选择的方案 ID(弹窗内可直接选方案,无需先展开侧边) */
@ -643,6 +652,7 @@ export default {
{ id: '4t', name: '4T', icon: 'T' },
{ id: 'start', name: '冲突', icon: 'chongtu' },
{ id: 'insert', name: '平台', icon: 'el-icon-s-platform' },
{ id: 'platformBoxSelect', name: '框选平台', icon: 'el-icon-crop', action: 'platformBoxSelect' },
{ id: 'pattern', name: '空域', icon: 'ky' },
{ id: 'deduction', name: '推演', icon: 'el-icon-video-play' },
{ id: 'modify', name: '测距', icon: 'cj' },
@ -670,6 +680,7 @@ export default {
},
// -
topNavItems: [
{ id: 'platformBoxSelect', name: '框选平台', icon: 'el-icon-crop' },
{ id: 'routeEdit', name: '航线编辑', icon: 'el-icon-edit-outline' },
{ id: 'militaryMarking', name: '军事标绘', icon: 'el-icon-crop' },
{ id: 'attributeEdit', name: '属性修改', icon: 'el-icon-setting' },
@ -1584,11 +1595,58 @@ export default {
/** 复制航线已放置:用当前偏移后的航点打开「保存新航线」弹窗 */
handleRouteCopyPlaced(points) {
this.pendingSegmentSplitAfterNewRoute = null;
this.tempMapPoints = points || [];
this.tempMapPlatform = null;
this.showNameDialog = true;
},
/** 右键拆分航段/拆分复制:拉取航点后进入地图两点选范围 */
async handleRouteSegmentPick({ routeId, mode }) {
if (routeId == null) return;
if (this.routeLocked[routeId]) {
this.$message.info('该航线已上锁,请先解锁后再操作');
return;
}
if (this.isRouteLockedByOther(routeId)) {
this.$message.warning('该航线正被其他成员编辑,请稍后再试');
return;
}
try {
const res = await getRoutes(routeId);
if (res.code !== 200 || !res.data) {
this.$message.error('获取航线数据失败');
return;
}
const waypoints = res.data.waypoints || [];
if (waypoints.length < 2) {
this.$message.warning('航线航点不足,无法选取航段');
return;
}
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.startRouteSegmentPickMode === 'function') {
const routeStyle = this.parseRouteStyle(res.data.attributes);
this.$refs.cesiumMap.startRouteSegmentPickMode(routeId, waypoints, mode, routeStyle);
}
} catch (e) {
this.$message.error('获取航线数据失败');
console.error(e);
}
},
/** 航段预览已放置:打开保存弹窗;拆分航段时记下待从原航线删除的航点 id */
handleRouteSegmentPlaced(payload) {
const { mode, sourceRouteId, removedWaypointIds, points } = payload || {};
if (!points || points.length < 2) return;
this.tempMapPoints = points;
this.tempMapPlatform = null;
if (mode === 'split' && sourceRouteId != null && removedWaypointIds && removedWaypointIds.length) {
this.pendingSegmentSplitAfterNewRoute = { sourceRouteId, waypointIds: removedWaypointIds };
} else {
this.pendingSegmentSplitAfterNewRoute = null;
}
this.showNameDialog = true;
},
/** 地图上拖拽航点结束:将新位置写回数据库并刷新显示 */
async handleWaypointPositionChanged({ dbId, routeId, lat, lng, alt }) {
if (this.isRouteLockedByOther(routeId)) {
@ -2561,6 +2619,7 @@ export default {
this.drawDom = false;
return;
}
this.pendingSegmentSplitAfterNewRoute = null;
this.tempMapPoints = points;
this.tempMapPlatform = (options && (options.platformId != null || options.platform)) ? options : null;
this.showNameDialog = true;
@ -2570,6 +2629,12 @@ export default {
this.saveDialogScenarioId = this.selectedPlanId || (this.plans[0] && this.plans[0].id) || null;
},
onSaveNewRouteDialogClose() {
this.tempMapPlatform = null;
this.saveDialogScenarioId = null;
this.pendingSegmentSplitAfterNewRoute = null;
},
openAddHoldDuringDrawing() {
this.addHoldContext = { mode: 'drawing' };
this.addHoldForm = { holdType: 'hold_circle', edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: 60 };
@ -2644,6 +2709,21 @@ export default {
return;
}
const pendingSplit = this.pendingSegmentSplitAfterNewRoute;
if (pendingSplit && pendingSplit.sourceRouteId != null && Array.isArray(pendingSplit.waypointIds) && pendingSplit.waypointIds.length > 0) {
const roomIdParam = this.currentRoomId != null ? { roomId: this.currentRoomId } : {};
try {
for (const wid of pendingSplit.waypointIds) {
await delWaypoints(wid, roomIdParam);
}
this.wsConnection?.sendSyncWaypoints?.(pendingSplit.sourceRouteId);
} catch (splitErr) {
console.error(splitErr);
this.$message.warning('新航线已保存,但从原航线切除所选航段失败,请手动删除原航线上的对应航点');
}
}
this.pendingSegmentSplitAfterNewRoute = null;
// 1.
this.selectedRouteId = newRouteId;
this.selectedRouteDetails = JSON.parse(JSON.stringify(savedRoute));
@ -3359,7 +3439,8 @@ export default {
'toggleRoute': () => this.toggleRoute(),
'layerFavorites': () => this.layerFavorites(),
'routeFavorites': () => this.routeFavorites(),
'refresh': () => this.captureMapScreenshot()
'refresh': () => this.captureMapScreenshot(),
'platformBoxSelect': () => this.togglePlatformBoxSelectMenu()
}
if (actionMap[actionId]) {
@ -3367,6 +3448,27 @@ export default {
}
},
/** 左侧「框选平台」:与菜单项 id 无关,统一用 action: platformBoxSelect 触发(含图标编辑添加的随机 id 项) */
togglePlatformBoxSelectMenu() {
this.platformBoxSelectMode = !this.platformBoxSelectMode
this.activeMenu = this.platformBoxSelectMode ? 'platformBoxSelect' : ''
if (!this.platformBoxSelectMode) {
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.exitPlatformBoxSelectMode === 'function') {
this.$refs.cesiumMap.exitPlatformBoxSelectMode()
}
} else {
this.drawDom = false
this.airspaceDrawDom = false
this.isRightPanelHidden = true
}
},
/** 地图内右键退出框选(子组件 emit) */
onRequestExitPlatformBoxSelect() {
this.platformBoxSelectMode = false
this.activeMenu = ''
},
/** 截图:隐藏上下左右菜单只保留地图,用 postRender + readPixels 避免 WebGL 缓冲被清空导致黑屏 */
async captureMapScreenshot() {
const cm = this.$refs.cesiumMap
@ -4062,6 +4164,27 @@ export default {
}).catch(() => {})
}, 500)
},
/** 框选整体拖拽结束:批量写库(避免多次 emit 被防抖合并为只保存最后一个) */
onPlatformIconsBatchUpdated(list) {
if (!list || !list.length) return
const payloads = list.filter(e => e && e.serverId)
if (!payloads.length) return
Promise.all(
payloads.map(ed =>
updateRoomPlatformIcon({
id: ed.serverId,
lng: ed.lng,
lat: ed.lat,
heading: ed.heading != null ? ed.heading : 0,
iconScale: ed.iconScale != null ? ed.iconScale : 1
})
)
)
.then(() => {
this.wsConnection?.sendSyncPlatformIcons?.()
})
.catch(() => {})
},
/** 平台图标从地图删除时同步删除服务端记录 */
onPlatformIconRemoved({ serverId }) {
if (!serverId) return
@ -4218,6 +4341,21 @@ export default {
},
selectMenu(item) {
// action handleMenuAction handleMenuAction
if (item.action === 'platformBoxSelect') {
return
}
// id action
if (item.id === 'platformBoxSelect') {
this.togglePlatformBoxSelectMenu()
return
}
if (this.platformBoxSelectMode) {
this.platformBoxSelectMode = false
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.exitPlatformBoxSelectMode === 'function') {
this.$refs.cesiumMap.exitPlatformBoxSelectMode()
}
}
this.activeMenu = item.id;
if (item.action) {
this.handleMenuAction(item.action)

Loading…
Cancel
Save