Browse Source

在航点的前后新增航点

mh
cuitw 1 month ago
parent
commit
8ade9f4da6
  1. 2
      ruoyi-admin/src/main/resources/application-druid.yml
  2. 1
      ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
  3. 35
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  4. 211
      ruoyi-ui/src/views/cesiumMap/index.vue
  5. 182
      ruoyi-ui/src/views/childRoom/index.vue
  6. 209
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue

2
ruoyi-admin/src/main/resources/application-druid.yml

@ -8,7 +8,7 @@ spring:
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: A20040303ctw!
password: 123456
# 从库数据源
slave:
# 从数据源开关/默认关闭

1
ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml

@ -40,6 +40,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="pointType != null and pointType != ''"> and point_type = #{pointType}</if>
<if test="holdParams != null and holdParams != ''"> and hold_params = #{holdParams}</if>
</where>
order by seq asc
</select>
<select id="selectRouteWaypointsById" parameterType="Long" resultMap="RouteWaypointsResult">

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

@ -7,8 +7,25 @@
</div>
</div>
<!-- 航线上锁/解锁复制 -->
<div class="menu-section" v-if="entityData && entityData.type === 'route'">
<!-- 航点编辑向前/向后增加航点 -->
<div class="menu-section" v-if="entityData && entityData.type === 'routeWaypoint'">
<div class="menu-title">航点</div>
<div class="menu-item" @click="handleEditWaypoint">
<span class="menu-icon">📝</span>
<span>编辑航点</span>
</div>
<div class="menu-item" @click="handleAddWaypointBefore">
<span class="menu-icon"></span>
<span>向前增加航点</span>
</div>
<div class="menu-item" @click="handleAddWaypointAfter">
<span class="menu-icon"></span>
<span>向后增加航点</span>
</div>
</div>
<!-- 航线上锁/解锁复制航点右键时也显示 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">
<span class="menu-icon">{{ isRouteLocked ? '🔓' : '🔒' }}</span>
@ -385,7 +402,7 @@ export default {
}
},
isRouteLocked() {
if (!this.entityData || this.entityData.type !== 'route' || this.entityData.routeId == null) return false
if (!this.entityData || this.entityData.routeId == null) return false
return !!this.routeLocked[this.entityData.routeId]
}
},
@ -433,6 +450,18 @@ export default {
this.$emit('copy-route')
},
handleEditWaypoint() {
this.$emit('open-waypoint-dialog', this.entityData.dbId, this.entityData.routeId, this.entityData.waypointIndex)
},
handleAddWaypointBefore() {
this.$emit('add-waypoint-at', { routeId: this.entityData.routeId, waypointIndex: this.entityData.waypointIndex, mode: 'before' })
},
handleAddWaypointAfter() {
this.$emit('add-waypoint-at', { routeId: this.entityData.routeId, waypointIndex: this.entityData.waypointIndex, mode: 'after' })
},
handleEditPlatform() {
this.$emit('edit-platform')
},

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

@ -45,6 +45,8 @@
@copy-route="handleCopyRouteFromMenu"
@edit-platform="openEditPlatformDialog"
@power-zone="openPowerZoneDialog"
@open-waypoint-dialog="handleContextMenuOpenWaypointDialog"
@add-waypoint-at="handleAddWaypointAt"
/>
<!-- 定位弹窗 -->
@ -270,7 +272,7 @@ export default {
fontSize: 16,
fontColor: '#333333',
iconSize: 144,
iconColor: '#ffffff'
iconColor: '#000000'
},
//
presetColors: [
@ -357,7 +359,10 @@ export default {
// 线线
copyPreviewWaypoints: null,
copyPreviewEntity: null,
copyPreviewMouseCartesian: null
copyPreviewMouseCartesian: null,
// /{ routeId, waypointIndex, mode: 'before'|'after', waypoints }线
addWaypointContext: null,
addWaypointPreviewEntity: null
}
},
components: {
@ -1614,11 +1619,31 @@ export default {
}
return !!nextLogical;
};
// 线穿线
// 便/
waypoints.forEach((wp, index) => {
if (isTurnWaypointWithArc(index)) return;
if (this.isHoldWaypoint(wp)) return;
const pos = originalPositions[index];
if (this.isHoldWaypoint(wp)) {
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}`,
name: wp.name || `盘旋${index + 1}`,
position: pos,
properties: {
isMissionWaypoint: true,
routeId: routeId,
dbId: wp.id,
},
point: {
pixelSize: Math.max(4, pixelSize - 2),
color: Cesium.Color.fromCssColorString(wpColor),
outlineColor: Cesium.Color.fromCssColorString(wpOutline),
outlineWidth: wpOutlineW,
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
label: { show: false }
});
return;
}
this.viewer.entities.add({
id: `wp_${routeId}_${wp.id}`,
name: wp.name || `WP${index + 1}`,
@ -1712,8 +1737,10 @@ export default {
platformId: platId
}).then(res => {
const style = res.data;
const color = (style && style.platformColor) ? style.platformColor : '#ffffff';
const size = (style && style.platformSize != null) ? Math.max(48, Math.min(256, Number(style.platformSize))) : 144;
const defaultColor = '#000000';
const defaultSize = 144;
const color = (style && style.platformColor) ? style.platformColor : defaultColor;
const size = (style && style.platformSize != null) ? Math.max(48, Math.min(256, Number(style.platformSize))) : defaultSize;
this.$set(this.platformCustomStyles, routeId, {
labelFontSize: style && style.labelFontSize,
labelFontColor: style && style.labelFontColor,
@ -1726,11 +1753,24 @@ export default {
if (style && style.powerZoneRadius != null && Number(style.powerZoneRadius) > 0) {
this.ensurePowerZoneForRoute(routeId, style.powerZoneRadius, style.powerZoneColor || 'rgba(255, 0, 0, 0.3)');
}
// Redis Redis便
if (!style || style.platformColor == null) {
savePlatformStyle({
roomId: String(currentRoomId),
routeId: routeId,
platformId: platId,
platformName: (platform && platform.name) || undefined,
labelFontSize: style && style.labelFontSize != null ? style.labelFontSize : 16,
labelFontColor: (style && style.labelFontColor) || '#333333',
platformSize: size,
platformColor: defaultColor
}).catch(() => {});
}
}).catch(() => {
addPlatformBillboard('#ffffff', 144);
addPlatformBillboard('#000000', 144);
});
} else {
addPlatformBillboard('#ffffff', 144);
addPlatformBillboard('#000000', 144);
}
//
@ -2621,6 +2661,29 @@ export default {
}
return;
}
//
if (this.addWaypointContext) {
const ctx = this.addWaypointContext;
const waypoints = ctx.waypoints || [];
const refAlt = ctx.mode === 'before'
? (waypoints[ctx.waypointIndex - 1] && Number(waypoints[ctx.waypointIndex - 1].alt)) || (waypoints[ctx.waypointIndex] && Number(waypoints[ctx.waypointIndex].alt)) || 5000
: (waypoints[ctx.waypointIndex] && Number(waypoints[ctx.waypointIndex].alt)) || (waypoints[ctx.waypointIndex + 1] && Number(waypoints[ctx.waypointIndex + 1].alt)) || 5000;
const placePosition = this.getClickPositionWithHeight(click.position, refAlt);
if (placePosition) {
const carto = Cesium.Cartographic.fromCartesian(placePosition);
const lng = Cesium.Math.toDegrees(carto.longitude);
const lat = Cesium.Math.toDegrees(carto.latitude);
const alt = carto.height;
this.$emit('add-waypoint-placed', {
routeId: ctx.routeId,
waypointIndex: ctx.waypointIndex,
mode: ctx.mode,
position: { lng, lat, alt }
});
}
this.clearAddWaypointContext();
return;
}
const pickedObject = this.viewer.scene.pick(click.position);
if (Cesium.defined(pickedObject) && pickedObject.id) {
@ -2768,6 +2831,14 @@ export default {
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
}
if (this.addWaypointContext) {
const cartesian = this.getClickPosition(movement.endPosition);
if (cartesian) {
this.addWaypointContext.mouseCartesian = cartesian;
this.updateAddWaypointPreview();
if (this.viewer.scene.requestRender) this.viewer.scene.requestRender();
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// pending
@ -2811,6 +2882,13 @@ export default {
this.contextMenu.visible = false;
return;
}
//
if (this.addWaypointContext) {
this.clearAddWaypointContext();
this.contextMenu.visible = false;
this.$message && this.$message.info('已取消增加航点');
return;
}
//
this.contextMenu.visible = false;
@ -2870,20 +2948,37 @@ export default {
}
}
}
// 线 routeId线/
// 线 routeId routeWaypoint /
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 || isLine) {
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 };
}
}
}
// 线hold-line-routeId-iwaypointIndex = i+1
if (!entityData && idStr && 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 + 1, fromHold: true };
}
}
}
// 线 allEntities routeId id
if (entityData && entityData.type === 'route' && entityData.id && !entityData.routeId) {
@ -2967,7 +3062,7 @@ export default {
const routeId = ed.routeId
const platformEntity = this.viewer && this.viewer.entities && this.viewer.entities.getById(`route-platform-${routeId}`)
let size = 144
let color = '#ffffff'
let color = '#000000'
if (platformEntity && platformEntity.billboard) {
const now = Cesium.JulianDate.now()
const widthValue = platformEntity.billboard.width && platformEntity.billboard.width.getValue
@ -3000,7 +3095,7 @@ export default {
const entity = this.viewer.entities.getById(`route-platform-${routeId}`)
if (entity && entity.billboard) {
const size = Math.max(48, Math.min(256, Number(this.editPlatformStyleForm.size) || 144))
const color = this.editPlatformStyleForm.color || '#ffffff'
const color = this.editPlatformStyleForm.color || '#000000'
entity.billboard.width = size
entity.billboard.height = size
entity.billboard.color = Cesium.Color.fromCssColorString(color)
@ -5323,6 +5418,86 @@ export default {
this.copyPreviewWaypoints = null;
this.copyPreviewMouseCartesian = null;
},
/** 右键菜单:打开航点编辑(支持 dbId 或 waypointIndex) */
handleContextMenuOpenWaypointDialog(dbId, routeId, waypointIndex) {
this.contextMenu.visible = false;
this.$emit('open-waypoint-dialog', dbId, routeId, waypointIndex);
},
/** 右键菜单:选择向前/向后增加航点,由父组件调用 startAddWaypointAt 传入 waypoints */
handleAddWaypointAt(payload) {
this.contextMenu.visible = false;
this.$emit('add-waypoint-at', payload);
},
/** 开始“在航点前/后增加航点”模式:显示预览折线,左键放置、右键取消。waypoints 为当前航线航点数组。 */
startAddWaypointAt(routeId, waypointIndex, mode, waypoints) {
if (!waypoints || waypoints.length === 0) return;
this.clearAddWaypointContext();
const ctx = {
routeId,
waypointIndex: Math.max(0, Math.min(waypointIndex, waypoints.length - 1)),
mode,
waypoints,
mouseCartesian: null
};
const toCartesian = (wp) => Cesium.Cartesian3.fromDegrees(parseFloat(wp.lng), parseFloat(wp.lat), Number(wp.alt) || 5000);
ctx.mouseCartesian = toCartesian(waypoints[ctx.waypointIndex]);
this.addWaypointContext = ctx;
this.$message && this.$message.info(mode === 'before' ? '点击地图在当前位置前插入新航点,右键取消' : '点击地图在当前位置后插入新航点,右键取消');
this.updateAddWaypointPreview();
},
/** 更新“增加航点”预览折线:上一/当前/下一与鼠标位置连线,含盘旋段视觉效果 */
updateAddWaypointPreview() {
const ctx = this.addWaypointContext;
if (!ctx || !this.viewer || !ctx.waypoints || ctx.waypoints.length === 0) return;
const waypoints = ctx.waypoints;
const idx = ctx.waypointIndex;
const toCartesian = (wp) => {
const lon = parseFloat(wp.lng);
const lat = parseFloat(wp.lat);
const alt = Number(wp.alt) || 5000;
return Cesium.Cartesian3.fromDegrees(lon, lat, alt);
};
let positions = [];
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];
}
} 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];
}
}
if (positions.length < 2) return;
if (this.addWaypointPreviewEntity) {
this.viewer.entities.remove(this.addWaypointPreviewEntity);
this.addWaypointPreviewEntity = null;
}
this.addWaypointPreviewEntity = this.viewer.entities.add({
polyline: {
positions: positions,
width: 3,
material: new Cesium.PolylineDashMaterialProperty({
color: Cesium.Color.fromCssColorString('#2E5C3E'),
dashLength: 16
}),
arcType: Cesium.ArcType.NONE
}
});
},
/** 清除“增加航点”模式及预览折线 */
clearAddWaypointContext() {
if (this.addWaypointPreviewEntity) {
this.viewer.entities.remove(this.addWaypointPreviewEntity);
this.addWaypointPreviewEntity = null;
}
this.addWaypointContext = null;
},
toggleRouteLock() {
const ed = this.contextMenu.entityData;
if (!ed || ed.type !== 'route' || ed.routeId == null) {
@ -5391,7 +5566,7 @@ export default {
//
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
let iconSize = 144
let iconColor = '#ffffff'
let iconColor = '#000000'
let platformName = '平台' //
//
@ -5493,7 +5668,7 @@ export default {
const platformEntity = this.viewer.entities.getById(`route-platform-${routeId}`)
if (platformEntity && platformEntity.billboard) {
const size = Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144))
const color = this.editPlatformForm.iconColor || '#ffffff'
const color = this.editPlatformForm.iconColor || '#000000'
platformEntity.billboard.width = size
platformEntity.billboard.height = size
platformEntity.billboard.color = Cesium.Color.fromCssColorString(color)
@ -5504,7 +5679,7 @@ export default {
labelFontSize: fontSize,
labelFontColor: fontColor,
platformSize: Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144)),
platformColor: this.editPlatformForm.iconColor || '#ffffff'
platformColor: this.editPlatformForm.iconColor || '#000000'
});
// Redis
@ -5519,7 +5694,7 @@ export default {
labelFontSize: fontSize,
labelFontColor: fontColor,
platformSize: Math.max(48, Math.min(256, Number(this.editPlatformForm.iconSize) || 144)),
platformColor: this.editPlatformForm.iconColor || '#ffffff'
platformColor: this.editPlatformForm.iconColor || '#000000'
};
savePlatformStyle(styleData).then(() => {
// console.log("");
@ -5660,7 +5835,7 @@ export default {
labelFontSize: style.labelFontSize,
labelFontColor: style.labelFontColor,
platformSize: style.platformSize != null ? style.platformSize : 144,
platformColor: style.platformColor != null ? style.platformColor : '#ffffff',
platformColor: style.platformColor != null ? style.platformColor : '#000000',
powerZoneRadius: style.powerZoneRadius,
powerZoneColor: style.powerZoneColor
});
@ -5729,7 +5904,7 @@ export default {
//
if (entity.billboard) {
const size = Math.max(48, Math.min(256, Number(style.platformSize) || 144));
const color = style.platformColor || '#ffffff';
const color = style.platformColor || '#000000';
entity.billboard.width = size;
entity.billboard.height = size;
entity.billboard.color = Cesium.Color.fromCssColorString(color);

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

@ -24,6 +24,8 @@
@open-route-dialog="handleOpenRouteEdit"
@copy-route="handleCopyRoute"
@route-copy-placed="handleRouteCopyPlaced"
@add-waypoint-at="handleAddWaypointAt"
@add-waypoint-placed="handleAddWaypointPlaced"
@waypoint-position-changed="handleWaypointPositionChanged"
@scale-click="handleScaleClick"
@platform-icon-updated="onPlatformIconUpdated"
@ -714,8 +716,21 @@ export default {
handleBottomPanelVisible(visible) {
this.bottomPanelVisible = visible
},
//
async handleOpenWaypointEdit(wpId, routeId) {
// wpId waypointIndex waypointIndex
async handleOpenWaypointEdit(wpId, routeId, waypointIndex) {
if (waypointIndex != null && (wpId == null || wpId === undefined)) {
try {
const response = await getRoutes(routeId);
if (response.code === 200 && response.data && response.data.waypoints) {
const wp = response.data.waypoints[waypointIndex];
if (wp) wpId = wp.id;
}
} catch (e) {}
}
if (wpId == null || wpId === undefined) {
this.$message.info('未找到该航点');
return;
}
console.log(`>>> [父组件接收] 航点 ID: ${wpId}, 所属航线 ID: ${routeId}`);
// 线
if (this.selectedRouteId != routeId) {
@ -812,6 +827,129 @@ export default {
}
},
/** 右键航点“向前/向后增加航点”:进入放置模式,传入 waypoints 给地图预览 */
handleAddWaypointAt({ routeId, waypointIndex, mode }) {
if (this.routeLocked[routeId]) {
this.$message.info('该航线已上锁,请先解锁');
return;
}
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || route.waypoints.length === 0) {
this.$message.warning('航线无航点数据');
return;
}
if (!this.$refs.cesiumMap || typeof this.$refs.cesiumMap.startAddWaypointAt !== 'function') return;
this.$refs.cesiumMap.startAddWaypointAt(routeId, waypointIndex, mode, route.waypoints);
},
/** 地图放置新航点后:调用 addWaypoints 插入,再按插入位置重排 seq 并重绘。向后添加时:当前第 K 个,新航点为第 K+1 个,原第 K+1 个及以后依次后移。 */
async handleAddWaypointPlaced({ routeId, waypointIndex, mode, position }) {
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints) {
this.$message.warning('航线不存在或无航点');
return;
}
const waypoints = route.waypoints;
const refWp = waypoints[waypointIndex];
// insertIndex = waypointIndex+1 (waypointIndex+2) waypointIndex+2 waypointIndex+3
const insertIndex = mode === 'before' ? waypointIndex : waypointIndex + 1;
const prevWp = insertIndex > 0 ? waypoints[insertIndex - 1] : null;
const startTime = prevWp && prevWp.startTime ? prevWp.startTime : 'K+00:00:00';
const count = waypoints.length + 1;
const newName = `WP${insertIndex + 1}`;
try {
const addRes = await addWaypoints({
routeId,
name: newName,
seq: insertIndex + 1,
lat: position.lat,
lng: position.lng,
alt: position.alt,
speed: (refWp && refWp.speed != null) ? refWp.speed : 800,
startTime,
turnAngle: (refWp && refWp.turnAngle != null) ? refWp.turnAngle : (insertIndex === 0 || insertIndex === count - 1 ? 0 : 45)
});
await this.getList();
let updated = this.routes.find(r => r.id === routeId);
if (!updated || !updated.waypoints || updated.waypoints.length !== count) {
this.$message.warning('添加航点后数据未刷新');
return;
}
const list = updated.waypoints;
const prevIds = new Set(waypoints.map(w => w.id));
const newWp = (addRes && addRes.data && addRes.data.id != null && list.find(w => w.id === addRes.data.id))
? list.find(w => w.id === addRes.data.id)
: list.find(w => !prevIds.has(w.id)) || list[list.length - 1];
if (!newWp) {
this.$message.warning('未找到新插入的航点');
return;
}
const others = list.filter(w => w.id !== newWp.id);
others.sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0));
const reordered = [...others.slice(0, insertIndex), newWp, ...others.slice(insertIndex)];
const routeInListFirst = this.routes.find(r => r.id === routeId);
if (routeInListFirst) routeInListFirst.waypoints = reordered;
if (this.selectedRouteId === routeId) this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints: reordered };
if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
this.$refs.cesiumMap.removeRouteById(routeId);
this.$refs.cesiumMap.renderRouteWaypoints(reordered, routeId, routeInListFirst?.platformId, routeInListFirst?.platform, this.parseRouteStyle(routeInListFirst?.attributes || route?.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
const isHold = (w) => (w.pointType || w.point_type) === 'hold_circle' || (w.pointType || w.point_type) === 'hold_ellipse';
for (let i = 0; i < reordered.length; i++) {
const wp = reordered[i];
const newSeq = i + 1;
const isNewlyInserted = wp.id === newWp.id;
const nameToUse = isNewlyInserted ? (isHold(wp) ? `HOLD${newSeq}` : `WP${newSeq}`) : (wp.name || (isHold(wp) ? `HOLD${newSeq}` : `WP${newSeq}`));
await updateWaypoints({
id: wp.id,
routeId,
name: nameToUse,
seq: newSeq,
lat: wp.lat,
lng: wp.lng,
alt: wp.alt,
speed: wp.speed,
startTime: wp.startTime,
turnAngle: wp.turnAngle,
...(wp.pointType != null && { pointType: wp.pointType }),
...(wp.holdParams != null && { holdParams: wp.holdParams }),
...(wp.labelFontSize != null && { labelFontSize: wp.labelFontSize }),
...(wp.labelColor != null && { labelColor: wp.labelColor })
});
}
await this.getList();
updated = this.routes.find(r => r.id === routeId);
if (!updated || !updated.waypoints) {
this.$message.warning('刷新后未拿到航线航点');
return;
}
const sortedWaypoints = updated.waypoints.slice().sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0));
updated.waypoints = sortedWaypoints;
const routeInList = this.routes.find(r => r.id === routeId);
if (routeInList) routeInList.waypoints = sortedWaypoints;
if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
if (roomId && updated.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId, platformId: updated.platformId });
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
} catch (_) {}
}
this.$refs.cesiumMap.removeRouteById(routeId);
this.$refs.cesiumMap.renderRouteWaypoints(sortedWaypoints, routeId, updated.platformId, updated.platform, this.parseRouteStyle(updated.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
if (this.selectedRouteId === routeId) {
this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints: sortedWaypoints };
}
this.$message.success('已添加航点');
} catch (e) {
this.$message.error(e.msg || e.message || '添加航点失败');
console.error(e);
}
},
/** 右键「复制航线」:拉取航点后进入复制预览,左键放置后弹窗保存 */
async handleCopyRoute(routeId) {
try {
@ -1033,7 +1171,7 @@ export default {
this.selectedRoute = route;
this.showRouteDialog = true;
},
// 线
// 线
async updateRoute(updatedRoute) {
const index = this.routes.findIndex(r => r.id === updatedRoute.id);
if (index === -1) return;
@ -1053,6 +1191,41 @@ export default {
platform: updatedRoute.platform,
attributes: updatedRoute.attributes
};
// 线
if (updatedRoute.waypoints && updatedRoute.waypoints.length > 0) {
for (const wp of updatedRoute.waypoints) {
const payload = {
id: wp.id,
routeId: wp.routeId,
name: wp.name,
seq: wp.seq,
lat: wp.lat,
lng: wp.lng,
alt: wp.alt,
speed: wp.speed,
startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00',
turnAngle: wp.turnAngle
};
if (wp.pointType != null) payload.pointType = wp.pointType;
if (wp.holdParams != null) payload.holdParams = wp.holdParams;
if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
if (wp.labelColor != null) payload.labelColor = wp.labelColor;
if (payload.turnAngle > 0 && this.$refs.cesiumMap) {
payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload);
} else {
payload.turnRadius = 0;
}
await updateWaypoints(payload);
}
const mergedWaypoints = (newRouteData.waypoints || []).map((oldWp) => {
const fromDialog = updatedRoute.waypoints.find((w) => w.id === oldWp.id);
return fromDialog ? { ...oldWp, ...fromDialog } : oldWp;
});
newRouteData.waypoints = mergedWaypoints;
if (this.selectedRouteDetails && this.selectedRouteId === updatedRoute.id) {
this.selectedRouteDetails.waypoints = mergedWaypoints;
}
}
this.routes.splice(index, 1, newRouteData);
if (this.selectedRouteDetails && this.selectedRouteId === updatedRoute.id) {
this.selectedRouteDetails.name = updatedRoute.name;
@ -1060,7 +1233,7 @@ export default {
this.selectedRouteDetails.platform = updatedRoute.platform;
this.selectedRouteDetails.attributes = updatedRoute.attributes;
}
this.$message.success('航线更新成功');
this.$message.success(updatedRoute.waypoints && updatedRoute.waypoints.length > 0 ? '航线与航点已保存' : '航线更新成功');
const routeStyle = updatedRoute.routeStyle || this.parseRouteStyle(updatedRoute.attributes);
if (this.$refs.cesiumMap && this.activeRouteIds.includes(updatedRoute.id)) {
const route = this.routes.find(r => r.id === updatedRoute.id);
@ -1074,7 +1247,6 @@ export default {
}
this.$refs.cesiumMap.removeRouteById(updatedRoute.id);
this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, updatedRoute.id, route.platformId, route.platform, routeStyle);
//
this.$nextTick(() => this.updateDeductionPositions());
}
}

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

@ -2,7 +2,7 @@
<el-dialog
title="编辑航线"
:visible.sync="visible"
width="560px"
:width="activeTab === 'waypoints' ? '920px' : '560px'"
:close-on-click-modal="false"
append-to-body
custom-class="blue-dialog route-edit-dialog"
@ -10,6 +10,7 @@
<div class="route-edit-tab-bar">
<button type="button" class="tab-bar-item" :class="{ active: activeTab === 'basic' }" @click="activeTab = 'basic'">基础</button>
<button type="button" class="tab-bar-item" :class="{ active: activeTab === 'platform' }" @click="activeTab = 'platform'">平台</button>
<button type="button" class="tab-bar-item" :class="{ active: activeTab === 'waypoints' }" @click="activeTab = 'waypoints'">航点</button>
</div>
<div v-show="activeTab === 'basic'" class="tab-pane-wrap">
<div class="tab-pane-body basic-tab-content">
@ -190,6 +191,91 @@
</template>
</div>
</div>
<div v-show="activeTab === 'waypoints'" class="tab-pane-wrap">
<div class="tab-pane-body waypoints-tab-content">
<div v-if="!waypointsTableData.length" class="empty-tip">该航线暂无航点数据</div>
<template v-else>
<div class="waypoints-table-actions">
<template v-if="!waypointsEditMode">
<el-button type="primary" size="mini" class="blue-btn" @click="waypointsEditMode = true"> </el-button>
</template>
<template v-else>
<el-button type="primary" size="mini" class="blue-btn" @click="confirmWaypointsEdit"> </el-button>
<el-button size="mini" @click="cancelWaypointsEdit"> </el-button>
</template>
</div>
<div class="waypoints-table-wrap">
<el-table :data="waypointsTableData" border size="small" max-height="340" class="waypoints-table">
<el-table-column type="index" label="序号" width="52" align="center" />
<el-table-column label="名称" min-width="80">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ scope.row.name || '' }}</span>
<el-input v-else v-model="scope.row.name" size="mini" placeholder="名称" />
</template>
</el-table-column>
<el-table-column label="经度" min-width="95">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.lng) }}</span>
<el-input v-else v-model.number="scope.row.lng" size="mini" placeholder="经度" />
</template>
</el-table-column>
<el-table-column label="纬度" min-width="95">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.lat) }}</span>
<el-input v-else v-model.number="scope.row.lat" size="mini" placeholder="纬度" />
</template>
</el-table-column>
<el-table-column label="高度(m)" min-width="88">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.alt) }}</span>
<el-input v-else v-model.number="scope.row.alt" size="mini" placeholder="高度" />
</template>
</el-table-column>
<el-table-column label="速度" min-width="80">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.speed) }}</span>
<el-input v-else v-model.number="scope.row.speed" size="mini" placeholder="速度" />
</template>
</el-table-column>
<el-table-column label="转弯坡度(°)" min-width="100">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.turnAngle) }}</span>
<el-input v-else v-model.number="scope.row.turnAngle" size="mini" placeholder="0" />
</template>
</el-table-column>
<el-table-column label="盘旋" width="72" align="center">
<template slot-scope="scope">
<el-tag :type="isHoldRow(scope.row) ? 'warning' : 'info'" size="mini">
{{ isHoldRow(scope.row) ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="相对K时(分)" min-width="110">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.minutesFromK) }}</span>
<el-input v-else v-model.number="scope.row.minutesFromK" size="mini" placeholder="0" />
</template>
</el-table-column>
<el-table-column label="字号" width="70">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.labelFontSize) }}</span>
<el-input v-else v-model.number="scope.row.labelFontSize" size="mini" placeholder="14" />
</template>
</el-table-column>
<el-table-column label="颜色" width="100">
<template slot-scope="scope">
<span v-if="!waypointsEditMode" class="color-text">{{ scope.row.labelColor || '' }}</span>
<div v-else class="table-color-wrap">
<el-color-picker v-model="scope.row.labelColor" size="mini" :predefine="presetColors" />
<span class="color-value">{{ scope.row.labelColor }}</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="visible = false"> </el-button>
@ -217,6 +303,8 @@ export default {
groundPlatforms: [],
selectedPlatformId: null,
selectedPlatform: null,
waypointsTableData: [],
waypointsEditMode: false,
form: {
id: '',
name: ''
@ -254,6 +342,7 @@ export default {
this.selectedPlatformId = val.platformId || null
this.selectedPlatform = val.platform ? { ...val.platform } : null
this.parseStyleFromRoute(val)
this.syncWaypointsTableData(val.waypoints || [])
}
},
immediate: true,
@ -352,6 +441,75 @@ export default {
attrs.lineStyle = { ...this.styleForm.line }
return JSON.stringify(attrs)
},
/** 将 startTime 字符串(如 K+00:40:00)转为相对 K 的分钟数 */
startTimeToMinutes(s) {
if (!s || typeof s !== 'string') return 0
const m = s.match(/K([+-])(\d{2}):(\d{2})/)
if (!m) return 0
const sign = m[1] === '+' ? 1 : -1
const h = parseInt(m[2], 10)
const min = parseInt(m[3], 10)
return sign * (h * 60 + min)
},
/** 将相对 K 的分钟数转为 startTime 字符串 */
minutesToStartTime(m) {
const num = Number(m)
if (isNaN(num)) return 'K+00:00:00'
if (num >= 0) {
const h = Math.floor(num / 60)
const min = num % 60
return `K+${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:00`
}
const abs = Math.abs(num)
const h = Math.floor(abs / 60)
const min = abs % 60
return `K-${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:00`
},
isHoldRow(row) {
const t = (row && (row.pointType || row.point_type)) || 'normal'
return t === 'hold_circle' || t === 'hold_ellipse'
},
syncWaypointsTableData(waypoints) {
this.waypointsTableData = (waypoints || []).map(wp => ({
...wp,
minutesFromK: this.startTimeToMinutes(wp.startTime),
labelFontSize: wp.labelFontSize != null ? Number(wp.labelFontSize) : 14,
labelColor: wp.labelColor || '#333333'
}))
this.waypointsEditMode = false
},
formatNum(val) {
if (val === undefined || val === null || val === '') return '—'
const n = Number(val)
return isNaN(n) ? String(val) : n
},
confirmWaypointsEdit() {
this.waypointsEditMode = false
this.$message.success('表格已保存,点击下方「确定」将提交航线与航点')
},
cancelWaypointsEdit() {
this.syncWaypointsTableData(this.route.waypoints || [])
this.$message.info('已取消编辑')
},
getWaypointsPayloadForSave() {
const routeId = this.form.id
return this.waypointsTableData.map(row => ({
id: row.id,
routeId,
seq: row.seq,
name: row.name,
lng: row.lng,
lat: row.lat,
alt: row.alt,
speed: row.speed,
startTime: this.minutesToStartTime(row.minutesFromK),
turnAngle: row.turnAngle != null ? row.turnAngle : 0,
pointType: row.pointType || null,
holdParams: row.holdParams || null,
labelFontSize: row.labelFontSize != null ? row.labelFontSize : 14,
labelColor: row.labelColor || '#333333'
}))
},
handleSave() {
const payload = {
...this.form,
@ -363,6 +521,9 @@ export default {
line: { ...this.styleForm.line }
}
}
if (this.waypointsTableData.length > 0) {
payload.waypoints = this.getWaypointsPayloadForSave()
}
this.$emit('save', payload)
this.visible = false
}
@ -662,6 +823,52 @@ export default {
font-size: 12px;
}
/* 航点表格页 */
.waypoints-tab-content {
min-height: 380px;
max-height: 380px;
overflow: hidden;
padding: 0 16px;
background: #fff;
}
.waypoints-table-actions {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.waypoints-table-wrap {
background: #fff;
border-radius: 8px;
padding: 12px 0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.05);
}
.waypoints-table {
width: 100%;
}
.waypoints-table .el-input-number {
width: 100%;
}
.waypoints-table .el-input-number .el-input__inner {
text-align: left;
}
.table-color-wrap {
display: inline-flex;
align-items: center;
gap: 6px;
}
.table-color-wrap .color-value {
font-size: 11px;
color: #909399;
max-width: 48px;
overflow: hidden;
text-overflow: ellipsis;
}
.waypoints-table .color-text {
font-size: 12px;
color: #606266;
}
.blue-btn {
background: #165dff;
border-color: #165dff;

Loading…
Cancel
Save