Browse Source

航线编辑与复制

master
cuitw 1 month ago
parent
commit
9363256d79
  1. 10
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  2. 332
      ruoyi-ui/src/views/cesiumMap/index.vue
  3. 10
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  4. 121
      ruoyi-ui/src/views/childRoom/index.vue

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

@ -7,13 +7,17 @@
</div> </div>
</div> </div>
<!-- 航线上锁/解锁上锁后不可编辑 --> <!-- 航线上锁/解锁复制 -->
<div class="menu-section" v-if="entityData && entityData.type === 'route'"> <div class="menu-section" v-if="entityData && entityData.type === 'route'">
<div class="menu-title">航线编辑</div> <div class="menu-title">航线编辑</div>
<div class="menu-item" @click="handleToggleRouteLock"> <div class="menu-item" @click="handleToggleRouteLock">
<span class="menu-icon">{{ isRouteLocked ? '🔓' : '🔒' }}</span> <span class="menu-icon">{{ isRouteLocked ? '🔓' : '🔒' }}</span>
<span>{{ isRouteLocked ? '解锁' : '上锁' }}</span> <span>{{ isRouteLocked ? '解锁' : '上锁' }}</span>
</div> </div>
<div class="menu-item" @click="handleCopyRoute">
<span class="menu-icon">📋</span>
<span>复制</span>
</div>
</div> </div>
<!-- 航线上飞机显示/隐藏标牌 --> <!-- 航线上飞机显示/隐藏标牌 -->
@ -417,6 +421,10 @@ export default {
this.$emit('toggle-route-lock') this.$emit('toggle-route-lock')
}, },
handleCopyRoute() {
this.$emit('copy-route')
},
toggleColorPicker(property) { toggleColorPicker(property) {
if (this.showColorPickerFor === property) { if (this.showColorPickerFor === property) {
this.showColorPickerFor = null this.showColorPickerFor = null

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

@ -42,6 +42,7 @@
@toggle-route-lock="toggleRouteLock" @toggle-route-lock="toggleRouteLock"
@start-route-before-platform="handleStartRouteBeforePlatform" @start-route-before-platform="handleStartRouteBeforePlatform"
@start-route-after-platform="handleStartRouteAfterPlatform" @start-route-after-platform="handleStartRouteAfterPlatform"
@copy-route="handleCopyRouteFromMenu"
/> />
<!-- 定位弹窗 --> <!-- 定位弹窗 -->
@ -58,6 +59,11 @@
{{ platformIconRotateTip }} {{ platformIconRotateTip }}
</div> </div>
<!-- 航线复制预览提示 -->
<div v-if="copyPreviewWaypoints && copyPreviewWaypoints.length >= 2" class="copy-route-tip">
点击地图放置复制航线右键取消
</div>
<!-- 半径输入弹窗 --> <!-- 半径输入弹窗 -->
<radius-dialog <radius-dialog
:visible="radiusDialogVisible" :visible="radiusDialogVisible"
@ -123,6 +129,11 @@ export default {
coordinateFormat: { coordinateFormat: {
type: String, type: String,
default: 'dms' // 'decimal' 'dms' default: 'dms' // 'decimal' 'dms'
},
/** 航线上锁状态(由父组件维护,与右侧列表锁图标同步) */
routeLocked: {
type: Object,
default: () => ({})
} }
}, },
watch: { watch: {
@ -186,8 +197,7 @@ export default {
}, },
// 线routeId -> true / false // 线routeId -> true / false
routeLabelVisible: {}, routeLabelVisible: {},
// 线routeId -> true / false // 线 prop routeLocked
routeLocked: {},
// 线{ platformInfo: { platformId, platform }, mode: 'before'|'after' } // 线{ platformInfo: { platformId, platform }, mode: 'before'|'after' }
platformRouteDrawing: null, platformRouteDrawing: null,
// //
@ -240,7 +250,18 @@ export default {
// //
entityClickDebounceTimer: null, entityClickDebounceTimer: null,
lastEntityClickTime: 0, lastEntityClickTime: 0,
cameraStateBeforeEntityClick: null cameraStateBeforeEntityClick: null,
// { entity, routeId, dbId, originalAlt } pending
waypointDragging: null,
waypointDragPending: null,
WAYPOINT_DRAG_THRESHOLD_PX: 8,
/** 拖拽航点前相机 enableInputs 状态,松开时恢复 */
waypointDragCameraInputsEnabled: undefined,
lastClickWasDrag: false,
// 线线
copyPreviewWaypoints: null,
copyPreviewEntity: null,
copyPreviewMouseCartesian: null
} }
}, },
components: { components: {
@ -1246,7 +1267,7 @@ export default {
dashLength: dashLen dashLength: dashLen
}) })
: Cesium.Color.fromCssColorString(lineColor); : Cesium.Color.fromCssColorString(lineColor);
// // id 线
const originalPositions = []; const originalPositions = [];
waypoints.forEach((wp) => { waypoints.forEach((wp) => {
const lon = parseFloat(wp.lng); const lon = parseFloat(wp.lng);
@ -1254,6 +1275,8 @@ export default {
const altValue = Number(wp.alt || 5000); const altValue = Number(wp.alt || 5000);
originalPositions.push(Cesium.Cartesian3.fromDegrees(lon, lat, altValue)); originalPositions.push(Cesium.Cartesian3.fromDegrees(lon, lat, altValue));
}); });
if (!this._routeWaypointIdsByRoute) this._routeWaypointIdsByRoute = {};
this._routeWaypointIdsByRoute[routeId] = waypoints.map((wp) => wp.id);
// i 线 // i 线
const isTurnWaypointWithArc = (i) => { const isTurnWaypointWithArc = (i) => {
if (i < 1 || i >= waypoints.length - 1) return false; if (i < 1 || i >= waypoints.length - 1) return false;
@ -1291,11 +1314,11 @@ export default {
}, },
label: { label: {
text: wp.name || `WP${index + 1}`, text: wp.name || `WP${index + 1}`,
font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px Microsoft YaHei`, font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif`,
pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)), pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#333333'), fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#2c2c2c'),
outlineColor: Cesium.Color.BLACK, outlineColor: Cesium.Color.fromCssColorString('#e8e8e8'),
outlineWidth: 1, outlineWidth: 0.5,
style: Cesium.LabelStyle.FILL_AND_OUTLINE style: Cesium.LabelStyle.FILL_AND_OUTLINE
} }
}); });
@ -1349,14 +1372,11 @@ export default {
properties: { routeId: routeId }, properties: { routeId: routeId },
label: { label: {
text: labelText, text: labelText,
font: '16px Microsoft YaHei', font: '15px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif',
fillColor: Cesium.Color.fromCssColorString('#333333'), fillColor: Cesium.Color.fromCssColorString('#2c2c2c'),
outlineColor: Cesium.Color.fromCssColorString('#e0e0e0'), outlineColor: Cesium.Color.fromCssColorString('#e8e8e8'),
outlineWidth: 1, outlineWidth: 0.5,
style: Cesium.LabelStyle.FILL_AND_OUTLINE, style: Cesium.LabelStyle.FILL_AND_OUTLINE,
showBackground: true,
backgroundColor: Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.95)'),
backgroundPadding: new Cesium.Cartesian2(10, 6),
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER, horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
pixelOffset: new Cesium.Cartesian2(0, -42), pixelOffset: new Cesium.Cartesian2(0, -42),
@ -1452,11 +1472,11 @@ export default {
}, },
label: { label: {
text: wpName, text: wpName,
font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px Microsoft YaHei`, font: `${wp.labelFontSize != null ? Math.min(28, Math.max(10, Number(wp.labelFontSize))) : 14}px PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif`,
pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)), pixelOffset: new Cesium.Cartesian2(0, -Math.max(14, pixelSize + 8)),
fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#333333'), fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#2c2c2c'),
outlineColor: Cesium.Color.BLACK, outlineColor: Cesium.Color.fromCssColorString('#e8e8e8'),
outlineWidth: 1, outlineWidth: 0.5,
style: Cesium.LabelStyle.FILL_AND_OUTLINE style: Cesium.LabelStyle.FILL_AND_OUTLINE
} }
}); });
@ -1469,11 +1489,14 @@ export default {
} }
} }
} }
// 线 PolylinelineWidth/lineMaterial 线3 // 线 CallbackProperty 线
const that = this;
const routeEntity = this.viewer.entities.add({ const routeEntity = this.viewer.entities.add({
id: lineId, id: lineId,
polyline: { polyline: {
positions: finalPathPositions, positions: new Cesium.CallbackProperty(function () {
return that.getRouteLinePositionsFromWaypointEntities(routeId) || finalPathPositions;
}, false),
width: lineWidth, width: lineWidth,
material: lineMaterial, material: lineMaterial,
arcType: Cesium.ArcType.NONE, arcType: Cesium.ArcType.NONE,
@ -1486,6 +1509,41 @@ export default {
} }
} }
}, },
/** 从各航点/弧线/盘旋实体取当前位置,供主航线折线实时连线(拖拽时动态跟随) */
getRouteLinePositionsFromWaypointEntities(routeId) {
const ids = this._routeWaypointIdsByRoute && this._routeWaypointIdsByRoute[routeId];
if (!ids || !ids.length || !this.viewer) return null;
const now = Cesium.JulianDate.now();
const positions = [];
for (let i = 0; i < ids.length; i++) {
const ent = this.viewer.entities.getById(`wp_${routeId}_${ids[i]}`);
if (ent && ent.position) {
const pos = ent.position.getValue(now);
if (pos) {
positions.push(Cesium.Cartesian3.clone(pos));
continue;
}
}
const arcEnt = this.viewer.entities.getById(`arc-line-${routeId}-${i}`);
if (arcEnt && arcEnt.polyline && arcEnt.polyline.positions) {
const arr = arcEnt.polyline.positions.getValue(now);
if (arr && arr.length) {
for (let k = 0; k < arr.length; k++) positions.push(Cesium.Cartesian3.clone(arr[k]));
continue;
}
}
const holdEnt = this.viewer.entities.getById(`hold-line-${routeId}-${i}`);
if (holdEnt && holdEnt.polyline && holdEnt.polyline.positions) {
const arr = holdEnt.polyline.positions.getValue(now);
if (arr && arr.length) {
for (let k = 0; k < arr.length; k++) positions.push(Cesium.Cartesian3.clone(arr[k]));
continue;
}
}
}
return positions.length > 0 ? positions : null;
},
// //
getWaypointRadius(wp) { getWaypointRadius(wp) {
const speed = wp.speed || 800; const speed = wp.speed || 800;
@ -1883,6 +1941,7 @@ export default {
} }
if (shouldRemove) this.viewer.entities.remove(entity); if (shouldRemove) this.viewer.entities.remove(entity);
} }
if (this._routeWaypointIdsByRoute) delete this._routeWaypointIdsByRoute[routeId];
this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}` && item.id !== `route-platform-label-${routeId}`); this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}` && item.id !== `route-platform-label-${routeId}`);
}, },
/** /**
@ -2042,6 +2101,43 @@ export default {
if (this.isDrawing) return; if (this.isDrawing) return;
//
if (this.lastClickWasDrag) {
this.lastClickWasDrag = false;
return;
}
// 线
if (this.copyPreviewWaypoints && this.copyPreviewWaypoints.length > 0) {
const placePosition = this.getClickPosition(click.position);
if (placePosition) {
const carto = Cesium.Cartographic.fromCartesian(placePosition);
const placeLat = Cesium.Math.toDegrees(carto.latitude);
const placeLng = Cesium.Math.toDegrees(carto.longitude);
const first = this.copyPreviewWaypoints[0];
const firstLat = parseFloat(first.lat);
const firstLng = parseFloat(first.lng);
const dLat = placeLat - firstLat;
const dLng = placeLng - firstLng;
const isHold = (pt) => pt === 'hold_circle' || pt === 'hold_ellipse';
const points = this.copyPreviewWaypoints.map((wp, idx) => ({
name: isHold(wp.pointType || wp.point_type) ? 'HOLD' : `WP${idx + 1}`,
lat: parseFloat(wp.lat) + dLat,
lng: parseFloat(wp.lng) + dLng,
alt: wp.alt != null ? Number(wp.alt) : 5000,
speed: wp.speed != null ? wp.speed : 800,
startTime: wp.startTime || 'K+00:00:00',
turnAngle: wp.turnAngle != null ? wp.turnAngle : (idx === 0 || idx === this.copyPreviewWaypoints.length - 1 ? 0 : 45),
labelFontSize: wp.labelFontSize != null ? wp.labelFontSize : 14,
labelColor: wp.labelColor || '#333333',
...(wp.pointType && { pointType: wp.pointType }),
...(wp.holdParams != null && { holdParams: typeof wp.holdParams === 'string' ? wp.holdParams : JSON.stringify(wp.holdParams) })
}));
this.clearCopyPreview();
this.$emit('route-copy-placed', points);
}
return;
}
const pickedObject = this.viewer.scene.pick(click.position); const pickedObject = this.viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject) && pickedObject.id) { if (Cesium.defined(pickedObject) && pickedObject.id) {
const entity = pickedObject.id; const entity = pickedObject.id;
@ -2116,6 +2212,100 @@ export default {
this.lastEntityClickTime = 0; this.lastEntityClickTime = 0;
} }
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK); }, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
// pending
this.handler.setInputAction((click) => {
if (this.isDrawing || this.copyPreviewWaypoints) return;
const pickedObject = this.viewer.scene.pick(click.position);
if (!Cesium.defined(pickedObject) || !pickedObject.id) return;
const now = Cesium.JulianDate.now();
const props = pickedObject.id.properties ? pickedObject.id.properties.getValue(now) : null;
if (!props || !props.isMissionWaypoint) return;
const routeId = props.routeId && (props.routeId.getValue ? props.routeId.getValue() : props.routeId);
const dbId = props.dbId && (props.dbId.getValue ? props.dbId.getValue() : props.dbId);
if (routeId == null || dbId == null) return;
if (this.routeLocked[routeId]) return;
const entity = pickedObject.id;
const carto = Cesium.Cartographic.fromCartesian(entity.position.getValue(Cesium.JulianDate.now()));
this.waypointDragPending = {
entity,
routeId,
dbId,
originalAlt: carto.height,
startScreenX: click.position.x,
startScreenY: click.position.y
};
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
// / MOUSE_MOVE
this.handler.setInputAction((movement) => {
if (this.waypointDragPending) {
const dx = movement.endPosition.x - this.waypointDragPending.startScreenX;
const dy = movement.endPosition.y - this.waypointDragPending.startScreenY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist >= this.WAYPOINT_DRAG_THRESHOLD_PX) {
if (this.entityClickDebounceTimer) {
clearTimeout(this.entityClickDebounceTimer);
this.entityClickDebounceTimer = null;
}
this.waypointDragging = {
entity: this.waypointDragPending.entity,
routeId: this.waypointDragPending.routeId,
dbId: this.waypointDragPending.dbId,
originalAlt: this.waypointDragPending.originalAlt
};
this.waypointDragPending = null;
// 便
if (this.viewer && this.viewer.scene && this.viewer.scene.screenSpaceCameraController) {
this.waypointDragCameraInputsEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
}
const pos = this.getClickPositionWithHeight(movement.endPosition, this.waypointDragging.originalAlt);
if (pos) {
this.waypointDragging.entity.position = pos;
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
}
if (this.waypointDragPending) return;
}
if (this.waypointDragging) {
const pos = this.getClickPositionWithHeight(movement.endPosition, this.waypointDragging.originalAlt);
if (pos) {
this.waypointDragging.entity.position = pos;
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
return;
}
if (this.copyPreviewWaypoints && this.copyPreviewWaypoints.length >= 2) {
const cartesian = this.getClickPosition(movement.endPosition);
if (cartesian) {
this.copyPreviewMouseCartesian = cartesian;
this.updateCopyPreviewPolyline();
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// pending
this.handler.setInputAction((click) => {
if (this.waypointDragging) {
if (this.viewer && this.viewer.scene && this.viewer.scene.screenSpaceCameraController && this.waypointDragCameraInputsEnabled !== undefined) {
this.viewer.scene.screenSpaceCameraController.enableInputs = this.waypointDragCameraInputsEnabled;
this.waypointDragCameraInputsEnabled = undefined;
}
const entity = this.waypointDragging.entity;
const routeId = this.waypointDragging.routeId;
const dbId = this.waypointDragging.dbId;
const pos = entity.position.getValue(Cesium.JulianDate.now());
const ll = this.cartesianToLatLngAlt(pos);
if (ll) {
this.$emit('waypoint-position-changed', { dbId, routeId, lat: ll.lat, lng: ll.lng, alt: ll.alt });
}
this.lastClickWasDrag = true;
this.waypointDragging = null;
}
this.waypointDragPending = null;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
} catch (error) { } catch (error) {
console.error('地图错误:', error) console.error('地图错误:', error)
// Cesium // Cesium
@ -2131,6 +2321,12 @@ export default {
if (this.isDrawing) { if (this.isDrawing) {
return; return;
} }
// 线
if (this.copyPreviewWaypoints) {
this.clearCopyPreview();
this.contextMenu.visible = false;
return;
}
// //
this.contextMenu.visible = false; this.contextMenu.visible = false;
@ -4149,6 +4345,23 @@ export default {
lng: Cesium.Math.toDegrees(cartographic.longitude) lng: Cesium.Math.toDegrees(cartographic.longitude)
} }
}, },
/** 笛卡尔坐标转经纬高(含高度,用于航点拖拽持久化) */
cartesianToLatLngAlt(cartesian) {
if (!Cesium.defined(cartesian)) return null
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
return {
lat: Cesium.Math.toDegrees(cartographic.latitude),
lng: Cesium.Math.toDegrees(cartographic.longitude),
alt: cartographic.height
}
},
/** 屏幕坐标转笛卡尔,指定高度(用于拖拽时保持航点高度) */
getClickPositionWithHeight(pixelPosition, height) {
const cartesian = this.viewer.camera.pickEllipsoid(pixelPosition, this.viewer.scene.globe.ellipsoid)
if (!Cesium.defined(cartesian)) return null
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
return Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height != null ? height : cartographic.height)
},
degreesToDMS(decimalDegrees) { degreesToDMS(decimalDegrees) {
const degrees = Math.floor(decimalDegrees) const degrees = Math.floor(decimalDegrees)
const minutesDecimal = (decimalDegrees - degrees) * 60 const minutesDecimal = (decimalDegrees - degrees) * 60
@ -4442,6 +4655,65 @@ export default {
} }
}, },
/** 右键航线:上锁/解锁,上锁后该航线不可编辑 */ /** 右键航线:上锁/解锁,上锁后该航线不可编辑 */
/** 右键菜单「复制航线」:通知父组件,由父组件拉取航点后调用 startRouteCopyPreview */
handleCopyRouteFromMenu() {
const ed = this.contextMenu.entityData;
if (!ed || ed.type !== 'route' || ed.routeId == null) {
this.contextMenu.visible = false;
return;
}
this.contextMenu.visible = false;
this.$emit('copy-route', ed.routeId);
},
/** 开始航线复制预览:整条航线跟随鼠标,左键放置后父组件弹窗保存 */
startRouteCopyPreview(waypoints) {
if (!waypoints || waypoints.length < 2) return;
this.clearCopyPreview();
this.copyPreviewWaypoints = waypoints;
this.copyPreviewMouseCartesian = this.viewer.camera.pickEllipsoid(
new Cesium.Cartesian2(this.viewer.scene.canvas.clientWidth / 2, this.viewer.scene.canvas.clientHeight / 2),
this.viewer.scene.globe.ellipsoid
) || Cesium.Cartesian3.fromDegrees(parseFloat(waypoints[0].lng), parseFloat(waypoints[0].lat), Number(waypoints[0].alt) || 5000);
this.updateCopyPreviewPolyline();
},
/** 更新复制预览折线:以当前鼠标位置为起点偏移整条航线 */
updateCopyPreviewPolyline() {
if (!this.copyPreviewWaypoints || this.copyPreviewWaypoints.length < 2 || !this.copyPreviewMouseCartesian) return;
const first = this.copyPreviewWaypoints[0];
const firstLon = parseFloat(first.lng) * (Math.PI / 180);
const firstLat = parseFloat(first.lat) * (Math.PI / 180);
const carto = Cesium.Cartographic.fromCartesian(this.copyPreviewMouseCartesian);
const dLon = carto.longitude - firstLon;
const dLat = carto.latitude - firstLat;
const positions = this.copyPreviewWaypoints.map(wp =>
Cesium.Cartesian3.fromRadians(
parseFloat(wp.lng) * (Math.PI / 180) + dLon,
parseFloat(wp.lat) * (Math.PI / 180) + dLat,
Number(wp.alt) || 5000
)
);
if (this.copyPreviewEntity) {
this.viewer.entities.remove(this.copyPreviewEntity);
this.copyPreviewEntity = null;
}
this.copyPreviewEntity = this.viewer.entities.add({
polyline: {
positions: positions,
width: 4,
material: Cesium.Color.fromCssColorString('rgba(0, 120, 255, 0.75)'),
arcType: Cesium.ArcType.NONE
}
});
},
/** 清除航线复制预览 */
clearCopyPreview() {
if (this.copyPreviewEntity) {
this.viewer.entities.remove(this.copyPreviewEntity);
this.copyPreviewEntity = null;
}
this.copyPreviewWaypoints = null;
this.copyPreviewMouseCartesian = null;
},
toggleRouteLock() { toggleRouteLock() {
const ed = this.contextMenu.entityData; const ed = this.contextMenu.entityData;
if (!ed || ed.type !== 'route' || ed.routeId == null) { if (!ed || ed.type !== 'route' || ed.routeId == null) {
@ -4450,7 +4722,6 @@ export default {
} }
const routeId = ed.routeId; const routeId = ed.routeId;
const nextLocked = !this.routeLocked[routeId]; const nextLocked = !this.routeLocked[routeId];
this.$set(this.routeLocked, routeId, nextLocked);
this.contextMenu.visible = false; this.contextMenu.visible = false;
this.$message && this.$message.success(nextLocked ? '航线已上锁,无法修改' : '航线已解锁,可以编辑'); this.$message && this.$message.success(nextLocked ? '航线已上锁,无法修改' : '航线已解锁,可以编辑');
this.$emit('route-lock-changed', { routeId, locked: nextLocked }); this.$emit('route-lock-changed', { routeId, locked: nextLocked });
@ -5933,6 +6204,23 @@ export default {
white-space: nowrap; white-space: nowrap;
} }
/* 航线复制预览提示 */
.copy-route-tip {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
z-index: 99;
background: rgba(0, 120, 255, 0.9);
color: #fff;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
pointer-events: none;
white-space: nowrap;
}
/* 地图右下角信息面板:比例尺在上、经纬度在下,整体略下移减少遮挡 */ /* 地图右下角信息面板:比例尺在上、经纬度在下,整体略下移减少遮挡 */
.map-info-panel { .map-info-panel {
position: absolute; position: absolute;

10
ruoyi-ui/src/views/childRoom/RightPanel.vue

@ -70,6 +70,11 @@
</el-tag> </el-tag>
<div class="tree-item-actions"> <div class="tree-item-actions">
<i class="el-icon-view" title="显示/隐藏" @click.stop="handleToggleRouteVisibility(route)"></i> <i class="el-icon-view" title="显示/隐藏" @click.stop="handleToggleRouteVisibility(route)"></i>
<i
:class="routeLocked[route.id] ? 'el-icon-lock' : 'el-icon-unlock'"
:title="routeLocked[route.id] ? '解锁' : '上锁'"
@click.stop="$emit('toggle-route-lock', route)"
></i>
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenRouteDialog(route)"></i> <i class="el-icon-edit" title="编辑" @click.stop="handleOpenRouteDialog(route)"></i>
<i class="el-icon-delete" title="删除" @click.stop="$emit('delete-route', route)"></i> <i class="el-icon-delete" title="删除" @click.stop="$emit('delete-route', route)"></i>
</div> </div>
@ -278,6 +283,11 @@ export default {
type: Array, type: Array,
default: () => [] default: () => []
}, },
/** 航线上锁状态:routeId -> true 上锁,与地图右键上锁/解锁同步 */
routeLocked: {
type: Object,
default: () => ({})
},
selectedPlanId: { selectedPlanId: {
type: [String, Number], type: [String, Number],
default: null default: null

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

@ -13,12 +13,17 @@
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')" :tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
:scaleConfig="scaleConfig" :scaleConfig="scaleConfig"
:coordinateFormat="coordinateFormat" :coordinateFormat="coordinateFormat"
:route-locked="routeLocked"
@draw-complete="handleMapDrawComplete" @draw-complete="handleMapDrawComplete"
@route-lock-changed="handleRouteLockChanged"
@drawing-points-update="missionDrawingPointsCount = $event" @drawing-points-update="missionDrawingPointsCount = $event"
@platform-route-drawing-started="missionDrawingActive = true" @platform-route-drawing-started="missionDrawingActive = true"
@drawing-cancelled="missionDrawingActive = false" @drawing-cancelled="missionDrawingActive = false"
@open-waypoint-dialog="handleOpenWaypointEdit" @open-waypoint-dialog="handleOpenWaypointEdit"
@open-route-dialog="handleOpenRouteEdit" @open-route-dialog="handleOpenRouteEdit"
@copy-route="handleCopyRoute"
@route-copy-placed="handleRouteCopyPlaced"
@waypoint-position-changed="handleWaypointPositionChanged"
@scale-click="handleScaleClick" @scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated" @platform-icon-updated="onPlatformIconUpdated"
@platform-icon-removed="onPlatformIconRemoved" @platform-icon-removed="onPlatformIconRemoved"
@ -173,6 +178,7 @@
:selected-route-id="selectedRouteId" :selected-route-id="selectedRouteId"
:routes="routes" :routes="routes"
:active-route-ids="activeRouteIds" :active-route-ids="activeRouteIds"
:route-locked="routeLocked"
:selected-route-details="selectedRouteDetails" :selected-route-details="selectedRouteDetails"
:conflicts="conflicts" :conflicts="conflicts"
:conflict-count="conflictCount" :conflict-count="conflictCount"
@ -192,6 +198,7 @@
@add-waypoint="addWaypoint" @add-waypoint="addWaypoint"
@cancel-route="cancelRoute" @cancel-route="cancelRoute"
@toggle-route-visibility="toggleRouteVisibility" @toggle-route-visibility="toggleRouteVisibility"
@toggle-route-lock="handleToggleRouteLockFromPanel"
@view-conflict="viewConflict" @view-conflict="viewConflict"
@resolve-conflict="resolveConflict" @resolve-conflict="resolveConflict"
@run-conflict-check="runConflictCheck" @run-conflict-check="runConflictCheck"
@ -589,6 +596,8 @@ export default {
plans: [], plans: [],
activeRightTab: 'plan', activeRightTab: 'plan',
activeRouteIds: [], // 线ID activeRouteIds: [], // 线ID
/** 航线上锁状态:routeId -> true 上锁,与地图右键及右侧列表锁图标同步 */
routeLocked: {},
// //
conflictCount: 2, conflictCount: 2,
conflicts: [ conflicts: [
@ -818,6 +827,106 @@ export default {
} }
}, },
/** 右键「复制航线」:拉取航点后进入复制预览,左键放置后弹窗保存 */
async handleCopyRoute(routeId) {
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.startRouteCopyPreview === 'function') {
this.$refs.cesiumMap.startRouteCopyPreview(waypoints);
this.$message.info('移动鼠标到目标位置,左键放置复制航线;右键取消');
}
} catch (e) {
this.$message.error('获取航线数据失败');
console.error(e);
}
},
/** 复制航线已放置:用当前偏移后的航点打开「保存新航线」弹窗 */
handleRouteCopyPlaced(points) {
this.tempMapPoints = points || [];
this.tempMapPlatform = null;
this.showNameDialog = true;
},
/** 地图上拖拽航点结束:将新位置写回数据库并刷新显示 */
async handleWaypointPositionChanged({ dbId, routeId, lat, lng, alt }) {
let waypoints = null;
let route = null;
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
waypoints = this.selectedRouteDetails.waypoints;
route = this.selectedRouteDetails;
}
if (!waypoints) {
const r = this.routes.find(r => r.id === routeId);
if (r && r.waypoints) {
waypoints = r.waypoints;
route = r;
}
}
if (!waypoints || !route) {
this.$message.error('未找到对应航线数据');
return;
}
const wp = waypoints.find(p => p.id === dbId);
if (!wp) {
this.$message.error('未找到对应航点');
return;
}
const payload = {
id: wp.id,
routeId: wp.routeId != null ? wp.routeId : routeId,
name: wp.name,
seq: wp.seq,
lat: Number(lat),
lng: Number(lng),
alt: Number(alt),
speed: wp.speed,
startTime: (wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00',
turnAngle: wp.turnAngle
};
if (wp.pointType != null) payload.pointType = wp.pointType;
if (wp.holdParams != null) payload.holdParams = wp.holdParams;
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
if (wp.labelColor != null) payload.labelColor = wp.labelColor;
try {
const response = await updateWaypoints(payload);
if (response.code === 200) {
const merged = { ...wp, ...payload };
const idx = waypoints.findIndex(p => p.id === dbId);
if (idx !== -1) waypoints.splice(idx, 1, merged);
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId);
if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged);
}
if (this.$refs.cesiumMap) {
this.$refs.cesiumMap.renderRouteWaypoints(
waypoints,
routeId,
route.platformId,
route.platform,
this.parseRouteStyle(route.attributes)
);
}
this.$message.success('航点位置已更新');
this.$nextTick(() => this.updateDeductionPositions());
} else {
throw new Error(response.msg || '更新失败');
}
} catch (error) {
console.error('更新航点位置失败:', error);
this.$message.error(error.message || '更新失败,请重试');
}
},
// 线 // 线
showOnlineMembersDialog() { showOnlineMembersDialog() {
this.showOnlineMembers = true; this.showOnlineMembers = true;
@ -2812,6 +2921,18 @@ export default {
}, },
// 线/ // 线/
/** 地图右键上锁/解锁后同步到列表 */
handleRouteLockChanged({ routeId, locked }) {
this.$set(this.routeLocked, routeId, locked);
},
/** 右侧列表锁图标点击:切换该航线上锁状态,与地图右键状态同步 */
handleToggleRouteLockFromPanel(route) {
if (!route || route.id == null) return;
const nextLocked = !this.routeLocked[route.id];
this.$set(this.routeLocked, route.id, nextLocked);
this.$message.success(nextLocked ? '航线已上锁,无法修改' : '航线已解锁,可以编辑');
},
toggleRouteVisibility(route) { toggleRouteVisibility(route) {
const index = this.activeRouteIds.indexOf(route.id); const index = this.activeRouteIds.indexOf(route.id);

Loading…
Cancel
Save