Browse Source

截图展示,以及各种功能

ctw
cuitw 6 days ago
parent
commit
7aa0690206
  1. 43
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java
  2. 18
      ruoyi-ui/src/api/system/routes.js
  3. 74
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  4. 698
      ruoyi-ui/src/views/cesiumMap/index.vue
  5. 481
      ruoyi-ui/src/views/childRoom/ScreenshotGalleryPanel.vue
  6. 62
      ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue
  7. 286
      ruoyi-ui/src/views/childRoom/index.vue
  8. 14
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

43
ruoyi-admin/src/main/java/com/ruoyi/web/controller/RoutesController.java

@ -192,6 +192,46 @@ public class RoutesController extends BaseController
}
/**
* Redis 获取截图展示数据GET + roomId 查询参数
* 与保存共用路径前缀方法不同避免与其它 GET 字面路径网关规则混淆
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping("/roomScreenshotGallery")
public AjaxResult getScreenshotGalleryData(@RequestParam Long roomId)
{
if (roomId == null) {
return AjaxResult.error("房间ID不能为空");
}
String key = "room:" + String.valueOf(roomId) + ":screenshot_gallery";
String val = fourTRedisTemplate.opsForValue().get(key);
if (val != null && !val.isEmpty()) {
try {
return success(JSON.parseObject(val));
} catch (Exception e) {
return success(val);
}
}
return success();
}
/**
* 保存截图展示悬浮窗数据到 RedisPOST 与上面 GET 同一路径
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@PostMapping("/roomScreenshotGallery")
public AjaxResult saveScreenshotGalleryData(@RequestBody java.util.Map<String, Object> params)
{
Object roomId = params.get("roomId");
Object data = params.get("data");
if (roomId == null || data == null) {
return AjaxResult.error("参数不完整");
}
String key = "room:" + String.valueOf(roomId) + ":screenshot_gallery";
fourTRedisTemplate.opsForValue().set(key, data.toString());
return success();
}
/**
* 保存六步法任务页数据到 Redis背景图标文本框
*/
@PreAuthorize("@ss.hasPermi('system:routes:edit')")
@ -395,9 +435,10 @@ public class RoutesController extends BaseController
/**
* 获取实体部署与航线详细信息
* 路径仅匹配数字 id避免与 /get4TData/getScreenshotGalleryData 等字面路径冲突
*/
@PreAuthorize("@ss.hasPermi('system:routes:query')")
@GetMapping(value = "/{id}")
@GetMapping(value = "/{id:\\d+}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(routesService.selectRoutesById(id));

18
ruoyi-ui/src/api/system/routes.js

@ -93,6 +93,24 @@ export function get4TData(params) {
})
}
// 截图展示:同一路径 GET 读 / POST 写(Redis),与 4T 同权限
export function saveScreenshotGalleryData(data) {
return request({
url: '/system/routes/roomScreenshotGallery',
method: 'post',
data,
headers: { repeatSubmit: false }
})
}
export function getScreenshotGalleryData(params) {
return request({
url: '/system/routes/roomScreenshotGallery',
method: 'get',
params
})
}
// 保存六步法任务页数据到 Redis(背景、图标、文本框)
export function saveTaskPageData(data) {
return request({

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

@ -9,9 +9,13 @@
</div>
</div>
<!-- 框选多平台批量删除 -->
<!-- 框选多平台复制摆放 / 批量删除 -->
<div class="menu-section" v-if="entityData && entityData.type === 'platformBoxSelection'">
<div class="menu-title">框选平台{{ entityData.count }} </div>
<div class="menu-item" @click="handleCopyBoxSelection">
<span class="menu-icon">📋</span>
<span>复制</span>
</div>
<div class="menu-item" @click="handleDeleteBoxSelection">
<span class="menu-icon">🗑</span>
<span>删除全部框选平台</span>
@ -537,6 +541,10 @@
<!-- 平台图标拖拽到地图的图标特有选项白板平台不显示探测区/威力区/航线 -->
<div class="menu-section" v-if="entityData.type === 'platformIcon' && !entityData.isWhiteboard">
<div class="menu-title">平台图标</div>
<div class="menu-item" @click="openRoomPlatformIconColorDialog">
<span class="menu-icon">🎨</span>
<span>图标颜色</span>
</div>
<div class="menu-item" @click="handleShowTransformBox">
<span class="menu-icon">🔄</span>
<span>显示伸缩框</span>
@ -601,21 +609,26 @@
</div>
<el-dialog
title="颜色与大小"
:title="platformIconColorDialogScope === 'room' ? '图标颜色' : '颜色与大小'"
:visible.sync="showWhiteboardPlatformStyleDialog"
width="360px"
append-to-body
:close-on-click-modal="false"
>
<el-form :model="whiteboardPlatformStyleForm" label-width="90px" size="small">
<el-form-item label="颜色">
<el-form-item label="着色">
<el-checkbox v-model="whiteboardPlatformStyleForm.useNativeColor" @change="onWhiteboardNativeColorChange">
与列表原图一致不着色
</el-checkbox>
</el-form-item>
<el-form-item label="颜色" v-show="!whiteboardPlatformStyleForm.useNativeColor">
<el-color-picker
v-model="whiteboardPlatformStyleForm.color"
:predefine="presetColors"
@active-change="handleWhiteboardColorActiveChange"
/>
</el-form-item>
<el-form-item label="大小">
<el-form-item v-if="platformIconColorDialogScope === 'whiteboard'" label="大小">
<el-select v-model="whiteboardPlatformStyleForm.iconScale" style="width:100%">
<el-option
v-for="scale in presetPlatformScales"
@ -693,8 +706,11 @@ export default {
showSizePicker: false,
sizePickerType: '',
showWhiteboardPlatformStyleDialog: false,
/** room:房间地图独立平台,仅颜色;whiteboard:白板平台颜色+缩放 */
platformIconColorDialogScope: 'whiteboard',
whiteboardPlatformStyleForm: {
color: '#008aff',
useNativeColor: true,
color: '#165dff',
iconScale: 1.5
},
showOpacityPicker: false,
@ -762,6 +778,10 @@ export default {
this.$emit('delete-box-selected-platforms')
},
handleCopyBoxSelection() {
this.$emit('copy-box-selected-platforms')
},
handleAdjustPosition() {
this.$emit('adjust-airspace-position')
},
@ -801,19 +821,59 @@ export default {
this.$emit('show-transform-box')
},
openWhiteboardPlatformStyleDialog() {
const color = (this.entityData && this.entityData.color) || '#008aff'
const raw = this.entityData && this.entityData.color
const useNative =
raw == null ||
raw === '' ||
(typeof raw === 'string' && raw.trim() === '')
const color = useNative ? '#165dff' : raw
const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1.5
this.platformIconColorDialogScope = 'whiteboard'
this.whiteboardPlatformStyleForm = {
useNativeColor: useNative,
color,
iconScale: Number.isFinite(iconScale) ? iconScale : 1.5
}
this.showWhiteboardPlatformStyleDialog = true
},
openRoomPlatformIconColorDialog() {
const raw = this.entityData && this.entityData.color
const useNative =
raw == null ||
raw === '' ||
(typeof raw === 'string' && raw.trim() === '')
const color = useNative ? '#165dff' : raw
const iconScale = this.entityData && this.entityData.iconScale != null ? Number(this.entityData.iconScale) : 1
this.platformIconColorDialogScope = 'room'
this.whiteboardPlatformStyleForm = {
useNativeColor: useNative,
color,
iconScale: Number.isFinite(iconScale) ? iconScale : 1
}
this.showWhiteboardPlatformStyleDialog = true
},
onWhiteboardNativeColorChange(native) {
if (!native && this.whiteboardPlatformStyleForm && !this.whiteboardPlatformStyleForm.color) {
this.whiteboardPlatformStyleForm.color = '#165dff'
}
},
handleWhiteboardColorActiveChange(color) {
if (color) this.whiteboardPlatformStyleForm.color = color
},
confirmWhiteboardPlatformStyle() {
const color = this.whiteboardPlatformStyleForm.color || '#008aff'
const useNative = !!this.whiteboardPlatformStyleForm.useNativeColor
const color = useNative
? null
: this.whiteboardPlatformStyleForm.color || '#165dff'
if (this.platformIconColorDialogScope === 'room') {
this.showWhiteboardPlatformStyleDialog = false
this.$emit('apply-room-platform-icon-color', {
id: this.entityData && this.entityData.id,
color
})
this.$emit('close-menu')
return
}
const iconScale = Number(this.whiteboardPlatformStyleForm.iconScale)
if (!Number.isFinite(iconScale) || iconScale <= 0) {
this.$message && this.$message.warning('请选择有效大小')

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

@ -68,8 +68,10 @@
@launch-missile="openLaunchMissileDialog"
@adjust-airspace-position="startAirspacePositionEdit"
@apply-whiteboard-platform-style="applyWhiteboardPlatformStyleFromMenu"
@apply-room-platform-icon-color="applyRoomPlatformIconColorFromMenu"
@close-menu="contextMenu.visible = false"
@delete-box-selected-platforms="deleteBoxSelectedPlatformsFromMenu"
@copy-box-selected-platforms="copyBoxSelectedPlatformsFromMenu"
/>
<!-- 定位弹窗 -->
@ -89,7 +91,7 @@
<!-- 框选平台模式说明 + 已选列表 -->
<div v-if="platformBoxSelectMode" class="platform-box-select-hud">
<div class="platform-box-select-tip">
已进入框选平台模式
{{ whiteboardMode ? '已进入白板框选模式' : '已进入框选平台模式' }}
</div>
<div
v-if="platformBoxSelectSelectedSummary.length"
@ -112,7 +114,8 @@
v-else
class="platform-box-select-panel platform-box-select-panel--empty"
>
尚未框选到平台在空白处拖拽矩形框住图标即可加入列表
<template v-if="whiteboardMode">尚未框选到白板平台在空白处拖拽矩形框住图标即可</template>
<template v-else>尚未框选到平台在空白处拖拽矩形框住图标即可加入列表</template>
</div>
</div>
<div v-show="platformBoxSelectOverlayVisible" class="platform-box-select-layer">
@ -131,6 +134,9 @@
</template>
<template v-else>点击地图放置复制航线右键取消</template>
</div>
<div v-if="platformCopyPlaceState && platformCopyPlaceState.items && platformCopyPlaceState.items.length" class="copy-route-tip">
实时预览中半透明图标为待放置的复制平台随鼠标移动在目标位置左键点击地图完成部署右键取消
</div>
<!-- 空域位置调整提示 -->
<div v-if="airspacePositionEditContext" class="copy-route-tip">
@ -659,6 +665,15 @@ export default {
TRANSFORM_HANDLE_POINT_SIZE: 11,
TRANSFORM_HANDLE_POINT_OUTLINE_WIDTH: 2,
TRANSFORM_HANDLE_GLOW_ALPHA: 0.10,
/** 2D:deg/px 超过此值开始缩小伸缩框(地图拉远) */
TRANSFORM_BOX_ZOOM_SHRINK_START_DEG_PER_PX: 4e-6,
/** 2D:deg/px 达到此值缩放到最小比例 */
TRANSFORM_BOX_ZOOM_SHRINK_END_DEG_PER_PX: 5.5e-5,
/** 3D:相机高度超过此值开始缩小(非 2D 时 dpp 为常数 fallback,用高度判断) */
TRANSFORM_BOX_ZOOM_3D_SHRINK_START_HEIGHT: 180000,
TRANSFORM_BOX_ZOOM_3D_SHRINK_END_HEIGHT: 4500000,
/** 拉远时伸缩框与手柄的最小视觉比例(相对 DESIRED_* 像素目标) */
TRANSFORM_BOX_ZOOM_MIN_VISUAL_SCALE: 0.5,
//
entityClickDebounceTimer: null,
lastEntityClickTime: 0,
@ -680,6 +695,11 @@ export default {
routeSegmentPickContext: null,
/** 航段移动预览元数据,与 copyPreviewWaypoints 同时使用 */
routeSegmentPlaceMeta: null,
/** 框选复制平台:{ items: [{ ecefDelta, template }], previewEntityDataList };ecefDelta 相对首点地心直角坐标偏移,保持编队形状 */
platformCopyPlaceState: null,
platformCopyMouseCartesian: null,
/** 上一帧鼠标屏幕坐标,preRender 中拾取并更新预览(与 MOUSE_MOVE 同步) */
platformCopyPointerScreen: null,
// /{ routeId, waypointIndex, mode: 'before'|'after', waypoints }线
addWaypointContext: null,
addWaypointSolidEntity: null,
@ -1850,6 +1870,45 @@ export default {
});
},
/** 白板平台:color 为空表示与列表原图一致(Cesium 使用原纹理 + 白色乘色) */
isWhiteboardPlatformNativeColor(c) {
return c == null || c === ''
},
/**
* 同步白板平台 billboardnull 为原图有颜色则转白底后乘色
*/
applyWhiteboardPlatformIconVisual(entityData) {
if (!entityData || !entityData.entity || !entityData.entity.billboard) return
const platform = entityData.platform || {}
const iconUrl = platform.imageUrl || platform.iconUrl
const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl()
const bb = entityData.entity.billboard
const native = this.isWhiteboardPlatformNativeColor(entityData.color)
if (native) {
entityData._whiteboardIconPrepared = true
bb.image = imageSrc
bb.color = Cesium.Color.WHITE
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) {
this.viewer.scene.requestRender()
}
return
}
entityData._whiteboardIconPrepared = false
this.loadAndWhitenImage(imageSrc).then((whiteImage) => {
if (!entityData.entity || !entityData.entity.billboard) return
if (this.isWhiteboardPlatformNativeColor(entityData.color)) return
entityData.entity.billboard.image = whiteImage
entityData.entity.billboard.color = Cesium.Color.fromCssColorString(
entityData.color || '#008aff'
)
entityData._whiteboardIconPrepared = true
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) {
this.viewer.scene.requestRender()
}
}).catch(() => {})
},
/**
* 生成圆角矩形 + 多色文本的 Canvas 图像
* @param {Object} options 配置项
@ -2041,6 +2100,32 @@ export default {
};
},
/**
* 地图拉远时缩小伸缩框/手柄的屏幕占比框按像素锚定时仍会比 footprint 显大需随视野收小
* @param {{ degPerPxLng: number, degPerPxLat: number }} dpp
* @returns {number} 0~1乘在 DESIRED_BOX_HALF_PX线宽点大小等上
*/
getPlatformTransformBoxVisualScale(dpp) {
if (!this.viewer) return 1;
const minS = this.TRANSFORM_BOX_ZOOM_MIN_VISUAL_SCALE;
if (this.viewer.scene.mode === Cesium.SceneMode.SCENE2D) {
const degPerPx = Math.min(dpp.degPerPxLng, dpp.degPerPxLat);
const t0 = this.TRANSFORM_BOX_ZOOM_SHRINK_START_DEG_PER_PX;
const t1 = this.TRANSFORM_BOX_ZOOM_SHRINK_END_DEG_PER_PX;
if (degPerPx <= t0) return 1;
if (degPerPx >= t1) return minS;
const u = (degPerPx - t0) / (t1 - t0);
return 1 - u * (1 - minS);
}
const h = this.viewer.camera.positionCartographic.height;
const h0 = this.TRANSFORM_BOX_ZOOM_3D_SHRINK_START_HEIGHT;
const h1 = this.TRANSFORM_BOX_ZOOM_3D_SHRINK_END_HEIGHT;
if (h <= h0) return 1;
if (h >= h1) return minS;
const u = (h - h0) / (h1 - h0);
return 1 - u * (1 - minS);
},
/** 更新平台图标 billboard 的宽高(根据 iconScale) */
updatePlatformIconBillboardSize(entityData) {
if (!entityData || !entityData.entity || !entityData.entity.billboard) return;
@ -2059,17 +2144,24 @@ export default {
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 vis = this.getPlatformTransformBoxVisualScale(dpp);
const baseHalfDeg = this.DESIRED_BOX_HALF_PX * vis * 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 rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * vis * dpp.degPerPxLat;
const rotOffMin = this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG * vis;
const rotOffset = Math.max(rotOffsetLat, rotOffMin);
const rotImg = Math.max(20, Math.round(36 * vis));
const ptSize = Math.max(5, this.TRANSFORM_HANDLE_POINT_SIZE * vis);
const ptOutline = Math.max(1, Math.round(this.TRANSFORM_HANDLE_POINT_OUTLINE_WIDTH * vis));
const fw = Math.max(1, this.TRANSFORM_HANDLE_FRAME_WIDTH * vis);
const glowW = Math.max(1, fw + 3 * vis);
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,
width: rotImg,
height: rotImg,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY
@ -2086,10 +2178,10 @@ export default {
id: id + '-scale-' + i,
position: pos,
point: {
pixelSize: this.TRANSFORM_HANDLE_POINT_SIZE,
pixelSize: ptSize,
color: Cesium.Color.WHITE.withAlpha(0.98),
outlineColor: Cesium.Color.fromCssColorString(this.TRANSFORM_HANDLE_PRIMARY_COLOR),
outlineWidth: this.TRANSFORM_HANDLE_POINT_OUTLINE_WIDTH,
outlineWidth: ptOutline,
disableDepthTestDistance: Number.POSITIVE_INFINITY
}
})
@ -2100,7 +2192,7 @@ export default {
id: id + '-scale-frame-glow',
polyline: {
positions: linePositions,
width: this.TRANSFORM_HANDLE_FRAME_WIDTH + 3,
width: glowW,
material: Cesium.Color.fromCssColorString(this.TRANSFORM_HANDLE_PRIMARY_COLOR).withAlpha(this.TRANSFORM_HANDLE_GLOW_ALPHA),
arcType: Cesium.ArcType.NONE,
disableDepthTestDistance: Number.POSITIVE_INFINITY
@ -2110,7 +2202,7 @@ export default {
id: id + '-scale-frame',
polyline: {
positions: linePositions,
width: this.TRANSFORM_HANDLE_FRAME_WIDTH,
width: fw,
material: Cesium.Color.fromCssColorString(this.TRANSFORM_HANDLE_PRIMARY_COLOR).withAlpha(0.98),
arcType: Cesium.ArcType.NONE,
disableDepthTestDistance: Number.POSITIVE_INFINITY
@ -2141,11 +2233,33 @@ export default {
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 vis = this.getPlatformTransformBoxVisualScale(dpp);
const baseHalfDeg = this.DESIRED_BOX_HALF_PX * vis * 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 rotOffsetLat = this.DESIRED_ROTATE_OFFSET_PX * vis * dpp.degPerPxLat;
const rotOffMin = this.PLATFORM_ROTATE_HANDLE_OFFSET_DEG * vis;
const rotOffset = Math.max(rotOffsetLat, rotOffMin);
entityData.transformHandles.rotation.position = Cesium.Cartesian3.fromDegrees(lng, lat + rotOffset);
const rot = entityData.transformHandles.rotation;
if (rot && rot.billboard) {
const rotImg = Math.max(20, Math.round(36 * vis));
rot.billboard.width = rotImg;
rot.billboard.height = rotImg;
}
const ptSize = Math.max(5, this.TRANSFORM_HANDLE_POINT_SIZE * vis);
const ptOutline = Math.max(1, Math.round(this.TRANSFORM_HANDLE_POINT_OUTLINE_WIDTH * vis));
entityData.transformHandles.scale.forEach((ent) => {
if (ent && ent.point) {
ent.point.pixelSize = ptSize;
ent.point.outlineWidth = ptOutline;
}
});
const fw = Math.max(1, this.TRANSFORM_HANDLE_FRAME_WIDTH * vis);
const glowW = Math.max(1, fw + 3 * vis);
const fr = entityData.transformHandles.frame;
const fg = entityData.transformHandles.frameGlow;
if (fr && fr.polyline) fr.polyline.width = fw;
if (fg && fg.polyline) fg.polyline.width = glowW;
const corners = [
Cesium.Cartesian3.fromDegrees(lng + half, lat + half),
Cesium.Cartesian3.fromDegrees(lng - half, lat + half),
@ -2318,6 +2432,15 @@ export default {
getPlatformStyle(query).then(res => {
const style = res.data;
if (!style) return;
const platEd = this.allEntities.find(e => e.type === 'platformIcon' && e.id === platformIconId)
if (platEd && platEd.entity && platEd.entity.billboard) {
const pc = style.platformColor != null && String(style.platformColor).trim() !== ''
? String(style.platformColor).trim()
: null
platEd.color = pc
platEd._whiteboardIconPrepared = false
this.applyWhiteboardPlatformIconVisual(platEd)
}
const normalized = this.normalizeZonesFromStyle(style)
const detectionZones = normalized.detectionZones || []
const powerZones = normalized.powerZones || []
@ -5135,6 +5258,56 @@ export default {
this.lastClickWasDrag = false;
return;
}
//
if (this.platformCopyPlaceState && this.platformCopyPlaceState.items && this.platformCopyPlaceState.items.length) {
const placePosition = this.pickMapPlaceCartesian(click.position)
if (placePosition) {
const isWbCopy = !!this.platformCopyPlaceState.isWhiteboardCopy
const { items } = this.platformCopyPlaceState
let platforms
if (isWbCopy) {
platforms = items.map((it) => {
const pos = Cesium.Cartesian3.add(placePosition, it.ecefDelta, new Cesium.Cartesian3())
const ll = this.cartesianToLatLng(pos)
const t = it.template
return {
platformId: t.platformId,
platform: t.platform || {},
platformName: t.platformName,
label: t.label,
heading: t.heading != null ? t.heading : 0,
iconScale: t.iconScale != null ? t.iconScale : 1.5,
color:
t.color != null && String(t.color).trim() !== ''
? t.color
: null,
lat: ll.lat,
lng: ll.lng
}
})
} else {
platforms = items.map((it) => {
const pos = Cesium.Cartesian3.add(placePosition, it.ecefDelta, new Cesium.Cartesian3())
const ll = this.cartesianToLatLng(pos)
return {
...it.template,
lat: ll.lat,
lng: ll.lng
}
})
}
const roomId = this.roomId != null ? this.roomId : (this.$route && this.$route.query && this.$route.query.roomId)
this.clearPlatformCopyPreview()
if (isWbCopy) {
this.$emit('whiteboard-platforms-copy-placed', { platforms })
} else {
this.$emit('platform-icons-copy-placed', { roomId, platforms })
}
} else {
this.$message && this.$message.warning('此处无法拾取地面坐标,请缩小地图或移动视角后再左键放置')
}
return
}
//
if (this.routeSegmentPickContext) {
const ctx = this.routeSegmentPickContext;
@ -5266,7 +5439,9 @@ export default {
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);
const platformIconData = this.allEntities.find(
e => e.type === 'platformIcon' && e.entity === entity && !e.isCopyPreview
);
const now = Cesium.JulianDate.now();
props = entity.properties ? entity.properties.getValue(now) : null;
isWaypoint = props && props.isMissionWaypoint;
@ -5335,7 +5510,9 @@ export default {
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);
const platformIconData = this.allEntities.find(
e => e.type === 'platformIcon' && e.entity === entity && !e.isCopyPreview
);
const now = Cesium.JulianDate.now();
const props = entity.properties ? entity.properties.getValue(now) : null;
const isEntity = platformIconData || (props && (props.isMissionWaypoint || props.isMissionRouteLine));
@ -5351,7 +5528,7 @@ export default {
// pending
this.handler.setInputAction((click) => {
if (this.isDrawing || this.copyPreviewWaypoints || this.routeSegmentPickContext) return;
if (this.isDrawing || this.copyPreviewWaypoints || this.routeSegmentPickContext || this.platformCopyPlaceState) return;
const pickedObject = this.viewer.scene.pick(click.position);
if (!Cesium.defined(pickedObject) || !pickedObject.id) return;
const now = Cesium.JulianDate.now();
@ -5377,6 +5554,14 @@ export default {
// / MOUSE_MOVE
this.handler.setInputAction((movement) => {
if (this.platformCopyPlaceState && this.platformCopyPlaceState.items && this.platformCopyPlaceState.items.length) {
if (!this.platformCopyPointerScreen) {
this.platformCopyPointerScreen = new Cesium.Cartesian2()
}
this.platformCopyPointerScreen.x = movement.endPosition.x
this.platformCopyPointerScreen.y = movement.endPosition.y
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender()
}
if (this.waypointDragPending) {
const dx = movement.endPosition.x - this.waypointDragPending.startScreenX;
const dy = movement.endPosition.y - this.waypointDragPending.startScreenY;
@ -5531,9 +5716,22 @@ export default {
const p = drillPicks[i]
const obj = p && p.id
if (!obj) continue
const ed = this.allEntities.find(
e => e.type === 'platformIcon' && !e.isWhiteboard && e.entity === obj
)
const ed =
this.allEntities.find(
e =>
e.type === 'platformIcon' &&
!e.isWhiteboard &&
!e.isCopyPreview &&
e.entity === obj
) ||
Object.values(this.whiteboardEntityDataMap || {}).find(
e =>
e &&
e.type === 'platformIcon' &&
e.isWhiteboard &&
!e.isCopyPreview &&
e.entity === obj
)
if (ed && multi.indexOf(ed) !== -1) {
hitInSelection = ed
break
@ -5573,6 +5771,12 @@ export default {
this.contextMenu.visible = false;
return;
}
if (this.platformCopyPlaceState && this.platformCopyPlaceState.items && this.platformCopyPlaceState.items.length) {
this.clearPlatformCopyPreview();
this.contextMenu.visible = false;
this.$message && this.$message.info('已取消复制平台');
return;
}
//
if (this.addWaypointContext) {
this.clearAddWaypointContext();
@ -5992,6 +6196,18 @@ export default {
this.draggingPlatformGroup = false
},
/** 框选统计用的候选平台:白板模式仅白板平台;否则仅房间地图平台 */
getPlatformBoxSelectCandidates() {
if (this.whiteboardMode) {
return Object.values(this.whiteboardEntityDataMap || {}).filter(
e => e && e.type === 'platformIcon' && e.isWhiteboard && e.entity && !e.isCopyPreview
)
}
return (this.allEntities || []).filter(
e => e && e.type === 'platformIcon' && !e.isWhiteboard && !e.isCopyPreview && e.entity
)
},
_finalizePlatformBoxSelect() {
if (!this.platformBoxSelectAnchor || !this.platformBoxSelectCurrent) return
const ax = this.platformBoxSelectAnchor.x
@ -6012,8 +6228,7 @@ export default {
const now = Cesium.JulianDate.now()
const scene = this.viewer.scene
const next = []
for (const ed of this.allEntities) {
if (ed.type !== 'platformIcon' || ed.isWhiteboard) continue
for (const ed of this.getPlatformBoxSelectCandidates()) {
if (!ed.entity || !ed.entity.position) continue
const pos = ed.entity.position.getValue(now)
if (!pos) continue
@ -6027,7 +6242,11 @@ export default {
this.platformMultiSelected = next
this.applyPlatformMultiSelectHighlight(next)
if (next.length === 0) {
this.$message && this.$message.info('框选范围内没有房间平台图标(仅统计地图上的独立平台图标)')
this.$message && this.$message.info(
this.whiteboardMode
? '框选范围内没有白板平台图标'
: '框选范围内没有房间平台图标(仅统计地图上的独立平台图标)'
)
}
},
@ -6036,7 +6255,7 @@ export default {
this.exitPlatformBoxSelectMode()
this.contextMenu.visible = false
this.$emit('request-exit-platform-box-select')
this.$message && this.$message.info('已退出框选平台模式')
this.$message && this.$message.info('已退出框选模式')
},
/** 退出框选模式时由父组件 ref 或 watch 调用:清理状态与相机锁定 */
@ -6062,6 +6281,7 @@ export default {
this.platformIconHandler.setInputAction((click) => {
if (this.isDrawing || this.rotatingPlatformIcon) return;
if (this.platformCopyPlaceState && this.platformCopyPlaceState.items && this.platformCopyPlaceState.items.length) return;
const picked = this.viewer.scene.pick(click.position);
this.clickedOnEmpty = !Cesium.defined(picked) || !picked.id;
let idStr = '';
@ -6094,13 +6314,17 @@ export default {
}
}
if (this.platformBoxSelectMode && !this.rotatingPlatformIcon) {
const isRoomPlatform = entityData && entityData.type === 'platformIcon' && !entityData.isWhiteboard;
if (isRoomPlatform && this.platformMultiSelected.length > 0 && this.platformMultiSelected.indexOf(entityData) !== -1) {
const isBoxTarget =
entityData &&
entityData.type === 'platformIcon' &&
!entityData.isCopyPreview &&
(this.whiteboardMode ? !!entityData.isWhiteboard : !entityData.isWhiteboard)
if (isBoxTarget && this.platformMultiSelected.length > 0 && this.platformMultiSelected.indexOf(entityData) !== -1) {
this._startPlatformGroupDrag(click.position);
this.clickedOnEmpty = false;
return;
}
if (!isRoomPlatform) {
if (!isBoxTarget) {
this._startPlatformBoxSelect(click.position);
return;
}
@ -6114,6 +6338,7 @@ export default {
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
this.platformIconHandler.setInputAction((movement) => {
if (this.platformCopyPlaceState && this.platformCopyPlaceState.items && this.platformCopyPlaceState.items.length) return;
if (this.platformBoxSelectAnchor && this.platformBoxSelectMode) {
this.platformBoxSelectCurrent = { x: movement.endPosition.x, y: movement.endPosition.y };
const ax = this.platformBoxSelectAnchor.x;
@ -6156,7 +6381,7 @@ export default {
}
}
if (this.draggingPlatformGroup && this.groupDragPickStart && this.groupDragEntityCartesians) {
const cur = this.viewer.camera.pickEllipsoid(movement.endPosition, this.viewer.scene.globe.ellipsoid);
const cur = this.pickMapPlaceCartesian(movement.endPosition)
if (cur) {
if (!this._cartScratchDelta) this._cartScratchDelta = new Cesium.Cartesian3();
Cesium.Cartesian3.subtract(cur, this.groupDragPickStart, this._cartScratchDelta);
@ -6203,7 +6428,8 @@ export default {
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);
const vis = this.getPlatformTransformBoxVisualScale(dpp);
const baseHalf = this.DESIRED_BOX_HALF_PX * vis * 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);
@ -6261,6 +6487,7 @@ export default {
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
this.platformIconHandler.setInputAction(() => {
if (this.platformCopyPlaceState && this.platformCopyPlaceState.items && this.platformCopyPlaceState.items.length) return;
if (this.platformBoxSelectAnchor) {
if (this.platformBoxSelectDragging) {
this._finalizePlatformBoxSelect();
@ -6277,7 +6504,20 @@ export default {
}
if (this.pendingGroupDrag || this.draggingPlatformGroup) {
if (this.draggingPlatformGroup && this.platformMultiSelected.length) {
this.$emit('platform-icons-batch-updated', [...this.platformMultiSelected]);
const list = [...this.platformMultiSelected]
if (this.whiteboardMode && list.some(e => e && e.isWhiteboard)) {
this.$emit(
'whiteboard-platforms-batch-updated',
list.filter(e => e && e.isWhiteboard).map(ed => ({
id: ed.id,
lat: ed.lat,
lng: ed.lng,
heading: ed.heading != null ? ed.heading : 0
}))
)
} else {
this.$emit('platform-icons-batch-updated', list)
}
}
this.pendingGroupDrag = false;
this.draggingPlatformGroup = false;
@ -8369,6 +8609,28 @@ export default {
const cartesian = this.viewer.camera.pickEllipsoid(pixelPosition, this.viewer.scene.globe.ellipsoid)
return cartesian
},
/** 放置/拖拽用:椭球拾取失败时尝试深度拾取与 globe 射线求交,便于 2D 与宽视角下复制预览跟随鼠标 */
pickMapPlaceCartesian(pixelPosition) {
if (!this.viewer || !pixelPosition) return undefined
const scene = this.viewer.scene
const ellipsoid = scene.globe.ellipsoid
let c = this.viewer.camera.pickEllipsoid(pixelPosition, ellipsoid)
if (c) return c
if (scene.pickPositionSupported) {
try {
const p = scene.pickPosition(pixelPosition)
if (p) return p
} catch (e) {}
}
const ray = this.viewer.camera.getPickRay(pixelPosition)
if (ray && scene.globe) {
try {
const g = scene.globe.pick(ray, scene)
if (g) return g
} catch (e) {}
}
return undefined
},
cartesianToLatLng(cartesian) {
const cartographic = Cesium.Cartographic.fromCartesian(cartesian)
return {
@ -8748,9 +9010,14 @@ export default {
if (data.iconScale != null) {
this.updatePlatformIconBillboardSize(data)
}
if (data.color) {
entity.billboard.color = Cesium.Color.fromCssColorString(data.color)
data._whiteboardIconPrepared = false
if (data.isWhiteboard) {
const plat = data.platform || {}
const iu = plat.imageUrl || plat.iconUrl || ''
const cc = data.color != null && String(data.color).trim() !== '' ? data.color : null
data._wbVisualSignature = `${cc == null ? '' : cc}\0${iu}`
}
this.applyWhiteboardPlatformIconVisual(data)
}
break
case 'text': {
@ -8988,6 +9255,254 @@ export default {
this.copyPreviewMouseCartesian = null;
this.routeSegmentPlaceMeta = null;
},
_buildWhiteboardCopyTemplate(ed) {
if (!ed) return null
const plat = ed.platform || {}
const platformId = ed.platformId != null ? ed.platformId : plat.id
if (platformId == null) return null
return {
platformId,
platform: { ...plat, id: platformId },
platformName: ed.platformName || plat.name || '',
label: ed.label || plat.name || '平台',
iconUrl: ed.imageUrl || plat.imageUrl || plat.iconUrl || '',
heading: ed.heading != null ? ed.heading : 0,
iconScale: ed.iconScale != null ? ed.iconScale : 1.5,
color:
ed.color != null && String(ed.color).trim() !== '' ? ed.color : null
}
},
_buildPlatformCopyTemplate(ed) {
if (!ed) return null
const plat = ed.platform || {}
const cache = this.platformIconZoneStyles && this.platformIconZoneStyles[ed.id]
let detectionZones = []
let powerZones = []
if (cache) {
try {
detectionZones = JSON.parse(JSON.stringify(cache.detectionZones || []))
powerZones = JSON.parse(JSON.stringify(cache.powerZones || []))
} catch (e) {
detectionZones = [...(cache.detectionZones || [])]
powerZones = [...(cache.powerZones || [])]
}
}
return {
platformId: ed.platformId != null ? ed.platformId : plat.id,
platformName: plat.name || ed.name || '',
platformType: plat.type || '',
iconUrl: ed.imageUrl || plat.imageUrl || plat.iconUrl || '',
heading: ed.heading != null ? ed.heading : 0,
iconScale: ed.iconScale != null ? ed.iconScale : 1,
detectionZones,
powerZones
}
},
updatePlatformCopyPreviewPositions() {
if (!this.platformCopyPlaceState || !this.platformCopyMouseCartesian || !this.viewer) return
const { items, previewEntityDataList } = this.platformCopyPlaceState
if (!items || !previewEntityDataList || items.length !== previewEntityDataList.length) return
const anchor = this.platformCopyMouseCartesian
for (let i = 0; i < items.length; i++) {
const ed = previewEntityDataList[i]
const it = items[i]
if (!ed || !it || !ed.entity || !it.ecefDelta) continue
const newPos = Cesium.Cartesian3.add(anchor, it.ecefDelta, new Cesium.Cartesian3())
const ll = this.cartesianToLatLng(newPos)
ed.lat = ll.lat
ed.lng = ll.lng
ed.entity.position = newPos
}
},
_bindPlatformCopyPreRender() {
if (this._platformCopyPreRenderListener || !this.viewer) return
this._platformCopyPreRenderListener = () => {
if (!this.platformCopyPlaceState || !this.platformCopyPointerScreen || !this.viewer) return
const c = this.pickMapPlaceCartesian(this.platformCopyPointerScreen)
if (c) {
this.platformCopyMouseCartesian = c
this.updatePlatformCopyPreviewPositions()
}
}
this.viewer.scene.preRender.addEventListener(this._platformCopyPreRenderListener)
},
_unbindPlatformCopyPreRender() {
if (!this._platformCopyPreRenderListener) return
if (this.viewer && this.viewer.scene) {
try {
this.viewer.scene.preRender.removeEventListener(this._platformCopyPreRenderListener)
} catch (e) {}
}
this._platformCopyPreRenderListener = null
},
clearPlatformCopyPreview() {
this._unbindPlatformCopyPreRender()
this.platformCopyPointerScreen = null
if (!this.platformCopyPlaceState || !this.viewer) {
this.platformCopyPlaceState = null
this.platformCopyMouseCartesian = null
return
}
const list = this.platformCopyPlaceState.previewEntityDataList || []
for (const ed of list) {
this.removeTransformHandles(ed)
if (this.selectedPlatformIcon === ed) this.selectedPlatformIcon = null
if (ed.entity) this.viewer.entities.remove(ed.entity)
const idx = this.allEntities.indexOf(ed)
if (idx !== -1) this.allEntities.splice(idx, 1)
}
this.platformCopyPlaceState = null
this.platformCopyMouseCartesian = null
},
/** 框选右键菜单:复制为新实例并进入跟随鼠标摆放(房间平台 / 白板平台) */
copyBoxSelectedPlatformsFromMenu() {
this.contextMenu.visible = false
const raw = [...(this.platformMultiSelected || [])]
const isWb = !!this.whiteboardMode
const list = raw.filter((e) => {
if (!e || e.type !== 'platformIcon' || e.isCopyPreview) return false
if (isWb) return !!e.isWhiteboard
return !e.isWhiteboard && e.serverId != null
})
if (!list.length) {
this.$message && this.$message.warning(
isWb ? '请先框选白板上的平台图标后再复制' : '请先框选已保存到房间的地图平台图标后再复制'
)
return
}
if (!this.viewer) return
this.clearPlatformCopyPreview()
const refLat = list[0].lat
const refLng = list[0].lng
const roomId = !isWb ? list[0].roomId : null
const now = Cesium.JulianDate.now()
const refCart = Cesium.Cartesian3.fromDegrees(refLng, refLat)
const items = []
const previewEntityDataList = []
const tseq = Date.now()
let idx = 0
for (const ed of list) {
const tpl = isWb ? this._buildWhiteboardCopyTemplate(ed) : this._buildPlatformCopyTemplate(ed)
if (!tpl || tpl.platformId == null) continue
let p = null
if (ed.entity && ed.entity.position) {
try {
p = ed.entity.position.getValue(now)
} catch (err) {}
}
if (!p) p = Cesium.Cartesian3.fromDegrees(ed.lng, ed.lat)
const ecefDelta = Cesium.Cartesian3.subtract(p, refCart, new Cesium.Cartesian3())
items.push({ ecefDelta, template: tpl })
const platform = isWb
? tpl.platform
: {
id: tpl.platformId,
name: tpl.platformName,
type: tpl.platformType,
imageUrl: tpl.iconUrl,
iconUrl: tpl.iconUrl
}
const iconUrl =
tpl.iconUrl || (platform && (platform.imageUrl || platform.iconUrl || ''))
const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl()
this.entityCounter++
const id = `platformIcon_copypreview_${tseq}_${idx++}`
const headingDeg = tpl.heading != null ? tpl.heading : 0
const rotation = Math.PI / 2 - (headingDeg * Math.PI / 180)
const iconScale = Math.max(
0.2,
Math.min(3, tpl.iconScale != null ? tpl.iconScale : (isWb ? 1.5 : 1))
)
const size = this.PLATFORM_ICON_BASE_SIZE * iconScale
const cartesian = Cesium.Cartesian3.add(refCart, ecefDelta, new Cesium.Cartesian3())
const ll0 = this.cartesianToLatLng(cartesian)
const lat = ll0.lat
const lng = ll0.lng
const platName =
(platform && (platform.name || platform.platformName)) ||
tpl.platformName ||
'平台'
const previewLabel = `[复制] ${platName}`
const wbNative = isWb && this.isWhiteboardPlatformNativeColor(tpl.color)
const billboardColor = isWb
? (wbNative
? Cesium.Color.WHITE.withAlpha(0.88)
: Cesium.Color.fromCssColorString(tpl.color || '#008aff').withAlpha(0.88))
: Cesium.Color.WHITE.withAlpha(0.88)
const entity = this.viewer.entities.add({
id,
name: platName,
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),
color: billboardColor
},
label: {
text: previewLabel,
font: '13px sans-serif',
fillColor: Cesium.Color.fromCssColorString('#0d47a1'),
outlineColor: Cesium.Color.WHITE,
outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
pixelOffset: new Cesium.Cartesian2(0, 36),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
scaleByDistance: new Cesium.NearFarScalar(500, 1.0, 200000, 0.45)
}
})
const entityData = {
id,
type: 'platformIcon',
isCopyPreview: true,
platformId: tpl.platformId,
platform,
name: platName,
heading: headingDeg,
lat,
lng,
entity,
imageUrl: iconUrl,
label: tpl.label || platName,
iconScale: tpl.iconScale != null ? tpl.iconScale : (isWb ? 1.5 : 1),
transformHandles: null,
serverId: null,
roomId: roomId || undefined
}
this.allEntities.push(entityData)
previewEntityDataList.push(entityData)
}
if (!previewEntityDataList.length) {
this.$message && this.$message.warning('无法复制:缺少平台类型信息')
return
}
this.platformCopyPlaceState = { items, previewEntityDataList, isWhiteboardCopy: isWb }
const canvas = this.viewer.scene.canvas
this.platformCopyPointerScreen = new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2)
this.platformCopyMouseCartesian =
this.pickMapPlaceCartesian(this.platformCopyPointerScreen) ||
Cesium.Cartesian3.fromDegrees(refLng, refLat)
this._bindPlatformCopyPreRender()
this.updatePlatformCopyPreviewPositions()
this.exitPlatformBoxSelectMode()
this.$emit('request-exit-platform-box-select')
this.$message && this.$message.info('预览随鼠标移动,左键点击地图放置')
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender()
},
/** 右键菜单:打开航点编辑(支持 dbId 或 waypointIndex) */
handleContextMenuOpenWaypointDialog(dbId, routeId, waypointIndex) {
this.contextMenu.visible = false;
@ -11208,8 +11723,18 @@ export default {
type: 'warning'
})
.then(() => {
const ids = list.map(ed => ed.id).filter(Boolean)
ids.forEach(id => this.removeEntity(id))
list.forEach((ed) => {
if (!ed || !ed.id) return
if (ed.isWhiteboard) {
this.$emit('whiteboard-entity-deleted', ed)
if (ed.entity) this.viewer.entities.remove(ed.entity)
if (this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[ed.id]) {
delete this.whiteboardEntityDataMap[ed.id]
}
} else {
this.removeEntity(ed.id)
}
})
this.platformMultiSelected = []
this.$message && this.$message.success(`已删除 ${n} 个平台图标`)
})
@ -11623,7 +12148,10 @@ export default {
heading: entityData.heading != null ? entityData.heading : 0,
iconScale: entityData.iconScale != null ? entityData.iconScale : 1.5,
label: entityData.label || '',
color: entityData.color || '#ffffff'
color:
entityData.color != null && String(entityData.color).trim() !== ''
? entityData.color
: null
})
} else if (this.getDrawingEntityTypes().includes(entityData.type)) {
this.notifyDrawingEntitiesChanged()
@ -11675,7 +12203,16 @@ export default {
this.contextMenu.visible = false
return
}
const color = (payload && payload.color) || ed.color || '#008aff'
let color
if (payload && Object.prototype.hasOwnProperty.call(payload, 'color')) {
color =
payload.color != null && String(payload.color).trim() !== ''
? payload.color
: null
} else {
color =
ed.color != null && String(ed.color).trim() !== '' ? ed.color : null
}
const rawScale = payload && payload.iconScale != null ? Number(payload.iconScale) : (ed.iconScale != null ? Number(ed.iconScale) : 1.5)
const iconScale = Math.max(0.2, Math.min(3, Number.isFinite(rawScale) ? rawScale : 1.5))
ed.color = color
@ -11711,6 +12248,46 @@ export default {
this.$message && this.$message.success('白板平台样式已更新')
},
/** 非白板:房间地图独立平台右键「图标颜色」,与白板相同着色逻辑;持久化走 platformStyle.platformColor(routeId=0) */
applyRoomPlatformIconColorFromMenu(payload) {
const targetId = payload && payload.id
const ed = this.allEntities.find(
e => e.type === 'platformIcon' && !e.isWhiteboard && !e.isCopyPreview && e.id === targetId
)
this.contextMenu.visible = false
if (!ed || !ed.entity || !ed.entity.billboard) {
return
}
let color = null
if (payload && Object.prototype.hasOwnProperty.call(payload, 'color')) {
color =
payload.color != null && String(payload.color).trim() !== ''
? String(payload.color).trim()
: null
}
ed.color = color
ed._whiteboardIconPrepared = false
this.applyWhiteboardPlatformIconVisual(ed)
if (this.selectedPlatformIcon && this.selectedPlatformIcon.id === ed.id && ed.transformHandles) {
this.updateTransformHandlePositions(ed)
}
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) {
this.viewer.scene.requestRender()
}
if (!ed.serverId || !ed.roomId) {
this.$message &&
this.$message.warning('图标颜色已预览,保存到房间后可永久保存着色(请等待拖拽放置完成后再试)')
return
}
this.$emit('room-platform-icon-style-updated', {
serverId: ed.serverId,
roomId: ed.roomId,
platformId: ed.platformId,
platformColor: color
})
this.$message && this.$message.success('图标颜色已更新')
},
/** 右键「在此之前插入航线」:先画平台前的航点,右键结束时将平台作为最后一站 */
handleStartRouteBeforePlatform() {
const entityData = this.contextMenu.entityData
@ -12362,36 +12939,25 @@ export default {
existing.lng = ed.lng
existing.heading = ed.heading != null ? ed.heading : 0
existing.iconScale = ed.iconScale != null ? ed.iconScale : 1.5
existing.color = ed.color || existing.color || '#008aff'
if (Object.prototype.hasOwnProperty.call(ed, 'color')) {
existing.color =
ed.color != null && String(ed.color).trim() !== '' ? ed.color : null
}
if (ed.platform) existing.platform = ed.platform
existing.entity.position = Cesium.Cartesian3.fromDegrees(ed.lng, ed.lat)
existing.entity.billboard.rotation = Math.PI / 2 - (existing.heading * Math.PI / 180)
this.updatePlatformIconBillboardSize(existing)
if (existing.entity.billboard) {
existing.entity.billboard.color = Cesium.Color.fromCssColorString(existing.color)
const plat = existing.platform || {}
const iconUrl = plat.imageUrl || plat.iconUrl || ''
const visSig = `${existing.color == null ? '' : existing.color}\0${iconUrl}`
if (existing._wbVisualSignature !== visSig) {
existing._wbVisualSignature = visSig
existing._whiteboardIconPrepared = false
this.applyWhiteboardPlatformIconVisual(existing)
}
this.ensureWhiteboardPlatformColorableImage(existing)
if (existing.transformHandles) this.updateTransformHandlePositions(existing)
if (this.viewer.scene && this.viewer.scene.requestRender) this.viewer.scene.requestRender()
},
/** 白板平台图标转白底后再着色,避免黑色图标乘色后看不出颜色变化 */
ensureWhiteboardPlatformColorableImage(entityData) {
if (!entityData || !entityData.entity || !entityData.entity.billboard) return
if (entityData._whiteboardIconPrepared) return
const platform = entityData.platform || {}
const iconUrl = platform.imageUrl || platform.iconUrl
const imageSrc = iconUrl ? this.formatPlatformIconUrl(iconUrl) : this.getDefaultPlatformIconDataUrl()
this.loadAndWhitenImage(imageSrc).then((whiteImage) => {
if (!entityData || !entityData.entity || !entityData.entity.billboard) return
entityData.entity.billboard.image = whiteImage
if (entityData.color) {
entityData.entity.billboard.color = Cesium.Color.fromCssColorString(entityData.color)
}
entityData._whiteboardIconPrepared = true
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) {
this.viewer.scene.requestRender()
}
}).catch(() => {})
},
/** 白板平台图标:添加 billboard,并存入 whiteboardEntityDataMap 以支持拖拽/旋转 */
addWhiteboardPlatformIcon(ed) {
const platform = ed.platform || {}
@ -12401,10 +12967,12 @@ export default {
const lat = ed.lat
const lng = ed.lng
const heading = ed.heading != null ? ed.heading : 0
const color = ed.color || '#008aff'
const color =
ed.color != null && String(ed.color).trim() !== '' ? ed.color : null
const rotation = Math.PI / 2 - (heading * Math.PI / 180)
const size = this.PLATFORM_ICON_BASE_SIZE * (ed.iconScale != null ? ed.iconScale : 1.5)
const cartesian = Cesium.Cartesian3.fromDegrees(lng, lat)
const native = this.isWhiteboardPlatformNativeColor(color)
const entity = this.viewer.entities.add({
id,
name: ed.label || platform.name || '平台',
@ -12413,7 +12981,9 @@ export default {
image: imageSrc,
width: size,
height: size,
color: Cesium.Color.fromCssColorString(color),
color: native
? Cesium.Color.WHITE
: Cesium.Color.fromCssColorString(color || '#008aff'),
verticalOrigin: Cesium.VerticalOrigin.CENTER,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
rotation,
@ -12437,7 +13007,10 @@ export default {
}
this.whiteboardEntityDataMap = this.whiteboardEntityDataMap || {}
this.whiteboardEntityDataMap[id] = entityData
this.ensureWhiteboardPlatformColorableImage(entityData)
entityData._whiteboardIconPrepared = false
entityData._wbVisualSignature = null
this.applyWhiteboardPlatformIconVisual(entityData)
entityData._wbVisualSignature = `${color == null ? '' : color}\0${iconUrl}`
},
/** 白板空域/图形实体:复用 importEntity 逻辑但不加入 allEntities */
addWhiteboardDrawingEntity(ed) {
@ -13579,6 +14152,7 @@ export default {
}
},
destroyViewer() {
this.clearPlatformCopyPreview()
this.clearAirspaceHighlight();
this.stopDrawing()
this.clearAll(false)

481
ruoyi-ui/src/views/childRoom/ScreenshotGalleryPanel.vue

@ -0,0 +1,481 @@
<template>
<!-- 截图展示 4T 相同的全屏透明层 + 可拖动/缩放面板不阻挡地图操作 -->
<div v-show="visible" class="screenshot-gallery-root" :class="{ 'sg-ready': layoutReady }">
<div class="sg-panel" :style="panelStyle">
<div class="sg-toolbar" @mousedown="onDragStart">
<span class="sg-title">截图展示</span>
<div class="sg-toolbar-btns" @mousedown.stop>
<input
ref="fileInput"
type="file"
accept="image/*"
multiple
class="sg-hidden-file"
@change="onFilesSelected"
/>
<button type="button" class="sg-txt-btn" @click="triggerPick">添加</button>
<button type="button" class="sg-txt-btn" :disabled="!hasImages" @click="removeCurrent">删除</button>
<button type="button" class="sg-icon-btn" :disabled="!canPrev" title="上一张" @click="prev"></button>
<span class="sg-counter">{{ pageLabel }}</span>
<button type="button" class="sg-icon-btn" :disabled="!canNext" title="下一张" @click="next"></button>
<button type="button" class="sg-close" title="关闭" @click="$emit('update:visible', false)">×</button>
</div>
</div>
<div class="sg-body">
<div v-if="!hasImages" class="sg-empty">点击添加插入图片多图可使用两侧箭头翻页</div>
<div v-else class="sg-img-frame">
<img :key="currentIndex" :src="currentSrc" alt="" class="sg-img" draggable="false" />
</div>
</div>
<div class="sg-resize" @mousedown="onResizeStart" title="拖动调整大小"></div>
</div>
</div>
</template>
<script>
import { saveScreenshotGalleryData, getScreenshotGalleryData } from '@/api/system/routes'
const MAX_IMAGES = 80
export default {
name: 'ScreenshotGalleryPanel',
props: {
visible: { type: Boolean, default: false },
roomId: { type: [String, Number], default: null }
},
data() {
return {
images: [],
currentIndex: 0,
layoutReady: false,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
panelLeft: null,
panelTop: null,
panelWidth: 520,
panelHeight: 400,
isResizing: false,
resizeStartX: 0,
resizeStartY: 0,
resizeStartW: 0,
resizeStartH: 0
}
},
computed: {
panelStyle() {
const left = this.panelLeft != null ? this.panelLeft : 24
const top = this.panelTop != null ? this.panelTop : 100
return {
left: `${left}px`,
top: `${top}px`,
width: `${this.panelWidth}px`,
height: `${this.panelHeight}px`
}
},
hasImages() {
return Array.isArray(this.images) && this.images.length > 0
},
currentSrc() {
if (!this.hasImages) return ''
return this.images[this.currentIndex] || ''
},
canPrev() {
return this.hasImages && this.currentIndex > 0
},
canNext() {
return this.hasImages && this.currentIndex < this.images.length - 1
},
pageLabel() {
if (!this.hasImages) return '0 / 0'
return `${this.currentIndex + 1} / ${this.images.length}`
}
},
watch: {
visible: {
handler(val) {
if (val && this.roomId) {
this.loadData()
}
},
immediate: true
},
roomId: {
handler(val) {
if (val && this.visible) {
this.loadData()
}
},
immediate: true
}
},
beforeDestroy() {
document.removeEventListener('mousemove', this.onDragMove)
document.removeEventListener('mouseup', this.onDragEnd)
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
},
methods: {
async loadData() {
this.layoutReady = false
if (!this.roomId) {
this.layoutReady = true
return
}
try {
const res = await getScreenshotGalleryData({ roomId: this.roomId })
let d = res && res.data
if (d) {
if (typeof d === 'string') {
try {
d = JSON.parse(d)
} catch (e) {
this.layoutReady = true
return
}
}
const imgs = d.images
this.images = Array.isArray(imgs) ? imgs.filter(Boolean) : []
this.currentIndex = this.images.length > 0 ? Math.min(this.currentIndex, this.images.length - 1) : 0
if (d.panelSize) {
const w = Number(d.panelSize.width)
const h = Number(d.panelSize.height)
if (!isNaN(w) && w >= 280 && w <= 1000) this.panelWidth = w
if (!isNaN(h) && h >= 200 && h <= window.innerHeight - 40) this.panelHeight = h
}
if (d.panelPosition) {
const left = Number(d.panelPosition.left)
const top = Number(d.panelPosition.top)
if (!isNaN(left) && left >= 0) this.panelLeft = Math.min(left, window.innerWidth - this.panelWidth)
if (!isNaN(top) && top >= 0) this.panelTop = Math.min(top, window.innerHeight - this.panelHeight)
}
}
} catch (e) {
console.warn('加载截图展示失败:', e)
} finally {
this.layoutReady = true
}
},
triggerPick() {
this.$refs.fileInput && this.$refs.fileInput.click()
},
readFileAsDataURL(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (ev) => resolve(ev.target.result)
reader.onerror = () => resolve(null)
reader.readAsDataURL(file)
})
},
async onFilesSelected(e) {
const files = e.target.files ? Array.from(e.target.files) : []
e.target.value = ''
const imageFiles = files.filter((f) => f.type && f.type.startsWith('image/'))
if (imageFiles.length === 0) return
const remain = MAX_IMAGES - this.images.length
if (remain <= 0) {
this.$message.warning(`最多保存 ${MAX_IMAGES} 张图片`)
return
}
const take = imageFiles.slice(0, remain)
if (take.length < imageFiles.length) {
this.$message.warning(`已达上限,本次仅添加 ${take.length}`)
}
const urls = []
for (const f of take) {
const u = await this.readFileAsDataURL(f)
if (u) urls.push(u)
}
if (urls.length === 0) return
this.images = [...this.images, ...urls]
this.currentIndex = this.images.length - 1
if (this.roomId) this.saveData().catch(() => {})
},
removeCurrent() {
if (!this.hasImages) return
this.images.splice(this.currentIndex, 1)
if (this.currentIndex >= this.images.length) {
this.currentIndex = Math.max(0, this.images.length - 1)
}
if (this.roomId) this.saveData().catch(() => {})
},
prev() {
if (this.canPrev) this.currentIndex -= 1
},
next() {
if (this.canNext) this.currentIndex += 1
},
async saveData() {
if (!this.roomId) {
this.$message.warning('请先进入任务房间后再保存')
return
}
try {
const payload = {
images: this.images,
panelSize: { width: this.panelWidth, height: this.panelHeight }
}
if (this.panelLeft != null && this.panelTop != null) {
payload.panelPosition = { left: this.panelLeft, top: this.panelTop }
}
await saveScreenshotGalleryData({
roomId: this.roomId,
data: JSON.stringify(payload)
})
} catch (e) {
console.error('保存截图展示失败:', e)
this.$message.error('保存截图展示失败,请检查网络或权限')
}
},
onDragStart(e) {
e.preventDefault()
this.isDragging = true
const currentLeft = this.panelLeft != null ? this.panelLeft : 24
const currentTop = this.panelTop != null ? this.panelTop : 100
this.dragStartX = e.clientX - currentLeft
this.dragStartY = e.clientY - currentTop
document.addEventListener('mousemove', this.onDragMove)
document.addEventListener('mouseup', this.onDragEnd)
},
onDragMove(e) {
if (!this.isDragging) return
e.preventDefault()
let left = e.clientX - this.dragStartX
let top = e.clientY - this.dragStartY
left = Math.max(0, Math.min(window.innerWidth - this.panelWidth, left))
top = Math.max(0, Math.min(window.innerHeight - this.panelHeight, top))
this.panelLeft = left
this.panelTop = top
},
onDragEnd() {
this.isDragging = false
document.removeEventListener('mousemove', this.onDragMove)
document.removeEventListener('mouseup', this.onDragEnd)
if (this.roomId) this.saveData()
},
onResizeStart(e) {
e.preventDefault()
e.stopPropagation()
this.isResizing = true
this.resizeStartX = e.clientX
this.resizeStartY = e.clientY
this.resizeStartW = this.panelWidth
this.resizeStartH = this.panelHeight
document.addEventListener('mousemove', this.onResizeMove)
document.addEventListener('mouseup', this.onResizeEnd)
},
onResizeMove(e) {
if (!this.isResizing) return
e.preventDefault()
const dx = e.clientX - this.resizeStartX
const dy = e.clientY - this.resizeStartY
let w = Math.max(280, Math.min(1000, this.resizeStartW + dx))
let h = Math.max(200, Math.min(window.innerHeight - 40, this.resizeStartH + dy))
this.panelWidth = w
this.panelHeight = h
},
onResizeEnd() {
this.isResizing = false
document.removeEventListener('mousemove', this.onResizeMove)
document.removeEventListener('mouseup', this.onResizeEnd)
if (this.roomId) this.saveData()
}
}
}
</script>
<style scoped>
.screenshot-gallery-root {
position: fixed;
inset: 0;
z-index: 200;
background: transparent;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease-out;
}
.screenshot-gallery-root.sg-ready {
opacity: 1;
pointer-events: none;
}
.screenshot-gallery-root.sg-ready .sg-panel {
pointer-events: auto;
}
.sg-panel {
position: fixed;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(22, 93, 255, 0.15);
border: 1px solid #e0edff;
overflow: hidden;
z-index: 201;
}
.sg-toolbar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px 8px 14px;
background: linear-gradient(180deg, #f9fcff 0%, #eef5ff 100%);
border-bottom: 1px solid #ddeaff;
cursor: move;
user-select: none;
}
.sg-title {
font-size: 14px;
font-weight: 600;
color: #165dff;
}
.sg-toolbar-btns {
display: flex;
align-items: center;
gap: 6px;
cursor: default;
}
.sg-hidden-file {
display: none;
}
.sg-txt-btn {
background: #ebf3ff;
color: #165dff;
border: 1px solid #cce5ff;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: background 0.15s;
}
.sg-txt-btn:hover:not(:disabled) {
background: #e0edff;
}
.sg-txt-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.sg-icon-btn {
width: 26px;
height: 26px;
padding: 0;
border: 1px solid #cce5ff;
background: #fff;
color: #165dff;
border-radius: 4px;
font-size: 16px;
line-height: 1;
cursor: pointer;
}
.sg-icon-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.sg-counter {
font-size: 12px;
color: #546e7a;
min-width: 52px;
text-align: center;
}
.sg-close {
width: 28px;
height: 28px;
margin-left: 4px;
border: 1px solid #cce5ff;
background: #ebf3ff;
color: #165dff;
border-radius: 50%;
font-size: 16px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.sg-close:hover {
background: #e0edff;
}
.sg-body {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
background: #f5f9ff;
}
.sg-empty {
font-size: 13px;
color: #78909c;
text-align: center;
padding: 24px 16px;
line-height: 1.6;
}
.sg-img-frame {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
.sg-img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
vertical-align: middle;
}
.sg-resize {
position: absolute;
right: 0;
bottom: 0;
width: 18px;
height: 18px;
cursor: nwse-resize;
user-select: none;
z-index: 5;
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.25) 50%);
}
.sg-resize:hover {
background: linear-gradient(to top left, transparent 50%, rgba(22, 93, 255, 0.45) 50%);
}
</style>

62
ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue

@ -29,12 +29,15 @@
</el-popover>
</div>
<!-- 绘制空域时间选择右侧 -->
<!-- 绘制空域 | 框选时间选择右侧 -->
<div class="wb-tools-section">
<span class="wb-label">绘制</span>
<el-button size="mini" :type="drawMode === 'airspace' ? 'primary' : 'default'" @click="toggleAirspaceDraw">
空域
</el-button>
<el-button size="mini" :type="platformBoxSelectActive ? 'primary' : 'default'" @click="togglePlatformBoxSelect">
框选
</el-button>
</div>
<!-- 白板方案选择新建退出与时间选择同高 -->
@ -83,15 +86,17 @@
</div>
</div>
<!-- 第二行平台空中 | 海上 | 地面 -->
<!-- 第二行平台空中 | 海上 | 地面网格独占一行避免 flex 挤压导致宽度为 0 -->
<div class="wb-row wb-row-platform">
<span class="wb-label">平台</span>
<el-radio-group v-model="platformFilter" size="mini" class="wb-platform-filter">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="air">空中</el-radio-button>
<el-radio-button label="sea">海上</el-radio-button>
<el-radio-button label="ground">地面</el-radio-button>
</el-radio-group>
<div class="wb-platform-row-head">
<span class="wb-label">平台</span>
<el-radio-group v-model="platformFilter" size="mini" class="wb-platform-filter">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="air">空中</el-radio-button>
<el-radio-button label="sea">海上</el-radio-button>
<el-radio-button label="ground">地面</el-radio-button>
</el-radio-group>
</div>
<div class="wb-platform-grid">
<div
v-for="p in filteredPlatforms"
@ -182,6 +187,11 @@ export default {
type: Boolean,
default: false
},
/** 与房间左侧「框选平台」同步:高亮并触发与主地图相同的框选逻辑 */
platformBoxSelectActive: {
type: Boolean,
default: false
},
roomId: {
type: [String, Number],
default: null
@ -271,6 +281,12 @@ export default {
}
},
immediate: true
},
platformBoxSelectActive(val) {
if (val && this.drawMode === 'airspace') {
this.drawMode = null
this.$emit('draw-mode-change', null)
}
}
},
methods: {
@ -446,6 +462,9 @@ export default {
toggleAirspaceDraw() {
this.drawMode = this.drawMode === 'airspace' ? null : 'airspace'
this.$emit('draw-mode-change', this.drawMode)
},
togglePlatformBoxSelect() {
this.$emit('toggle-platform-box-select')
}
}
}
@ -463,7 +482,7 @@ export default {
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08);
z-index: 85;
padding: 10px 16px;
max-height: 200px;
max-height: min(38vh, 320px);
overflow-y: auto;
}
@ -479,8 +498,17 @@ export default {
}
.wb-row-platform {
align-items: flex-start;
flex-direction: column;
align-items: stretch;
width: 100%;
gap: 8px;
}
.wb-platform-row-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.wb-time-section,
@ -532,12 +560,14 @@ export default {
.wb-platform-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
gap: 6px;
flex: 1;
min-width: 0;
max-height: 70px;
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
gap: 8px;
width: 100%;
min-height: 64px;
max-height: 140px;
overflow-x: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.wb-platform-item {

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

@ -45,6 +45,9 @@
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
@platform-icons-batch-updated="onPlatformIconsBatchUpdated"
@platform-icons-copy-placed="onPlatformIconsCopyPlaced"
@whiteboard-platforms-batch-updated="onWhiteboardPlatformsBatchUpdated"
@whiteboard-platforms-copy-placed="onWhiteboardPlatformsCopyPlaced"
@platform-icon-removed="onPlatformIconRemoved"
@viewer-ready="onViewerReady"
@drawing-entities-changed="onDrawingEntitiesChanged"
@ -52,6 +55,7 @@
@whiteboard-draw-complete="handleWhiteboardDrawComplete"
@whiteboard-platform-updated="handleWhiteboardPlatformUpdated"
@whiteboard-platform-style-updated="handleWhiteboardPlatformStyleUpdated"
@room-platform-icon-style-updated="handleRoomPlatformIconStyleUpdated"
@whiteboard-entity-deleted="handleWhiteboardEntityDeleted"
@whiteboard-drawing-updated="handleWhiteboardDrawingUpdated" />
<div v-show="!screenshotMode" class="map-overlay-text">
@ -433,6 +437,12 @@
:room-id="currentRoomId"
/>
<screenshot-gallery-panel
v-if="showScreenshotGalleryPanel && !screenshotMode"
:visible.sync="showScreenshotGalleryPanel"
:room-id="currentRoomId"
/>
<!-- 冲突列表弹窗可拖动可调整大小分页点击左侧冲突按钮即打开并自动检测 -->
<conflict-drawer
v-if="!screenshotMode"
@ -447,6 +457,7 @@
<whiteboard-panel
v-show="showWhiteboardPanel && !screenshotMode"
:visible="showWhiteboardPanel"
:platform-box-select-active="platformBoxSelectMode"
:room-id="currentRoomId"
:whiteboards="whiteboards"
:current-whiteboard="currentWhiteboard"
@ -463,6 +474,7 @@
@rename-time-block="handleWhiteboardRenameTimeBlock"
@delete-time-block="handleWhiteboardDeleteTimeBlock"
@draw-mode-change="handleWhiteboardDrawModeChange"
@toggle-platform-box-select="togglePlatformBoxSelectMenu"
@export-whiteboard="handleWhiteboardExport"
@import-whiteboard="handleWhiteboardImport"
/>
@ -527,6 +539,7 @@ import RightPanel from './RightPanel'
import BottomLeftPanel from './BottomLeftPanel'
import TopHeader from './TopHeader'
import FourTPanel from './FourTPanel'
import ScreenshotGalleryPanel from './ScreenshotGalleryPanel'
import ConflictDrawer from './ConflictDrawer'
import WhiteboardPanel from './WhiteboardPanel'
import { createRoomWebSocket } from '@/utils/websocket';
@ -579,6 +592,7 @@ export default {
BottomLeftPanel,
TopHeader,
FourTPanel,
ScreenshotGalleryPanel,
ConflictDrawer,
WhiteboardPanel
},
@ -680,6 +694,7 @@ export default {
{ id: 'deduction', name: '推演', icon: 'el-icon-video-play' },
{ id: 'modify', name: '测距', icon: 'cj' },
{ id: 'refresh', name: '截图', icon: 'screenshot', action: 'refresh' },
{ id: 'screenshotGallery', name: '截图展示', icon: 'el-icon-picture-outline-round' },
{ id: 'basemap', name: '底图', icon: 'dt' },
{ id: 'datacard', name: '数据卡', icon: 'shujukapian' },
{ id: 'save', name: '保存', icon: 'el-icon-document-checked' },
@ -741,6 +756,8 @@ export default {
showKTimePopup: false,
// 4T4T/
show4TPanel: false,
/** 截图展示悬浮窗(多图翻页,与 4T 同属穿透交互) */
showScreenshotGalleryPanel: false,
/** 冲突列表弹窗(点击左侧冲突按钮即打开并自动执行检测) */
showConflictDrawer: false,
/** 冲突检测进行中(避免主线程长时间阻塞导致页面卡死) */
@ -2379,33 +2396,42 @@ export default {
this.selectedPlatform = JSON.parse(JSON.stringify(platform));
this.showPlatformDialog = true;
},
/**
* 平台库 type UI 分类对齐后端为 Aircraft/Ship/Vehicle/Radar见平台库管理
* 历史上曾用 Air/Sea/Ground需一并兼容
*/
classifyPlatformLibBucket(typeStr) {
const t = String(typeStr || '').trim().toLowerCase()
if (['ship', 'sea'].includes(t)) return 'sea'
if (['ground', 'vehicle'].includes(t)) return 'ground'
if (['air', 'aircraft', 'radar'].includes(t)) return 'air'
return 'air'
},
/** 从数据库查询并分拣平台库数据 */
getPlatformList() {
listLib().then(res => {
const allData = res.rows || [];
this.airPlatforms = [];
this.seaPlatforms = [];
this.groundPlatforms = [];
const allData = res.rows || []
this.airPlatforms = []
this.seaPlatforms = []
this.groundPlatforms = []
allData.forEach(item => {
const iconUrl = item.iconUrl || ''
const platform = {
id: item.id,
name: item.name,
type: item.type,
specsJson: item.specsJson,
imageUrl: item.iconUrl || '',
icon: item.iconUrl ? '' : 'el-icon-picture-outline',
imageUrl: iconUrl,
iconUrl,
icon: iconUrl ? '' : 'el-icon-picture-outline',
status: 'ready'
};
if (item.type === 'Air') {
this.airPlatforms.push(platform);
} else if (item.type === 'Sea') {
this.seaPlatforms.push(platform);
} else if (item.type === 'Ground') {
this.groundPlatforms.push(platform);
}
});
});
const bucket = this.classifyPlatformLibBucket(item.type)
if (bucket === 'sea') this.seaPlatforms.push(platform)
else if (bucket === 'ground') this.groundPlatforms.push(platform)
else this.airPlatforms.push(platform)
})
})
},
/** 导入确认:将弹窗填写的模版数据存入数据库 */
handleImportConfirm(formData) {
@ -3755,6 +3781,7 @@ export default {
} else {
this.drawDom = false
this.airspaceDrawDom = false
this.whiteboardAirspaceDraw = false
this.isRightPanelHidden = true
}
},
@ -3859,7 +3886,9 @@ export default {
this.whiteboardAirspaceDraw = false
this.isRightPanelHidden = true
this.show4TPanel = false
this.showScreenshotGalleryPanel = false
await this.loadWhiteboards()
this.getPlatformList()
if (this.whiteboards.length === 0) {
await this.handleWhiteboardCreate()
} else {
@ -3938,6 +3967,14 @@ export default {
handleWhiteboardExit() {
this.showWhiteboardPanel = false
this.whiteboardAirspaceDraw = false
if (this.platformBoxSelectMode) {
this.platformBoxSelectMode = false
this.activeMenu = ''
const cm = this.$refs.cesiumMap
if (cm && typeof cm.exitPlatformBoxSelectMode === 'function') {
cm.exitPlatformBoxSelectMode()
}
}
this.currentWhiteboard = null
this.currentWhiteboardTimeBlock = null
this.loadRoomDrawings()
@ -4005,6 +4042,14 @@ export default {
handleWhiteboardDrawModeChange(mode) {
this.whiteboardAirspaceDraw = mode === 'airspace'
if (mode === 'airspace' && this.platformBoxSelectMode) {
this.platformBoxSelectMode = false
this.activeMenu = ''
const cm = this.$refs.cesiumMap
if (cm && typeof cm.exitPlatformBoxSelectMode === 'function') {
cm.exitPlatformBoxSelectMode()
}
}
},
handleWhiteboardDrawComplete(entityData) {
@ -4071,7 +4116,12 @@ export default {
})
if (idx >= 0) {
const updated = { ...ents[idx] }
if (stylePayload.color) updated.color = stylePayload.color
if ('color' in stylePayload) {
updated.color =
stylePayload.color != null && String(stylePayload.color).trim() !== ''
? stylePayload.color
: null
}
if (stylePayload.iconScale != null) updated.iconScale = stylePayload.iconScale
ents[idx] = updated
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents }
@ -4081,18 +4131,51 @@ export default {
styleScale = updated.iconScale
}
//
if (!styleColor || styleScale == null) return
const redisStyle = {}
if ('color' in stylePayload) redisStyle.color = styleColor
if (stylePayload.iconScale != null) redisStyle.iconScale = styleScale
if (!Object.keys(redisStyle).length) return
saveWhiteboardPlatformStyle({
schemeId: this.currentWhiteboard.id,
platformInstanceId,
style: {
color: styleColor,
iconScale: styleScale
}
style: redisStyle
}).catch(() => {})
},
/** 房间地图独立平台:图标颜色写入 Redis platformStyle(与探测区等同键合并,不影响 icon_scale 表字段) */
async handleRoomPlatformIconStyleUpdated(payload) {
if (!payload || payload.serverId == null || payload.roomId == null) return
const roomId = payload.roomId
const serverId = payload.serverId
const platformId = payload.platformId
const platformColor = payload.platformColor
try {
const res = await getPlatformStyle({
roomId,
routeId: 0,
platformId: platformId || undefined,
platformIconInstanceId: serverId
})
const base = res && res.data && typeof res.data === 'object' ? { ...res.data } : {}
const stylePayload = {
...base,
roomId: String(roomId),
routeId: 0,
platformId: platformId != null ? platformId : base.platformId,
platformIconInstanceId: serverId,
platformColor:
platformColor != null && String(platformColor).trim() !== ''
? String(platformColor).trim()
: null
}
await savePlatformStyle(stylePayload)
this.wsConnection?.sendSyncPlatformStyles?.()
} catch (e) {
console.warn('handleRoomPlatformIconStyleUpdated failed', e)
this.$message && this.$message.error('保存图标颜色失败')
}
},
/** 白板平台从右键菜单删除后,从 contentByTime 移除并保存 */
handleWhiteboardEntityDeleted(entityData) {
if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !entityData || !entityData.id) return
@ -4136,7 +4219,10 @@ export default {
heading: e.heading != null ? e.heading : 0,
iconScale: e.iconScale != null ? e.iconScale : 1.5,
label: e.label || '',
color: e.color || '#165dff'
color:
e.color != null && String(e.color).trim() !== ''
? e.color
: null
}
}
const base = { type: e.type, id: e.id, color: e.color || '#165dff' }
@ -4277,7 +4363,10 @@ export default {
heading: ent.heading != null ? ent.heading : 0,
iconScale: ent.iconScale != null ? ent.iconScale : 1.5,
label: ent.label || '平台',
color: ent.color || '#165dff'
color:
ent.color != null && String(ent.color).trim() !== ''
? ent.color
: null
}
}
if (ent.type === 'text' && ent.data) {
@ -4470,7 +4559,11 @@ export default {
lng: pos.lng,
heading: 0,
label: platform.name || '平台',
color: platform.color || '#165dff'
// null
color:
platform.color != null && String(platform.color).trim() !== ''
? platform.color
: null
}
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] }
@ -4546,6 +4639,145 @@ export default {
})
.catch(() => {})
},
/** 框选复制摆放确定:批量新增实例并同步探测/威力区样式(与导入 JSON 逻辑一致) */
async onPlatformIconsCopyPlaced({ roomId, platforms }) {
const rId = roomId != null ? roomId : this.currentRoomId
if (!rId || !platforms || !platforms.length) return
let ok = 0
for (const item of platforms) {
if (item == null || item.platformId == null || item.lat == null || item.lng == null) continue
const payload = {
roomId: rId,
platformId: item.platformId,
platformName: item.platformName || '',
platformType: item.platformType || '',
iconUrl: item.iconUrl || '',
lng: Number(item.lng),
lat: Number(item.lat),
heading: item.heading != null ? Number(item.heading) : 0,
iconScale: item.iconScale != null ? Number(item.iconScale) : 1
}
try {
const res = await addRoomPlatformIcon(payload)
if (res.code !== 200 || !res.data || res.data.id == null) continue
const instanceId = res.data.id
const det = Array.isArray(item.detectionZones) ? item.detectionZones : []
const pow = Array.isArray(item.powerZones) ? item.powerZones : []
const firstD = det[0]
const firstP = pow[0]
const stylePayload = {
roomId: String(rId),
routeId: 0,
platformId: item.platformId || undefined,
platformIconInstanceId: instanceId,
platformName: item.platformName || undefined,
detectionZones: det,
powerZones: pow,
detectionZoneRadius: firstD ? firstD.radiusKm : undefined,
detectionZoneColor: firstD ? firstD.color : undefined,
detectionZoneOpacity: firstD ? firstD.opacity : undefined,
detectionZoneVisible: firstD ? firstD.visible !== false : undefined,
powerZoneRadius: firstP ? firstP.radiusKm : undefined,
powerZoneAngle: firstP ? firstP.angleDeg : undefined,
powerZoneColor: firstP ? firstP.color : undefined,
powerZoneOpacity: firstP ? firstP.opacity : undefined,
powerZoneVisible: firstP ? firstP.visible !== false : undefined
}
try {
await savePlatformStyle(stylePayload)
} catch (se) {
console.warn('复制平台样式保存失败', se)
}
ok++
} catch (e) {
console.warn('复制平台新增失败', e)
}
}
const cm = this.$refs.cesiumMap
if (cm && typeof cm.loadRoomPlatformIcons === 'function') {
const listRes = await listRoomPlatformIcons(rId)
if (listRes.code === 200 && listRes.data) {
cm.loadRoomPlatformIcons(rId, listRes.data)
}
}
this.wsConnection?.sendSyncPlatformIcons?.()
this.wsConnection?.sendSyncPlatformStyles?.()
if (ok === 0) {
this.$message.warning('未能复制任何平台,请检查网络或权限')
} else {
this.$message.success(`已复制并放置 ${ok} 个平台图标`)
}
},
/** 白板框选:整体拖拽结束后批量写回当前时间块 */
onWhiteboardPlatformsBatchUpdated(updates) {
if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !updates || !updates.length) return
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] }
const ents = [...(currentContent.entities || [])]
const normalizeId = (id) => {
if (id == null) return ''
const str = String(id)
return str.startsWith('wb_') ? str.slice(3) : str
}
let changed = false
for (const u of updates) {
if (!u || u.id == null) continue
const targetId = String(u.id)
const targetIdNormalized = normalizeId(targetId)
const idx = ents.findIndex((e) => {
const eid = String((e && e.id) || '')
return eid === targetId || normalizeId(eid) === targetIdNormalized
})
if (idx >= 0) {
ents[idx] = {
...ents[idx],
lat: u.lat,
lng: u.lng,
heading: u.heading != null ? u.heading : 0
}
changed = true
}
}
if (changed) {
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents }
this.saveCurrentWhiteboard({ contentByTime })
}
},
/** 白板框选复制:左键放置后写入当前时间块 */
async onWhiteboardPlatformsCopyPlaced({ platforms }) {
if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock || !platforms || !platforms.length) return
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] }
const ents = [...(currentContent.entities || [])]
const t0 = Date.now()
let added = 0
for (let i = 0; i < platforms.length; i++) {
const p = platforms[i]
if (!p || p.platformId == null || p.lat == null || p.lng == null) continue
const entityId = 'wb_' + t0 + '_' + i + '_' + Math.random().toString(36).slice(2)
ents.push({
id: entityId,
type: 'platformIcon',
platformId: p.platformId,
platform: p.platform || { id: p.platformId, name: p.platformName || '' },
platformName: p.platformName || (p.platform && p.platform.name) || '',
lat: Number(p.lat),
lng: Number(p.lng),
heading: p.heading != null ? Number(p.heading) : 0,
iconScale: p.iconScale != null ? Number(p.iconScale) : 1.5,
label: p.label || p.platformName || '',
color:
p.color != null && String(p.color).trim() !== ''
? p.color
: null
})
added++
}
if (!added) return
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: ents }
await this.saveCurrentWhiteboard({ contentByTime })
this.$message.success(`已复制并放置 ${added} 个白板平台`)
},
/** 平台图标从地图删除时同步删除服务端记录 */
onPlatformIconRemoved({ serverId }) {
if (!serverId) return
@ -4934,6 +5166,8 @@ export default {
} else if (item.id === '4t') {
// 4T4T
this.show4TPanel = !this.show4TPanel;
} else if (item.id === 'screenshotGallery') {
this.showScreenshotGalleryPanel = !this.showScreenshotGalleryPanel;
} else if (item.id === 'whiteboard') {
// /退
this.toggleWhiteboardMode();

14
ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

@ -548,6 +548,13 @@ export default {
this.airPlatforms = []
this.seaPlatforms = []
this.groundPlatforms = []
const classify = (typeStr) => {
const t = String(typeStr || '').trim().toLowerCase()
if (['ship', 'sea'].includes(t)) return 'sea'
if (['ground', 'vehicle'].includes(t)) return 'ground'
if (['air', 'aircraft', 'radar'].includes(t)) return 'air'
return 'air'
}
allData.forEach(item => {
const platform = {
id: item.id,
@ -556,9 +563,10 @@ export default {
imageUrl: item.iconUrl || '',
icon: item.iconUrl ? '' : 'el-icon-picture-outline'
}
if (item.type === 'Air') this.airPlatforms.push(platform)
else if (item.type === 'Sea') this.seaPlatforms.push(platform)
else if (item.type === 'Ground') this.groundPlatforms.push(platform)
const b = classify(item.type)
if (b === 'sea') this.seaPlatforms.push(platform)
else if (b === 'ground') this.groundPlatforms.push(platform)
else this.airPlatforms.push(platform)
})
}).catch(() => {
this.$message.error('加载平台列表失败')

Loading…
Cancel
Save