Browse Source

可拖拽图标

master
ctw 2 months ago
parent
commit
b9f3daa8c6
  1. 2
      ruoyi-ui/.env.development
  2. 30
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  3. 526
      ruoyi-ui/src/views/cesiumMap/index.vue
  4. 58
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  5. 87
      ruoyi-ui/src/views/childRoom/index.vue
  6. 2
      ruoyi-ui/vue.config.js

2
ruoyi-ui/.env.development

@ -8,7 +8,7 @@ ENV = 'development'
VUE_APP_BASE_API = '/dev-api'
# 访问地址(绕过 /dev-api 代理,用于解决静态资源/图片访问 401 认证问题)
VUE_APP_BACKEND_URL = 'http://localhost:8080'
VUE_APP_BACKEND_URL = 'http://127.0.0.1:8080'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

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

@ -272,6 +272,24 @@
</div>
</div>
</div>
<!-- 平台图标拖拽到地图的图标特有选项 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon'">
<div class="menu-title">平台图标</div>
<div class="menu-item" @click.stop="handleShowTransformBox">
<span class="menu-icon">📐</span>
<span>显示伸缩框</span>
</div>
<div class="menu-item" @click="handleEditPlatformPosition">
<span class="menu-icon">📍</span>
<span>修改位置</span>
</div>
<div class="menu-item" @click="handleEditPlatformHeading">
<span class="menu-icon">🧭</span>
<span>修改朝向</span>
<span class="menu-value">{{ entityData.heading != null ? entityData.heading + '°' : '0°' }}</span>
</div>
</div>
</div>
</template>
@ -324,6 +342,18 @@ export default {
this.$emit('delete')
},
handleShowTransformBox() {
this.$emit('show-transform-box')
},
handleEditPlatformPosition() {
this.$emit('edit-platform-position')
},
handleEditPlatformHeading() {
this.$emit('edit-platform-heading')
},
toggleColorPicker(property) {
if (this.showColorPickerFor === property) {
this.showColorPickerFor = null

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

@ -34,6 +34,9 @@
:entity-data="contextMenu.entityData"
@delete="deleteEntityFromContextMenu"
@update-property="updateEntityProperty"
@edit-platform-position="openPlatformIconPositionDialog"
@edit-platform-heading="openPlatformIconHeadingDialog"
@show-transform-box="showPlatformIconTransformBox"
/>
<!-- 定位弹窗 -->
@ -44,6 +47,11 @@
@cancel="handleLocateCancel"
/>
<!-- 平台图标旋转模式提示 -->
<div v-if="platformIconRotateTip" class="platform-icon-rotate-tip">
{{ platformIconRotateTip }}
</div>
<!-- 地图右下角比例尺 + 经纬度 -->
<div class="map-info-panel">
<div class="scale-bar" @click="handleScaleClick">
@ -174,7 +182,25 @@ export default {
currentScaleUnit: 'm',
isApplyingScale: false,
//
locateDialogVisible: false
locateDialogVisible: false,
//
draggingPlatformIcon: null,
rotatingPlatformIcon: null,
platformIconRotateTip: '',
platformIconDragCameraEnabled: true,
selectedPlatformIcon: null,
pendingDragIcon: null,
dragStartScreenPos: null,
draggingRotateHandle: null,
draggingScaleHandle: null,
clickedOnEmpty: false,
PLATFORM_ICON_BASE_SIZE: 72,
PLATFORM_SCALE_BOX_HALF_DEG: 0.0005,
PLATFORM_SCALE_BOX_MIN_HALF_DEG: 0.0007,
PLATFORM_ROTATE_HANDLE_OFFSET_DEG: 0.0009,
PLATFORM_DRAG_THRESHOLD_PX: 10,
DESIRED_BOX_HALF_PX: 58,
DESIRED_ROTATE_OFFSET_PX: 50
}
},
components: {
@ -538,6 +564,234 @@ export default {
const backendUrl = process.env.VUE_APP_BACKEND_URL || '';
return backendUrl + cleanPath;
},
/** 默认平台图标(无 imageUrl 时使用):简单飞机剪影 SVG */
getDefaultPlatformIconDataUrl() {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="%23666" d="M28 14L18 8l-4 2v4L8 10 4 14l4 2 2 4-4 2v4l4 2 4-2 2 4 2-2v-6l8-4 4-2v-4l-4 2-8-4z"/></svg>';
return 'data:image/svg+xml,' + encodeURIComponent(svg);
},
/** 伸缩框旋转手柄图标 SVG:蓝底、白边、白色弧形箭头 */
getRotationHandleIconDataUrl() {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">' +
'<circle cx="16" cy="16" r="14" fill="#008aff" stroke="#fff" stroke-width="2"/>' +
'<path fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ' +
'd="M9 16 A7 7 0 0 1 23 16"/>' +
'<path fill="#fff" stroke="#fff" stroke-width="1" d="M22 14.2 L25 16 L22 17.8 Z"/>' +
'</svg>';
return 'data:image/svg+xml,' + encodeURIComponent(svg);
},
/** 从手柄实体 id 解析出平台图标 entityData 与类型(rotate / scale-0~3) */
getPlatformIconDataFromHandleId(handleEntityId) {
if (!handleEntityId || typeof handleEntityId !== 'string') return null;
if (handleEntityId.endsWith('-rotate-handle')) {
const baseId = handleEntityId.replace(/-rotate-handle$/, '');
const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId);
return ed ? { entityData: ed, type: 'rotate' } : null;
}
const scaleIdx = handleEntityId.lastIndexOf('-scale-');
if (scaleIdx !== -1) {
const baseId = handleEntityId.substring(0, scaleIdx);
const cornerIndex = parseInt(handleEntityId.substring(scaleIdx + 7), 10);
if (!isNaN(cornerIndex) && cornerIndex >= 0 && cornerIndex <= 3) {
const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === baseId);
return ed ? { entityData: ed, type: 'scale', cornerIndex } : null;
}
}
return null;
},
/** 在当前视野下,图标位置处 1 像素对应的经纬度(用于伸缩框固定屏幕尺寸) */
getDegreesPerPixelAt(lng, lat) {
if (!this.viewer || this.viewer.scene.mode !== Cesium.SceneMode.SCENE2D) {
return { degPerPxLng: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX, degPerPxLat: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX };
}
const center = Cesium.Cartesian3.fromDegrees(lng, lat);
const east = Cesium.Cartesian3.fromDegrees(lng + 0.005, lat);
const north = Cesium.Cartesian3.fromDegrees(lng, lat + 0.005);
const sc = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, center);
const se = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, east);
const sn = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, north);
if (!sc || !se || !sn) {
return { degPerPxLng: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX, degPerPxLat: this.PLATFORM_SCALE_BOX_HALF_DEG / this.DESIRED_BOX_HALF_PX };
}
const pxLng = Math.max(1, Math.abs(se.x - sc.x));
const pxLat = Math.max(1, Math.abs(sn.y - sc.y));
return {
degPerPxLng: 0.005 / pxLng,
degPerPxLat: 0.005 / pxLat
};
},
/** 更新平台图标 billboard 的宽高(根据 iconScale) */
updatePlatformIconBillboardSize(entityData) {
if (!entityData || !entityData.entity || !entityData.entity.billboard) return;
const scale = Math.max(0.2, Math.min(3, entityData.iconScale || 1));
entityData.iconScale = scale;
const size = this.PLATFORM_ICON_BASE_SIZE * scale;
entityData.entity.billboard.width = new Cesium.ConstantProperty(size);
entityData.entity.billboard.height = new Cesium.ConstantProperty(size);
},
/** 显示伸缩框:旋转手柄 + 四角缩放手柄 + 矩形边线(按屏幕像素固定尺寸,任意缩放都易点) */
showTransformHandles(entityData) {
if (!this.viewer || !entityData || entityData.type !== 'platformIcon') return;
this.removeTransformHandles(entityData);
const id = entityData.id;
const lng = entityData.lng;
const lat = entityData.lat;
const dpp = this.getDegreesPerPixelAt(lng, lat);
const baseHalfDeg = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
const half = Math.max((entityData.iconScale || 1) * baseHalfDeg, baseHalfDeg * 0.5);
const rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * dpp.degPerPxLat;
const rotOffset = Math.max(rotOffsetLat, this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG);
const rotationHandle = this.viewer.entities.add({
id: id + '-rotate-handle',
position: Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset),
billboard: {
image: this.getRotationHandleIconDataUrl(),
width: 36,
height: 36,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
const corners = [
Cesium.Cartesian3.fromDegrees(lng + half, lat + half),
Cesium.Cartesian3.fromDegrees(lng - half, lat + half),
Cesium.Cartesian3.fromDegrees(lng - half, lat - half),
Cesium.Cartesian3.fromDegrees(lng + half, lat - half)
];
const scaleHandles = corners.map((pos, i) =>
this.viewer.entities.add({
id: id + '-scale-' + i,
position: pos,
point: {
pixelSize: 14,
color: Cesium.Color.WHITE,
outlineColor: Cesium.Color.fromCssColorString('#008aff'),
outlineWidth: 3,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
})
);
const linePositions = [corners[0], corners[1], corners[2], corners[3], corners[0]];
const frameLine = this.viewer.entities.add({
id: id + '-scale-frame',
polyline: {
positions: linePositions,
width: 3,
material: Cesium.Color.fromCssColorString('#008aff'),
clampToGround: true,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
});
entityData.transformHandles = {
rotation: rotationHandle,
scale: scaleHandles,
frame: frameLine
};
},
/** 移除伸缩框手柄与边线 */
removeTransformHandles(entityData) {
if (!entityData || !entityData.transformHandles) return;
const h = entityData.transformHandles;
if (h.rotation) this.viewer.entities.remove(h.rotation);
if (h.scale) h.scale.forEach(e => this.viewer.entities.remove(e));
if (h.frame) this.viewer.entities.remove(h.frame);
entityData.transformHandles = null;
},
/** 根据图标当前位置、iconScale 与当前视野更新伸缩框(保持固定像素尺寸) */
updateTransformHandlePositions(entityData) {
if (!entityData || !entityData.transformHandles) return;
const lng = entityData.lng;
const lat = entityData.lat;
const dpp = this.getDegreesPerPixelAt(lng, lat);
const baseHalfDeg = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
const half = Math.max((entityData.iconScale || 1) * baseHalfDeg, baseHalfDeg * 0.5);
const rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * dpp.degPerPxLat;
const rotOffset = Math.max(rotOffsetLat, this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG);
entityData.transformHandles.rotation.position = Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset);
const corners = [
Cesium.Cartesian3.fromDegrees(lng + half, lat + half),
Cesium.Cartesian3.fromDegrees(lng - half, lat + half),
Cesium.Cartesian3.fromDegrees(lng - half, lat - half),
Cesium.Cartesian3.fromDegrees(lng + half, lat - half)
];
entityData.transformHandles.scale.forEach((ent, i) => { ent.position = corners[i]; });
const linePositions = [corners[0], corners[1], corners[2], corners[3], corners[0]];
entityData.transformHandles.frame.polyline.positions = new Cesium.ConstantProperty(linePositions);
this.viewer.scene.requestRender();
},
/**
* 从右侧平台列表拖拽放置到地图在放置点添加平台图标实体支持后续修改位置与朝向
* @param {Object} platform - 平台数据 { id, name, type, imageUrl, iconUrl, icon, color }
* @param {number} clientX - 放置点的视口 X
* @param {number} clientY - 放置点的视口 Y
*/
addPlatformIconFromDrag(platform, clientX, clientY) {
if (!this.viewer || !platform) return;
const canvas = this.viewer.scene.canvas;
const rect = canvas.getBoundingClientRect();
const x = clientX - rect.left;
const y = clientY - rect.top;
const cartesian = this.viewer.camera.pickEllipsoid(new Cesium.Cartesian2(x, y), this.viewer.scene.globe.ellipsoid);
if (!cartesian) {
this.$message && this.$message.warning('请将图标放置到地图有效区域内');
return;
}
const iconUrl = platform.imageUrl || platform.iconUrl;
const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl();
this.entityCounter++;
const id = `platformIcon_${this.entityCounter}`;
const headingDeg = 0;
const rotation = Math.PI / 2 - (headingDeg * Math.PI / 180);
const iconScale = 1.0;
const size = this.PLATFORM_ICON_BASE_SIZE * iconScale;
const entity = this.viewer.entities.add({
id,
name: platform.name || '平台',
position: cartesian,
billboard: {
image: imageSrc,
width: size,
height: size,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
rotation,
scaleByDistance: new Cesium.NearFarScalar(500, 1.2, 200000, 0.35),
translucencyByDistance: new Cesium.NearFarScalar(1000, 1.0, 500000, 0.6)
}
});
const { lat, lng } = this.cartesianToLatLng(cartesian);
const entityData = {
id,
type: 'platformIcon',
platformId: platform.id,
platform,
name: platform.name || '平台',
heading: headingDeg,
lat,
lng,
entity,
imageUrl: iconUrl,
label: platform.name || '平台',
iconScale,
transformHandles: null
};
this.allEntities.push(entityData);
this.$nextTick(() => {
if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon);
this.selectedPlatformIcon = entityData;
this.showTransformHandles(entityData);
this.$message && this.$message.success('已放置。上方箭头旋转、四角调大小、拖动图标移动;点击空白收起');
});
},
//线
renderRouteWaypoints(waypoints, routeId = 'default', platformId, platform, style) {
if (!waypoints || waypoints.length < 1) return;
@ -1238,6 +1492,7 @@ export default {
})
this.initScaleBar()
this.initPointMovement()
this.initPlatformIconInteraction()
this.initRightClickHandler()
this.initHoverHandler()
this.initMouseCoordinates()
@ -1254,6 +1509,10 @@ export default {
const pickedObject = this.viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject) && pickedObject.id) {
const entity = pickedObject.id;
const idStr = (entity && entity.id) ? entity.id : '';
if (idStr && (idStr.endsWith('-rotate-handle') || idStr.indexOf('-scale-') !== -1)) return;
const platformIconData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === entity);
if (platformIconData) return;
// --- ---
console.log(">>> [点击检测] 实体ID:", entity.id);
@ -1328,6 +1587,183 @@ export default {
}
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
},
/** 平台图标图形化操作:伸缩框(旋转手柄 + 四角缩放)、拖拽移动、单击选中 */
initPlatformIconInteraction() {
const canvas = this.viewer.scene.canvas;
this.platformIconHandler = new Cesium.ScreenSpaceEventHandler(canvas);
this.platformIconHandler.setInputAction((click) => {
if (this.isDrawing || this.rotatingPlatformIcon) return;
const picked = this.viewer.scene.pick(click.position);
this.clickedOnEmpty = !Cesium.defined(picked) || !picked.id;
if (picked && picked.id) {
const idStr = typeof picked.id === 'string' ? picked.id : (picked.id.id || '');
if (idStr.endsWith('-scale-frame')) {
this.clickedOnEmpty = false;
return;
}
const handleInfo = this.getPlatformIconDataFromHandleId(idStr);
if (handleInfo) {
this.clickedOnEmpty = false;
if (handleInfo.type === 'rotate') {
this.draggingRotateHandle = handleInfo.entityData;
this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
return;
}
if (handleInfo.type === 'scale') {
this.draggingScaleHandle = { entityData: handleInfo.entityData, cornerIndex: handleInfo.cornerIndex };
this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
return;
}
}
const entityData = this.allEntities.find(e => e.type === 'platformIcon' && e.entity === picked.id);
if (entityData) {
this.pendingDragIcon = entityData;
this.dragStartScreenPos = { x: click.position.x, y: click.position.y };
return;
}
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
this.platformIconHandler.setInputAction((movement) => {
if (this.draggingRotateHandle) {
const ed = this.draggingRotateHandle;
if (ed.entity && ed.entity.position) {
const now = Cesium.JulianDate.now();
const position = ed.entity.position.getValue(now);
if (position) {
const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, position);
if (screenPos) {
const dx = movement.endPosition.x - screenPos.x;
const dy = movement.endPosition.y - screenPos.y;
const screenAngle = Math.atan2(dy, dx);
ed.entity.billboard.rotation = -screenAngle;
let headingDeg = (screenAngle + Math.PI / 2) * (180 / Math.PI);
if (headingDeg < 0) headingDeg += 360;
if (headingDeg >= 360) headingDeg -= 360;
ed.heading = Math.round(headingDeg);
}
}
}
this.viewer.scene.requestRender();
return;
}
if (this.draggingScaleHandle) {
const { entityData: ed, cornerIndex } = this.draggingScaleHandle;
const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
if (cartesian) {
const { lat: newLat, lng: newLng } = this.cartesianToLatLng(cartesian);
const lng = ed.lng;
const lat = ed.lat;
const dpp = this.getDegreesPerPixelAt(lng, lat);
const baseHalf = this.DESIRED_BOX_HALF_PX * Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
let newHalfDeg;
if (cornerIndex === 0) newHalfDeg = Math.min(newLng - lng, newLat - lat);
else if (cornerIndex === 1) newHalfDeg = Math.min(lng - newLng, newLat - lat);
else if (cornerIndex === 2) newHalfDeg = Math.min(lng - newLng, lat - newLat);
else newHalfDeg = Math.min(newLng - lng, lat - newLat);
if (newHalfDeg > 0.0001 && baseHalf > 1e-10) {
ed.iconScale = Math.max(0.2, Math.min(3, newHalfDeg / baseHalf));
this.updatePlatformIconBillboardSize(ed);
this.updateTransformHandlePositions(ed);
}
}
return;
}
if (this.pendingDragIcon) {
const dx = movement.endPosition.x - this.dragStartScreenPos.x;
const dy = movement.endPosition.y - this.dragStartScreenPos.y;
if (Math.sqrt(dx * dx + dy * dy) > (this.PLATFORM_DRAG_THRESHOLD_PX || 10)) {
this.draggingPlatformIcon = this.pendingDragIcon;
this.pendingDragIcon = null;
this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs;
this.viewer.scene.screenSpaceCameraController.enableInputs = false;
}
}
if (this.draggingPlatformIcon) {
const cartesian = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
if (cartesian) {
this.draggingPlatformIcon.entity.position = cartesian;
const { lat, lng } = this.cartesianToLatLng(cartesian);
this.draggingPlatformIcon.lat = lat;
this.draggingPlatformIcon.lng = lng;
if (this.selectedPlatformIcon === this.draggingPlatformIcon) {
this.updateTransformHandlePositions(this.draggingPlatformIcon);
}
}
this.viewer.scene.requestRender();
}
if (this.rotatingPlatformIcon && this.rotatingPlatformIcon.entity && this.rotatingPlatformIcon.entity.position) {
const now = Cesium.JulianDate.now();
const position = this.rotatingPlatformIcon.entity.position.getValue(now);
if (position) {
const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(this.viewer.scene, position);
if (screenPos) {
const dx = movement.endPosition.x - screenPos.x;
const dy = movement.endPosition.y - screenPos.y;
const screenAngle = Math.atan2(dy, dx);
this.rotatingPlatformIcon.entity.billboard.rotation = -screenAngle;
let headingDeg = (screenAngle + Math.PI / 2) * (180 / Math.PI);
if (headingDeg < 0) headingDeg += 360;
if (headingDeg >= 360) headingDeg -= 360;
this.rotatingPlatformIcon.heading = Math.round(headingDeg);
}
}
this.viewer.scene.requestRender();
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
this.platformIconHandler.setInputAction(() => {
if (this.pendingDragIcon) {
if (this.selectedPlatformIcon === this.pendingDragIcon) {
this.removeTransformHandles(this.selectedPlatformIcon);
this.selectedPlatformIcon = null;
} else {
if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon);
this.selectedPlatformIcon = this.pendingDragIcon;
this.showTransformHandles(this.pendingDragIcon);
}
this.pendingDragIcon = null;
this.dragStartScreenPos = null;
}
if (this.clickedOnEmpty && this.selectedPlatformIcon) {
this.removeTransformHandles(this.selectedPlatformIcon);
this.selectedPlatformIcon = null;
}
this.clickedOnEmpty = false;
if (this.draggingRotateHandle) {
this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
this.draggingRotateHandle = null;
}
if (this.draggingScaleHandle) {
this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
this.draggingScaleHandle = null;
}
if (this.draggingPlatformIcon) {
this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
this.draggingPlatformIcon = null;
}
if (this.rotatingPlatformIcon) {
this.viewer.scene.screenSpaceCameraController.enableInputs = this.platformIconDragCameraEnabled;
this.rotatingPlatformIcon = null;
this.platformIconRotateTip = '';
}
}, Cesium.ScreenSpaceEventType.LEFT_UP);
const onCameraMoveEnd = () => {
if (this.selectedPlatformIcon && this.selectedPlatformIcon.transformHandles) {
this.updateTransformHandlePositions(this.selectedPlatformIcon);
}
};
this.viewer.camera.moveEnd.addEventListener(onCameraMoveEnd);
this.platformIconCameraListener = () => {
this.viewer.camera.moveEnd.removeEventListener(onCameraMoveEnd);
};
},
//
initHoverHandler() {
//
@ -3385,6 +3821,53 @@ export default {
this.contextMenu.visible = false
}
},
/** 右键「显示伸缩框」:选中该图标并显示旋转/缩放手柄 */
showPlatformIconTransformBox() {
const fromMenu = this.contextMenu.entityData
if (!fromMenu || fromMenu.type !== 'platformIcon' || !fromMenu.entity) {
this.contextMenu.visible = false
return
}
const ed = this.allEntities.find(e => e.type === 'platformIcon' && e.id === fromMenu.id) || fromMenu
if (!ed.entity) {
this.contextMenu.visible = false
return
}
if (ed.lat == null || ed.lng == null) {
const now = Cesium.JulianDate.now()
const pos = ed.entity.position && ed.entity.position.getValue(now)
if (pos) {
const ll = this.cartesianToLatLng(pos)
ed.lat = ll.lat
ed.lng = ll.lng
}
}
if (this.selectedPlatformIcon) this.removeTransformHandles(this.selectedPlatformIcon)
this.selectedPlatformIcon = ed
this.showTransformHandles(ed)
this.contextMenu.visible = false
this.viewer.scene.requestRender()
this.$message && this.$message.success('已显示伸缩框')
},
/** 位置改为图形化:直接拖动图标即可,无需弹窗 */
openPlatformIconPositionDialog() {
this.contextMenu.visible = false
this.$message && this.$message.info('请直接拖动图标以修改位置')
},
/** 进入旋转模式:移动鼠标设置朝向,单击结束(期间锁定地图) */
openPlatformIconHeadingDialog() {
const ed = this.contextMenu.entityData
if (!ed || ed.type !== 'platformIcon' || !ed.entity) return
this.rotatingPlatformIcon = ed
this.platformIconRotateTip = '移动鼠标设置朝向,单击结束'
this.platformIconDragCameraEnabled = this.viewer.scene.screenSpaceCameraController.enableInputs
this.viewer.scene.screenSpaceCameraController.enableInputs = false
this.contextMenu.visible = false
this.$message && this.$message.info('移动鼠标可调整朝向,单击地图任意处结束')
},
removeEntity(id) {
//
const index = this.allEntities.findIndex(e =>
@ -3394,23 +3877,23 @@ export default {
)
if (index > -1) {
const entity = this.allEntities[index]
//
if (entity.type === 'platformIcon') {
this.removeTransformHandles(entity)
if (this.selectedPlatformIcon === entity) this.selectedPlatformIcon = null
}
//
if (entity instanceof Cesium.Entity) {
// A: Cesium Entity
this.viewer.entities.remove(entity)
} else if (entity.entity) {
// B: entity
this.viewer.entities.remove(entity.entity)
}
// 线
if (entity.type === 'line' && entity.pointEntities) {
entity.pointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity)
})
}
//
this.allEntities.splice(index, 1)
//
if (this.selectedEntity && (this.selectedEntity.id === id || (this.selectedEntity.entity && this.selectedEntity.entity.id === id))) {
this.selectedEntity = null
}
@ -3466,12 +3949,15 @@ export default {
this.viewer.entities.remove(entity);
}
// 线
if (item.type === 'line' && item.pointEntities) {
item.pointEntities.forEach(pointEntity => {
this.viewer.entities.remove(pointEntity);
});
}
if (item.type === 'platformIcon') {
this.removeTransformHandles(item);
if (this.selectedPlatformIcon === item) this.selectedPlatformIcon = null;
}
} catch (e) {
console.warn('删除实体失败:', e);
}
@ -4093,6 +4579,15 @@ export default {
this.pointMovementHandler = null
}
if (this.platformIconHandler) {
this.platformIconHandler.destroy()
this.platformIconHandler = null
}
if (typeof this.platformIconCameraListener === 'function') {
this.platformIconCameraListener()
this.platformIconCameraListener = null
}
if (this.rightClickHandler) {
this.rightClickHandler.destroy()
this.rightClickHandler = null
@ -4209,6 +4704,23 @@ export default {
display: none !important;
}
/* 平台图标旋转模式提示条:放在顶部菜单下方,避免被遮挡(顶部栏约 60px) */
.platform-icon-rotate-tip {
position: absolute;
top: 72px;
left: 50%;
transform: translateX(-50%);
z-index: 99;
background: rgba(0, 138, 255, 0.95);
color: #fff;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
pointer-events: none;
white-space: nowrap;
}
/* 地图右下角信息面板:比例尺在上、经纬度在下,整体略下移减少遮挡 */
.map-info-panel {
position: absolute;

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

@ -164,8 +164,10 @@
<div
v-for="platform in airPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
class="platform-item platform-item-draggable"
draggable="true"
@click="handlePlatformItemClick(platform, $event)"
@dragstart="onPlatformDragStart($event, platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<img v-if="isImg(platform.imageUrl)"
@ -195,8 +197,10 @@
<div
v-for="platform in seaPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
class="platform-item platform-item-draggable"
draggable="true"
@click="handlePlatformItemClick(platform, $event)"
@dragstart="onPlatformDragStart($event, platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<img v-if="isImg(platform.imageUrl)"
@ -221,8 +225,10 @@
<div
v-for="platform in groundPlatforms"
:key="platform.id"
class="platform-item"
@click="handleOpenPlatformDialog(platform)"
class="platform-item platform-item-draggable"
draggable="true"
@click="handlePlatformItemClick(platform, $event)"
@dragstart="onPlatformDragStart($event, platform)"
>
<div class="platform-icon" :style="{ color: platform.color }">
<img v-if="isImg(platform.imageUrl)"
@ -313,7 +319,8 @@ export default {
return {
activePlatformTab: 'air',
expandedPlans: [], //
expandedRoutes: [] // 线
expandedRoutes: [], // 线
platformJustDragged: false //
}
},
watch: {
@ -504,6 +511,35 @@ export default {
handleOpenPlatformDialog(platform) {
this.$emit('open-platform-dialog', platform)
},
/** 平台项点击:若刚拖拽过则不打开弹窗,避免误触 */
handlePlatformItemClick(platform, ev) {
if (this.platformJustDragged) {
this.platformJustDragged = false
return
}
this.handleOpenPlatformDialog(platform)
},
/** 拖拽平台图标到地图时传递平台数据 */
onPlatformDragStart(ev, platform) {
this.platformJustDragged = true
setTimeout(() => { this.platformJustDragged = false }, 300)
try {
ev.dataTransfer.setData('application/json', JSON.stringify({
id: platform.id,
name: platform.name,
type: platform.type,
imageUrl: platform.imageUrl,
iconUrl: platform.iconUrl,
icon: platform.icon,
color: platform.color
}))
ev.dataTransfer.effectAllowed = 'copy'
} catch (e) {
console.warn('Platform drag start failed', e)
}
}
}
}
@ -829,6 +865,14 @@ export default {
box-shadow: 0 2px 8px rgba(0, 138, 255, 0.15);
}
.platform-item-draggable {
cursor: grab;
}
.platform-item-draggable:active {
cursor: grabbing;
}
.platform-icon {
width: 40px;
height: 40px;

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

@ -1,8 +1,13 @@
<template>
<!-- 以地图为绝对定位背景所有组件浮动其上 -->
<div class="mission-planning-container">
<!-- 地图背景 -->
<div id="gis-map-background" class="map-background">
<!-- 地图背景支持从右侧平台列表拖拽图标到地图 -->
<div
id="gis-map-background"
class="map-background"
@dragover.prevent="handleMapDragover"
@drop="handleMapDrop"
>
<!-- cesiummap组件 -->
<cesiumMap ref="cesiumMap" :drawDomClick="drawDom || airspaceDrawDom"
:tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
@ -423,13 +428,13 @@ export default {
showScaleDialog: false,
currentScale: {
scaleNumerator: 1,
scaleDenominator: 20,
unit: 'km'
scaleDenominator: 1000,
unit: 'm'
},
scaleConfig: {
scaleNumerator: 1,
scaleDenominator: 20,
unit: 'km'
scaleDenominator: 1000,
unit: 'm'
},
showExternalParamsDialog: false,
currentExternalParams: {},
@ -610,12 +615,10 @@ export default {
isAdmin() {
const roles = this.$store.getters.roles || [];
const id = this.$store.getters.id;
const userLevel = String(this.$store.getters.userLevel);
return (
id === '1' || // ID
userLevel === '1' || // user_level
roles.includes('admin') || //
roles.includes('manager') //
roles.includes('admin') ||
String(id) === '1' ||
(Array.isArray(roles) && roles.some(r => String(r).toLowerCase() === 'admin'))
);
},
canSetKTime() {
@ -637,7 +640,7 @@ export default {
addHoldDialogTip() {
if (!this.addHoldContext) return '';
if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。';
return `将冲突位置「${this.addHoldContext.toName}」替换为盘旋航点,到计划时间后沿切线飞往下一航点,路径顺序不变`;
return `${this.addHoldContext.fromName}${this.addHoldContext.toName} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)`;
}
},
mounted() {
@ -1248,7 +1251,6 @@ export default {
openKTimeSetDialog() {
console.log("当前登录 ID (myId):", this.$store.getters.id);
console.log("当前房间 ownerId:", this.roomDetail ? this.roomDetail.ownerId : '无房间信息');
console.log("当前房间 userLevel:", this.$store.getters.userLevel);
console.log("当前角色 roles:", this.$store.getters.roles);
if (!this.canSetKTime) {
this.$message.info('仅房主或管理员可设定或修改 K 时');
@ -1553,6 +1555,27 @@ export default {
this.showScaleDialog = true
},
/** 拖拽经过地图时允许放置 */
handleMapDragover(ev) {
ev.preventDefault()
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'copy'
},
/** 将平台图标放置到地图上 */
handleMapDrop(ev) {
ev.preventDefault()
const raw = ev.dataTransfer && ev.dataTransfer.getData('application/json')
if (!raw) return
try {
const platform = JSON.parse(raw)
if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.addPlatformIconFromDrag === 'function') {
this.$refs.cesiumMap.addPlatformIconFromDrag(platform, ev.clientX, ev.clientY)
}
} catch (e) {
console.warn('Parse platform drag data failed', e)
}
},
handleScaleUnitChange(unit) {
this.scaleConfig.unit = unit
if (this.$refs.cesiumMap) {
@ -2231,32 +2254,36 @@ export default {
return;
}
const waypoints = route.waypoints;
const targetWp = waypoints[legIndex + 1];
if (!targetWp || !targetWp.id) {
this.$message.warning('目标航点无效');
return;
}
const prevWp = waypoints[legIndex];
const nextWp = waypoints[legIndex + 1];
const newSeq = (prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1) + 1;
const baseSeq = prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1;
const holdParams = this.addHoldForm.holdType === 'hold_circle'
? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise }
: { semiMajor: this.addHoldForm.semiMajor, semiMinor: this.addHoldForm.semiMinor, headingDeg: this.addHoldForm.headingDeg, clockwise: this.addHoldForm.clockwise };
const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes))
? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes))
: (targetWp.startTime || 'K+01:00');
: (nextWp.startTime || 'K+01:00');
try {
await updateWaypoints({
id: targetWp.id,
routeId: routeId,
await addWaypoints({
routeId,
name: 'HOLD',
seq: targetWp.seq,
lat: targetWp.lat,
lng: targetWp.lng,
alt: targetWp.alt,
speed: targetWp.speed != null ? targetWp.speed : 800,
seq: newSeq,
lat: nextWp.lat,
lng: nextWp.lng,
alt: nextWp.alt != null ? nextWp.alt : prevWp.alt,
speed: prevWp.speed || 800,
startTime,
turnAngle: targetWp.turnAngle != null ? targetWp.turnAngle : 0,
pointType: this.addHoldForm.holdType,
holdParams: JSON.stringify(holdParams)
});
await delWaypoints(nextWp.id);
for (let i = legIndex + 2; i < waypoints.length; i++) {
const w = waypoints[i];
if (w.id) {
await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) });
}
}
this.showAddHoldDialog = false;
this.addHoldContext = null;
await this.getList();
@ -2266,9 +2293,9 @@ export default {
this.$refs.cesiumMap.renderRouteWaypoints(updated.waypoints, routeId, updated.platformId, updated.platform, this.parseRouteStyle(updated.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
this.$message.success('已将冲突位置航点替换为盘旋航点');
this.$message.success('已添加盘旋航点');
} catch (e) {
this.$message.error(e.msg || '替换盘旋失败');
this.$message.error(e.msg || '添加盘旋失败');
console.error(e);
}
},

2
ruoyi-ui/vue.config.js

@ -15,7 +15,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
const baseUrl = 'http://localhost:8080' // 后端接口
const baseUrl = 'http://127.0.0.1:8080' // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口
// 定义 Cesium 源码路径

Loading…
Cancel
Save