From f1d82060431412a24eb2db7ee6ac1cf7990f1652 Mon Sep 17 00:00:00 2001
From: cuitw <1051735452@qq.com>
Date: Thu, 26 Feb 2026 16:31:38 +0800
Subject: [PATCH] =?UTF-8?q?=E6=AF=8F=E4=B8=AA=E7=82=B9=E5=8F=AF=E5=88=87?=
=?UTF-8?q?=E6=8D=A2=E7=9B=98=E6=97=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-ui/src/views/cesiumMap/ContextMenu.vue | 8 +
ruoyi-ui/src/views/cesiumMap/index.vue | 250 ++++++++++++++++++++++-----
ruoyi-ui/src/views/childRoom/index.vue | 107 +++++++++++-
3 files changed, 320 insertions(+), 45 deletions(-)
diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
index 4fda021..d36ef5a 100644
--- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
+++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
@@ -22,6 +22,10 @@
向后增加航点
+
@@ -462,6 +466,10 @@ export default {
this.$emit('add-waypoint-at', { routeId: this.entityData.routeId, waypointIndex: this.entityData.waypointIndex, mode: 'after' })
},
+ handleToggleWaypointHold() {
+ this.$emit('toggle-waypoint-hold', { routeId: this.entityData.routeId, dbId: this.entityData.dbId, waypointIndex: this.entityData.waypointIndex })
+ },
+
handleEditPlatform() {
this.$emit('edit-platform')
},
diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue
index 4f83882..4e64cef 100644
--- a/ruoyi-ui/src/views/cesiumMap/index.vue
+++ b/ruoyi-ui/src/views/cesiumMap/index.vue
@@ -47,6 +47,7 @@
@power-zone="openPowerZoneDialog"
@open-waypoint-dialog="handleContextMenuOpenWaypointDialog"
@add-waypoint-at="handleAddWaypointAt"
+ @toggle-waypoint-hold="handleToggleWaypointHold"
/>
@@ -91,6 +92,7 @@
:max="32"
controls-position="right"
style="width: 100%;"
+ @change="handleEditPlatformFormChange"
/>
@@ -98,6 +100,7 @@
v-model="editPlatformForm.fontColor"
size="small"
:predefine="presetColors"
+ @change="handleEditPlatformFormChange"
/>
@@ -109,6 +112,7 @@
:max="256"
controls-position="right"
style="width: 100%;"
+ @change="handleEditPlatformFormChange"
/>
@@ -116,11 +120,12 @@
v-model="editPlatformForm.iconColor"
size="small"
:predefine="presetColors"
+ @change="handleEditPlatformFormChange"
/>
@@ -240,6 +245,7 @@ export default {
missionHoldParamsByIndex: {},
missionPendingHold: null,
tempHoldEntity: null,
+ tempHoldOutlineEntity: null, // 盘旋圆/椭圆轮廓
activeCursorPosition: null, // 实时鼠标位置
// 实体管理
allEntities: [], // 所有绘制的实体
@@ -274,6 +280,8 @@ export default {
iconSize: 144,
iconColor: '#000000'
},
+ /** 编辑平台属性时用于还原的原始样式快照(只影响预览,不直接改缓存与后端) */
+ editPlatformOriginalStyle: null,
// 编辑平台属性:字体颜色、平台颜色预选
presetColors: [
'#000000', '#333333', '#666666', '#999999', '#FFFFFF',
@@ -474,6 +482,94 @@ export default {
}, 1000)
},
+ /** 编辑平台属性表单变更时,仅做实时预览(不保存到缓存与后端) */
+ handleEditPlatformFormChange() {
+ const routeId = this.editPlatformForm.routeId
+ if (!routeId || !this.viewer || !this.viewer.entities) return
+
+ const fontSize = Math.max(10, Math.min(32, Number(this.editPlatformForm.fontSize) || 16))
+ const fontColor = this.editPlatformForm.fontColor || '#333333'
+ const iconSize = Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144))
+ const iconColor = this.editPlatformForm.iconColor || '#000000'
+
+ // 实时更新标牌外观
+ const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
+ if (labelEntity) {
+ if (labelEntity.billboard) {
+ const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 }
+ const labelResult = this.createRoundedLabelCanvas({
+ name: data.name,
+ altitude: data.altitude,
+ speed: data.speed,
+ heading: data.headingDeg,
+ fontSize,
+ fontColor
+ })
+ labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas)
+ labelEntity.billboard.scale = labelResult.scale
+ } else if (labelEntity.label) {
+ labelEntity.label.font = `${fontSize}px Microsoft YaHei`
+ labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
+ labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
+ }
+ }
+
+ // 实时更新平台外观
+ const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
+ if (platformEntity && platformEntity.billboard) {
+ platformEntity.billboard.width = iconSize
+ platformEntity.billboard.height = iconSize
+ platformEntity.billboard.color = Cesium.Color.fromCssColorString(iconColor)
+ }
+
+ if (this.viewer.scene && this.viewer.scene.requestRenderMode) {
+ this.viewer.scene.requestRender()
+ }
+ },
+
+ /** 取消编辑平台属性:还原到打开弹窗前的样式,只关闭弹窗不保存 */
+ cancelEditPlatform() {
+ const snapshot = this.editPlatformOriginalStyle
+ if (snapshot && snapshot.routeId && this.viewer && this.viewer.entities) {
+ const { routeId, fontSize, fontColor, iconSize, iconColor } = snapshot
+
+ const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`)
+ if (labelEntity) {
+ if (labelEntity.billboard) {
+ const data = labelEntity.labelDataCache || { name: '平台', altitude: 0, speed: 0, headingDeg: 0 }
+ const labelResult = this.createRoundedLabelCanvas({
+ name: data.name,
+ altitude: data.altitude,
+ speed: data.speed,
+ heading: data.headingDeg,
+ fontSize,
+ fontColor
+ })
+ labelEntity.billboard.image = new Cesium.ConstantProperty(labelResult.canvas)
+ labelEntity.billboard.scale = labelResult.scale
+ } else if (labelEntity.label) {
+ labelEntity.label.font = `${fontSize}px Microsoft YaHei`
+ labelEntity.label.fillColor = Cesium.Color.fromCssColorString(fontColor)
+ labelEntity.label.backgroundColor = Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.6)')
+ }
+ }
+
+ const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
+ if (platformEntity && platformEntity.billboard) {
+ platformEntity.billboard.width = iconSize
+ platformEntity.billboard.height = iconSize
+ platformEntity.billboard.color = Cesium.Color.fromCssColorString(iconColor)
+ }
+
+ if (this.viewer.scene && this.viewer.scene.requestRenderMode) {
+ this.viewer.scene.requestRender()
+ }
+ }
+
+ this.editPlatformDialogVisible = false
+ this.editPlatformOriginalStyle = null
+ },
+
applyScaleToCamera(metersPerPixel) {
if (!this.viewer || !this.viewer.camera) return
@@ -924,9 +1020,14 @@ export default {
setTimeout(() => window.removeEventListener('contextmenu', this.preventContextMenu, true), 200);
return;
}
- let pointsToEmit = this.drawingPoints;
+ let pointsToEmit;
+ if (this.missionPendingHold && this.drawingPoints.length >= 2) {
+ pointsToEmit = this.getMissionRouteSolidPositions();
+ } else {
+ pointsToEmit = [...this.drawingPoints];
+ }
if (pr.mode === 'before') {
- if (this.drawingPoints.length < 1) {
+ if (pointsToEmit.length < 1) {
this.$message && this.$message.info('已取消');
this.platformRouteDrawing = null;
this.stopDrawing();
@@ -935,7 +1036,7 @@ export default {
return;
}
// 顺序反转:先点的为倒数第二个点,最后点的为起点;平台为最后一个点
- pointsToEmit = [...this.drawingPoints].reverse();
+ pointsToEmit = [...pointsToEmit].reverse();
pointsToEmit.push(platformCartesian);
const lastId = `temp_wp_${pointsToEmit.length}`;
this.viewer.entities.add({
@@ -970,16 +1071,22 @@ export default {
return;
}
const latLngPoints = [];
+ const holdCenter = this.missionPendingHold ? this.missionPendingHold.center : null;
pointsToEmit.forEach((pos, i) => {
const coords = this.cartesianToLatLng(pos);
- const name = (pr.mode === 'after' && i === 0) || (pr.mode === 'before' && i === pointsToEmit.length - 1) ? pr.platformName : `WP${i + 1}`;
+ const isPlatform = (pr.mode === 'after' && i === 0) || (pr.mode === 'before' && i === pointsToEmit.length - 1);
+ const isHold = holdCenter && Cesium.Cartesian3.equalsEpsilon(pos, holdCenter, 0.1);
latLngPoints.push({
id: i + 1,
- name,
+ name: isPlatform ? pr.platformName : (isHold ? 'HOLD' : `WP${i + 1}`),
lat: coords.lat,
lng: coords.lng,
alt: 5000,
- speed: 800
+ speed: 800,
+ ...(isHold && {
+ pointType: this.missionPendingHold.params.radius != null ? 'hold_circle' : 'hold_ellipse',
+ holdParams: JSON.stringify(this.missionPendingHold.params)
+ })
});
});
this.$emit('draw-complete', latLngPoints, pr.platformInfo);
@@ -1002,16 +1109,41 @@ export default {
center: Cesium.Cartesian3.clone(last),
params: holdParams
};
+ if (this.tempHoldOutlineEntity) {
+ try { this.viewer.entities.remove(this.tempHoldOutlineEntity); } catch (e) {}
+ this.tempHoldOutlineEntity = null;
+ }
if (this.tempHoldEntity) {
try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {}
this.tempHoldEntity = null;
}
+ const center = this.missionPendingHold.center;
+ const p = holdParams;
+ const isCircle = p.radius != null;
+ const radius = isCircle ? (p.radius || 500) : 500;
+ const semiMajor = !isCircle ? (p.semiMajor ?? p.semiMajorAxis ?? 500) : radius;
+ const semiMinor = !isCircle ? (p.semiMinor ?? p.semiMinorAxis ?? 300) : radius;
+ const headingRad = !isCircle ? ((p.headingDeg ?? 0) * Math.PI) / 180 : 0;
+ this.tempHoldOutlineEntity = this.viewer.entities.add({
+ id: 'temp_hold_outline',
+ position: center,
+ ellipse: {
+ semiMajorAxis: semiMajor,
+ semiMinorAxis: semiMinor,
+ rotation: headingRad,
+ material: Cesium.Color.TRANSPARENT,
+ outline: true,
+ outlineColor: Cesium.Color.ORANGE.withAlpha(0.8),
+ outlineWidth: 2,
+ arcType: Cesium.ArcType.NONE
+ }
+ });
this.tempHoldEntity = this.viewer.entities.add({
id: 'temp_hold_preview',
name: 'HOLD',
- position: this.missionPendingHold.center,
+ position: center,
point: { pixelSize: 10, color: Cesium.Color.ORANGE, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, disableDepthTestDistance: Number.POSITIVE_INFINITY },
- label: { text: 'HOLD', font: '14px Microsoft YaHei', fillColor: Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1 }
+ label: { text: '盘旋', font: '14px Microsoft YaHei', pixelOffset: new Cesium.Cartesian2(0, -20), fillColor: Cesium.Color.ORANGE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1 }
});
// 更新实线预览(含盘旋点)
if (this.tempEntity) {
@@ -1132,10 +1264,10 @@ export default {
const colorLabel = '#888888'; // 属性名灰色
const colorValue = fontColor; // 属性值(默认黑,可配置)
- // 文本内容
- const labelAlt = '高度: ';
- const labelSpeed = ' 速度: ';
- const labelHeading = ' 航向: ';
+ // 文本内容(h: 高度,v: 速度,s: 航向)
+ const labelAlt = 'h: ';
+ const labelSpeed = ' v: ';
+ const labelHeading = ' s: ';
const textAlt = altitude + 'm';
const textSpeed = speed + 'km/h';
const textHeading = Math.round(heading) + '°';
@@ -1619,9 +1751,8 @@ export default {
}
return !!nextLogical;
};
- // 遍历并绘制航点标记:转弯半径处不画中心点;盘旋处在圆心画一小点便于右键“向前/向后增加航点”
+ // 遍历并绘制航点标记:转弯半径处也画中心点+标签(与普通航点一致);盘旋处在圆心画点+标签
waypoints.forEach((wp, index) => {
- if (isTurnWaypointWithArc(index)) return;
const pos = originalPositions[index];
if (this.isHoldWaypoint(wp)) {
this.viewer.entities.add({
@@ -1640,7 +1771,15 @@ export default {
outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
- label: { show: false }
+ label: {
+ text: wp.name || `盘旋${index + 1}`,
+ 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)),
+ fillColor: Cesium.Color.fromCssColorString(wp.labelColor || '#2c2c2c'),
+ outlineColor: Cesium.Color.fromCssColorString('#e8e8e8'),
+ outlineWidth: 0.5,
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE
+ }
});
return;
}
@@ -1849,7 +1988,7 @@ export default {
const exit = params && params.radius != null
? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
: this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
- finalPathPositions.push(entry);
+ let fullCirclePoints;
let arcPoints;
if (params && params.radius != null) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
@@ -1857,7 +1996,8 @@ export default {
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
const toEntry = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
- arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
+ fullCirclePoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
+ arcPoints = this.getCircleArcEntryToExit(currPos, radius, entry, exit, clockwise, 48);
} else {
const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3());
@@ -1865,15 +2005,17 @@ export default {
const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
const thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE));
const entryLocalAngle = thetaE - headingRad;
- arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
+ fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
+ arcPoints = this.getEllipseArcEntryToExit(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 48);
}
- for (let k = 1; k < arcPoints.length; k++) finalPathPositions.push(arcPoints[k]);
- finalPathPositions.push(exit);
+ // 整圈 + entry→exit 弧段,避免弦线:entry → 整圈(回到entry) → 弧段到exit
+ const holdPositions = [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
+ for (let k = 0; k < holdPositions.length; k++) finalPathPositions.push(holdPositions[k]);
// 盘旋不单独着色:仅作为主航线取点数据源,show:false 由主航线折线用 lineWidth/lineMaterial 统一绘制
this.viewer.entities.add({
id: `hold-line-${routeId}-${i}`,
show: false,
- polyline: { positions: [entry, ...arcPoints.slice(1), exit], width: lineWidth, material: lineMaterial, arcType: Cesium.ArcType.NONE, zIndex: 20 },
+ polyline: { positions: holdPositions, width: lineWidth, material: lineMaterial, arcType: Cesium.ArcType.NONE, zIndex: 20 },
properties: { routeId: routeId }
});
lastPos = exit;
@@ -2022,22 +2164,16 @@ export default {
}
},
- /** 圆上从 entry 到 exit 的弧段(按顺时针/逆时针),采样点数 */
- getCircleArcEntryToExit(centerCartesian, radiusMeters, entryCartesian, exitCartesian, clockwise, numPoints) {
+ /** 圆上整圈(360°)采样,从 startAngleRad 起按顺时针/逆时针,用于盘旋段渲染为整圆 */
+ getCircleFullCircle(centerCartesian, radiusMeters, startAngleRad, clockwise, numPoints) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
- const toEntry = Cesium.Cartesian3.subtract(entryCartesian, centerCartesian, new Cesium.Cartesian3());
- const toExit = Cesium.Cartesian3.subtract(exitCartesian, centerCartesian, new Cesium.Cartesian3());
- let entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
- let exitAngle = Math.atan2(Cesium.Cartesian3.dot(toExit, east), Cesium.Cartesian3.dot(toExit, north));
- let diff = exitAngle - entryAngle;
const sign = clockwise ? -1 : 1;
- if (sign * diff <= 0) diff += sign * 2 * Math.PI;
const points = [];
for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints;
- const angle = entryAngle + sign * t * Math.abs(diff);
+ const angle = startAngleRad + sign * t * 2 * Math.PI;
const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
@@ -2048,16 +2184,24 @@ export default {
return points;
},
- /** 圆上整圈(360°)采样,从 startAngleRad 起按顺时针/逆时针,用于盘旋段渲染为整圆 */
- getCircleFullCircle(centerCartesian, radiusMeters, startAngleRad, clockwise, numPoints) {
+ /** 圆上从 entry 到 exit 的弧段(按顺时针/逆时针),避免整圈后产生弦线 */
+ getCircleArcEntryToExit(centerCartesian, radiusMeters, entryCartesian, exitCartesian, clockwise, numPoints) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
const east = Cesium.Matrix4.getColumn(enu, 0, new Cesium.Cartesian3());
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
+ const toAngle = (cart) => {
+ const toP = Cesium.Cartesian3.subtract(cart, centerCartesian, new Cesium.Cartesian3());
+ return Math.atan2(Cesium.Cartesian3.dot(toP, east), Cesium.Cartesian3.dot(toP, north));
+ };
+ let entryAngle = toAngle(entryCartesian);
+ let exitAngle = toAngle(exitCartesian);
+ let diff = exitAngle - entryAngle;
const sign = clockwise ? -1 : 1;
+ if (sign * diff <= 0) diff += sign * 2 * Math.PI;
const points = [];
for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints;
- const angle = startAngleRad + sign * t * 2 * Math.PI;
+ const angle = entryAngle + sign * t * Math.abs(diff);
const offset = Cesium.Cartesian3.add(
Cesium.Cartesian3.multiplyByScalar(north, Math.cos(angle) * radiusMeters, new Cesium.Cartesian3()),
Cesium.Cartesian3.multiplyByScalar(east, Math.sin(angle) * radiusMeters, new Cesium.Cartesian3()),
@@ -2325,8 +2469,8 @@ export default {
const exit = params && params.radius != null
? this.getCircleTangentExitPoint(currPos, nextPos || currPos, radius, clockwise)
: this.getEllipseTangentExitPoint(currPos, nextPos || currPos, semiMajor, semiMinor, headingRad, clockwise);
- path.push(toLngLatAlt(entry));
- const arcStartIdx = path.length - 1;
+ const arcStartIdx = path.length;
+ let fullCirclePoints;
let arcPoints;
if (params && params.radius != null) {
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
@@ -2334,7 +2478,8 @@ export default {
const north = Cesium.Matrix4.getColumn(enu, 1, new Cesium.Cartesian3());
const toEntry = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
const entryAngle = Math.atan2(Cesium.Cartesian3.dot(toEntry, east), Cesium.Cartesian3.dot(toEntry, north));
- arcPoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
+ fullCirclePoints = this.getCircleFullCircle(currPos, radius, entryAngle, clockwise, 48);
+ arcPoints = this.getCircleArcEntryToExit(currPos, radius, entry, exit, clockwise, 48);
} else {
const enuE = Cesium.Transforms.eastNorthUpToFixedFrame(currPos);
const eastE = Cesium.Matrix4.getColumn(enuE, 0, new Cesium.Cartesian3());
@@ -2342,10 +2487,11 @@ export default {
const toEntryE = Cesium.Cartesian3.subtract(entry, currPos, new Cesium.Cartesian3());
const thetaE = Math.atan2(Cesium.Cartesian3.dot(toEntryE, eastE), Cesium.Cartesian3.dot(toEntryE, northE));
const entryLocalAngle = thetaE - headingRad;
- arcPoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
+ fullCirclePoints = this.getEllipseFullCircle(currPos, semiMajor, semiMinor, headingRad, entryLocalAngle, clockwise, 48);
+ arcPoints = this.getEllipseArcEntryToExit(currPos, semiMajor, semiMinor, headingRad, entry, exit, clockwise, 48);
}
- for (let k = 1; k < arcPoints.length; k++) path.push(toLngLatAlt(arcPoints[k]));
- path.push(toLngLatAlt(exit));
+ const holdPositions = [entry, ...fullCirclePoints.slice(1), ...arcPoints.slice(1)];
+ for (let k = 0; k < holdPositions.length; k++) path.push(toLngLatAlt(holdPositions[k]));
holdArcRanges[i - 1] = { start: arcStartIdx, end: path.length - 1 };
segmentEndIndices[i - 1] = path.length - 1;
lastPos = exit;
@@ -2418,14 +2564,14 @@ export default {
return heading;
},
- /** 格式化飞机标牌文案:名字、高度(m)、速度(km/h)、航向(°) */
+ /** 格式化飞机标牌文案:名字、h(m)、v(km/h)、s(°) */
formatPlatformLabelText(data) {
const name = (data && data.name != null) ? String(data.name) : '—';
const alt = (data && data.altitude != null) ? Number(data.altitude) : 0;
const speed = (data && data.speed != null) ? Number(data.speed) : 0;
const hdg = (data && data.headingDeg != null) ? Number(data.headingDeg) : 0;
const headingNorm = ((hdg % 360) + 360) % 360;
- return `${name}\n高度: ${Math.round(alt)}m 速度: ${Math.round(speed)}km/h 航向: ${Math.round(headingNorm)}°`;
+ return `${name}\nh: ${Math.round(alt)}m v: ${Math.round(speed)}km/h s: ${Math.round(headingNorm)}°`;
},
/** 动态推演:更新某条航线的平台图标位置与朝向(position: { lng, lat, alt } 或 Cesium.Cartesian3;directionPoint 为用于计算机头朝向的另一点;labelData 可选,用于更新标牌 { name, altitude, speed, headingDeg }) */
@@ -3579,11 +3725,16 @@ export default {
if (entity.id && (
entity.id.toString().startsWith('temp_wp_') ||
entity.id.toString().includes('temp-preview') ||
- entity.id === 'temp_hold_preview'
+ entity.id === 'temp_hold_preview' ||
+ entity.id === 'temp_hold_outline'
)) {
this.viewer.entities.remove(entity);
}
}
+ if (this.tempHoldOutlineEntity) {
+ try { this.viewer.entities.remove(this.tempHoldOutlineEntity); } catch (e) {}
+ this.tempHoldOutlineEntity = null;
+ }
if (this.tempHoldEntity) {
try { this.viewer.entities.remove(this.tempHoldEntity); } catch (e) {}
this.tempHoldEntity = null;
@@ -5428,6 +5579,10 @@ export default {
this.contextMenu.visible = false;
this.$emit('add-waypoint-at', payload);
},
+ handleToggleWaypointHold(payload) {
+ this.contextMenu.visible = false;
+ this.$emit('toggle-waypoint-hold', payload);
+ },
/** 开始“在航点前/后增加航点”模式:显示预览折线,左键放置、右键取消。waypoints 为当前航线航点数组。 */
startAddWaypointAt(routeId, waypointIndex, mode, waypoints) {
if (!waypoints || waypoints.length === 0) return;
@@ -5601,6 +5756,15 @@ export default {
this.editPlatformForm.platformName = platformName
this.editPlatformForm.platformId = ed.platformId || 0
+ // 记录打开弹窗时的原始样式,供取消时还原、以及区分“预览 vs 真正保存”
+ this.editPlatformOriginalStyle = {
+ routeId,
+ fontSize,
+ fontColor,
+ iconSize,
+ iconColor
+ }
+
// 异步获取最新航线信息,更新 platformId 和 platformName
if (routeId) {
getRoutes(routeId).then(response => {
diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue
index 03f794e..5df45de 100644
--- a/ruoyi-ui/src/views/childRoom/index.vue
+++ b/ruoyi-ui/src/views/childRoom/index.vue
@@ -26,6 +26,7 @@
@route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt"
@add-waypoint-placed="handleAddWaypointPlaced"
+ @toggle-waypoint-hold="handleToggleWaypointHold"
@waypoint-position-changed="handleWaypointPositionChanged"
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
@@ -613,7 +614,7 @@ export default {
deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs
showAddHoldDialog: false,
addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName }
- addHoldForm: { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: null },
+ addHoldForm: { holdType: 'hold_circle', radius: 15000, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: null },
missionDrawingActive: false,
missionDrawingPointsCount: 0,
isPlaying: false,
@@ -950,6 +951,108 @@ export default {
}
},
+ /** 右键航点“切换盘旋航点”:普通航点设为盘旋(圆形默认),盘旋航点设为普通;支持首尾航点 */
+ async handleToggleWaypointHold({ routeId, dbId, waypointIndex }) {
+ if (this.routeLocked[routeId]) {
+ this.$message.info('该航线已上锁,请先解锁');
+ return;
+ }
+ let route = this.routes.find(r => r.id === routeId);
+ let waypoints = route && route.waypoints;
+ if (!waypoints || waypoints.length === 0) {
+ try {
+ const res = await getRoutes(routeId);
+ if (res.code === 200 && res.data && res.data.waypoints) {
+ waypoints = res.data.waypoints;
+ route = { ...route, waypoints };
+ }
+ } catch (e) {
+ this.$message.error('获取航线失败');
+ return;
+ }
+ }
+ if (!waypoints || waypoints.length === 0) {
+ this.$message.warning('航线无航点');
+ return;
+ }
+ const wp = dbId != null ? waypoints.find(w => w.id === dbId) : waypoints[waypointIndex];
+ if (!wp) {
+ this.$message.warning('未找到该航点');
+ return;
+ }
+ const index = waypoints.indexOf(wp);
+ const total = waypoints.length;
+ const isFirstOrLast = index === 0 || index === total - 1;
+ const isHold = this.isHoldWaypoint(wp);
+ let pointType;
+ let holdParams;
+ let turnAngle;
+ if (isHold) {
+ pointType = 'normal';
+ holdParams = null;
+ turnAngle = isFirstOrLast ? 0 : (Number(wp.turnAngle) || 45);
+ } else {
+ pointType = 'hold_circle';
+ holdParams = JSON.stringify({ radius: 15000, clockwise: true });
+ turnAngle = 0;
+ }
+ try {
+ const payload = {
+ id: wp.id,
+ routeId,
+ name: wp.name,
+ seq: wp.seq,
+ lat: wp.lat,
+ lng: wp.lng,
+ alt: wp.alt,
+ speed: wp.speed,
+ startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00',
+ turnAngle,
+ pointType
+ };
+ if (holdParams != null) payload.holdParams = holdParams;
+ else payload.holdParams = '';
+ if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
+ if (wp.labelColor != null) payload.labelColor = wp.labelColor;
+ if (turnAngle > 0 && this.$refs.cesiumMap) {
+ payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload);
+ } else {
+ payload.turnRadius = 0;
+ }
+ const response = await updateWaypoints(payload);
+ if (response.code !== 200) throw new Error(response.msg || '更新失败');
+ const merged = { ...wp, ...payload };
+ const routeInList = this.routes.find(r => r.id === routeId);
+ if (routeInList && routeInList.waypoints) {
+ const idx = routeInList.waypoints.findIndex(p => p.id === wp.id);
+ if (idx !== -1) routeInList.waypoints.splice(idx, 1, merged);
+ }
+ if (this.selectedRouteId === routeId && this.selectedRouteDetails && this.selectedRouteDetails.waypoints) {
+ const idxS = this.selectedRouteDetails.waypoints.findIndex(p => p.id === wp.id);
+ if (idxS !== -1) this.selectedRouteDetails.waypoints.splice(idxS, 1, merged);
+ }
+ if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
+ const r = this.routes.find(rr => rr.id === routeId);
+ if (r && r.waypoints) {
+ const roomId = this.currentRoomId;
+ if (roomId && r.platformId) {
+ try {
+ const styleRes = await getPlatformStyle({ roomId, routeId, platformId: r.platformId });
+ if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
+ } catch (_) {}
+ }
+ this.$refs.cesiumMap.removeRouteById(routeId);
+ this.$refs.cesiumMap.renderRouteWaypoints(r.waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes));
+ this.$nextTick(() => this.updateDeductionPositions());
+ }
+ }
+ this.$message.success(isHold ? '已设为普通航点' : '已设为盘旋航点');
+ } catch (e) {
+ this.$message.error(e.msg || e.message || '切换失败');
+ console.error(e);
+ }
+ },
+
/** 右键「复制航线」:拉取航点后进入复制预览,左键放置后弹窗保存 */
async handleCopyRoute(routeId) {
try {
@@ -1400,7 +1503,7 @@ export default {
openAddHoldDuringDrawing() {
this.addHoldContext = { mode: 'drawing' };
- this.addHoldForm = { holdType: 'hold_circle', radius: 500, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 };
+ this.addHoldForm = { holdType: 'hold_circle', radius: 15000, semiMajor: 500, semiMinor: 300, headingDeg: 0, clockwise: true, startTime: '', startTimeMinutes: 60 };
this.showAddHoldDialog = true;
},