Browse Source

Merge branch 'ctw' of http://124.70.32.114:3100/woka/cesium-map-object into mh

# Conflicts:
#	ruoyi-ui/src/views/cesiumMap/index.vue
mh
menghao 2 weeks ago
parent
commit
7e650dd463
  1. 6
      package-lock.json
  2. 4
      ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java
  3. 25
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  4. 439
      ruoyi-ui/src/views/cesiumMap/index.vue
  5. 155
      ruoyi-ui/src/views/childRoom/WhiteboardPanel.vue
  6. 371
      ruoyi-ui/src/views/childRoom/index.vue
  7. 52
      ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue
  8. 73
      ruoyi-ui/src/views/index.vue

6
package-lock.json

@ -1,6 +0,0 @@
{
"name": "cesium-map-object",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

4
ruoyi-admin/src/main/java/com/ruoyi/websocket/service/WhiteboardRoomService.java

@ -18,7 +18,7 @@ public class WhiteboardRoomService {
private static final String ROOM_WHITEBOARDS_PREFIX = "room:";
private static final String ROOM_WHITEBOARDS_SUFFIX = ":whiteboards";
private static final int EXPIRE_HOURS = 24;
private static final int EXPIRE_DAYS = 60;
@Autowired
@Qualifier("stringObjectRedisTemplate")
@ -119,6 +119,6 @@ public class WhiteboardRoomService {
private void saveWhiteboards(Long roomId, List<Object> list) {
String key = whiteboardsKey(roomId);
redisTemplate.opsForValue().set(key, list, EXPIRE_HOURS, TimeUnit.HOURS);
redisTemplate.opsForValue().set(key, list, EXPIRE_DAYS, TimeUnit.DAYS);
}
}

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

@ -1,5 +1,12 @@
<template>
<div class="context-menu" v-if="visible" :style="positionStyle">
<!-- 重叠/接近时切换选择其他图形 -->
<div class="menu-section" v-if="pickList && pickList.length > 1">
<div class="menu-item" @click="$emit('switch-pick')">
<span class="menu-icon">🔄</span>
<span>切换选择 ({{ (pickIndex || 0) + 1 }}/{{ pickList.length }})</span>
</div>
</div>
<div class="menu-section" v-if="!entityData || (entityData.type !== 'routePlatform' && entityData.type !== 'route')">
<div class="menu-item" @click="handleDelete">
<span class="menu-icon">🗑</span>
@ -83,7 +90,7 @@
</span>
</el-dialog>
<!-- 航线上锁/解锁复制航点右键时也显示 routeId -->
<!-- 航线上锁/解锁复制单条航线推演航点右键时也显示 routeId -->
<div class="menu-section" v-if="entityData && (entityData.type === 'route' || entityData.type === 'routeWaypoint')">
<div class="menu-title">航线编辑</div>
<div class="menu-item" @click="handleToggleRouteLock">
@ -94,6 +101,10 @@
<span class="menu-icon">📋</span>
<span>复制</span>
</div>
<div class="menu-item" @click="handleSingleRouteDeduction">
<span class="menu-icon"></span>
<span>单条航线推演</span>
</div>
</div>
<!-- 航线上飞机显示/隐藏/编辑标牌编辑平台 -->
@ -534,6 +545,14 @@ export default {
type: Object,
default: () => ({ x: 0, y: 0 })
},
pickList: {
type: Array,
default: null
},
pickIndex: {
type: Number,
default: 0
},
entityData: {
type: Object,
default: null
@ -671,6 +690,10 @@ export default {
this.$emit('copy-route')
},
handleSingleRouteDeduction() {
this.$emit('single-route-deduction', this.entityData.routeId)
},
handleEditWaypoint() {
this.$emit('open-waypoint-dialog', this.entityData.dbId, this.entityData.routeId, this.entityData.waypointIndex)
},

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

@ -34,10 +34,13 @@
:visible="contextMenu.visible"
:position="contextMenu.position"
:entity-data="contextMenu.entityData"
:pick-list="contextMenu.pickList"
:pick-index="contextMenu.pickIndex"
:route-locked="routeLocked"
:detection-zone-visible="contextMenuZoneDetectionVisible"
:power-zone-visible="contextMenuZonePowerVisible"
@delete="deleteEntityFromContextMenu"
@switch-pick="handleContextMenuSwitchPick"
@update-property="updateEntityProperty"
@edit-platform-position="openPlatformIconPositionDialog"
@edit-platform-heading="openPlatformIconHeadingDialog"
@ -47,6 +50,7 @@
@start-route-before-platform="handleStartRouteBeforePlatform"
@start-route-after-platform="handleStartRouteAfterPlatform"
@copy-route="handleCopyRouteFromMenu"
@single-route-deduction="handleSingleRouteDeductionFromMenu"
@edit-platform="openEditPlatformDialog"
@detection-zone="openDetectionZoneDialog"
@power-zone="openPowerZoneDialog"
@ -442,6 +446,10 @@ export default {
position: { x: 0, y: 0 },
entityData: null
},
/** 右键菜单高亮的空域实体(用于关闭时恢复样式) */
_contextMenuHighlightedEntityData: null,
/** 高亮闪烁定时器 */
_contextMenuHighlightTimer: null,
editPlatformLabelDialogVisible: false,
editPlatformLabelForm: {
routeId: null,
@ -606,6 +614,7 @@ export default {
copyPreviewMouseCartesian: null,
// /{ routeId, waypointIndex, mode: 'before'|'after', waypoints }线
addWaypointContext: null,
addWaypointSolidEntity: null,
addWaypointPreviewEntity: null,
// { entityData } CallbackProperty
airspacePositionEditContext: null,
@ -701,10 +710,17 @@ export default {
deep: true,
immediate: true,
handler(entities) {
if (this.whiteboardMode && entities && entities.length > 0) {
this.renderWhiteboardEntities(entities)
if (this.whiteboardMode) {
if (entities && entities.length > 0) {
this.renderWhiteboardEntities(entities)
} else {
this.clearWhiteboardEntities()
}
}
}
},
'contextMenu.visible'(val) {
if (!val) this.clearAirspaceHighlight()
}
},
computed: {
@ -2543,20 +2559,21 @@ export default {
const nextPosCloned = nextPos ? Cesium.Cartesian3.clone(nextPos) : null;
const routeIdHold = routeId;
const that = this;
const buildHoldPositions = (radiusOrEllipse) => {
const buildHoldPositions = (radiusOrEllipse, centerOverride) => {
const isCircleArg = typeof radiusOrEllipse === 'number';
const R = isCircleArg ? radiusOrEllipse : 0;
const smj = isCircleArg ? defaultSemiMajor : (radiusOrEllipse.semiMajor ?? defaultSemiMajor);
const smn = isCircleArg ? defaultSemiMinor : (radiusOrEllipse.semiMinor ?? defaultSemiMinor);
const hd = isCircleArg ? defaultHeadingRad : ((radiusOrEllipse.headingDeg != null ? radiusOrEllipse.headingDeg * Math.PI / 180 : defaultHeadingRad));
const centerPos = centerOverride || currPosCloned;
let entry; let exit; let centerForCircle;
if (useCircle) {
centerForCircle = that.getHoldCenterFromPrevNext(lastPosCloned, currPosCloned, R, clockwise);
centerForCircle = that.getHoldCenterFromPrevNext(lastPosCloned, centerPos, R, clockwise);
entry = that.getCircleTangentEntryPoint(centerForCircle, lastPosCloned, R, clockwise);
exit = that.getCircleTangentExitPoint(centerForCircle, nextPosCloned || currPosCloned, R, clockwise);
exit = that.getCircleTangentExitPoint(centerForCircle, nextPosCloned || centerPos, R, clockwise);
} else {
entry = that.getEllipseTangentEntryPoint(currPosCloned, lastPosCloned, smj, smn, hd, clockwise);
exit = that.getEllipseTangentExitPoint(currPosCloned, nextPosCloned || currPosCloned, smj, smn, hd, clockwise);
entry = that.getEllipseTangentEntryPoint(centerPos, lastPosCloned, smj, smn, hd, clockwise);
exit = that.getEllipseTangentExitPoint(centerPos, nextPosCloned || centerPos, smj, smn, hd, clockwise);
}
let arcPoints;
if (useCircle) {
@ -2573,10 +2590,10 @@ export default {
if (!arcPoints || arcPoints.length < 2) arcPoints = [Cesium.Cartesian3.clone(entry), Cesium.Cartesian3.clone(exit)];
return arcPoints;
} else {
const tEntry = that.cartesianToEllipseParam(currPosCloned, smj, smn, hd, entry);
const tEntry = that.cartesianToEllipseParam(centerPos, smj, smn, hd, entry);
const entryLocalAngle = Math.atan2(smn * Math.sin(tEntry), smj * Math.cos(tEntry));
const fullCirclePoints = that.getEllipseFullCircle(currPosCloned, smj, smn, hd, entryLocalAngle, clockwise, 128);
arcPoints = that.buildEllipseHoldArc(currPosCloned, smj, smn, hd, entry, exit, clockwise, 120);
const fullCirclePoints = that.getEllipseFullCircle(centerPos, smj, smn, hd, entryLocalAngle, clockwise, 128);
arcPoints = that.buildEllipseHoldArc(centerPos, smj, smn, hd, entry, exit, clockwise, 120);
return [entry, ...(fullCirclePoints || []).slice(1), ...(arcPoints || []).slice(1)];
}
};
@ -2586,17 +2603,26 @@ export default {
nextPosCloned,
params && params.headingDeg != null ? params.headingDeg : 0
);
const buildRaceTrackPositions = () => that.buildRaceTrackWithEntryExit(currPosCloned, lastPosCloned, nextPosCloned, raceTrackDirectionRad, edgeLengthM, arcRadiusM, clockwise, 24);
const buildRaceTrackPositions = (centerOverride) => that.buildRaceTrackWithEntryExit(centerOverride || currPosCloned, lastPosCloned, nextPosCloned, raceTrackDirectionRad, edgeLengthM, arcRadiusM, clockwise, 24);
const holdPositions = useCircle ? buildHoldPositions(radius) : buildRaceTrackPositions();
for (let k = 0; k < holdPositions.length; k++) finalPathPositions.push(holdPositions[k]);
const wpIdHold = wp.id;
const getHoldPositions = () => {
let centerOverride = null;
if (that.waypointDragging && that.waypointDragging.routeId === routeIdHold && that.waypointDragging.dbId === wpIdHold) {
const wpEnt = that.viewer.entities.getById(`wp_${routeIdHold}_${wpIdHold}`);
if (wpEnt && wpEnt.position) {
const p = wpEnt.position.getValue(Cesium.JulianDate.now());
if (p) centerOverride = p;
}
}
if (useCircle) {
const R = (that._routeHoldRadiiByRoute && that._routeHoldRadiiByRoute[routeIdHold] && that._routeHoldRadiiByRoute[routeIdHold][legIndexHold] != null)
? that._routeHoldRadiiByRoute[routeIdHold][legIndexHold]
: turnRadiusForHold;
return buildHoldPositions(R);
return buildHoldPositions(R, centerOverride);
}
return buildRaceTrackPositions();
return buildRaceTrackPositions(centerOverride);
};
this.viewer.entities.add({
id: `hold-line-${routeId}-${i}`,
@ -2643,10 +2669,20 @@ export default {
const currPosClonedForArc = Cesium.Cartesian3.clone(currPos);
if (radius > 0) {
const currPosCloned = Cesium.Cartesian3.clone(currPos);
const routeIdCloned = routeId;
const dbIdCloned = wp.id;
const routeIdArc = routeId;
const dbIdArc = wp.id;
const that = this;
const getArcPoints = () => that.computeArcPositions(lastPosCloned, currPosCloned, nextLogicalCloned, radius);
const getArcPoints = () => {
let centerPos = currPosCloned;
if (that.waypointDragging && that.waypointDragging.routeId === routeIdArc && that.waypointDragging.dbId === dbIdArc) {
const wpEnt = that.viewer.entities.getById(`wp_${routeIdArc}_${dbIdArc}`);
if (wpEnt && wpEnt.position) {
const p = wpEnt.position.getValue(Cesium.JulianDate.now());
if (p) centerPos = p;
}
}
return that.computeArcPositions(lastPosCloned, centerPos, nextLogicalCloned, radius);
};
this.viewer.entities.add({
id: `arc-line-${routeId}-${i}`,
show: false,
@ -2665,9 +2701,16 @@ export default {
const arcWpOutline = wp.outlineColor != null ? wp.outlineColor : defaultWpOutline;
[0, 1].forEach((idx) => {
const suffix = idx === 0 ? '_entry' : '_exit';
const getPos = () => { const pts = getArcPoints(); return idx === 0 ? pts[0] : pts[pts.length - 1]; };
const entId = `wp_${routeId}_${wp.id}${suffix}`;
const getPos = () => {
if (that.waypointDragging && that.waypointDragging.entity && (that.waypointDragging.entity.id === entId) && that.waypointDragPreview && that.waypointDragPreview.routeId === routeIdArc && that.waypointDragPreview.dbId === dbIdArc) {
return that.waypointDragPreview.position;
}
const pts = getArcPoints();
return idx === 0 ? pts[0] : pts[pts.length - 1];
};
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}${suffix}`,
id: entId,
name: wpName,
position: new Cesium.CallbackProperty(getPos, false),
properties: { isMissionWaypoint: true, routeId: routeId, dbId: wp.id },
@ -2693,6 +2736,11 @@ export default {
const hitLineId = lineId + '-hit';
const hasHold = waypoints.some((wp) => that.isHoldWaypoint(wp));
const routePositionsCallback = new Cesium.CallbackProperty(function () {
// 使线
if (that.waypointDragging && that.waypointDragging.routeId === routeId) {
const livePos = that.getRouteLinePositionsFromWaypointEntities(routeId);
if (livePos && livePos.length > 0) return livePos;
}
if (hasHold) {
const pathPos = that.getRoutePathPositionsForLine(routeId);
if (pathPos && pathPos.length > 0) return pathPos;
@ -5154,10 +5202,13 @@ export default {
}
}
if (this.addWaypointContext) {
const cartesian = this.getClickPosition(movement.endPosition);
const ctx = this.addWaypointContext;
const refAlt = ctx.mode === 'before'
? (ctx.waypoints[ctx.waypointIndex - 1] && Number(ctx.waypoints[ctx.waypointIndex - 1].alt)) || (ctx.waypoints[ctx.waypointIndex] && Number(ctx.waypoints[ctx.waypointIndex].alt)) || 5000
: (ctx.waypoints[ctx.waypointIndex] && Number(ctx.waypoints[ctx.waypointIndex].alt)) || (ctx.waypoints[ctx.waypointIndex + 1] && Number(ctx.waypoints[ctx.waypointIndex + 1].alt)) || 5000;
const cartesian = this.getClickPositionWithHeight(movement.endPosition, refAlt);
if (cartesian) {
this.addWaypointContext.mouseCartesian = cartesian;
this.updateAddWaypointPreview();
ctx.mouseCartesian = cartesian;
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
}
@ -5256,6 +5307,11 @@ export default {
//
this.contextMenu.visible = false;
// 使 drillPick /11x11 便
const drillPicks = this.viewer.scene.drillPick(click.position, 20, 11, 11);
const pickList = [];
const seenEntities = new Set();
const pickedObject = this.viewer.scene.pick(click.position)
if (Cesium.defined(pickedObject) && pickedObject.id) {
const pickedEntity = pickedObject.id
@ -5453,18 +5509,46 @@ export default {
if (entityData && entityData.type === 'route' && entityData.id && !entityData.routeId) {
entityData = { ...entityData, routeId: entityData.id.replace('route-line-', '') };
}
for (const pick of drillPicks) {
const pickedEntity = pick.id || pick.object;
if (!pickedEntity || seenEntities.has(pickedEntity)) continue;
const entityData = this.resolveEntityDataFromPick(pickedEntity);
if (entityData) {
this.contextMenu = {
visible: true,
position: {
x: click.position.x,
y: click.position.y
},
entityData: entityData
};
seenEntities.add(pickedEntity);
pickList.push({ entityData, pickedEntity });
}
}
}
if (pickList.length > 0) {
const { entityData, pickedEntity } = pickList[0];
const anchorCartesian = this.getContextMenuAnchorCartesian(entityData, pickedEntity, click.position);
this.contextMenu = {
visible: true,
position: { x: click.position.x, y: click.position.y },
entityData,
anchorCartesian,
pickList,
pickIndex: 0
};
if (['polygon', 'rectangle', 'circle', 'sector', 'auxiliaryLine', 'arrow'].includes(entityData.type)) {
this.applyAirspaceHighlight(entityData);
}
}
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
// 使/
const that = this;
const updateContextMenuPosition = () => {
if (!that.contextMenu.visible || !that.contextMenu.anchorCartesian || !that.viewer || !that.viewer.scene) return;
try {
const screenPos = Cesium.SceneTransforms.worldToWindowCoordinates(that.viewer.scene, that.contextMenu.anchorCartesian);
if (Cesium.defined(screenPos)) {
that.contextMenu.position = { x: screenPos.x, y: screenPos.y };
}
} catch (e) {}
};
this.viewer.scene.preRender.addEventListener(updateContextMenuPosition);
this._contextMenuCameraListener = updateContextMenuPosition;
},
openEditPlatformLabelDialog() {
@ -5610,7 +5694,7 @@ export default {
if (!entityData && idStr.startsWith('wb_')) {
entityData = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[idStr];
}
if (entityData) {
if (entityData && entityData.type === 'platformIcon') {
this.pendingDragIcon = entityData;
this.dragStartScreenPos = { x: click.position.x, y: click.position.y };
return;
@ -6109,6 +6193,12 @@ export default {
this.addPointEntity(lat, lng)
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
// 退
this.drawingHandler.setInputAction(() => {
this.stopDrawing();
this.drawingMode = null;
this.$message && this.$message.info('已退出绘制');
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
// 线
startLineDrawing() {
@ -6234,7 +6324,7 @@ export default {
});
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 3.
// 3. 退
this.drawingHandler.setInputAction(() => {
//
if (this.tempPreviewEntity) {
@ -6257,6 +6347,9 @@ export default {
}
//
this.activeCursorPosition = null;
// 退
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
finishLineDrawing() {
@ -6362,7 +6455,7 @@ export default {
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 4.
// 4. 退
this.drawingHandler.setInputAction(() => {
if (this.drawingPoints.length >= 3) {
this.finishPolygonDrawing(); //
@ -6371,6 +6464,9 @@ export default {
}
//
this.activeCursorPosition = null;
// 退
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
finishPolygonDrawing() {
@ -6480,9 +6576,11 @@ export default {
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 4.
// 4. 退
this.drawingHandler.setInputAction(() => {
this.cancelDrawing();
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
finishRectangleDrawing() {
@ -6737,10 +6835,12 @@ export default {
console.warn('Mouse click error:', e);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 5.
// 5. 退
this.drawingHandler.setInputAction(() => {
try {
this.cancelDrawing();
this.stopDrawing();
this.drawingMode = null;
} catch (e) {
console.warn('Right click error:', e);
}
@ -6945,9 +7045,11 @@ export default {
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 4.
// 4. 退
this.drawingHandler.setInputAction(() => {
this.cancelDrawing();
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
//
@ -7181,6 +7283,8 @@ export default {
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
this.drawingHandler.setInputAction(() => {
this.cancelDrawing()
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
},
finishAuxiliaryLineDrawing() {
@ -7301,9 +7405,11 @@ export default {
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 4.
// 4. 退
this.drawingHandler.setInputAction(() => {
this.cancelDrawing();
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
//
@ -7391,9 +7497,11 @@ export default {
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 3.
// 3. 退
this.drawingHandler.setInputAction(() => {
this.cancelDrawing();
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
//
@ -7482,9 +7590,11 @@ export default {
fileInput.click();
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 3.
// 3. 退
this.drawingHandler.setInputAction(() => {
this.cancelDrawing();
this.stopDrawing();
this.drawingMode = null;
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
},
//
@ -8156,6 +8266,18 @@ export default {
this.contextMenu.visible = false;
this.$emit('copy-route', ed.routeId);
},
/** 右键「单条航线推演」:弹出时间轴,仅推演该航线 */
handleSingleRouteDeductionFromMenu(routeId) {
const ed = this.contextMenu.entityData;
const rid = routeId != null ? routeId : (ed && ed.routeId);
if (rid == null) {
this.contextMenu.visible = false;
return;
}
this.contextMenu.visible = false;
this.$emit('single-route-deduction', rid);
},
/** 开始航线复制预览:整条航线跟随鼠标,左键放置后父组件弹窗保存 */
startRouteCopyPreview(waypoints) {
if (!waypoints || waypoints.length < 2) return;
@ -8240,7 +8362,7 @@ export default {
this.$message && this.$message.info(`${mode === 'before' ? '在当前位置前' : '在当前位置后'}插入${modeLabel},点击地图放置,右键取消`);
this.updateAddWaypointPreview();
},
/** 更新“增加航点”预览折线:上一/当前/下一与鼠标位置连线,含盘旋段视觉效果 */
/** 更新“增加航点”预览折线:与新建航线一致,已确定段实线 + 最后一段到鼠标的虚线实时预览 */
updateAddWaypointPreview() {
const ctx = this.addWaypointContext;
if (!ctx || !this.viewer || !ctx.waypoints || ctx.waypoints.length === 0) return;
@ -8252,30 +8374,48 @@ export default {
const alt = Number(wp.alt) || 5000;
return Cesium.Cartesian3.fromDegrees(lon, lat, alt);
};
let positions = [];
const color = Cesium.Color.fromCssColorString('#64748b');
const that = this;
// 1. 线
if (this.addWaypointSolidEntity) {
this.viewer.entities.remove(this.addWaypointSolidEntity);
this.addWaypointSolidEntity = null;
}
let solidPositions = [];
if (ctx.mode === 'before') {
const prev = idx > 0 ? toCartesian(waypoints[idx - 1]) : null;
const curr = toCartesian(waypoints[idx]);
if (ctx.mouseCartesian) {
if (prev) positions = [prev, ctx.mouseCartesian, curr];
else positions = [ctx.mouseCartesian, curr];
}
solidPositions = waypoints.slice(0, idx).map(wp => toCartesian(wp));
} else {
const curr = toCartesian(waypoints[idx]);
const next = idx + 1 < waypoints.length ? toCartesian(waypoints[idx + 1]) : null;
if (ctx.mouseCartesian) {
if (next) positions = [curr, ctx.mouseCartesian, next];
else positions = [curr, ctx.mouseCartesian];
}
solidPositions = waypoints.slice(0, idx + 1).map(wp => toCartesian(wp));
}
if (solidPositions.length >= 2) {
this.addWaypointSolidEntity = this.viewer.entities.add({
polyline: {
positions: solidPositions,
width: 2,
material: color,
arcType: Cesium.ArcType.NONE
}
});
}
if (positions.length < 2) return;
// 2. 线使 CallbackProperty
if (this.addWaypointPreviewEntity) {
this.viewer.entities.remove(this.addWaypointPreviewEntity);
this.addWaypointPreviewEntity = null;
}
const getAnchor = () => (ctx.mode === 'before' ? (idx > 0 ? toCartesian(waypoints[idx - 1]) : null) : toCartesian(waypoints[idx]));
const getNext = () => (ctx.mode === 'before' ? toCartesian(waypoints[idx]) : (idx + 1 < waypoints.length ? toCartesian(waypoints[idx + 1]) : null));
this.addWaypointPreviewEntity = this.viewer.entities.add({
polyline: {
positions: positions,
positions: new Cesium.CallbackProperty(() => {
const anchor = getAnchor();
const next = getNext();
const mouse = that.addWaypointContext && that.addWaypointContext.mouseCartesian;
if (!mouse) return anchor && next ? [anchor, next] : (anchor ? [anchor, anchor] : (next ? [next, next] : []));
if (anchor && next) return [anchor, mouse, next];
if (anchor) return [anchor, mouse];
if (next) return [mouse, next];
return [mouse, mouse];
}, false),
width: 2,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString('#64748b'),
@ -8287,6 +8427,10 @@ export default {
},
/** 清除“增加航点”模式及预览折线 */
clearAddWaypointContext() {
if (this.addWaypointSolidEntity) {
this.viewer.entities.remove(this.addWaypointSolidEntity);
this.addWaypointSolidEntity = null;
}
if (this.addWaypointPreviewEntity) {
this.viewer.entities.remove(this.addWaypointPreviewEntity);
this.addWaypointPreviewEntity = null;
@ -8603,6 +8747,168 @@ export default {
return null;
},
/** 从拾取的实体解析出 entityData(供 drillPick 等多选场景复用) */
resolveEntityDataFromPick(pickedEntity) {
if (!pickedEntity) return null;
const idStr = typeof pickedEntity.id === 'string' ? pickedEntity.id : (pickedEntity.id || '');
let entityData = null;
if (idStr.startsWith('route-platform-')) {
const routeId = idStr.replace('route-platform-', '').replace('route-platform-label-', '');
let platformId = 0, platformName = '平台';
if (pickedEntity.properties) {
const now = Cesium.JulianDate.now();
if (pickedEntity.properties.platformId) platformId = pickedEntity.properties.platformId.getValue ? pickedEntity.properties.platformId.getValue(now) : pickedEntity.properties.platformId;
if (pickedEntity.properties.platformName) platformName = pickedEntity.properties.platformName.getValue ? pickedEntity.properties.platformName.getValue(now) : pickedEntity.properties.platformName;
}
entityData = { type: 'routePlatform', routeId, entity: pickedEntity, platformId, platformName, labelVisible: this.routeLabelVisible[routeId] !== false };
const now = Cesium.JulianDate.now();
if (pickedEntity.position) {
const pos = pickedEntity.position.getValue ? pickedEntity.position.getValue(now) : pickedEntity.position;
if (pos) {
const ll = this.cartesianToLatLng(pos);
if (ll) { entityData.lat = ll.lat; entityData.lng = ll.lng; }
}
}
}
if (!entityData && idStr.startsWith('wb_')) entityData = this.whiteboardEntityDataMap && this.whiteboardEntityDataMap[idStr];
if (!entityData) {
entityData = this.allEntities.find(e => e.entity === pickedEntity || e === pickedEntity);
if (!entityData) {
for (const lineEntity of this.allEntities) {
if (lineEntity.type === 'line' && lineEntity.pointEntities && lineEntity.pointEntities.includes(pickedEntity)) {
entityData = lineEntity; break;
}
}
}
if (!entityData) {
for (const powerZoneEntity of this.allEntities) {
if (powerZoneEntity.type === 'powerZone' && powerZoneEntity.centerEntity === pickedEntity) {
entityData = powerZoneEntity; break;
}
}
}
if (!entityData && pickedEntity.properties) {
const now = Cesium.JulianDate.now();
const props = pickedEntity.properties.getValue ? pickedEntity.properties.getValue(now) : null;
if (props) {
const isWp = props.isMissionWaypoint && props.isMissionWaypoint.getValue ? props.isMissionWaypoint.getValue() : props.isMissionWaypoint;
const isLine = props.isMissionRouteLine && props.isMissionRouteLine.getValue ? props.isMissionRouteLine.getValue() : props.isMissionRouteLine;
if (isWp) {
let rId = props.routeId; if (rId && rId.getValue) rId = rId.getValue();
let dbId = props.dbId; if (dbId && dbId.getValue) dbId = dbId.getValue();
const ids = this._routeWaypointIdsByRoute && this._routeWaypointIdsByRoute[rId];
const waypointIndex = ids && dbId != null ? ids.indexOf(dbId) : -1;
if (rId != null) entityData = { type: 'routeWaypoint', routeId: rId, dbId, waypointIndex };
} else if (isLine) {
let rId = props.routeId; if (rId && rId.getValue) rId = rId.getValue();
if (rId) entityData = { type: 'route', routeId: rId };
}
}
}
if (!entityData && idStr.startsWith('hold-line-')) {
const parts = idStr.split('-');
if (parts.length >= 4) {
const routeId = parts[2];
const segIdx = parseInt(parts[3], 10);
if (!isNaN(segIdx)) entityData = { type: 'routeWaypoint', routeId, waypointIndex: segIdx, fromHold: true };
}
}
}
if (entityData && entityData.type === 'route' && entityData.id && !entityData.routeId) {
entityData = { ...entityData, routeId: entityData.id.replace('route-line-', '') };
}
return entityData;
},
/** 为空域/辅助线/箭头实体应用高亮闪烁(右键选中时,保持原色,出现与消失闪烁) */
applyAirspaceHighlight(entityData) {
if (!entityData || !entityData.entity) return;
const types = ['polygon', 'rectangle', 'circle', 'sector', 'auxiliaryLine', 'arrow'];
if (!types.includes(entityData.type)) return;
this.clearAirspaceHighlight();
const entity = entityData.entity;
this._contextMenuHighlightedEntityData = entityData;
let visible = true;
this._contextMenuHighlightTimer = setInterval(() => {
if (!this._contextMenuHighlightedEntityData || this._contextMenuHighlightedEntityData !== entityData) return;
visible = !visible;
try {
entity.show = visible;
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) {
this.viewer.scene.requestRender();
}
} catch (e) {}
}, 350);
},
/** 清除空域高亮,恢复显示 */
clearAirspaceHighlight() {
if (this._contextMenuHighlightTimer) {
clearInterval(this._contextMenuHighlightTimer);
this._contextMenuHighlightTimer = null;
}
const entityData = this._contextMenuHighlightedEntityData;
if (!entityData || !entityData.entity) {
this._contextMenuHighlightedEntityData = null;
return;
}
try {
entityData.entity.show = true;
} catch (e) { console.warn('clearAirspaceHighlight:', e); }
this._contextMenuHighlightedEntityData = null;
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRender) {
this.viewer.scene.requestRender();
}
},
/** 获取右键菜单的锚点(笛卡尔坐标),用于随地图移动时更新菜单位置 */
getContextMenuAnchorCartesian(entityData, pickedEntity, clickPosition) {
if (!entityData || !this.viewer || !this.viewer.scene) return null;
const now = Cesium.JulianDate.now();
// 使
const airspaceCenter = this.getAirspaceCenter(entityData);
if (airspaceCenter) return airspaceCenter;
// entity position
const ent = entityData.entity || pickedEntity;
if (ent && ent.position) {
try {
const pos = ent.position.getValue ? ent.position.getValue(now) : ent.position;
if (pos) return Cesium.Cartesian3.clone(pos);
} catch (e) {}
}
// line使线
if (entityData.type === 'line' && entityData.positions && entityData.positions.length > 0) {
const sum = entityData.positions.reduce((acc, p) => Cesium.Cartesian3.add(acc, p, new Cesium.Cartesian3()), new Cesium.Cartesian3());
return Cesium.Cartesian3.divideByScalar(sum, entityData.positions.length, new Cesium.Cartesian3());
}
// point position
if (entityData.type === 'point' && entityData.position) {
return Cesium.Cartesian3.clone(entityData.position);
}
if (entityData.type === 'point' && (entityData.lat != null || entityData.lng != null)) {
return Cesium.Cartesian3.fromDegrees(entityData.lng || 0, entityData.lat || 0);
}
// powerZone
if (entityData.type === 'powerZone' && entityData.centerEntity && entityData.centerEntity.position) {
try {
const pos = entityData.centerEntity.position.getValue ? entityData.centerEntity.position.getValue(now) : entityData.centerEntity.position;
if (pos) return Cesium.Cartesian3.clone(pos);
} catch (e) {}
}
if (entityData.type === 'powerZone' && entityData.center) {
const c = entityData.center;
if (typeof c.lng === 'number' && typeof c.lat === 'number') {
return Cesium.Cartesian3.fromDegrees(c.lng, c.lat);
}
}
// 使
if (clickPosition) {
const cartesian = this.viewer.camera.pickEllipsoid(clickPosition, this.viewer.scene.globe.ellipsoid);
if (cartesian) return cartesian;
}
return null;
},
/** 应用空域位置调整并退出模式 */
applyAirspacePositionEdit(newCenter) {
const ctx = this.airspacePositionEditContext;
@ -10237,6 +10543,21 @@ export default {
}
},
/** 右键菜单:切换选择重叠/接近的图形 */
handleContextMenuSwitchPick() {
const pickList = this.contextMenu.pickList;
if (!pickList || pickList.length <= 1) return;
const nextIndex = ((this.contextMenu.pickIndex || 0) + 1) % pickList.length;
const { entityData, pickedEntity } = pickList[nextIndex];
const anchorCartesian = this.getContextMenuAnchorCartesian(entityData, pickedEntity, null);
this.contextMenu.entityData = entityData;
this.contextMenu.pickIndex = nextIndex;
if (anchorCartesian) this.contextMenu.anchorCartesian = anchorCartesian;
if (['polygon', 'rectangle', 'circle', 'sector', 'auxiliaryLine', 'arrow'].includes(entityData.type)) {
this.applyAirspaceHighlight(entityData);
}
},
//
deleteEntityFromContextMenu() {
if (this.contextMenu.entityData) {
@ -12453,6 +12774,7 @@ export default {
}
},
destroyViewer() {
this.clearAirspaceHighlight();
this.stopDrawing()
this.clearAll(false)
if (this.entityClickDebounceTimer) {
@ -12484,6 +12806,10 @@ export default {
this.rightClickHandler.destroy()
this.rightClickHandler = null
}
if (this._contextMenuCameraListener && this.viewer && this.viewer.scene) {
this.viewer.scene.preRender.removeEventListener(this._contextMenuCameraListener)
this._contextMenuCameraListener = null
}
if (this._boundPreventContextMenuWindow) {
window.removeEventListener('contextmenu', this._boundPreventContextMenuWindow, true)
this._boundPreventContextMenuWindow = null
@ -12675,6 +13001,11 @@ export default {
width: 100vw;
height: 100vh;
position: relative;
/* 防止拖拽地图/航点时误触导致整页文字被选中 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
#cesiumViewer {

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

@ -67,6 +67,13 @@
</el-dropdown-menu>
</el-dropdown>
</div>
<el-button type="text" size="small" @click="$emit('export-whiteboard')" title="导出当前时间块" :disabled="!currentTimeBlock">
<i class="el-icon-download"></i> 导出
</el-button>
<el-button type="text" size="small" @click="triggerImport" title="导入到当前时间块" :disabled="!currentTimeBlock">
<i class="el-icon-upload2"></i> 导入
</el-button>
<input ref="importFileInput" type="file" accept=".json" style="display:none" @change="onImportFileChange" />
<el-button type="text" size="small" @click="$emit('create-whiteboard')" title="新建白板">
<i class="el-icon-plus"></i> 新建
</el-button>
@ -103,7 +110,7 @@
</div>
<!-- 添加时间块弹窗 -->
<el-dialog title="添加时间块" :visible.sync="showAddTimeBlock" width="400px" append-to-body @close="newTimeBlockValue = null; newTimeBlockInput = ''">
<el-dialog title="添加时间块" :visible.sync="showAddTimeBlock" width="400px" append-to-body @close="newTimeBlockInput = ''">
<el-form label-width="100px" size="small">
<el-form-item label="快捷选择">
<div class="time-presets">
@ -117,17 +124,8 @@
</el-tag>
</div>
</el-form-item>
<el-form-item label="选择时间">
<el-time-picker
v-model="newTimeBlockValue"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择 K+ 后的时间"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="或手动输入">
<el-input v-model="newTimeBlockInput" placeholder="如 K+00:05:00" size="small" />
<el-form-item label="手动输入">
<el-input v-model="newTimeBlockInput" placeholder="如 -5 或 10,表示 K-5 或 K+10 分钟" size="small" @input="onManualInputChange" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
@ -150,7 +148,7 @@
</el-dialog>
<!-- 修改时间弹窗与新建时间块同结构 -->
<el-dialog title="修改时间" :visible.sync="showRenameTimeBlock" width="400px" append-to-body @open="initModifyTimeBlock" @close="renameTimeBlockValue = null; renameTimeBlockInput = ''">
<el-dialog title="修改时间" :visible.sync="showRenameTimeBlock" width="400px" append-to-body @open="initModifyTimeBlock" @close="renameTimeBlockInput = ''">
<el-form label-width="100px" size="small">
<el-form-item label="快捷选择">
<div class="time-presets">
@ -164,17 +162,8 @@
</el-tag>
</div>
</el-form-item>
<el-form-item label="选择时间">
<el-time-picker
v-model="renameTimeBlockValue"
format="HH:mm:ss"
value-format="HH:mm:ss"
placeholder="选择 K+ 后的时间"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="或手动输入">
<el-input v-model="renameTimeBlockInput" placeholder="如 K+00:10:00" size="small" />
<el-form-item label="手动输入">
<el-input v-model="renameTimeBlockInput" placeholder="如 -5 或 10,表示 K-5 或 K+10 分钟" size="small" @input="onRenameManualInputChange" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
@ -228,15 +217,14 @@ export default {
renameWhiteboardName: '',
showAddTimeBlock: false,
showRenameTimeBlock: false,
newTimeBlockValue: null,
newTimeBlockInput: '',
renameTimeBlockValue: null,
renameTimeBlockInput: ''
}
},
computed: {
timeBlockPresets() {
return [
{ label: 'K-5', value: 'K-00:05:00' },
{ label: 'K+0', value: 'K+00:00:00' },
{ label: 'K+5', value: 'K+00:05:00' },
{ label: 'K+10', value: 'K+00:10:00' },
@ -301,9 +289,11 @@ export default {
},
compareTimeBlock(a, b) {
const parse = (s) => {
const m = /K\+(\d+):(\d+):(\d+)/.exec(s)
if (!m) return 0
return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10)
const mPlus = /K\+(\d+):(\d+):(\d+)/.exec(s)
if (mPlus) return parseInt(mPlus[1], 10) * 3600 + parseInt(mPlus[2], 10) * 60 + parseInt(mPlus[3], 10)
const mMinus = /K-(\d+):(\d+):(\d+)/.exec(s)
if (mMinus) return -(parseInt(mMinus[1], 10) * 3600 + parseInt(mMinus[2], 10) * 60 + parseInt(mMinus[3], 10))
return 0
}
return parse(a) - parse(b)
},
@ -338,34 +328,68 @@ export default {
exitWhiteboard() {
this.$emit('exit-whiteboard')
},
triggerImport() {
this.$refs.importFileInput && this.$refs.importFileInput.click()
},
onImportFileChange(ev) {
const file = ev.target.files && ev.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
try {
const json = JSON.parse(e.target.result)
this.$emit('import-whiteboard', json)
} catch (err) {
this.$message.error('文件解析失败,请确保是有效的白板导出文件')
}
ev.target.value = ''
}
reader.readAsText(file, 'UTF-8')
},
selectTimeBlock(tb) {
this.currentTimeBlock = tb
this.$emit('select-time-block', tb)
},
selectTimePreset(value) {
this.$emit('add-time-block', value)
this.newTimeBlockValue = null
this.newTimeBlockInput = ''
this.showAddTimeBlock = false
},
onManualInputChange(val) {
let v = (val || '').replace(/[^\d-]/g, '')
if (v.startsWith('-')) v = '-' + v.slice(1).replace(/-/g, '')
else v = v.replace(/-/g, '')
if (v !== val) this.$nextTick(() => { this.newTimeBlockInput = v })
},
onRenameManualInputChange(val) {
let v = (val || '').replace(/[^\d-]/g, '')
if (v.startsWith('-')) v = '-' + v.slice(1).replace(/-/g, '')
else v = v.replace(/-/g, '')
if (v !== val) this.$nextTick(() => { this.renameTimeBlockInput = v })
},
parseNumericToTimeBlock(input) {
const s = (input || '').trim()
if (!s) return null
const num = parseInt(s, 10)
if (isNaN(num)) return null
const abs = Math.abs(num)
const h = Math.floor(abs / 60)
const m = abs % 60
const pad = (n) => String(n).padStart(2, '0')
return num < 0 ? `K-${pad(h)}:${pad(m)}:00` : `K+${pad(h)}:${pad(m)}:00`
},
addTimeBlock() {
let timeStr = ''
if (this.newTimeBlockValue) {
timeStr = 'K+' + this.newTimeBlockValue
} else {
const input = (this.newTimeBlockInput || '').trim()
if (!input) {
this.$message.warning('请选择时间或输入格式,如 K+00:05:00')
return
}
if (!/^K\+\d+:\d{2}:\d{2}$/.test(input)) {
this.$message.warning('格式应为 K+HH:MM:SS,如 K+00:05:00')
return
}
timeStr = input
const input = (this.newTimeBlockInput || '').trim()
if (!input) {
this.$message.warning('请输入数字,如 -5 或 10')
return
}
const timeStr = this.parseNumericToTimeBlock(input)
if (!timeStr) {
this.$message.warning('请输入有效数字,负数表示 K-,正数表示 K+')
return
}
this.$emit('add-time-block', timeStr)
this.newTimeBlockValue = null
this.newTimeBlockInput = ''
this.showAddTimeBlock = false
},
@ -374,40 +398,37 @@ export default {
},
initModifyTimeBlock() {
if (!this.currentTimeBlock) return
const m = /K\+(\d+):(\d{2}):(\d{2})/.exec(this.currentTimeBlock)
if (m) {
this.renameTimeBlockValue = `${String(m[1]).padStart(2, '0')}:${m[2]}:${m[3]}`
this.renameTimeBlockInput = this.currentTimeBlock
const s = this.currentTimeBlock
const mPlus = /K\+(\d+):(\d+):(\d+)/.exec(s)
const mMinus = /K-(\d+):(\d+):(\d+)/.exec(s)
if (mPlus) {
const totalMin = parseInt(mPlus[1], 10) * 60 + parseInt(mPlus[2], 10) + parseInt(mPlus[3], 10) / 60
this.renameTimeBlockInput = String(Math.round(totalMin))
} else if (mMinus) {
const totalMin = parseInt(mMinus[1], 10) * 60 + parseInt(mMinus[2], 10) + parseInt(mMinus[3], 10) / 60
this.renameTimeBlockInput = String(-Math.round(totalMin))
} else {
this.renameTimeBlockValue = null
this.renameTimeBlockInput = this.currentTimeBlock
this.renameTimeBlockInput = ''
}
},
selectRenamePreset(value) {
this.$emit('rename-time-block', this.currentTimeBlock, value)
this.renameTimeBlockValue = null
this.renameTimeBlockInput = ''
this.showRenameTimeBlock = false
},
renameTimeBlock() {
let timeStr = ''
if (this.renameTimeBlockValue) {
timeStr = 'K+' + this.renameTimeBlockValue
} else {
const input = (this.renameTimeBlockInput || '').trim()
if (!input) {
this.$message.warning('请选择时间或输入格式,如 K+00:05:00')
return
}
if (!/^K\+\d+:\d{2}:\d{2}$/.test(input)) {
this.$message.warning('格式应为 K+HH:MM:SS,如 K+00:05:00')
return
}
timeStr = input
const input = (this.renameTimeBlockInput || '').trim()
if (!input) {
this.$message.warning('请输入数字,如 -5 或 10')
return
}
const timeStr = this.parseNumericToTimeBlock(input)
if (!timeStr) {
this.$message.warning('请输入有效数字,负数表示 K-,正数表示 K+')
return
}
if (!this.currentTimeBlock) return
this.$emit('rename-time-block', this.currentTimeBlock, timeStr)
this.renameTimeBlockValue = null
this.renameTimeBlockInput = ''
this.showRenameTimeBlock = false
},

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

@ -30,6 +30,7 @@
@open-waypoint-dialog="handleOpenWaypointEdit"
@open-route-dialog="handleOpenRouteEdit"
@copy-route="handleCopyRoute"
@single-route-deduction="handleSingleRouteDeduction"
@route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt"
@add-waypoint-placed="handleAddWaypointPlaced"
@ -229,6 +230,10 @@
<i class="el-icon-arrow-down"></i>
</div>
<div class="timeline-controls">
<div class="timeline-mode-badge" :class="deductionMode">
{{ deductionMode === 'single' ? '单条航线推演' : '全航线推演' }}
<span v-if="deductionMode === 'single' && singleRouteDeductionRouteName" class="route-name">{{ singleRouteDeductionRouteName }}</span>
</div>
<div class="current-time blue-time">
<i class="el-icon-time"></i>
<span class="time-text">{{ currentTime }}</span>
@ -441,6 +446,8 @@
@rename-time-block="handleWhiteboardRenameTimeBlock"
@delete-time-block="handleWhiteboardDeleteTimeBlock"
@draw-mode-change="handleWhiteboardDrawModeChange"
@export-whiteboard="handleWhiteboardExport"
@import-whiteboard="handleWhiteboardImport"
/>
<el-dialog
@ -768,6 +775,10 @@ export default {
playbackInterval: null,
/** 播放时上一帧时间戳(毫秒),用于按真实经过时间推进,避免 setInterval 不准导致时间轴变慢 */
_playbackLastTickTime: null,
/** 推演模式:'all' 全航线推演(左侧菜单点击),'single' 单条航线推演(右键航线选择) */
deductionMode: 'all',
/** 单条航线推演时的航线 ID(deductionMode='single' 时有效) */
singleRouteDeductionId: null,
timelineHoverTime: '',
timelineHoverVisible: false,
timelineHoverPercent: 0,
@ -877,6 +888,12 @@ export default {
if (this.roomDetail && this.roomDetail.parentId == null) return false;
return this.isRoomOwner || this.isAdmin;
},
/** 单条航线推演时的航线名称(用于时间轴模式标识显示) */
singleRouteDeductionRouteName() {
if (this.deductionMode !== 'single' || this.singleRouteDeductionId == null) return '';
const route = this.routes.find(r => r.id === this.singleRouteDeductionId);
return route ? (route.callSign || route.name || `航线${this.singleRouteDeductionId}`) : '';
},
/** 格式化的 K 时(基准时刻),供右上角显示 */
kTimeDisplay() {
if (!this.roomDetail || !this.roomDetail.kAnchorTime) return '';
@ -972,25 +989,12 @@ export default {
});
return bars;
},
/** 白板模式下当前时间块应显示的实体(继承逻辑:当前时刻 = 上一时刻 + 本时刻差异) */
/** 白板模式下当前时间块应显示的实体(每块只显示自身内容,新建时复制前块,后续互不影响) */
whiteboardDisplayEntities() {
if (!this.showWhiteboardPanel || !this.currentWhiteboard || !this.currentWhiteboardTimeBlock) return []
const wb = this.currentWhiteboard
const contentByTime = wb.contentByTime || {}
const timeBlocks = (wb.timeBlocks || []).slice().sort((a, b) => this.compareWhiteboardTimeBlock(a, b))
const idx = timeBlocks.indexOf(this.currentWhiteboardTimeBlock)
if (idx < 0) return []
const merged = {}
for (let i = 0; i <= idx; i++) {
const tb = timeBlocks[i]
const ents = (contentByTime[tb] && contentByTime[tb].entities) || []
ents.forEach(e => {
const id = e.id || (e.data && e.data.id)
if (id) merged[id] = e
else merged['_noid_' + Math.random()] = e
})
}
return Object.values(merged)
const contentByTime = this.currentWhiteboard.contentByTime || {}
const currentContent = contentByTime[this.currentWhiteboardTimeBlock]
return (currentContent && currentContent.entities) || []
},
/** 所有平台(用于导入航线时选择默认平台) */
@ -1533,6 +1537,51 @@ export default {
}
},
/** 右键「单条航线推演」:弹出时间轴,仅推演该航线;其他航线平台保持起点位置 */
async handleSingleRouteDeduction(routeId) {
let route = this.routes.find(r => r.id === routeId);
if (!route) {
this.$message.warning('未找到该航线');
return;
}
if (!route.waypoints || route.waypoints.length === 0) {
try {
const res = await getRoutes(routeId);
if (res.code === 200 && res.data) {
route = { ...route, waypoints: res.data.waypoints || [] };
const idx = this.routes.findIndex(r => r.id === routeId);
if (idx > -1) this.$set(this.routes, idx, route);
}
} catch (_) {}
}
if (!route.waypoints || route.waypoints.length === 0) {
this.$message.warning('该航线无有效航点,无法推演');
return;
}
this.deductionMode = 'single';
this.singleRouteDeductionId = routeId;
if (!this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
this.activeRouteIds = [...this.activeRouteIds, routeId];
const roomId = this.currentRoomId;
if (roomId && route.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId, platformId: route.platformId });
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
} catch (_) {}
}
if (route.waypoints.some(wp => this.isHoldWaypoint(wp))) {
const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
this.getPositionAtMinutesFromK(route.waypoints, minMinutes, minMinutes, maxMinutes, routeId);
}
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, routeId, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
this.showKTimePopup = true;
this.$nextTick(() => {
this.updateTimeFromProgress();
});
this.$message.info('单条航线推演模式:时间轴仅控制当前航线');
},
/** 复制航线已放置:用当前偏移后的航点打开「保存新航线」弹窗 */
handleRouteCopyPlaced(points) {
this.tempMapPoints = points || [];
@ -3128,7 +3177,7 @@ export default {
openExportRoutesDialog() {
this.showExportRoutesDialog = true;
},
/** 导出航线:获取完整数据并下载 JSON */
/** 导出航线:获取完整数据并下载 JSON,支持多条航线一起导出 */
async handleExportRoutes(selectedIds) {
if (!selectedIds || selectedIds.length === 0) return;
try {
@ -3152,6 +3201,10 @@ export default {
});
}
}
if (routeDataList.length === 0) {
this.$message.error('未能获取到任何航线数据,请检查网络或权限后重试');
return;
}
const exportData = {
version: 1,
exportTime: new Date().toISOString(),
@ -3165,7 +3218,7 @@ export default {
a.click();
URL.revokeObjectURL(url);
this.showExportRoutesDialog = false;
this.$message.success(`已导出 ${selectedIds.length} 条航线`);
this.$message.success(`已导出 ${routeDataList.length} 条航线`);
} catch (err) {
console.error('导出航线失败:', err);
this.$message.error('导出失败,请重试');
@ -3387,9 +3440,11 @@ export default {
/** 白板:K+HH:MM:SS 时间块比较 */
compareWhiteboardTimeBlock(a, b) {
const parse = (s) => {
const m = /K\+(\d+):(\d+):(\d+)/.exec(s)
if (!m) return 0
return parseInt(m[1], 10) * 3600 + parseInt(m[2], 10) * 60 + parseInt(m[3], 10)
const mPlus = /K\+(\d+):(\d+):(\d+)/.exec(s)
if (mPlus) return parseInt(mPlus[1], 10) * 3600 + parseInt(mPlus[2], 10) * 60 + parseInt(mPlus[3], 10)
const mMinus = /K-(\d+):(\d+):(\d+)/.exec(s)
if (mMinus) return -(parseInt(mMinus[1], 10) * 3600 + parseInt(mMinus[2], 10) * 60 + parseInt(mMinus[3], 10))
return 0
}
return parse(a) - parse(b)
},
@ -3494,6 +3549,20 @@ export default {
this.currentWhiteboardTimeBlock = tb
},
/** 获取指定时间块之前(含)的累积实体列表 */
getMergedEntitiesBeforeTimeBlock(timeBlocks, contentByTime, endIdx) {
const merged = {}
for (let i = 0; i <= endIdx; i++) {
const tb = timeBlocks[i]
const ents = (contentByTime[tb] && contentByTime[tb].entities) || []
ents.forEach(e => {
const id = e.id || (e.data && e.data.id)
if (id) merged[id] = e
else merged['_noid_' + Math.random()] = e
})
}
return Object.values(merged)
},
async handleWhiteboardAddTimeBlock(tb) {
if (!this.currentWhiteboard) return
const blocks = [...(this.currentWhiteboard.timeBlocks || [])]
@ -3504,7 +3573,9 @@ export default {
blocks.push(tb)
blocks.sort((a, b) => this.compareWhiteboardTimeBlock(a, b))
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
contentByTime[tb] = { entities: [] }
const idx = blocks.indexOf(tb)
const initialEntities = idx > 0 ? this.getMergedEntitiesBeforeTimeBlock(blocks, contentByTime, idx - 1) : []
contentByTime[tb] = { entities: initialEntities }
await this.saveCurrentWhiteboard({ timeBlocks: blocks, contentByTime })
this.currentWhiteboardTimeBlock = tb
},
@ -3587,14 +3658,185 @@ export default {
}
},
/** 白板导出:序列化当前时间块全部平台和空域(含位置、大小、样式) */
serializeWhiteboardEntityForExport(e) {
if (!e || !e.type) return null
if (e.type === 'platformIcon') {
return {
type: 'platformIcon',
id: e.id,
platformId: e.platformId,
platform: e.platform || {},
platformName: e.platformName || '',
lat: e.lat,
lng: e.lng,
heading: e.heading != null ? e.heading : 0,
iconScale: e.iconScale != null ? e.iconScale : 1.5,
label: e.label || '',
color: e.color || '#008aff'
}
}
const base = { type: e.type, id: e.id, color: e.color || '#008aff' }
const data = e.data ? { ...e.data } : {}
switch (e.type) {
case 'polygon': {
const pts = e.points || (e.data && e.data.points)
if (pts && pts.length >= 3) {
data.points = pts
data.opacity = e.opacity != null ? e.opacity : (e.data && e.data.opacity != null ? e.data.opacity : 0)
data.width = e.width != null ? e.width : (e.data && e.data.width != null ? e.data.width : 2)
data.borderColor = e.borderColor || e.data?.borderColor || e.color
}
break
}
case 'rectangle': {
const pts = e.points || (e.data && e.data.points)
if (pts && pts.length >= 2) {
const lngs = pts.map(p => p.lng)
const lats = pts.map(p => p.lat)
data.coordinates = e.data?.coordinates || { west: Math.min(...lngs), south: Math.min(...lats), east: Math.max(...lngs), north: Math.max(...lats) }
data.points = pts
}
data.opacity = e.opacity != null ? e.opacity : (e.data && e.data.opacity != null ? e.data.opacity : 0)
data.width = e.width != null ? e.width : (e.data && e.data.width != null ? e.data.width : 2)
data.borderColor = e.borderColor || e.data?.borderColor || e.color
break
}
case 'circle':
data.center = e.center || (e.data && e.data.center) || (e.points && e.points[0] ? e.points[0] : null)
data.radius = e.radius != null ? e.radius : (e.data && e.data.radius != null ? e.data.radius : 1000)
data.opacity = e.opacity != null ? e.opacity : (e.data && e.data.opacity != null ? e.data.opacity : 0)
data.width = e.width != null ? e.width : (e.data && e.data.width != null ? e.data.width : 2)
data.borderColor = e.borderColor || e.data?.borderColor || e.color
break
case 'sector':
data.center = e.center || (e.data && e.data.center) || (e.points && e.points[0] ? e.points[0] : null)
data.radius = e.radius != null ? e.radius : (e.data && e.data.radius != null ? e.data.radius : 1000)
data.startAngle = e.startAngle != null ? e.startAngle : (e.data && e.data.startAngle != null ? e.data.startAngle : 0)
data.endAngle = e.endAngle != null ? e.endAngle : (e.data && e.data.endAngle != null ? e.data.endAngle : Math.PI * 0.5)
data.opacity = e.opacity != null ? e.opacity : (e.data && e.data.opacity != null ? e.data.opacity : 0.5)
data.width = e.width != null ? e.width : (e.data && e.data.width != null ? e.data.width : 2)
data.borderColor = e.borderColor || e.data?.borderColor || e.color
break
case 'line':
case 'auxiliaryLine': {
const linePts = e.points || (e.data && e.data.points)
if (linePts && linePts.length >= 2) {
data.points = linePts
data.width = e.width != null ? e.width : (e.data && e.data.width != null ? e.data.width : 2)
data.color = e.color || e.data?.color
}
break
}
case 'arrow': {
const arrowPts = e.points || (e.data && e.data.points)
if (arrowPts && arrowPts.length >= 2) {
data.points = arrowPts
data.width = e.width != null ? e.width : (e.data && e.data.width != null ? e.data.width : 12)
data.color = e.color || e.data?.color
}
break
}
case 'text':
data.position = e.data?.position || (e.data?.lat != null && e.data?.lng != null ? { lat: e.data.lat, lng: e.data.lng } : null)
data.text = e.data?.text || e.label || ''
data.fontSize = e.data?.fontSize || 14
data.color = e.data?.color || e.color
break
case 'image':
data.imageUrl = e.data?.imageUrl
data.lat = e.data?.lat
data.lng = e.data?.lng
data.width = e.data?.width
data.height = e.data?.height
break
case 'powerZone':
data.center = e.center || (e.data?.center)
data.radius = e.data?.radius != null ? e.data.radius : 50000
data.color = e.data?.color || e.color
data.opacity = e.data?.opacity != null ? e.data.opacity : 0.3
break
default:
return { ...base, data: e.data || {} }
}
return { ...base, data }
},
handleWhiteboardExport() {
if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock) {
this.$message.warning('请先选择时间块')
return
}
const contentByTime = this.currentWhiteboard.contentByTime || {}
const currentContent = contentByTime[this.currentWhiteboardTimeBlock]
const entities = (currentContent && currentContent.entities) || []
const serialized = entities.map(e => this.serializeWhiteboardEntityForExport(e)).filter(Boolean)
const payload = {
version: '1.0',
timeBlock: this.currentWhiteboardTimeBlock,
exportTime: new Date().toISOString(),
whiteboardName: this.currentWhiteboard.name || '白板方案',
entities: serialized
}
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `白板_${this.currentWhiteboardTimeBlock.replace(/[+:]/g, '_')}_${Date.now()}.json`
a.click()
URL.revokeObjectURL(url)
this.$message.success('导出成功')
},
handleWhiteboardImport(payload) {
if (!this.currentWhiteboard || !this.currentWhiteboardTimeBlock) {
this.$message.warning('请先选择时间块')
return
}
const entities = Array.isArray(payload) ? payload : (payload && payload.entities)
if (!entities || !Array.isArray(entities)) {
this.$message.error('无效的导入文件格式')
return
}
const contentByTime = { ...(this.currentWhiteboard.contentByTime || {}) }
const currentContent = contentByTime[this.currentWhiteboardTimeBlock] || { entities: [] }
const existing = currentContent.entities || []
const newEntities = entities.map(e => {
const newId = 'wb_' + Date.now() + '_' + Math.random().toString(36).slice(2)
const ent = { ...e, id: newId }
if (ent.type === 'platformIcon') {
return {
id: newId,
type: 'platformIcon',
platformId: ent.platformId,
platform: ent.platform || {},
platformName: ent.platformName || '',
lat: ent.lat,
lng: ent.lng,
heading: ent.heading != null ? ent.heading : 0,
iconScale: ent.iconScale != null ? ent.iconScale : 1.5,
label: ent.label || '平台',
color: ent.color || '#008aff'
}
}
if (ent.type === 'text' && ent.data) {
ent.label = ent.data.text || ent.label || ''
}
if (ent.data) ent.data = { ...ent.data }
return ent
})
const merged = [...existing, ...newEntities]
contentByTime[this.currentWhiteboardTimeBlock] = { ...currentContent, entities: merged }
this.saveCurrentWhiteboard({ contentByTime })
this.$message.success(`已导入 ${newEntities.length} 个实体`)
},
async saveCurrentWhiteboard(patch) {
if (!this.currentRoomId || !this.currentWhiteboard) return
const wb = { ...this.currentWhiteboard, ...patch }
this.currentWhiteboard = wb
const i = this.whiteboards.findIndex(w => w.id === wb.id)
if (i >= 0) this.whiteboards[i] = wb
try {
await updateWhiteboard(this.currentRoomId, wb.id, wb)
this.currentWhiteboard = wb
const i = this.whiteboards.findIndex(w => w.id === wb.id)
if (i >= 0) this.whiteboards[i] = wb
} catch (e) {
this.$message.error('保存白板失败')
}
@ -4029,7 +4271,9 @@ export default {
this.isRightPanelHidden = true;
console.log('空域绘制模式:', this.airspaceDrawDom, 999999)
} else if (item.id === 'deduction') {
// /K
// 线/K
this.deductionMode = 'all';
this.singleRouteDeductionId = null;
this.showKTimePopup = !this.showKTimePopup;
if (this.showKTimePopup) {
this.$nextTick(() => this.updateTimeFromProgress());
@ -4122,7 +4366,9 @@ export default {
this.updateDeductionPositions();
// 线+线 Redis plan.roomId
const roomId = this.currentRoomId;
const routeIds = (this.activeRouteIds || []).slice().sort((a, b) => (a - b));
const routeIds = (this.deductionMode === 'single' && this.singleRouteDeductionId != null)
? [this.singleRouteDeductionId]
: (this.activeRouteIds || []).slice().sort((a, b) => (a - b));
const isParentRoom = this.roomDetail && this.roomDetail.parentId == null;
if (roomId != null && routeIds.length > 0 && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadMissilesFromRedis === 'function') {
const routePlatforms = this.routes
@ -4200,12 +4446,15 @@ export default {
this.$refs.cesiumMap.loadMissilesFromRedis(roomId, routePlatforms);
},
/** 仅针对当前展示的航线(activeRouteIds):从这些航线的航点中取推演时间范围(相对 K 的分钟数) */
/** 仅针对当前展示的航线(activeRouteIds 或单条模式下的 singleRouteDeductionId):从这些航线的航点中取推演时间范围(相对 K 的分钟数) */
getDeductionTimeRange() {
let minMinutes = 0;
let maxMinutes = 120;
const minutesList = [];
this.activeRouteIds.forEach(routeId => {
const routeIds = this.deductionMode === 'single' && this.singleRouteDeductionId != null
? [this.singleRouteDeductionId]
: this.activeRouteIds;
routeIds.forEach(routeId => {
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || !route.waypoints.length) return;
route.waypoints.forEach(wp => {
@ -4787,13 +5036,34 @@ export default {
return ((deg % 360) + 360) % 360;
},
/** 仅根据当前展示的航线(activeRouteIds)更新平台图标位置与标牌,并汇总航段提示 */
/** 仅根据当前展示的航线(activeRouteIds 或单条模式下的 singleRouteDeductionId)更新平台图标位置与标牌,并汇总航段提示 */
updateDeductionPositions() {
if (!this.$refs.cesiumMap || !this.$refs.cesiumMap.updatePlatformPosition) return;
const minutesFromK = this.deductionMinutesFromK != null ? this.deductionMinutesFromK : 0;
const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
const allWarnings = [];
this.activeRouteIds.forEach(routeId => {
const isSingleMode = this.deductionMode === 'single' && this.singleRouteDeductionId != null;
const routeIdsToUpdate = isSingleMode ? [this.singleRouteDeductionId] : this.activeRouteIds;
// 线
if (isSingleMode) {
this.activeRouteIds.forEach(routeId => {
if (routeId === this.singleRouteDeductionId) return;
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || route.waypoints.length === 0) return;
const firstWp = route.waypoints[0];
const pos = { lng: firstWp.lng, lat: firstWp.lat, alt: firstWp.alt != null ? firstWp.alt : 0 };
const nextWp = route.waypoints[1];
const nextPos = nextWp ? { lng: nextWp.lng, lat: nextWp.lat, alt: nextWp.alt != null ? nextWp.alt : 0 } : null;
const labelData = {
name: (route.platform && route.platform.name) ? route.platform.name : '平台',
altitude: pos.alt,
speed: Number(firstWp.speed) || 800,
headingDeg: nextPos ? this.headingDegFromPositions(pos, nextPos) : 0
};
this.$refs.cesiumMap.updatePlatformPosition(routeId, pos, nextPos, labelData);
});
}
routeIdsToUpdate.forEach(routeId => {
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || route.waypoints.length === 0) return;
const { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId);
@ -5688,6 +5958,17 @@ export default {
width: 100vw;
height: 100vh;
overflow: hidden;
/* 防止拖拽地图/航点/面板时误触导致整页文字被选中 */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* 输入框、文本域仍允许选中文字 */
.mission-planning-container input,
.mission-planning-container textarea {
user-select: text;
-webkit-user-select: text;
}
/* 地图背景:使用相对路径便于 IDE 与构建解析;若需图片请将 map-background.png 放到 src/assets/ */
@ -5958,6 +6239,32 @@ export default {
margin-bottom: 5px;
}
.timeline-mode-badge {
font-size: 12px;
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(0, 138, 255, 0.3);
color: #008aff;
background: rgba(0, 138, 255, 0.08);
}
.timeline-mode-badge.single {
background: rgba(255, 152, 0, 0.15);
border-color: rgba(255, 152, 0, 0.4);
color: #e65100;
}
.timeline-mode-badge .route-name {
margin-left: 6px;
font-weight: 500;
padding-left: 6px;
border-left: 1px solid rgba(0, 0, 0, 0.15);
}
.timeline-mode-badge.single .route-name {
border-left-color: rgba(255, 152, 0, 0.4);
}
.current-time {
display: flex;
align-items: center;

52
ruoyi-ui/src/views/dialogs/ExportRoutesDialog.vue

@ -3,7 +3,7 @@
title="导出航线"
:visible.sync="visible"
width="720px"
top="52vh"
top="22vh"
append-to-body
class="export-routes-dialog"
@close="handleClose"
@ -18,9 +18,10 @@
<el-button type="text" size="small" @click="selectNone">全不选</el-button>
</div>
<div class="tree-list">
<!-- 按方案分组的航线 -->
<div
v-for="plan in plansWithRoutes"
:key="plan.id"
:key="'plan-' + plan.id"
class="tree-item plan-item"
>
<div class="tree-item-header" @click="togglePlan(plan.id)">
@ -33,7 +34,35 @@
<div v-if="expandedPlans.includes(plan.id)" class="tree-children route-children">
<div
v-for="route in planRoutes(plan.id)"
:key="route.id"
:key="'route-' + route.id"
class="tree-item route-item"
:class="{ selected: selectedIds.includes(route.id) }"
@click.stop="toggleRouteSelect(route.id)"
>
<el-checkbox
:value="selectedIds.includes(route.id)"
@change="(v) => setRouteSelected(route.id, v)"
@click.native.stop
>
<span class="route-name">{{ route.name }}</span>
<span class="route-meta">{{ route.points || (route.waypoints && route.waypoints.length) || 0 }} 个航点</span>
</el-checkbox>
</div>
</div>
</div>
<!-- 未归属方案的航线避免因 scenarioId 不匹配而遗漏 -->
<div v-if="orphanRoutesList.length > 0" class="tree-item plan-item">
<div class="tree-item-header" @click="togglePlan('orphan')">
<i :class="expandedPlans.includes('orphan') ? 'el-icon-folder-opened' : 'el-icon-folder'" class="tree-icon"></i>
<div class="tree-item-info">
<div class="tree-item-name">未归属方案</div>
<div class="tree-item-meta">{{ orphanRoutesList.length }} 个航线</div>
</div>
</div>
<div v-if="expandedPlans.includes('orphan')" class="tree-children route-children">
<div
v-for="route in orphanRoutesList"
:key="'orphan-' + route.id"
class="tree-item route-item"
:class="{ selected: selectedIds.includes(route.id) }"
@click.stop="toggleRouteSelect(route.id)"
@ -95,19 +124,32 @@ export default {
/** 有航线的方案列表(用于展示) */
plansWithRoutes() {
return this.plans.filter(p => this.planRoutes(p.id).length > 0);
},
/** 未归属任何方案的航线(scenarioId 为空或不匹配任何 plan) */
orphanRoutesList() {
const planIds = (this.plans || []).map(p => p.id);
return this.routes.filter(r => {
if (r.scenarioId == null) return true;
const sid = Number(r.scenarioId);
return !planIds.some(pid => Number(pid) === sid);
});
}
},
watch: {
value(v) {
if (v) {
this.selectedIds = this.routes.map(r => r.id);
this.expandedPlans = this.plansWithRoutes.map(p => p.id);
this.expandedPlans = [
...this.plansWithRoutes.map(p => p.id),
...(this.orphanRoutesList.length > 0 ? ['orphan'] : [])
];
}
}
},
methods: {
/** 获取某方案下的航线(使用宽松比较,兼容 scenarioId 与 planId 的类型差异) */
planRoutes(planId) {
return this.routes.filter(r => r.scenarioId === planId);
return this.routes.filter(r => r.scenarioId != null && (r.scenarioId === planId || Number(r.scenarioId) === Number(planId)));
},
togglePlan(planId) {
const idx = this.expandedPlans.indexOf(planId);

73
ruoyi-ui/src/views/index.vue

@ -3,53 +3,25 @@
<el-row :gutter="20">
<el-col :span="24">
<div class="header-section">
<h1 class="system-title">联合作战指挥系统入口</h1>
<p class="system-desc">请选择下方功能模块进入</p>
<h1 class="system-title">联合作战指挥系统</h1>
<p class="system-desc">点击下方进入作战系统</p>
</div>
</el-col>
</el-row>
<el-row :gutter="30" type="flex" justify="center" style="margin-top: 30px;">
<el-col :xs="24" :sm="8" :md="6">
<el-card shadow="hover" class="entry-card" @click.native="goCesium">
<el-row type="flex" justify="center" style="margin-top: 30px;">
<el-col :xs="24" :sm="12" :md="8">
<el-card shadow="hover" class="entry-card" @click.native="goCombatSystem">
<div class="card-content">
<div class="icon-wrapper blue">
<i class="el-icon-map-location"></i>
</div>
<h3>三维全屏地图</h3>
<p>Cesium三维地球态势展示</p>
<el-button type="text" class="enter-btn">点击进入 &gt;</el-button>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="8" :md="6">
<el-card shadow="hover" class="entry-card" @click.native="goSelectRoom">
<div class="card-content">
<div class="icon-wrapper green">
<i class="el-icon-office-building"></i>
</div>
<h3>选择/管理房间</h3>
<p>进入大厅查看或创建作战室</p>
<el-button type="text" class="enter-btn">点击进入 &gt;</el-button>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="8" :md="6">
<el-card shadow="hover" class="entry-card" @click.native="goChildRoom">
<div class="card-content">
<div class="icon-wrapper orange">
<div class="icon-wrapper primary">
<i class="el-icon-s-operation"></i>
</div>
<h3>进入子房间</h3>
<p>直接进入作战推演与标绘界面</p>
<h3>进入作战系统</h3>
<p>选择房间进入作战推演与标绘界面</p>
<el-button type="text" class="enter-btn">点击进入 &gt;</el-button>
</div>
</el-card>
</el-col>
</el-row>
<div class="footer-tips">
@ -67,20 +39,9 @@ export default {
};
},
methods: {
// 1. Cesium
goCesium() {
// path router/index.js
this.$router.push({ path: "/cesiumMap" }).catch(() => {});
},
// 2.
goSelectRoom() {
//
goCombatSystem() {
this.$router.push({ path: "/selectRoom" }).catch(() => {});
},
// 3. ()
goChildRoom() {
this.$router.push({ path: "/childRoom" }).catch(() => {});
}
}
};
@ -165,20 +126,10 @@ export default {
font-size: 40px;
margin-bottom: 10px;
&.blue {
background-color: rgba(64, 158, 255, 0.1);
&.primary {
background-color: rgba(64, 158, 255, 0.15);
color: #409EFF;
}
&.green {
background-color: rgba(103, 194, 58, 0.1);
color: #67C23A;
}
&.orange {
background-color: rgba(230, 162, 60, 0.1);
color: #E6A23C;
}
}
/* 进入按钮动画 */

Loading…
Cancel
Save