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; },