Browse Source

K时和简单的航线动态

wxp
ctw 2 months ago
parent
commit
8ad1c6e9e9
  1. 16
      ruoyi-ui/src/views/cesiumMap/index.vue
  2. 13
      ruoyi-ui/src/views/childRoom/RightPanel.vue
  3. 34
      ruoyi-ui/src/views/childRoom/TopHeader.vue
  4. 394
      ruoyi-ui/src/views/childRoom/index.vue
  5. 59
      ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue

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

@ -562,6 +562,22 @@ export default {
}
this.allEntities = this.allEntities.filter(item => item.id !== routeId && item.id !== `route-platform-${routeId}`);
},
/** 动态推演:更新某条航线的平台图标位置(position: { lng, lat, alt } 或 Cesium.Cartesian3) */
updatePlatformPosition(routeId, position) {
if (!this.viewer) return;
const entity = this.viewer.entities.getById(`route-platform-${routeId}`);
if (!entity || !entity.position) return;
let cartesian;
if (position && position.x !== undefined && position.y !== undefined && position.z !== undefined) {
cartesian = position;
} else if (position && position.lng != null && position.lat != null) {
const alt = position.alt != null ? Number(position.alt) : 0;
cartesian = Cesium.Cartesian3.fromDegrees(Number(position.lng), Number(position.lat), alt);
} else {
return;
}
entity.position = cartesian;
},
checkCesiumLoaded() {
if (typeof Cesium === 'undefined') {
console.error('Cesium未加载,请检查CDN链接');

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

@ -85,7 +85,7 @@
<i class="el-icon-location tree-icon"></i>
<div class="tree-item-info">
<div class="tree-item-name">{{ point.name }}</div>
<div class="tree-item-meta">高度: {{ point.alt }}m | 速度: {{ point.speed }}</div>
<div class="tree-item-meta">高度: {{ point.alt }}m | 速度: {{ point.speed }}<template v-if="point.startTime"> | 相对K: {{ formatWaypointKTime(point.startTime) }}</template></div>
</div>
<div class="tree-item-actions">
<i class="el-icon-edit" title="编辑" @click.stop="handleOpenWaypointDialog(point, index, route.waypoints.length)"></i>
@ -334,6 +334,17 @@ export default {
}
},
methods: {
/** 航点 startTime(如 K+00:40:00)格式化为简短显示:K+40 或 K-15 */
formatWaypointKTime(startTime) {
if (!startTime || typeof startTime !== 'string') return '—';
const m = startTime.match(/K([+-])(\d{2}):(\d{2})/);
if (!m) return startTime;
const sign = m[1];
const h = parseInt(m[2], 10);
const min = parseInt(m[3], 10);
const totalMin = h * 60 + min;
return totalMin === 0 ? 'K+0' : `K${sign}${totalMin}`;
},
// /
togglePlan(planId) {
const index = this.expandedPlans.indexOf(planId)

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

@ -233,11 +233,18 @@
</div>
</div>
<div class="info-box">
<div
class="info-box"
:class="{ 'clickable': true }"
@click="$emit('set-k-time')"
>
<i class="el-icon-timer info-icon"></i>
<div class="info-content">
<div class="info-label">{{ $t('topHeader.info.combatTime') }}</div>
<div class="info-value">{{ combatTime }}</div>
<div class="info-value">
{{ combatTime }}
<i v-if="canSetKTime" class="el-icon-edit-outline set-k-hint" title="点击设定或修改 K 时(房主/管理员可随时更改)"></i>
</div>
</div>
</div>
@ -312,6 +319,14 @@ export default {
type: String,
default: ''
},
roomDetail: {
type: Object,
default: null
},
canSetKTime: {
type: Boolean,
default: false
},
userAvatar: {
type: String,
default: 'https://cube.elemecdn.com/0/88dd03f9bf287d08f58fbcf58fddbf4a8c6/avatar.png'
@ -389,9 +404,7 @@ export default {
this.$emit('import-layer')
},
importRoute() {
this.$emit('import-route')
},
exportPlan() {
this.$emit('export-plan')
@ -856,6 +869,17 @@ export default {
font-weight: 600;
}
.info-box.clickable {
cursor: pointer;
}
.info-box .set-k-hint {
margin-left: 4px;
font-size: 12px;
color: #008aff;
vertical-align: middle;
}
.info-icon {
font-size: 20px;
color: #008aff;

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

@ -36,6 +36,26 @@
<el-button type="primary" @click="confirmSaveNewRoute"> </el-button>
</div>
</el-dialog>
<!-- 设定/修改 K 时弹窗房主或管理员可随时打开并修改 -->
<el-dialog title="设定 / 修改 K 时" :visible.sync="showKTimeSetDialog" width="420px" :append-to-body="true">
<el-form label-width="90px">
<el-form-item label="K 时(基准)">
<el-date-picker
v-model="kTimeForm.dateTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="选择日期和时间"
style="width: 100%"
/>
</el-form-item>
<p class="k-time-tip">航线的任务时间将以此 K 时为基准进行加减航点表时间为相对 K 的分钟数房主/管理员可随时再次点击作战时间修改 K </p>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="showKTimeSetDialog = false"> </el-button>
<el-button type="primary" @click="saveKTime"> </el-button>
</div>
</el-dialog>
</div>
<!-- 顶部导航栏 -->
<top-header
@ -43,9 +63,12 @@
:online-count="onlineCount"
:combat-time="combatTime"
:astro-time="astroTime"
:room-detail="roomDetail"
:can-set-k-time="canSetKTime"
:user-avatar="userAvatar"
:is-icon-edit-mode="isIconEditMode"
@select-nav="selectTopNav"
@set-k-time="openKTimeSetDialog"
@save-plan="savePlan"
@import-plan-file="importPlanFile"
@import-acd="importACD"
@ -156,6 +179,7 @@
<div class="popup-hide-btn" @click="hideKTimePopup" title="隐藏K时">
<i class="el-icon-arrow-down"></i>
</div>
<p class="deduction-hint">仅推演当前展示的航线K 时可随时由房主/管理员在右上角作战时间处修改</p>
<div class="timeline-controls">
<div class="current-time blue-time">
<i class="el-icon-time"></i>
@ -198,8 +222,13 @@
</div>
</div>
</div>
<div v-if="deductionWarnings.length > 0" class="deduction-warnings">
<i class="el-icon-warning-outline"></i>
<span>{{ deductionWarnings[0] }}</span>
<el-tooltip v-if="deductionWarnings.length > 1" :content="deductionWarnings.join(';')" placement="top">
<span class="warnings-more"> {{ deductionWarnings.length }} </span>
</el-tooltip>
</div>
</div>
<!-- 在线成员弹窗 -->
@ -301,6 +330,7 @@ import { listScenario,addScenario,delScenario} from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes } from "@/api/system/routes";
import { updateWaypoints } from "@/api/system/waypoints";
import { listLib,addLib,delLib} from "@/api/system/lib";
import { getRooms, updateRooms } from "@/api/system/rooms";
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue";
export default {
name: 'MissionPlanningView',
@ -353,6 +383,9 @@ export default {
onlineCount: 30,
combatTime: 'K+01:30:45',
astroTime: '',
roomDetail: null,
showKTimeSetDialog: false,
kTimeForm: { dateTime: null },
//
isMenuHidden: true, //
@ -468,6 +501,8 @@ export default {
//
timeProgress: 45,
currentTime: 'K+01:15:30',
deductionMinutesFromK: 0,
deductionWarnings: [],
isPlaying: false,
playbackSpeed: 1,
playbackInterval: null,
@ -476,6 +511,33 @@ export default {
userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
};
},
watch: {
timeProgress: {
handler() {
this.updateTimeFromProgress();
},
immediate: true
}
},
computed: {
isRoomOwner() {
if (!this.roomDetail || this.roomDetail.ownerId == null) return false;
const myId = this.$store.getters.id;
return String(myId) === String(this.roomDetail.ownerId);
},
isAdmin() {
const roles = this.$store.getters.roles || [];
const id = this.$store.getters.id;
return (
roles.includes('admin') ||
String(id) === '1' ||
(Array.isArray(roles) && roles.some(r => String(r).toLowerCase() === 'admin'))
);
},
canSetKTime() {
return this.isRoomOwner || this.isAdmin;
}
},
mounted() {
this.getList();
//
@ -503,6 +565,7 @@ export default {
console.log("从路由接收到的真实房间 ID:", this.currentRoomId);
this.getList();
this.getPlatformList();
if (this.currentRoomId) this.getRoomDetail();
},
methods: {
//
@ -956,12 +1019,27 @@ export default {
} else {
updatedWaypoint.turnRadius = 0;
}
const response = await updateWaypoints(updatedWaypoint);
// startTimeK
const payload = {
id: updatedWaypoint.id,
routeId: updatedWaypoint.routeId,
name: updatedWaypoint.name,
seq: updatedWaypoint.seq,
lat: updatedWaypoint.lat,
lng: updatedWaypoint.lng,
alt: updatedWaypoint.alt,
speed: updatedWaypoint.speed,
startTime: (updatedWaypoint.startTime != null && updatedWaypoint.startTime !== '')
? updatedWaypoint.startTime
: 'K+00:00:00',
turnAngle: updatedWaypoint.turnAngle
};
const response = await updateWaypoints(payload);
if (response.code === 200) {
const index = this.selectedRouteDetails.waypoints.findIndex(p => p.id === updatedWaypoint.id);
if (index !== -1) {
//
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint });
// payload startTime
this.selectedRouteDetails.waypoints.splice(index, 1, { ...updatedWaypoint, ...payload });
//
if (this.$refs.cesiumMap) {
//
@ -998,18 +1076,72 @@ export default {
},
updateCombatTime() {
// K
//
const now = new Date();
const baseSeconds = 5400; // 130 = 5400
const currentSeconds = now.getSeconds() + now.getMinutes() * 60 + now.getHours() * 3600;
const combatSeconds = baseSeconds + (currentSeconds % 86400);
const hours = Math.floor(combatSeconds / 3600);
const minutes = Math.floor((combatSeconds % 3600) / 60);
const seconds = combatSeconds % 60;
this.combatTime = `K+${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
if (this.roomDetail && this.roomDetail.kAnchorTime) {
const k0 = new Date(this.roomDetail.kAnchorTime).getTime();
const now = Date.now();
const offsetMs = now - k0;
const sign = offsetMs >= 0 ? '+' : '-';
const absMs = Math.abs(offsetMs);
const hours = Math.floor(absMs / 3600000);
const minutes = Math.floor((absMs % 3600000) / 60000);
const seconds = Math.floor((absMs % 60000) / 1000);
this.combatTime = `K${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
this.combatTime = '未设定';
}
},
getRoomDetail() {
if (!this.currentRoomId) return;
getRooms(this.currentRoomId).then(res => {
if (res.code === 200 && res.data) this.roomDetail = res.data;
}).catch(() => {});
},
/** 将任意日期字符串格式化为 yyyy-MM-dd HH:mm:ss,供日期选择器使用 */
formatKTimeForPicker(val) {
if (!val) return null;
const d = new Date(val);
if (isNaN(d.getTime())) return null;
const y = d.getFullYear();
const m = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const h = d.getHours().toString().padStart(2, '0');
const min = d.getMinutes().toString().padStart(2, '0');
const s = d.getSeconds().toString().padStart(2, '0');
return `${y}-${m}-${day} ${h}:${min}:${s}`;
},
openKTimeSetDialog() {
if (!this.canSetKTime) {
this.$message.info('仅房主或管理员可设定或修改 K 时');
return;
}
if (!this.currentRoomId) {
this.$message.warning('请先进入任务房间');
return;
}
if (!this.roomDetail || !this.roomDetail.id) {
this.$message.warning('房间信息加载中或未找到,请稍后再试');
return;
}
const existing = this.roomDetail.kAnchorTime
? this.formatKTimeForPicker(this.roomDetail.kAnchorTime)
: null;
this.kTimeForm.dateTime = existing || this.formatKTimeForPicker(this.astroTime) || this.formatKTimeForPicker(new Date());
this.showKTimeSetDialog = true;
},
saveKTime() {
if (!this.roomDetail || !this.kTimeForm.dateTime) {
this.$message.warning('请选择 K 时');
return;
}
updateRooms({ id: this.roomDetail.id, kAnchorTime: this.kTimeForm.dateTime }).then(res => {
if (res.code === 200) {
this.$message.success('K 时已设定');
this.showKTimeSetDialog = false;
this.getRoomDetail();
} else {
this.$message.error(res.msg || '设定失败');
}
}).catch(() => this.$message.error('设定失败'));
},
//
@ -1401,6 +1533,9 @@ export default {
} else if (item.id === 'deduction') {
// /K
this.showKTimePopup = !this.showKTimePopup;
if (this.showKTimePopup) {
this.$nextTick(() => this.updateTimeFromProgress());
}
//
this.drawDom = false;
this.airspaceDrawDom = false;
@ -1440,7 +1575,7 @@ export default {
if (this.timeProgress >= 100) {
this.timeProgress = 0;
}
this.updateTimeFromProgress();
// watch timeProgress updateTimeFromProgress
}, 100);
},
@ -1470,15 +1605,168 @@ export default {
},
updateTimeFromProgress() {
const totalSeconds = Math.floor(this.timeProgress * 72);
const hours = Math.floor(totalSeconds / 3600) - 2;
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const sign = hours >= 0 ? '+' : '-';
const absHours = Math.abs(hours);
this.currentTime = `K${sign}${String(absHours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
const span = Math.max(0, maxMinutes - minMinutes) || 120;
const currentMinutesFromK = minMinutes + (this.timeProgress / 100) * span;
this.deductionMinutesFromK = currentMinutesFromK;
const sign = currentMinutesFromK >= 0 ? '+' : '-';
const absMin = Math.abs(Math.floor(currentMinutesFromK));
const hours = Math.floor(absMin / 60);
const minutes = absMin % 60;
this.currentTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
this.updateDeductionPositions();
},
/** 仅针对当前展示的航线(activeRouteIds):从这些航线的航点中取推演时间范围(相对 K 的分钟数) */
getDeductionTimeRange() {
let minMinutes = 0;
let maxMinutes = 120;
const minutesList = [];
this.activeRouteIds.forEach(routeId => {
const route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || !route.waypoints.length) return;
route.waypoints.forEach(wp => {
const m = this.waypointStartTimeToMinutes(wp.startTime);
minutesList.push(m);
});
});
if (minutesList.length > 0) {
minMinutes = Math.min(...minutesList);
maxMinutes = Math.max(...minutesList);
if (maxMinutes <= minMinutes) maxMinutes = minMinutes + 120;
}
return { minMinutes, maxMinutes };
},
/** 将航点 startTime 字符串转为相对 K 的分钟数 */
waypointStartTimeToMinutes(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);
},
/** 两航点间近似距离(米),含高度差 */
segmentDistance(wp1, wp2) {
const R = 6371000;
const lat1 = (wp1.lat * Math.PI) / 180;
const lat2 = (wp2.lat * Math.PI) / 180;
const dlat = ((wp2.lat - wp1.lat) * Math.PI) / 180;
const dlng = ((wp2.lng - wp1.lng) * Math.PI) / 180;
const a = Math.sin(dlat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const horizontal = R * c;
const dalt = (wp2.alt || 0) - (wp1.alt || 0);
return Math.sqrt(horizontal * horizontal + dalt * dalt);
},
/**
* 按速度与计划时间构建航线时间轴含飞行段与提前到达则等待的等待段并做航段校验
* 若所有航点相对 K 时相同则在 [globalMin, globalMax] 内按航点顺序均匀分布避免闪现
*/
buildRouteTimeline(waypoints, globalMin, globalMax) {
const warnings = [];
if (!waypoints || waypoints.length === 0) return { segments: [], warnings };
const points = waypoints.map(wp => ({
lng: parseFloat(wp.lng),
lat: parseFloat(wp.lat),
alt: Number(wp.alt) || 0,
minutes: this.waypointStartTimeToMinutes(wp.startTime),
speed: Number(wp.speed) || 800
}));
const allSame = points.every(p => p.minutes === points[0].minutes);
if (allSame && points.length > 1) {
const span = Math.max(globalMax - globalMin, 1);
points.forEach((p, i) => {
p.minutes = globalMin + (span * i) / (points.length - 1);
});
} else {
points.sort((a, b) => a.minutes - b.minutes);
}
if (points.length === 1) {
const p = points[0];
const pos = { lng: p.lng, lat: p.lat, alt: p.alt };
return {
segments: [{ startTime: globalMin, endTime: globalMax, startPos: pos, endPos: pos, type: 'wait' }],
warnings
};
}
const effectiveTime = [points[0].minutes];
const segments = [];
for (let i = 0; i < points.length - 1; i++) {
const dist = this.segmentDistance(points[i], points[i + 1]);
const speedKmh = points[i].speed || 800;
const travelMin = (dist / 1000) * (60 / speedKmh);
const actualArrival = effectiveTime[i] + travelMin;
const scheduled = points[i + 1].minutes;
if (travelMin > 0 && scheduled - points[i].minutes > 0) {
const requiredSpeedKmh = (dist / 1000) / ((scheduled - points[i].minutes) / 60);
if (actualArrival > scheduled) {
warnings.push(
`某航段:距离约 ${(dist / 1000).toFixed(1)}km,计划 ${(scheduled - points[i].minutes).toFixed(0)} 分钟,当前速度 ${speedKmh}km/h 无法按时到达,约需 ≥${Math.ceil(requiredSpeedKmh)}km/h,请调整相对K时或速度。`
);
} else if (actualArrival < scheduled - 0.5) {
warnings.push('存在航段将提前到达下一航点,平台将在该点等待至计划时间再飞往下一段。');
}
}
effectiveTime[i + 1] = Math.max(actualArrival, scheduled);
const posCur = { lng: points[i].lng, lat: points[i].lat, alt: points[i].alt };
const posNext = { lng: points[i + 1].lng, lat: points[i + 1].lat, alt: points[i + 1].alt };
segments.push({ startTime: effectiveTime[i], endTime: actualArrival, startPos: posCur, endPos: posNext, type: 'fly' });
if (actualArrival < effectiveTime[i + 1]) {
segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait' });
}
}
return { segments, warnings };
},
/** 从时间轴中取当前推演时间对应的位置 */
getPositionFromTimeline(segments, minutesFromK) {
if (!segments || segments.length === 0) return null;
if (minutesFromK <= segments[0].startTime) return segments[0].startPos;
const last = segments[segments.length - 1];
if (minutesFromK >= last.endTime) return last.endPos;
for (let i = 0; i < segments.length; i++) {
const s = segments[i];
if (minutesFromK < s.endTime) {
const t = (minutesFromK - s.startTime) / (s.endTime - s.startTime);
if (s.type === 'wait') return s.startPos;
return {
lng: s.startPos.lng + (s.endPos.lng - s.startPos.lng) * t,
lat: s.startPos.lat + (s.endPos.lat - s.startPos.lat) * t,
alt: s.startPos.alt + (s.endPos.alt - s.startPos.alt) * t
};
}
}
return last.endPos;
},
/** 根据当前推演时间(相对 K 的分钟)得到平台位置,使用速度与等待逻辑;返回 { position, warnings } */
getPositionAtMinutesFromK(waypoints, minutesFromK, globalMin, globalMax) {
if (!waypoints || waypoints.length === 0) return { position: null, warnings: [] };
const { segments, warnings } = this.buildRouteTimeline(waypoints, globalMin, globalMax);
const position = this.getPositionFromTimeline(segments, minutesFromK);
return { position, warnings };
},
/** 仅根据当前展示的航线(activeRouteIds)更新平台图标位置,并汇总航段提示 */
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 route = this.routes.find(r => r.id === routeId);
if (!route || !route.waypoints || route.waypoints.length === 0) return;
const { position, warnings } = this.getPositionAtMinutesFromK(route.waypoints, minutesFromK, minMinutes, maxMinutes);
if (warnings && warnings.length) allWarnings.push(...warnings);
if (position) this.$refs.cesiumMap.updatePlatformPosition(routeId, position);
});
this.deductionWarnings = [...new Set(allWarnings)];
},
//
@ -1497,9 +1785,14 @@ export default {
},
formatTimeTooltip(val) {
const hours = Math.floor(val / 4);
const minutes = (val % 4) * 15;
return `K+${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
const span = Math.max(0, maxMinutes - minMinutes) || 120;
const minutesFromK = minMinutes + (val / 100) * span;
const sign = minutesFromK >= 0 ? '+' : '-';
const absMin = Math.abs(Math.floor(minutesFromK));
const hours = Math.floor(absMin / 60);
const minutes = absMin % 60;
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
},
selectPlan(plan) {
if (plan && plan.id) {
@ -1971,6 +2264,47 @@ export default {
pointer-events: auto;
}
.k-time-tip {
font-size: 12px;
color: #909399;
margin: 8px 0 0;
line-height: 1.5;
}
.deduction-hint {
margin: 0 0 8px 0;
font-size: 12px;
color: #909399;
line-height: 1.4;
}
.deduction-warnings {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding: 6px 10px;
background: rgba(230, 162, 60, 0.15);
border: 1px solid rgba(230, 162, 60, 0.5);
border-radius: 6px;
font-size: 12px;
color: #b88230;
}
.deduction-warnings i {
flex-shrink: 0;
}
.deduction-warnings span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.deduction-warnings .warnings-more {
flex-shrink: 0;
color: #008aff;
cursor: help;
}
.popup-hide-btn {
position: absolute;
top: -28px;

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

@ -47,14 +47,16 @@
</div>
</el-form-item>
<el-form-item label="起始时间" prop="startTime">
<el-time-picker
v-model="formData.startTime"
placeholder="请选择起始时间"
value-format="HH:mm:ss"
<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%;"
>
</el-time-picker>
/>
<div class="form-tip">正数=K 之后负数=K 之前40 表示 K+00:40-15 表示 K-00:15</div>
</el-form-item>
</el-form>
@ -98,7 +100,7 @@ export default {
alt: 5000,
speed: 800,
turnAngle: 0,
startTime: '',
minutesFromK: 0,
currentIndex: -1,
totalPoints: 0,
isBankDisabled: false
@ -118,9 +120,6 @@ export default {
turnAngle: [
//
{ required: true, validator: validateNumber, message: '请输入有效转弯坡度', trigger: ['blur', 'change'] }
],
startTime: [
{ required: true, message: '请选择起始时间', trigger: 'change' }
]
}
};
@ -147,7 +146,7 @@ export default {
name: this.waypoint.name || '',
alt: this.waypoint.alt !== undefined && this.waypoint.alt !== null ? Number(this.waypoint.alt) : 0,
speed: this.waypoint.speed !== undefined && this.waypoint.speed !== null ? Number(this.waypoint.speed) : 0,
startTime: this.waypoint.startTime || '',
minutesFromK: this.startTimeToMinutes(this.waypoint.startTime),
currentIndex: index,
totalPoints: total,
isBankDisabled: locked,
@ -166,13 +165,40 @@ export default {
saveWaypoint() {
this.$refs.formRef.validate((valid) => {
if (valid) {
const { minutesFromK, ...rest } = this.formData;
const startTimeStr = this.minutesToStartTime(minutesFromK);
this.$emit('save', {
...this.waypoint,
...this.formData
...rest,
startTime: startTimeStr
});
this.closeDialog();
}
});
},
/** 将 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`;
}
}
};
@ -262,4 +288,11 @@ export default {
border-top: 1px solid #e8e8e8;
gap: 10px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
line-height: 1.4;
}
</style>

Loading…
Cancel
Save