diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java index 89155ff..8108a8e 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoomsController.java @@ -2,8 +2,6 @@ package com.ruoyi.web.controller; import java.util.List; import javax.servlet.http.HttpServletResponse; - -import com.ruoyi.common.utils.SecurityUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoutesService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoutesService.java index 74dd885..6620433 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoutesService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IRoutesService.java @@ -58,4 +58,4 @@ public interface IRoutesService * @return 结果 */ public int deleteRoutesById(Long id); -} +} \ No newline at end of file diff --git a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue index 6c50534..c7e10f7 100644 --- a/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue +++ b/ruoyi-ui/src/views/cesiumMap/ContextMenu.vue @@ -16,13 +16,21 @@ - + @@ -388,6 +396,14 @@ export default { this.$emit('toggle-route-lock') }, + handleEditPlatform() { + this.$emit('edit-platform') + }, + + handlePowerZone() { + this.$emit('power-zone') + }, + toggleColorPicker(property) { if (this.showColorPickerFor === property) { this.showColorPickerFor = null diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index 55593ca..fa18607 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/ruoyi-ui/src/views/cesiumMap/index.vue @@ -40,6 +40,8 @@ @show-transform-box="showPlatformIconTransformBox" @toggle-route-label="toggleRouteLabelVisibility" @toggle-route-lock="toggleRouteLock" + @edit-platform="openEditPlatformDialog" + @power-zone="openPowerZoneDialog" /> @@ -62,6 +64,75 @@ @confirm="handleRadiusConfirm" /> + + + + 标牌设置 + + + + + + + + 平台设置 + + + + + + + + + 取 消 + 确 定 + + + + + + + + + + + + + + + 取 消 + 确 定 + + +
@@ -171,8 +242,30 @@ export default { position: { x: 0, y: 0 }, entityData: null }, + editPlatformLabelDialogVisible: false, + editPlatformLabelForm: { + routeId: null, + fontSize: 16, + color: '#333333' + }, + editPlatformDialogVisible: false, + editPlatformForm: { + routeId: null, + fontSize: 16, + fontColor: '#333333', + iconSize: 144, + iconColor: '#ffffff' + }, + powerZoneDialogVisible: false, + powerZoneForm: { + routeId: null, + radius: 1000, + color: 'rgba(255, 0, 0, 0.3)' + }, // 航线飞机标牌显示状态:routeId -> true 显示 / false 隐藏,不设则默认显示 routeLabelVisible: {}, + // 航线飞机标牌样式:routeId -> { fontSize, fontColor } + routeLabelStyles: {}, // 航线上锁状态:routeId -> true 上锁(不可编辑)/ false 或未设 可编辑 routeLocked: {}, // 默认样式 @@ -587,10 +680,191 @@ export default { /** 默认平台图标(无 imageUrl 时使用):简单飞机剪影 SVG */ getDefaultPlatformIconDataUrl() { - const svg = ''; + // 修改默认图标颜色为白色,方便后续染色 + const svg = ''; return 'data:image/svg+xml,' + encodeURIComponent(svg); }, + /** + * 加载图片并转为白色(保留透明度),确保黑色图标可以被 Cesium 正确染色 + * @param {string} url 图片 URL + * @returns {Promise} 返回处理后的 Canvas 或原 URL + */ + loadAndWhitenImage(url) { + return new Promise((resolve) => { + // 如果是 Data URL 且已经是上述默认白色 SVG,直接返回 + if (url && url.startsWith('data:image/svg+xml') && url.includes('fill="white"')) { + resolve(url); + return; + } + + const img = new Image(); + img.crossOrigin = "Anonymous"; + img.src = url; + img.onload = () => { + try { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + + // 获取像素数据 + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + // 遍历像素,将所有非透明像素转为白色 + for(let i = 0; i < data.length; i += 4) { + // Alpha > 0 则处理 + if(data[i+3] > 0) { + data[i] = 255; // R + data[i+1] = 255; // G + data[i+2] = 255; // B + // Alpha 保持不变 + } + } + + ctx.putImageData(imageData, 0, 0); + resolve(canvas); // Cesium 支持直接使用 Canvas + } catch (e) { + console.warn("图片转白处理失败 (可能是跨域问题),使用原图:", e); + resolve(url); + } + }; + img.onerror = () => { + console.warn("图片加载失败,使用原图:", url); + resolve(url); + }; + }); + }, + + /** + * 生成圆角矩形 + 多色文本的 Canvas 图像 + * @param {Object} options 配置项 + * @param {string} options.name 平台名称(蓝色) + * @param {string} options.altitude 高度值 + * @param {string} options.speed 速度值 + * @param {string} options.heading 航向值 + * @param {number} options.fontSize 字号,默认 16 + * @param {string} options.fontColor 属性值颜色,默认黑色 + * @returns {HTMLCanvasElement} + */ + createRoundedLabelCanvas(options) { + const { name, altitude, speed, heading, fontSize = 16, fontColor = '#000000' } = options; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // 设置字体 + const font = `${fontSize}px "Microsoft YaHei"`; + ctx.font = font; + + // 颜色定义 + const colorName = '#0078FF'; // 平台名蓝色 + const colorLabel = '#888888'; // 属性名灰色 + const colorValue = fontColor; // 属性值(默认黑,可配置) + + // 文本内容 + const labelAlt = '高度: '; + const labelSpeed = ' 速度: '; + const labelHeading = ' 航向: '; + const textAlt = altitude + 'm'; + const textSpeed = speed + 'km/h'; + const textHeading = Math.round(heading) + '°'; + + // 计算各部分宽度 + const wName = ctx.measureText(name).width; + + const wLabelAlt = ctx.measureText(labelAlt).width; + const wValAlt = ctx.measureText(textAlt).width; + + const wLabelSpeed = ctx.measureText(labelSpeed).width; + const wValSpeed = ctx.measureText(textSpeed).width; + + const wLabelHeading = ctx.measureText(labelHeading).width; + const wValHeading = ctx.measureText(textHeading).width; + + // 总宽与总高 + // 第一行:名字居中 + // 第二行:高度 + 速度 + 航向 + const line1Width = wName; + const line2Width = wLabelAlt + wValAlt + wLabelSpeed + wValSpeed + wLabelHeading + wValHeading; + + const paddingX = 12; + const paddingY = 8; + const lineHeight = fontSize * 1.4; + const totalWidth = Math.max(line1Width, line2Width) + paddingX * 2; + const totalHeight = lineHeight * 2 + paddingY * 2; + + // 设置 Canvas 大小 (根据设备像素比优化清晰度,这里简单处理,Cesium Billboard 会自动缩放) + canvas.width = totalWidth; + canvas.height = totalHeight; + + // 重新设置字体(因为重设 width 会重置 context) + ctx.font = font; + ctx.textBaseline = 'top'; + + // 1. 绘制圆角矩形背景 + const r = 8; // 圆角半径 + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; // 背景白,微透 + ctx.beginPath(); + ctx.moveTo(r, 0); + ctx.lineTo(totalWidth - r, 0); + ctx.quadraticCurveTo(totalWidth, 0, totalWidth, r); + ctx.lineTo(totalWidth, totalHeight - r); + ctx.quadraticCurveTo(totalWidth, totalHeight, totalWidth - r, totalHeight); + ctx.lineTo(r, totalHeight); + ctx.quadraticCurveTo(0, totalHeight, 0, totalHeight - r); + ctx.lineTo(0, r); + ctx.quadraticCurveTo(0, 0, r, 0); + ctx.closePath(); + ctx.fill(); + + // 描边(可选,这里加个淡淡的边框) + ctx.strokeStyle = '#e0e0e0'; + ctx.lineWidth = 1; + ctx.stroke(); + + // 2. 绘制第一行:平台名(蓝色,居中) + const line1X = (totalWidth - wName) / 2; + const line1Y = paddingY; + ctx.fillStyle = colorName; + ctx.fillText(name, line1X, line1Y); + + // 3. 绘制第二行:属性(灰名黑值,左对齐,整体居中) + const line2X = (totalWidth - line2Width) / 2; + const line2Y = paddingY + lineHeight; + let currentX = line2X; + + // 高度 + ctx.fillStyle = colorLabel; + ctx.fillText(labelAlt, currentX, line2Y); + currentX += wLabelAlt; + + ctx.fillStyle = colorValue; + ctx.fillText(textAlt, currentX, line2Y); + currentX += wValAlt; + + // 速度 + ctx.fillStyle = colorLabel; + ctx.fillText(labelSpeed, currentX, line2Y); + currentX += wLabelSpeed; + + ctx.fillStyle = colorValue; + ctx.fillText(textSpeed, currentX, line2Y); + currentX += wValSpeed; + + // 航向 + ctx.fillStyle = colorLabel; + ctx.fillText(labelHeading, currentX, line2Y); + currentX += wLabelHeading; + + ctx.fillStyle = colorValue; + ctx.fillText(textHeading, currentX, line2Y); + + return canvas; + }, + /** 伸缩框旋转手柄图标 SVG:蓝底、白边、白色弧形箭头 */ getRotationHandleIconDataUrl() { const svg = '' + @@ -1017,6 +1291,7 @@ export default { image: fullUrl, width: 144, height: 144, + color: Cesium.Color.WHITE, // 初始颜色为白色(不染色) verticalOrigin: Cesium.VerticalOrigin.CENTER, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, scaleByDistance: new Cesium.NearFarScalar(500, 2.0, 200000, 0.4), @@ -1024,6 +1299,23 @@ export default { ...(initialRotation !== undefined && { rotation: initialRotation }) } }); + + // 异步加载并处理图片为白色,以便支持后续染色 + this.loadAndWhitenImage(fullUrl).then(whiteImage => { + const target = this.viewer.entities.getById(platformBillboardId); + if (target && target.billboard) { + target.billboard.image = whiteImage; + // 注意:此时如果用户没有修改过颜色,target.billboard.color 默认为 WHITE,显示白图 + // 如果用户希望"未编辑时显示原图",这里可能会把黑图变成白图。 + // 但根据用户需求"一开始加入平台时将平台的颜色固定为白色",这正是想要的效果。 + + // 触发一次渲染刷新 + if (this.viewer.scene.requestRenderMode) { + this.viewer.scene.requestRender(); + } + } + }); + // 飞机标牌:显示名字、高度、速度、航向,随飞机实时运动 const firstWp = waypoints[0]; const firstAlt = firstWp && (firstWp.alt != null) ? Number(firstWp.alt) : 0; @@ -1038,22 +1330,31 @@ export default { headingDeg: initialHeadingDeg }); const labelShow = this.routeLabelVisible[routeId] !== false - this.viewer.entities.add({ + + // 初始化样式 + if (!this.routeLabelStyles[routeId]) { + this.$set(this.routeLabelStyles, routeId, { fontSize: 16, fontColor: '#000000' }); + } + const currentStyle = this.routeLabelStyles[routeId]; + + // 使用 Canvas 生成圆角标牌 + const labelCanvas = this.createRoundedLabelCanvas({ + name: (platform && platform.name) || '平台', + altitude: firstAlt, + speed: firstSpeed, + heading: Math.round(initialHeadingDeg), + fontSize: currentStyle.fontSize, + fontColor: currentStyle.fontColor + }); + + const labelEntity = this.viewer.entities.add({ id: platformLabelId, name: '平台标牌', position: originalPositions[0], show: labelShow, properties: { routeId: routeId }, - label: { - text: labelText, - font: '16px Microsoft YaHei', - fillColor: Cesium.Color.fromCssColorString('#333333'), - outlineColor: Cesium.Color.fromCssColorString('#e0e0e0'), - outlineWidth: 1, - style: Cesium.LabelStyle.FILL_AND_OUTLINE, - showBackground: true, - backgroundColor: Cesium.Color.fromCssColorString('rgba(255, 255, 255, 0.95)'), - backgroundPadding: new Cesium.Cartesian2(10, 6), + billboard: { + image: labelCanvas, // 使用 Canvas 作为图片 verticalOrigin: Cesium.VerticalOrigin.BOTTOM, horizontalOrigin: Cesium.HorizontalOrigin.CENTER, pixelOffset: new Cesium.Cartesian2(0, -42), @@ -1061,6 +1362,13 @@ export default { scaleByDistance: new Cesium.NearFarScalar(500, 1.0, 200000, 0.65) } }); + // 缓存初始数据,供样式更新使用 + labelEntity.labelDataCache = { + name: (platform && platform.name) || '平台', + altitude: firstAlt, + speed: firstSpeed, + headingDeg: initialHeadingDeg + }; } // 绘制连线(含盘旋弧) if (waypoints.length > 1) { @@ -1565,6 +1873,11 @@ export default { return { path, segmentEndIndices, holdArcRanges }; }, + removePowerZoneByRouteId(routeId) { + const zoneId = `power-zone-${routeId}`; + this.viewer.entities.removeById(zoneId); + }, + removeRouteById(routeId) { // 从地图上移除所有属于该 routeId 的实体 const entityList = this.viewer.entities.values; @@ -1572,7 +1885,7 @@ export default { const entity = entityList[i]; let shouldRemove = false; // 平台图标与标牌实体:通过 id 匹配 - if (entity.id === `route-platform-${routeId}` || entity.id === `route-platform-label-${routeId}`) { + if (entity.id === `route-platform-${routeId}` || entity.id === `route-platform-label-${routeId}` || entity.id === `power-zone-${routeId}`) { shouldRemove = true; } else if (entity.properties && entity.properties.routeId) { const id = entity.properties.routeId.getValue && entity.properties.routeId.getValue(); @@ -1641,8 +1954,69 @@ export default { const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`); if (labelEntity && labelEntity.position) { labelEntity.position = cartesian; - if (labelData && labelEntity.label) { - labelEntity.label.text = this.formatPlatformLabelText(labelData); + if (labelData) { + // 更新缓存数据 + if (!labelEntity.labelDataCache) labelEntity.labelDataCache = {}; + Object.assign(labelEntity.labelDataCache, labelData); + + if (labelEntity.billboard) { + const style = this.routeLabelStyles[routeId] || { fontSize: 16, fontColor: '#000000' }; + + // 预处理显示数据(取整),既解决了小数点过长问题,也用于差异检测 + const displayData = { + name: labelData.name || '平台', + altitude: Math.round(Number(labelData.altitude || 0)), + speed: Math.round(Number(labelData.speed || 0)), + heading: Math.round(Number(labelData.headingDeg || 0)) + }; + + // 差异检测:如果关键数据和样式都没变,就不重绘 + const last = labelEntity._lastRenderParams; + const now = Date.now(); + // 节流控制:只有当数据变化且距离上次更新超过 1000ms 时才更新(首次除外) + // 这样可以保证位置平滑移动(position每帧更新),但纹理贴图每秒只更新一次,彻底解决闪烁 + if (last && + last.name === displayData.name && + last.altitude === displayData.altitude && + last.speed === displayData.speed && + last.heading === displayData.heading && + last.fontSize === style.fontSize && + last.fontColor === style.fontColor) { + // 数据完全一致,无需更新 + return; + } + + // 如果数据变了,但还没到 1秒 间隔,且不是强制更新(比如刚修改了样式),则跳过 + if (last && (now - (labelEntity._lastUpdateTime || 0) < 1000)) { + return; + } + + const canvas = this.createRoundedLabelCanvas({ + name: displayData.name, + altitude: displayData.altitude, + speed: displayData.speed, + heading: displayData.heading, + fontSize: style.fontSize, + fontColor: style.fontColor + }); + // 直接赋值 canvas,避免 ConstantProperty 包装可能带来的额外开销(Cesium 会自动处理) + labelEntity.billboard.image = canvas; + + // 记录本次渲染参数和时间 + labelEntity._lastRenderParams = { + ...displayData, + fontSize: style.fontSize, + fontColor: style.fontColor + }; + labelEntity._lastUpdateTime = now; + + // 确保在 requestRenderMode 下能刷出来 + if (this.viewer.scene.requestRenderMode) { + this.viewer.scene.requestRender(); + } + } else if (labelEntity.label) { + labelEntity.label.text = this.formatPlatformLabelText(labelData); + } } } }, @@ -1904,6 +2278,114 @@ export default { }, Cesium.ScreenSpaceEventType.RIGHT_CLICK) }, + openEditPlatformLabelDialog() { + const ed = this.contextMenu.entityData + if (!ed || ed.type !== 'routePlatform' || ed.routeId == null) { + this.contextMenu.visible = false + return + } + const routeId = ed.routeId + const labelEntity = this.viewer && this.viewer.entities && this.viewer.entities.getById(`route-platform-label-${routeId}`) + let fontSize = 16 + let color = '#333333' + if (labelEntity && labelEntity.label) { + const now = Cesium.JulianDate.now() + const fontValue = labelEntity.label.font && labelEntity.label.font.getValue + ? labelEntity.label.font.getValue(now) + : labelEntity.label.font + if (fontValue && typeof fontValue === 'string') { + const match = fontValue.match(/(\d+)px/) + if (match) fontSize = parseInt(match[1], 10) + } + const fill = labelEntity.label.fillColor + if (fill) { + const c = fill.getValue ? fill.getValue(now) : fill + if (c && c.toCssColorString) { + color = c.toCssColorString() + } + } + } + this.editPlatformLabelForm = { + routeId, + fontSize, + color + } + this.contextMenu.visible = false + this.editPlatformLabelDialogVisible = true + }, + + applyEditPlatformLabel() { + const routeId = this.editPlatformLabelForm.routeId + if (!routeId || !this.viewer || !this.viewer.entities) { + this.editPlatformLabelDialogVisible = false + return + } + const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`) + if (labelEntity && labelEntity.label) { + const size = Math.max(10, Math.min(32, Number(this.editPlatformLabelForm.fontSize) || 16)) + const color = this.editPlatformLabelForm.color || '#333333' + labelEntity.label.font = `${size}px Microsoft YaHei` + labelEntity.label.fillColor = Cesium.Color.fromCssColorString(color) + if (this.viewer.scene && this.viewer.scene.requestRenderMode) { + this.viewer.scene.requestRender() + } + } + this.editPlatformLabelDialogVisible = false + }, + + openEditPlatformStyleDialog() { + const ed = this.contextMenu.entityData + if (!ed || ed.type !== 'routePlatform' || ed.routeId == null) { + this.contextMenu.visible = false + return + } + const routeId = ed.routeId + const platformEntity = this.viewer && this.viewer.entities && this.viewer.entities.getById(`route-platform-${routeId}`) + let size = 144 + let color = '#ffffff' + if (platformEntity && platformEntity.billboard) { + const now = Cesium.JulianDate.now() + const widthValue = platformEntity.billboard.width && platformEntity.billboard.width.getValue + ? platformEntity.billboard.width.getValue(now) + : platformEntity.billboard.width + if (widthValue != null) size = Number(widthValue) + const col = platformEntity.billboard.color + if (col) { + const c = col.getValue ? col.getValue(now) : col + if (c && c.toCssColorString) { + color = c.toCssColorString() + } + } + } + this.editPlatformStyleForm = { + routeId, + size, + color + } + this.contextMenu.visible = false + this.editPlatformStyleDialogVisible = true + }, + + applyEditPlatformStyle() { + const routeId = this.editPlatformStyleForm.routeId + if (!routeId || !this.viewer || !this.viewer.entities) { + this.editPlatformStyleDialogVisible = false + return + } + const entity = this.viewer.entities.getById(`route-platform-${routeId}`) + if (entity && entity.billboard) { + const size = Math.max(48, Math.min(256, Number(this.editPlatformStyleForm.size) || 144)) + const color = this.editPlatformStyleForm.color || '#ffffff' + entity.billboard.width = size + entity.billboard.height = size + entity.billboard.color = Cesium.Color.fromCssColorString(color) + if (this.viewer.scene && this.viewer.scene.requestRenderMode) { + this.viewer.scene.requestRender() + } + } + this.editPlatformStyleDialogVisible = false + }, + /** 平台图标图形化操作:伸缩框(旋转手柄 + 四角缩放)、拖拽移动、单击选中 */ initPlatformIconInteraction() { const canvas = this.viewer.scene.canvas; @@ -4231,6 +4713,192 @@ export default { this.contextMenu.visible = false if (this.viewer.scene.requestRenderMode) this.viewer.scene.requestRender() }, + /** 右键飞机:打开编辑平台对话框(合并了标牌和平台) */ + openEditPlatformDialog() { + const ed = this.contextMenu.entityData + if (!ed || ed.type !== 'routePlatform' || ed.routeId == null) { + this.contextMenu.visible = false + return + } + const routeId = ed.routeId + + // 读取标牌设置 + const labelEntity = this.viewer.entities.getById(`route-platform-label-${routeId}`) + let fontSize = 16 + let fontColor = '#333333' + + if (this.routeLabelStyles[routeId]) { + fontSize = this.routeLabelStyles[routeId].fontSize; + fontColor = this.routeLabelStyles[routeId].fontColor; + } else if (labelEntity && labelEntity.label) { + const now = Cesium.JulianDate.now() + const fontValue = labelEntity.label.font && labelEntity.label.font.getValue + ? labelEntity.label.font.getValue(now) + : labelEntity.label.font + if (fontValue && typeof fontValue === 'string') { + const match = fontValue.match(/(\d+)px/) + if (match) fontSize = parseInt(match[1], 10) + } + const fill = labelEntity.label.fillColor + if (fill) { + const c = fill.getValue ? fill.getValue(now) : fill + if (c && c.toCssColorString) { + fontColor = c.toCssColorString() + } + } + } + + // 读取平台设置 + const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`) + let iconSize = 144 + let iconColor = '#ffffff' + if (platformEntity && platformEntity.billboard) { + const now = Cesium.JulianDate.now() + const widthValue = platformEntity.billboard.width && platformEntity.billboard.width.getValue + ? platformEntity.billboard.width.getValue(now) + : platformEntity.billboard.width + if (widthValue != null) iconSize = Number(widthValue) + const col = platformEntity.billboard.color + if (col) { + const c = col.getValue ? col.getValue(now) : col + if (c && c.toCssColorString) { + iconColor = c.toCssColorString() + } + } + } + + this.editPlatformForm.routeId = routeId + this.editPlatformForm.fontSize = fontSize + this.editPlatformForm.fontColor = fontColor + this.editPlatformForm.iconSize = iconSize + this.editPlatformForm.iconColor = iconColor + + this.contextMenu.visible = false + this.editPlatformDialogVisible = true + }, + + /** 应用编辑:同时更新标牌和平台 */ + applyEditPlatform() { + // 强制失去焦点,确保 el-input-number 的 v-model 更新 + if (document.activeElement) { + document.activeElement.blur(); + } + + // 使用 setTimeout 确保 v-model 数据已同步更新 + setTimeout(() => { + const routeId = this.editPlatformForm.routeId + if (!routeId || !this.viewer || !this.viewer.entities) { + this.editPlatformDialogVisible = false + return + } + + // 保存样式 + const fontSize = Math.max(10, Math.min(32, Number(this.editPlatformForm.fontSize) || 16)); + const fontColor = this.editPlatformForm.fontColor || '#333333'; + this.$set(this.routeLabelStyles, routeId, { fontSize, fontColor }); + + // 更新标牌 + 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 canvas = this.createRoundedLabelCanvas({ + name: data.name, + altitude: data.altitude, + speed: data.speed, + heading: data.headingDeg, + fontSize: fontSize, + fontColor: fontColor + }); + // 使用 ConstantProperty 确保更新 + labelEntity.billboard.image = new Cesium.ConstantProperty(canvas); + } 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) { + const size = Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144)) + const color = this.editPlatformForm.iconColor || '#ffffff' + platformEntity.billboard.width = size + platformEntity.billboard.height = size + platformEntity.billboard.color = Cesium.Color.fromCssColorString(color) + } + + if (this.viewer.scene && this.viewer.scene.requestRenderMode) { + this.viewer.scene.requestRender() + // 额外再刷一次,确保纹理上传后能显示 + setTimeout(() => this.viewer.scene.requestRender(), 50) + } + this.editPlatformDialogVisible = false + }, 0); + }, + + /** 右键飞机:打开威力区设置弹窗 */ + openPowerZoneDialog() { + const ed = this.contextMenu.entityData + if (!ed || ed.type !== 'routePlatform' || ed.routeId == null) { + this.contextMenu.visible = false + return + } + // 重置表单,保留上次的 routeId + this.powerZoneForm = { + routeId: ed.routeId, + radius: 1000, + color: 'rgba(255, 0, 0, 0.3)' + } + this.contextMenu.visible = false + this.powerZoneDialogVisible = true + }, + + /** 应用威力区:以当前平台位置为中心画圆 */ + applyPowerZone() { + const routeId = this.powerZoneForm.routeId + if (!routeId || !this.viewer) { + this.powerZoneDialogVisible = false + return + } + const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`) + if (!platformEntity) { + this.$message.error('未找到对应平台,无法添加威力区') + this.powerZoneDialogVisible = false + return + } + + // 移除旧的威力区(如果存在) + const oldZoneId = `power-zone-${routeId}` + this.viewer.entities.removeById(oldZoneId) + + // 添加新威力区 + // 使用 CallbackProperty 绑定位置,使其随飞机移动 + this.viewer.entities.add({ + id: oldZoneId, + position: new Cesium.CallbackProperty(() => { + const now = Cesium.JulianDate.now() + return platformEntity.position.getValue(now) + }, false), + ellipse: { + semiMinorAxis: this.powerZoneForm.radius, + semiMajorAxis: this.powerZoneForm.radius, + material: Cesium.Color.fromCssColorString(this.powerZoneForm.color), + outline: true, + outlineColor: Cesium.Color.fromCssColorString(this.powerZoneForm.color).withAlpha(1.0), + outlineWidth: 2 + } + }) + + if (this.viewer.scene.requestRenderMode) { + this.viewer.scene.requestRender() + } + this.powerZoneDialogVisible = false + this.$message.success(`已添加半径 ${this.powerZoneForm.radius} 米的威力区`) + }, // 从右键菜单删除实体 deleteEntityFromContextMenu() { if (this.contextMenu.entityData) { @@ -5338,4 +6006,38 @@ export default { min-width: 200px; text-align: right; } + +/* 优化:居中且带圆角 */ +::v-deep .el-dialog { + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + margin: 0 !important; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +::v-deep .el-dialog .el-dialog__header { + background-color: #f5f7fa; + border-bottom: 1px solid #ebeef5; + padding: 15px 20px; +} + +::v-deep .el-dialog .el-dialog__title { + font-weight: 600; + font-size: 16px; +} + +::v-deep .el-dialog .el-dialog__body { + padding: 20px 25px; +} + +::v-deep .el-dialog .el-dialog__footer { + border-top: 1px solid #ebeef5; + padding: 10px 20px; + background-color: #fff; +} diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 9d1aacf..6654661 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -990,6 +990,8 @@ export default { // 同步地图:如果该航线正在显示,立即清除 if (this.$refs.cesiumMap) { this.$refs.cesiumMap.removeRouteById(route.id); + // 同时清除该航线的威力区 + this.$refs.cesiumMap.removePowerZoneByRouteId(route.id); } // 同步状态:从选中列表中移除该 ID const idx = this.activeRouteIds.indexOf(route.id); @@ -2566,6 +2568,8 @@ export default { this.activeRouteIds.splice(index, 1); if (this.$refs.cesiumMap) { this.$refs.cesiumMap.removeRouteById(route.id); + // 隐藏航线时,同时移除关联的威力区 + this.$refs.cesiumMap.removePowerZoneByRouteId(route.id); } if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) { if (this.activeRouteIds.length > 0) {