Browse Source

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

# Conflicts:
#	ruoyi-ui/src/views/childRoom/index.vue
mh
cuitw 4 weeks ago
parent
commit
53f5b0fbaa
  1. 65
      ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java
  2. 62
      ruoyi-system/src/main/java/com/ruoyi/system/domain/WaypointDisplayStyle.java
  3. 59
      ruoyi-system/src/main/java/com/ruoyi/system/typehandler/WaypointDisplayStyleTypeHandler.java
  4. 14
      ruoyi-system/src/main/resources/mapper/system/RouteWaypointsMapper.xml
  5. 27
      ruoyi-system/src/main/resources/mapper/system/route_waypoints_display_style_json.sql
  6. 8
      ruoyi-ui/src/layout/components/TagsView/index.vue
  7. 19
      ruoyi-ui/src/views/cesiumMap/ContextMenu.vue
  8. 994
      ruoyi-ui/src/views/cesiumMap/index.vue
  9. 47
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  10. 243
      ruoyi-ui/src/views/childRoom/index.vue
  11. 67
      ruoyi-ui/src/views/dialogs/RouteEditDialog.vue
  12. 104
      ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue
  13. 15
      ruoyi-ui/src/views/selectRoom/index.vue

65
ruoyi-system/src/main/java/com/ruoyi/system/domain/RouteWaypoints.java

@ -1,6 +1,7 @@
package com.ruoyi.system.domain;
import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@ -63,11 +64,8 @@ public class RouteWaypoints extends BaseEntity
@Excel(name = "盘旋参数")
private String holdParams;
/** 航点标签文字大小(px),用于地图显示 */
private Integer labelFontSize;
/** 航点标签文字颜色(如 #333333),用于地图显示 */
private String labelColor;
/** 航点显示样式 JSON:字号、文字颜色、标记大小与颜色等,对应表 display_style 列 */
private WaypointDisplayStyle displayStyle;
public void setId(Long id)
{
@ -185,20 +183,66 @@ public class RouteWaypoints extends BaseEntity
return holdParams;
}
@JsonIgnore
public WaypointDisplayStyle getDisplayStyle() {
return displayStyle;
}
@JsonIgnore
public void setDisplayStyle(WaypointDisplayStyle displayStyle) {
this.displayStyle = displayStyle;
}
private WaypointDisplayStyle getOrCreateDisplayStyle() {
if (displayStyle == null) {
displayStyle = new WaypointDisplayStyle();
}
return displayStyle;
}
/** API/前端:地图上航点名称字号,默认 16 */
public void setLabelFontSize(Integer labelFontSize) {
this.labelFontSize = labelFontSize;
getOrCreateDisplayStyle().setLabelFontSize(labelFontSize);
}
public Integer getLabelFontSize() {
return labelFontSize;
return displayStyle != null && displayStyle.getLabelFontSize() != null ? displayStyle.getLabelFontSize() : 16;
}
/** API/前端:地图上航点名称颜色,默认 #000000 */
public void setLabelColor(String labelColor) {
this.labelColor = labelColor;
getOrCreateDisplayStyle().setLabelColor(labelColor);
}
public String getLabelColor() {
return labelColor;
return displayStyle != null && displayStyle.getLabelColor() != null ? displayStyle.getLabelColor() : "#000000";
}
/** API/前端:航点圆点填充色,默认 #ffffff */
public void setColor(String color) {
getOrCreateDisplayStyle().setColor(color);
}
public String getColor() {
return displayStyle != null && displayStyle.getColor() != null ? displayStyle.getColor() : "#ffffff";
}
/** API/前端:航点圆点直径(像素),默认 12 */
public void setPixelSize(Integer pixelSize) {
getOrCreateDisplayStyle().setPixelSize(pixelSize);
}
public Integer getPixelSize() {
return displayStyle != null && displayStyle.getPixelSize() != null ? displayStyle.getPixelSize() : 12;
}
/** API/前端:航点圆点边框颜色,默认 #000000 */
public void setOutlineColor(String outlineColor) {
getOrCreateDisplayStyle().setOutlineColor(outlineColor);
}
public String getOutlineColor() {
return displayStyle != null && displayStyle.getOutlineColor() != null ? displayStyle.getOutlineColor() : "#000000";
}
@Override
@ -216,8 +260,7 @@ public class RouteWaypoints extends BaseEntity
.append("turnAngle", getTurnAngle())
.append("pointType", getPointType())
.append("holdParams", getHoldParams())
.append("labelFontSize", getLabelFontSize())
.append("labelColor", getLabelColor())
.append("displayStyle", displayStyle)
.toString();
}

62
ruoyi-system/src/main/java/com/ruoyi/system/domain/WaypointDisplayStyle.java

@ -0,0 +1,62 @@
package com.ruoyi.system.domain;
import java.io.Serializable;
/**
* 航点显示样式地图上名称字号/颜色标记大小/颜色对应表字段 display_style JSON 结构
*/
public class WaypointDisplayStyle implements Serializable {
private static final long serialVersionUID = 1L;
/** 航点名称在地图上的字号,默认 16 */
private Integer labelFontSize;
/** 航点名称在地图上的颜色,默认 #000000 */
private String labelColor;
/** 航点圆点填充色,默认 #ffffff */
private String color;
/** 航点圆点直径(像素),默认 12 */
private Integer pixelSize;
/** 航点圆点边框颜色,默认 #000000 */
private String outlineColor;
public Integer getLabelFontSize() {
return labelFontSize;
}
public void setLabelFontSize(Integer labelFontSize) {
this.labelFontSize = labelFontSize;
}
public String getLabelColor() {
return labelColor;
}
public void setLabelColor(String labelColor) {
this.labelColor = labelColor;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Integer getPixelSize() {
return pixelSize;
}
public void setPixelSize(Integer pixelSize) {
this.pixelSize = pixelSize;
}
public String getOutlineColor() {
return outlineColor;
}
public void setOutlineColor(String outlineColor) {
this.outlineColor = outlineColor;
}
}

59
ruoyi-system/src/main/java/com/ruoyi/system/typehandler/WaypointDisplayStyleTypeHandler.java

@ -0,0 +1,59 @@
package com.ruoyi.system.typehandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.system.domain.WaypointDisplayStyle;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MyBatis 类型处理器display_style TEXT/JSON WaypointDisplayStyle 互转
*/
@MappedTypes(WaypointDisplayStyle.class)
public class WaypointDisplayStyleTypeHandler extends BaseTypeHandler<WaypointDisplayStyle> {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, WaypointDisplayStyle parameter, JdbcType jdbcType) throws SQLException {
try {
ps.setString(i, MAPPER.writeValueAsString(parameter));
} catch (Exception e) {
throw new SQLException("WaypointDisplayStyle serialize error", e);
}
}
@Override
public WaypointDisplayStyle getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parse(json);
}
@Override
public WaypointDisplayStyle getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parse(json);
}
@Override
public WaypointDisplayStyle getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parse(json);
}
private static WaypointDisplayStyle parse(String json) {
if (json == null || json.trim().isEmpty()) {
return null;
}
try {
return MAPPER.readValue(json, WaypointDisplayStyle.class);
} catch (Exception e) {
return null;
}
}
}

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

@ -17,12 +17,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="turnAngle" column="turn_angle" />
<result property="pointType" column="point_type" />
<result property="holdParams" column="hold_params" />
<result property="labelFontSize" column="label_font_size" />
<result property="labelColor" column="label_color" />
<result property="displayStyle" column="display_style" typeHandler="com.ruoyi.system.typehandler.WaypointDisplayStyleTypeHandler" />
</resultMap>
<sql id="selectRouteWaypointsVo">
select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle, point_type, hold_params, label_font_size, label_color from route_waypoints
select id, route_id, name, seq, lat, lng, alt, speed, start_time, turn_angle, point_type, hold_params, display_style from route_waypoints
</sql>
<select id="selectRouteWaypointsList" parameterType="RouteWaypoints" resultMap="RouteWaypointsResult">
@ -66,8 +65,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="turnAngle != null">turn_angle,</if>
<if test="pointType != null and pointType != ''">point_type,</if>
<if test="holdParams != null">hold_params,</if>
<if test="labelFontSize != null">label_font_size,</if>
<if test="labelColor != null">label_color,</if>
<if test="displayStyle != null">display_style,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="routeId != null">#{routeId},</if>
@ -81,8 +79,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="turnAngle != null">#{turnAngle},</if>
<if test="pointType != null and pointType != ''">#{pointType},</if>
<if test="holdParams != null">#{holdParams},</if>
<if test="labelFontSize != null">#{labelFontSize},</if>
<if test="labelColor != null">#{labelColor},</if>
<if test="displayStyle != null">#{displayStyle, typeHandler=com.ruoyi.system.typehandler.WaypointDisplayStyleTypeHandler},</if>
</trim>
</insert>
@ -100,8 +97,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="turnAngle != null">turn_angle = #{turnAngle},</if>
<if test="pointType != null">point_type = #{pointType},</if>
<if test="holdParams != null">hold_params = #{holdParams},</if>
<if test="labelFontSize != null">label_font_size = #{labelFontSize},</if>
<if test="labelColor != null">label_color = #{labelColor},</if>
<if test="displayStyle != null">display_style = #{displayStyle, typeHandler=com.ruoyi.system.typehandler.WaypointDisplayStyleTypeHandler},</if>
</trim>
where id = #{id}
</update>

27
ruoyi-system/src/main/resources/mapper/system/route_waypoints_display_style_json.sql

@ -0,0 +1,27 @@
-- 将航点显示相关字段合并为单列 JSON:display_style
-- 执行前请备份。若表带 schema(如 ry.route_waypoints)请自行替换表名。
-- 1. 新增 JSON 列
ALTER TABLE route_waypoints
ADD COLUMN display_style JSON DEFAULT NULL COMMENT '航点显示样式JSON: labelFontSize,labelColor,color,pixelSize,outlineColor';
-- 2. 从旧列回填(仅存在 label_font_size / label_color 时)
UPDATE route_waypoints
SET display_style = JSON_OBJECT(
'labelFontSize', COALESCE(label_font_size, 16),
'labelColor', COALESCE(label_color, '#000000'),
'color', '#ffffff',
'pixelSize', 12,
'outlineColor', '#000000'
)
WHERE display_style IS NULL;
-- 3. 删除旧列(若你曾加过 color/pixel_size/outline_color 三列,也一并删除)
ALTER TABLE route_waypoints
DROP COLUMN label_font_size,
DROP COLUMN label_color;
-- 若存在以下列则逐条执行(没有则跳过):
-- ALTER TABLE route_waypoints DROP COLUMN color;
-- ALTER TABLE route_waypoints DROP COLUMN pixel_size;
-- ALTER TABLE route_waypoints DROP COLUMN outline_color;

8
ruoyi-ui/src/layout/components/TagsView/index.vue

@ -223,7 +223,13 @@ export default {
this.left = left
}
this.top = e.clientY
const padding = 12
const menuHeight = 250
const winH = window.innerHeight
let top = e.clientY
if (top + menuHeight + padding > winH) top = winH - menuHeight - padding
if (top < padding) top = padding
this.top = top
this.visible = true
this.selectedTag = tag
},

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

@ -445,10 +445,25 @@ export default {
}
},
computed: {
/** 根据视口边界修正菜单位置,避免菜单在屏幕底部或右侧被截断 */
adjustedPosition() {
const padding = 12
const menuMaxWidth = 220
const menuMaxHeight = 560
const winW = typeof window !== 'undefined' ? window.innerWidth : 1920
const winH = typeof window !== 'undefined' ? window.innerHeight : 1080
let x = this.position.x
let y = this.position.y
if (x + menuMaxWidth + padding > winW) x = winW - menuMaxWidth - padding
if (x < padding) x = padding
if (y + menuMaxHeight + padding > winH) y = winH - menuMaxHeight - padding
if (y < padding) y = padding
return { x, y }
},
positionStyle() {
return {
left: this.position.x + 'px',
top: this.position.y + 'px'
left: this.adjustedPosition.x + 'px',
top: this.adjustedPosition.y + 'px'
}
},
isRouteLocked() {

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

File diff suppressed because it is too large

47
ruoyi-ui/src/views/childRoom/TopHeader.vue

@ -216,6 +216,22 @@
</div>
<div class="header-right">
<!-- 地图拖动开关点击小手图标后才允许拖动地图 -->
<div
class="map-drag-toggle"
:class="{ active: mapDragEnabled }"
:title="mapDragEnabled ? '已开启拖动,点击可关闭' : '点击开启地图拖动'"
@click="$emit('toggle-map-drag')"
>
<svg class="hand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="20" height="20">
<path d="M18 11.5V9a2 2 0 0 0-2-2 2 2 0 0 0-2 2v1.4" />
<path d="M14 10V8a2 2 0 0 0-2-2 2 2 0 0 0-2 2v2" />
<path d="M10 9.9V9a2 2 0 0 0-2-2 2 2 0 0 0-2 2v5" />
<path d="M6 14a2 2 0 0 0-2-2 2 2 0 0 0-2 2" />
<path d="M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-4a8 8 0 0 1-8-8 2 2 0 1 1 4 0" />
</svg>
<span class="map-drag-label">{{ mapDragEnabled ? '拖动开' : '拖动' }}</span>
</div>
<!-- 作战信息区域 -->
<div class="combat-info-group">
<div
@ -350,6 +366,11 @@ export default {
scaleDenominator: 1000,
unit: 'm'
})
},
/** 是否允许地图拖动(与小手图标联动) */
mapDragEnabled: {
type: Boolean,
default: false
}
},
data() {
@ -877,6 +898,32 @@ export default {
gap: 20px;
}
.map-drag-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
color: #909399;
transition: color 0.2s, background 0.2s;
}
.map-drag-toggle:hover {
color: #409eff;
background: rgba(64, 158, 255, 0.08);
}
.map-drag-toggle.active {
color: #409eff;
background: rgba(64, 158, 255, 0.12);
}
.map-drag-toggle .hand-icon {
flex-shrink: 0;
}
.map-drag-toggle .map-drag-label {
font-size: 13px;
white-space: nowrap;
}
.combat-info-group {
display: flex;
align-items: center;

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

@ -14,6 +14,7 @@
:scaleConfig="scaleConfig"
:coordinateFormat="coordinateFormat"
:bottomPanelVisible="bottomPanelVisible"
:map-drag-enabled="mapDragEnabled"
:hide-map-info="sixStepsOverlayVisible"
:route-locked="routeLocked"
:route-locked-by-other-ids="routeLockedByOtherRouteIds"
@ -99,7 +100,9 @@
:user-avatar="userAvatar"
:is-icon-edit-mode="isIconEditMode"
:current-scale-config="scaleConfig"
:map-drag-enabled="mapDragEnabled"
@select-nav="selectTopNav"
@toggle-map-drag="mapDragEnabled = !mapDragEnabled"
@set-k-time="openKTimeSetDialog"
@save-plan="savePlan"
@import-plan-file="importPlanFile"
@ -471,6 +474,8 @@ export default {
return {
drawDom:false,
airspaceDrawDom:false,
/** 是否允许地图拖动(由顶部小手图标切换,默认关闭) */
mapDragEnabled: false,
// 线
showOnlineMembers: false,
//
@ -1507,6 +1512,7 @@ export default {
} catch (_) {}
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
} catch (_) {}
} else {
@ -1681,11 +1687,11 @@ export default {
//
this.showPlatformDialog = false;
},
/** 新建航线时写入数据库的默认样式(与地图默认显示一致:墨绿色实线线宽3) */
/** 新建航线时写入数据库的默认样式(简约风格:灰蓝配色、细描边) */
getDefaultRouteAttributes() {
const defaultAttrs = {
waypointStyle: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 },
lineStyle: { style: 'solid', width: 3, color: '#2E5C3E', gapColor: '#000000', dashLength: 20 }
waypointStyle: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
lineStyle: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
};
return JSON.stringify(defaultAttrs);
},
@ -1769,6 +1775,9 @@ export default {
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 (wp.pixelSize != null) payload.pixelSize = wp.pixelSize;
if (wp.color != null) payload.color = wp.color;
if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor;
if (payload.turnAngle > 0 && this.$refs.cesiumMap) {
payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload);
} else {
@ -2143,9 +2152,14 @@ export default {
if (updatedWaypoint.holdParams != null) payload.holdParams = updatedWaypoint.holdParams;
if (updatedWaypoint.labelFontSize != null) payload.labelFontSize = updatedWaypoint.labelFontSize;
if (updatedWaypoint.labelColor != null) payload.labelColor = updatedWaypoint.labelColor;
if (updatedWaypoint.pixelSize != null) payload.pixelSize = updatedWaypoint.pixelSize;
if (updatedWaypoint.color != null) payload.color = updatedWaypoint.color;
if (updatedWaypoint.outlineColor != null) payload.outlineColor = updatedWaypoint.outlineColor;
const response = await updateWaypoints(payload);
if (response.code === 200) {
const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id);
const roomId = this.currentRoomId;
const sd = this.selectedRouteDetails;
const index = sd.waypoints.findIndex(p => p.id === updatedWaypoint.id);
if (index !== -1) {
// payload startTime
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint, ...payload });
@ -2156,8 +2170,6 @@ export default {
if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged);
}
if (this.$refs.cesiumMap) {
const roomId = this.currentRoomId;
const sd = this.selectedRouteDetails;
if (roomId && sd.platformId) {
try {
const styleRes = await getPlatformStyle({ roomId, routeId: sd.id, platformId: sd.platformId });
@ -3217,8 +3229,9 @@ export default {
/**
* 按速度与计划时间构建航线时间轴含飞行段盘旋段与提前到达则等待的等待段
* pathData 可选{ path, segmentEndIndices, holdArcRanges } getRoutePathWithSegmentIndices 提供用于输出 hold
* holdRadiusByLegIndex 可选{ [legIndex]: number }为盘旋段指定半径用于推演时落点精准在切点
*/
buildRouteTimeline(waypoints, globalMin, globalMax, pathData) {
buildRouteTimeline(waypoints, globalMin, globalMax, pathData, holdRadiusByLegIndex) {
const warnings = [];
if (!waypoints || waypoints.length === 0) return { segments: [], warnings };
const points = waypoints.map((wp, idx) => ({
@ -3252,6 +3265,7 @@ export default {
const effectiveTime = [points[0].minutes];
const segments = [];
const lateArrivalLegs = []; //
const holdDelayConflicts = []; //
const path = pathData && pathData.path;
const segmentEndIndices = pathData && pathData.segmentEndIndices;
const holdArcRanges = pathData && pathData.holdArcRanges || {};
@ -3267,20 +3281,53 @@ export default {
const speedKmh = points[i].speed || 800;
const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmh);
const arrivalEntry = effectiveTime[i] + travelToEntryMin;
const holdEndTime = points[i + 1].minutes;
const holdEndTime = points[i + 1].minutes; // K+10
const exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : (toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt });
let loopEndIdx = 1;
for (let k = 1; k < Math.min(holdPathSlice.length, 120); k++) {
if (this.segmentDistance(holdPathSlice[0], holdPathSlice[k]) < 80) { loopEndIdx = k; break; }
}
const holdClosedLoopPath = holdPathSlice.slice(0, loopEndIdx + 1);
const holdLoopLength = this.pathSliceDistance(holdClosedLoopPath) || 1;
let exitIdxOnLoop = 0;
let minD = 1e9;
for (let k = 0; k <= loopEndIdx; k++) {
const d = this.segmentDistance(holdPathSlice[k], exitPos);
if (d < minD) { minD = d; exitIdxOnLoop = k; }
}
const holdExitDistanceOnLoop = this.pathSliceDistance(holdPathSlice.slice(0, exitIdxOnLoop + 1));
const speedMpMin = (speedKmh * 1000) / 60;
const requiredDistAtK10 = (holdEndTime - arrivalEntry) * speedMpMin;
let n = Math.ceil((requiredDistAtK10 - holdExitDistanceOnLoop) / holdLoopLength);
if (n < 0 || !Number.isFinite(n)) n = 0;
const segmentEndTime = arrivalEntry + (holdExitDistanceOnLoop + n * holdLoopLength) / speedMpMin;
if (segmentEndTime > holdEndTime) {
const delaySec = Math.round((segmentEndTime - holdEndTime) * 60);
const holdWp = waypoints[i + 1];
warnings.push(`盘旋「${holdWp.name || 'WP' + (i + 2)}」:到设定时间时未在切出点,继续盘旋至切出点,实际切出将延迟 ${delaySec} 秒。`);
holdDelayConflicts.push({
legIndex: i,
holdCenter: holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null,
setExitTime: holdEndTime,
actualExitTime: segmentEndTime,
delayMinutes: segmentEndTime - holdEndTime,
delaySeconds: delaySec,
fromName: waypoints[i].name,
toName: (waypoints[i + 1] && waypoints[i + 1].name) ? waypoints[i + 1].name : `盘旋${i + 2}`
});
}
const distExitToNext = this.pathSliceDistance(toNextSlice);
const travelExitMin = (distExitToNext / 1000) * (60 / speedKmh);
const arrivalNext = holdEndTime + travelExitMin;
const arrivalNext = segmentEndTime + travelExitMin;
effectiveTime[i + 1] = holdEndTime;
if (i + 2 < points.length) effectiveTime[i + 2] = arrivalNext;
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt };
const entryPos = toEntrySlice.length ? toEntrySlice[toEntrySlice.length - 1] : posCur;
const exitPos = holdPathSlice.length ? holdPathSlice[holdPathSlice.length - 1] : entryPos;
const holdDurationMin = holdEndTime - arrivalEntry;
const holdWp = waypoints[i + 1];
const holdParams = this.parseHoldParams(holdWp);
const holdCenter = holdWp ? { lng: parseFloat(holdWp.lng), lat: parseFloat(holdWp.lat), alt: Number(holdWp.alt) || 0 } : null;
const holdRadius = holdParams && holdParams.radius != null ? holdParams.radius : null;
const overrideR = holdRadiusByLegIndex && holdRadiusByLegIndex[i] != null ? holdRadiusByLegIndex[i] : null;
const holdRadius = (overrideR != null && Number.isFinite(overrideR)) ? overrideR : (holdParams && holdParams.radius != null ? holdParams.radius : null);
const holdClockwise = holdParams && holdParams.clockwise !== false;
const holdCircumference = holdRadius != null ? 2 * Math.PI * holdRadius : null;
const holdEntryAngle = holdCenter && entryPos && holdRadius != null
@ -3289,13 +3336,15 @@ export default {
segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice });
segments.push({
startTime: arrivalEntry,
endTime: holdEndTime,
endTime: segmentEndTime,
startPos: entryPos,
endPos: exitPos,
type: 'hold',
legIndex: i,
holdPath: holdPathSlice,
holdDurationMin,
holdClosedLoopPath,
holdLoopLength,
holdExitDistanceOnLoop,
speedKmh: points[i].speed || 800,
holdCenter,
holdRadius,
@ -3303,7 +3352,7 @@ export default {
holdClockwise,
holdEntryAngle
});
segments.push({ startTime: holdEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice });
segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice });
i++;
continue;
}
@ -3349,7 +3398,7 @@ export default {
earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name });
}
}
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs };
return { segments, warnings, earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts };
},
/** 从路径片段中按比例 t(0~1)取点:按弧长比例插值 */
@ -3401,24 +3450,14 @@ export default {
return s.startPos;
}
if (s.type === 'hold' && s.holdPath && s.holdPath.length) {
const durationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = (speedKmh * (durationMin / 60)) * 1000;
if (s.holdCircumference != null && s.holdCircumference > 0 && s.holdCenter && s.holdRadius != null) {
const currentDistM = t * totalHoldDistM;
const distOnLap = currentDistM % s.holdCircumference;
const angleRad = (distOnLap / s.holdCircumference) * (2 * Math.PI);
const signedAngle = s.holdClockwise ? -angleRad : angleRad;
const entryAngle = s.holdEntryAngle != null ? s.holdEntryAngle : 0;
const angle = entryAngle + signedAngle;
return this.positionOnCircle(s.holdCenter.lng, s.holdCenter.lat, s.holdCenter.alt, s.holdRadius, angle);
// K
if (s.holdClosedLoopPath && s.holdClosedLoopPath.length >= 2 && s.holdLoopLength > 0 && s.speedKmh != null) {
const distM = (minutesFromK - s.startTime) * (s.speedKmh * 1000 / 60);
const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength;
const tPath = distOnLoop / s.holdLoopLength;
return this.getPositionAlongPathSlice(s.holdClosedLoopPath, tPath);
}
const holdPathLen = this.pathSliceDistance(s.holdPath);
if (holdPathLen <= 0) return this.getPositionAlongPathSlice(s.holdPath, t);
const currentDistM = t * totalHoldDistM;
const positionOnLap = currentDistM % holdPathLen;
const tLap = holdPathLen > 0 ? positionOnLap / holdPathLen : 0;
return this.getPositionAlongPathSlice(s.holdPath, tLap);
return this.getPositionAlongPathSlice(s.holdPath, t);
}
if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) {
return this.getPositionAlongPathSlice(s.pathSlice, t);
@ -3439,17 +3478,125 @@ export default {
return last.endPos;
},
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;返回 { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment },currentSegment 含 speedKmh 用于标牌 */
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) {
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;若有地图 ref 则沿转弯弧/盘旋弧路径运动;盘旋半径由系统根据 k+10 落点反算,使平滑落在切点。routeId 可选,传入时会把计算半径同步给地图以实时渲染盘旋轨迹与切点进入。返回 { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } */
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax, routeId) {
if (!waypoints || waypoints.length === 0) return { position: null, nextPosition: null, previousPosition: null, warnings: [], earlyArrivalLegs: [], currentSegment: null };
const cesiumMap = this.$refs.cesiumMap;
let pathData = null;
if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(waypoints);
if (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) {
const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints);
if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) {
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
}
}
const { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData);
let { segments, warnings, earlyArrivalLegs } = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData);
const holdRadiusByLegIndex = {};
const holdEllipseParamsByLegIndex = {};
if (cesiumMap && segments && pathData) {
for (let idx = 0; idx < segments.length; idx++) {
const s = segments[idx];
if (s.type !== 'hold' || s.holdCenter == null) continue;
const i = s.legIndex;
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
const prevWp = waypoints[i];
const holdWp = waypoints[i + 1];
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp;
if (!prevWp || !holdWp) continue;
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0);
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0);
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian;
const clockwise = s.holdClockwise !== false;
const isEllipse = (waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse') || s.holdRadius == null;
if (isEllipse && cesiumMap.computeEllipseParamsForDuration) {
const holdParams = this.parseHoldParams(holdWp);
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0;
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500;
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300;
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM, headingDeg, a0, b0);
if (out && out.semiMajor != null && out.semiMinor != null) holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg: headingDeg };
} else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) {
const R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM);
if (R != null && Number.isFinite(R)) holdRadiusByLegIndex[i] = R;
}
}
const hasCircle = Object.keys(holdRadiusByLegIndex).length > 0;
const hasEllipse = Object.keys(holdEllipseParamsByLegIndex).length > 0;
if (hasCircle || hasEllipse) {
let pathData2 = null;
let segments2 = null;
for (let iter = 0; iter < 2; iter++) {
const ret2 = cesiumMap.getRoutePathWithSegmentIndices(waypoints, { holdRadiusByLegIndex, holdEllipseParamsByLegIndex });
if (!ret2.path || ret2.path.length === 0 || !ret2.segmentEndIndices || ret2.segmentEndIndices.length === 0) break;
pathData2 = { path: ret2.path, segmentEndIndices: ret2.segmentEndIndices, holdArcRanges: ret2.holdArcRanges || {} };
const out = this.buildRouteTimeline(waypoints, globalMin, globalMax, pathData2, holdRadiusByLegIndex);
segments2 = out.segments;
let changed = false;
if (hasCircle) {
const nextRadii = {};
for (let idx = 0; idx < segments2.length; idx++) {
const s = segments2[idx];
if (s.type !== 'hold' || s.holdRadius == null || s.holdCenter == null) continue;
const i = s.legIndex;
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
const prevWp = waypoints[i];
const holdWp = waypoints[i + 1];
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp;
if (!prevWp || !holdWp) continue;
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0);
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0);
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian;
const Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM);
if (Rnew != null && Number.isFinite(Rnew)) {
if (holdRadiusByLegIndex[i] == null || Math.abs(Rnew - holdRadiusByLegIndex[i]) > 1) changed = true;
nextRadii[i] = Rnew;
}
}
Object.assign(holdRadiusByLegIndex, nextRadii);
}
if (hasEllipse) {
for (let idx = 0; idx < segments2.length; idx++) {
const s = segments2[idx];
if (s.type !== 'hold' || s.holdRadius != null || s.holdCenter == null) continue;
const i = s.legIndex;
const holdWp = waypoints[i + 1];
const holdParams = this.parseHoldParams(holdWp);
const holdDurationMin = s.holdDurationMin != null ? s.holdDurationMin : (s.endTime - s.startTime);
const speedKmh = s.speedKmh != null ? s.speedKmh : 800;
const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
const prevWp = waypoints[i];
const nextWp = (i + 2) < waypoints.length ? waypoints[i + 2] : holdWp;
if (!prevWp || !holdWp || !cesiumMap.computeEllipseParamsForDuration) continue;
const centerCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(holdWp.lng), parseFloat(holdWp.lat), Number(holdWp.alt) || 0);
const prevCartesian = Cesium.Cartesian3.fromDegrees(parseFloat(prevWp.lng), parseFloat(prevWp.lat), Number(prevWp.alt) || 0);
const nextCartesian = nextWp ? Cesium.Cartesian3.fromDegrees(parseFloat(nextWp.lng), parseFloat(nextWp.lat), Number(nextWp.alt) || 0) : centerCartesian;
const headingDeg = holdParams && holdParams.headingDeg != null ? holdParams.headingDeg : 0;
const a0 = holdParams && (holdParams.semiMajor != null || holdParams.semiMajorAxis != null) ? (holdParams.semiMajor ?? holdParams.semiMajorAxis) : 500;
const b0 = holdParams && (holdParams.semiMinor != null || holdParams.semiMinorAxis != null) ? (holdParams.semiMinor ?? holdParams.semiMinorAxis) : 300;
const out = cesiumMap.computeEllipseParamsForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM, headingDeg, a0, b0);
if (out && out.semiMajor != null) {
const old = holdEllipseParamsByLegIndex[i];
if (!old || Math.abs(out.semiMajor - old.semiMajor) > 1) changed = true;
holdEllipseParamsByLegIndex[i] = { semiMajor: out.semiMajor, semiMinor: out.semiMinor, headingDeg: headingDeg };
}
}
}
if (!changed || iter === 1) break;
}
if (pathData2) pathData = pathData2;
if (segments2) segments = segments2;
if (routeId != null) {
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, holdRadiusByLegIndex);
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, holdEllipseParamsByLegIndex);
}
} else if (routeId != null) {
if (cesiumMap.setRouteHoldRadii) cesiumMap.setRouteHoldRadii(routeId, {});
if (cesiumMap.setRouteHoldEllipseParams) cesiumMap.setRouteHoldEllipseParams(routeId, {});
}
}
const path = pathData ? pathData.path : null;
const segmentEndIndices = pathData ? pathData.segmentEndIndices : null;
const position = this.getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices);
@ -3498,7 +3645,7 @@ export default {
this.activeRouteIds.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);
const { position, nextPosition, previousPosition, warnings, earlyArrivalLegs, currentSegment } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes, routeId);
if (warnings && warnings.length) allWarnings.push(...warnings);
if (position) {
const directionPoint = nextPosition || previousPosition;
@ -3687,6 +3834,7 @@ export default {
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
}
this.$nextTick(() => this.updateDeductionPositions());
} catch (_) {}
}
const firstId = planRouteIds[0];
@ -3789,6 +3937,7 @@ export default {
} catch (_) {}
}
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
this.$nextTick(() => this.updateDeductionPositions());
}
} else {
this.$message.warning('该航线暂无坐标数据,无法在地图展示');
@ -3967,7 +4116,7 @@ export default {
pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
}
}
const { earlyArrivalLegs, lateArrivalLegs } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData);
const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData);
const routeName = route.name || `航线${route.id}`;
(earlyArrivalLegs || []).forEach(leg => {
@ -3993,6 +4142,20 @@ export default {
severity: 'high'
});
});
(holdDelayConflicts || []).forEach(conf => {
list.push({
id: id++,
title: '盘旋时间不足',
routeName,
fromWaypoint: conf.fromName,
toWaypoint: conf.toName,
time: this.minutesToStartTime(conf.setExitTime),
position: conf.holdCenter ? `经度 ${conf.holdCenter.lng.toFixed(5)}°, 纬度 ${conf.holdCenter.lat.toFixed(5)}°` : undefined,
suggestion: `警告:设定的盘旋时间不足以支撑战斗机完成最后一圈,实际切出将延迟 ${conf.delaySeconds} 秒。`,
severity: 'high',
holdCenter: conf.holdCenter
});
});
});
this.conflicts = list;

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

@ -262,7 +262,7 @@
<el-input v-else v-model.number="scope.row.labelFontSize" size="mini" placeholder="14" />
</template>
</el-table-column>
<el-table-column label="颜色" width="100">
<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">
@ -271,6 +271,30 @@
</div>
</template>
</el-table-column>
<el-table-column label="标记大小" width="80">
<template slot-scope="scope">
<span v-if="!waypointsEditMode">{{ formatNum(scope.row.pixelSize) }}</span>
<el-input v-else v-model.number="scope.row.pixelSize" size="mini" placeholder="12" :min="4" :max="24" />
</template>
</el-table-column>
<el-table-column label="标记颜色" width="100">
<template slot-scope="scope">
<span v-if="!waypointsEditMode" class="color-text">{{ scope.row.color || '' }}</span>
<div v-else class="table-color-wrap">
<el-color-picker v-model="scope.row.color" size="mini" :predefine="presetColors" />
<span class="color-value">{{ scope.row.color }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="边框颜色" width="100">
<template slot-scope="scope">
<span v-if="!waypointsEditMode" class="color-text">{{ scope.row.outlineColor || '' }}</span>
<div v-else class="table-color-wrap">
<el-color-picker v-model="scope.row.outlineColor" size="mini" :predefine="presetColors" />
<span class="color-value">{{ scope.row.outlineColor }}</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
@ -323,22 +347,23 @@ export default {
selectedPlatform: null,
waypointsTableData: [],
waypointsEditMode: false,
skipBasicStyleSyncOnce: true,
form: {
id: '',
name: ''
},
// 线线线3 attributes 退
// 线 attributes 退
defaultStyle: {
waypoint: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 },
line: { style: 'solid', width: 3, color: '#800080', gapColor: '#000000', dashLength: 20 }
waypoint: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
line: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
},
styleForm: {
waypoint: { pixelSize: 7, color: '#ffffff', outlineColor: '#0078FF', outlineWidth: 2 },
line: { style: 'solid', width: 3, color: '#800080', gapColor: '#000000', dashLength: 20 }
waypoint: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
line: { style: 'solid', width: 3, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
},
presetColors: [
'#ffffff', '#000000', '#0078FF', '#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
'#909399', '#303133', '#00CED1', '#FF1493', '#FFD700', '#4B0082', '#00FF00', '#FF4500'
'#f1f5f9', '#64748b', '#334155', '#94a3b8', '#e2e8f0', '#0078FF', '#409EFF', '#67C23A',
'#E6A23C', '#F56C6C', '#909399', '#00CED1', '#FF1493', '#FFD700', '#4B0082', '#FF4500'
]
}
},
@ -382,8 +407,24 @@ export default {
if (val) {
this.loadPosition()
if (this.activeTab === 'platform') this.loadPlatforms()
this.skipBasicStyleSyncOnce = true
}
},
'styleForm.waypoint': {
handler(wpStyle) {
if (!wpStyle || !this.waypointsTableData.length) return
if (this.skipBasicStyleSyncOnce) {
this.skipBasicStyleSyncOnce = false
return
}
this.waypointsTableData.forEach(row => {
row.color = wpStyle.color || '#f1f5f9'
row.pixelSize = wpStyle.pixelSize != null ? wpStyle.pixelSize : 10
row.outlineColor = wpStyle.outlineColor || '#64748b'
})
},
deep: true
},
activeTab(val) {
if (val === 'platform' && this.visible) this.loadPlatforms()
if (val === 'waypoints' && this.panelWidth < 920) this.panelWidth = 920
@ -593,7 +634,10 @@ export default {
...wp,
minutesFromK: this.startTimeToMinutes(wp.startTime),
labelFontSize: wp.labelFontSize != null ? Number(wp.labelFontSize) : 14,
labelColor: wp.labelColor || '#333333'
labelColor: wp.labelColor || '#333333',
pixelSize: wp.pixelSize != null ? Number(wp.pixelSize) : 10,
color: wp.color || '#f1f5f9',
outlineColor: wp.outlineColor || '#64748b'
}))
this.waypointsEditMode = false
},
@ -626,7 +670,10 @@ export default {
pointType: row.pointType || null,
holdParams: row.holdParams || null,
labelFontSize: row.labelFontSize != null ? row.labelFontSize : 14,
labelColor: row.labelColor || '#333333'
labelColor: row.labelColor || '#333333',
color: row.color != null && row.color !== '' ? row.color : '#f1f5f9',
pixelSize: row.pixelSize != null ? row.pixelSize : 10,
outlineColor: row.outlineColor != null && row.outlineColor !== '' ? row.outlineColor : '#64748b'
}))
},
handleSave() {

104
ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue

@ -9,7 +9,7 @@
</div>
<div class="dialog-body">
<el-form :model="formData" :rules="rules" ref="formRef" label-width="100px" size="small">
<el-form :model="formData" :rules="rules" ref="formRef" label-width="130px" size="small" class="waypoint-form">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入航点名称"></el-input>
</el-form-item>
@ -21,13 +21,39 @@
:max="28"
controls-position="right"
placeholder="字号(px)"
style="width: 100%;"
class="full-width-input"
/>
<div class="form-tip">航点名称在地图上的显示字号微软雅黑字体</div>
</el-form-item>
<el-form-item label="地图文字颜色" prop="labelColor">
<el-color-picker v-model="formData.labelColor" size="small" />
<span class="color-value">{{ formData.labelColor }}</span>
<!-- 颜色前两项并排利用横向空间第三项单独一行 -->
<el-row :gutter="20" class="color-form-row">
<el-col :span="12">
<el-form-item label="地图文字颜色" prop="labelColor" class="color-form-item-inline">
<el-color-picker v-model="formData.labelColor" size="small" />
<span class="color-value">{{ formData.labelColor }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="航点标记颜色" prop="color" class="color-form-item-inline">
<el-color-picker v-model="formData.color" size="small" />
<span class="color-value">{{ formData.color }}</span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="航点标记边框颜色" prop="outlineColor">
<el-color-picker v-model="formData.outlineColor" size="small" />
<span class="color-value">{{ formData.outlineColor }}</span>
</el-form-item>
<el-form-item label="航点标记大小" prop="pixelSize">
<el-input-number
v-model="formData.pixelSize"
:min="4"
:max="24"
controls-position="right"
placeholder="像素"
class="full-width-input"
/>
</el-form-item>
<el-form-item label="高度" prop="alt">
@ -36,7 +62,7 @@
:min="0"
controls-position="right"
placeholder="请输入高度"
style="width: 100%;"
class="full-width-input"
></el-input-number>
</el-form-item>
@ -46,7 +72,7 @@
:min="0"
controls-position="right"
placeholder="请输入速度"
style="width: 100%;"
class="full-width-input"
></el-input-number>
</el-form-item>
@ -55,10 +81,10 @@
v-model="formData.turnAngle"
controls-position="right"
placeholder="请输入转弯坡度"
style="width: 100%;"
class="full-width-input"
:disabled="formData.isBankDisabled"
></el-input-number>
<div v-if="formData.isBankDisabled" style="color: #909399; font-size: 12px; margin-top: 4px;">
<div v-if="formData.isBankDisabled" class="form-tip form-warn">
首尾航点坡度已锁定为 0不可编辑
</div>
</el-form-item>
@ -70,17 +96,17 @@
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.pointType === 'hold_circle'" label="半径(米)">
<el-input-number v-model="formData.holdRadius" :min="100" :max="50000" style="width:100%" />
<el-input-number v-model="formData.holdRadius" :min="100" :max="50000" class="full-width-input" />
</el-form-item>
<template v-if="formData.pointType === 'hold_ellipse'">
<el-form-item label="长半轴(米)">
<el-input-number v-model="formData.holdSemiMajor" :min="100" :max="50000" style="width:100%" />
<el-input-number v-model="formData.holdSemiMajor" :min="100" :max="50000" class="full-width-input" />
</el-form-item>
<el-form-item label="短半轴(米)">
<el-input-number v-model="formData.holdSemiMinor" :min="50" :max="50000" style="width:100%" />
<el-input-number v-model="formData.holdSemiMinor" :min="50" :max="50000" class="full-width-input" />
</el-form-item>
<el-form-item label="长轴方位(度)">
<el-input-number v-model="formData.holdHeadingDeg" :min="-180" :max="180" style="width:100%" />
<el-input-number v-model="formData.holdHeadingDeg" :min="-180" :max="180" class="full-width-input" />
</el-form-item>
</template>
<el-form-item label="盘旋方向">
@ -91,16 +117,15 @@
</el-form-item>
</template>
<el-form-item label="相对 K 时(分钟)" prop="minutesFromK">
<el-form-item label="相对K时(分)" prop="minutesFromK">
<el-input-number
v-model="formData.minutesFromK"
:min="-9999"
:max="9999"
controls-position="right"
placeholder="相对 K 的分钟数,正数表示 K 后,负数表示 K 前"
style="width: 100%;"
placeholder="正数 K 后,负数 K 前"
class="full-width-input"
/>
<div class="form-tip">正数=K 之后负数=K 之前40 表示 K+00:40-15 表示 K-00:15</div>
</el-form-item>
</el-form>
@ -155,7 +180,10 @@ export default {
holdHeadingDeg: 0,
holdClockwise: true,
labelFontSize: 14,
labelColor: '#333333'
labelColor: '#334155',
pixelSize: 10,
color: '#f1f5f9',
outlineColor: '#64748b'
},
rules: {
name: [
@ -214,7 +242,10 @@ export default {
}
} catch (e) {}
const labelFontSize = this.waypoint.labelFontSize != null ? Number(this.waypoint.labelFontSize) : 14;
const labelColor = this.waypoint.labelColor || '#333333';
const labelColor = this.waypoint.labelColor || '#334155';
const pixelSize = this.waypoint.pixelSize != null ? Number(this.waypoint.pixelSize) : 10;
const color = this.waypoint.color || '#f1f5f9';
const outlineColor = this.waypoint.outlineColor != null ? this.waypoint.outlineColor : '#64748b';
this.formData = {
name: this.waypoint.name || '',
alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0,
@ -231,7 +262,10 @@ export default {
holdHeadingDeg,
holdClockwise,
labelFontSize: Math.min(28, Math.max(10, labelFontSize)),
labelColor
labelColor,
pixelSize: Math.min(24, Math.max(4, pixelSize)),
color,
outlineColor
};
this.$nextTick(() => {
@ -253,7 +287,10 @@ export default {
...rest,
startTime: startTimeStr,
labelFontSize: this.formData.labelFontSize,
labelColor: this.formData.labelColor
labelColor: this.formData.labelColor,
pixelSize: this.formData.pixelSize,
color: this.formData.color,
outlineColor: this.formData.outlineColor
};
if (this.formData.pointType && this.formData.pointType !== 'normal') {
payload.pointType = this.formData.pointType;
@ -328,7 +365,7 @@ export default {
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 500px;
max-width: 640px;
max-height: 90vh;
overflow-y: auto;
animation: dialog-fade-in 0.3s ease;
@ -384,6 +421,22 @@ export default {
gap: 10px;
}
.waypoint-form >>> .el-form-item__label {
white-space: nowrap;
}
.full-width-input {
width: 100%;
}
.color-form-row {
margin-bottom: 0;
}
.waypoint-form >>> .color-form-row .el-form-item {
margin-bottom: 18px;
}
.form-tip {
font-size: 12px;
color: #909399;
@ -391,9 +444,14 @@ export default {
line-height: 1.4;
}
.form-warn {
color: #e6a23c;
}
.color-value {
margin-left: 8px;
font-size: 12px;
color: #606266;
word-break: keep-all;
}
</style>

15
ruoyi-ui/src/views/selectRoom/index.vue

@ -214,9 +214,20 @@ export default {
}
},
showContextMenu(event, room) {
const padding = 12
const menuWidth = 160
const menuHeight = 90
const winW = window.innerWidth
const winH = window.innerHeight
let x = event.clientX
let y = event.clientY
if (x + menuWidth + padding > winW) x = winW - menuWidth - padding
if (x < padding) x = padding
if (y + menuHeight + padding > winH) y = winH - menuHeight - padding
if (y < padding) y = padding
this.contextMenu.visible = true
this.contextMenu.x = event.clientX
this.contextMenu.y = event.clientY
this.contextMenu.x = x
this.contextMenu.y = y
this.contextMenu.room = room
},
hideContextMenu() {

Loading…
Cancel
Save