From 8ad1c6e9e96cefab5368103752708f44ba8a30fb Mon Sep 17 00:00:00 2001 From: ctw <1051735452@qq.com> Date: Thu, 5 Feb 2026 10:56:25 +0800 Subject: [PATCH] =?UTF-8?q?K=E6=97=B6=E5=92=8C=E7=AE=80=E5=8D=95=E7=9A=84?= =?UTF-8?q?=E8=88=AA=E7=BA=BF=E5=8A=A8=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-ui/src/views/cesiumMap/index.vue | 16 + ruoyi-ui/src/views/childRoom/RightPanel.vue | 13 +- ruoyi-ui/src/views/childRoom/TopHeader.vue | 34 +- ruoyi-ui/src/views/childRoom/index.vue | 394 ++++++++++++++++++++-- ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue | 59 +++- 5 files changed, 467 insertions(+), 49 deletions(-) diff --git a/ruoyi-ui/src/views/cesiumMap/index.vue b/ruoyi-ui/src/views/cesiumMap/index.vue index cf4a945..37896eb 100644 --- a/ruoyi-ui/src/views/cesiumMap/index.vue +++ b/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链接'); diff --git a/ruoyi-ui/src/views/childRoom/RightPanel.vue b/ruoyi-ui/src/views/childRoom/RightPanel.vue index d972568..3187dc1 100644 --- a/ruoyi-ui/src/views/childRoom/RightPanel.vue +++ b/ruoyi-ui/src/views/childRoom/RightPanel.vue @@ -85,7 +85,7 @@
{{ point.name }}
-
高度: {{ point.alt }}m | 速度: {{ point.speed }}
+
高度: {{ point.alt }}m | 速度: {{ point.speed }}
@@ -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) diff --git a/ruoyi-ui/src/views/childRoom/TopHeader.vue b/ruoyi-ui/src/views/childRoom/TopHeader.vue index b3e3b34..b04d5d2 100644 --- a/ruoyi-ui/src/views/childRoom/TopHeader.vue +++ b/ruoyi-ui/src/views/childRoom/TopHeader.vue @@ -233,11 +233,18 @@
-
+
{{ $t('topHeader.info.combatTime') }}
-
{{ combatTime }}
+
+ {{ combatTime }} + +
@@ -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; diff --git a/ruoyi-ui/src/views/childRoom/index.vue b/ruoyi-ui/src/views/childRoom/index.vue index 041697d..d50aaea 100644 --- a/ruoyi-ui/src/views/childRoom/index.vue +++ b/ruoyi-ui/src/views/childRoom/index.vue @@ -36,6 +36,26 @@ 确 定
+ + + + + + + +

航线的任务时间将以此 K 时为基准进行加减;航点表时间为相对 K 的分钟数。房主/管理员可随时再次点击「作战时间」修改 K 时。

+
+ +
+

仅推演当前展示的航线;K 时可随时由房主/管理员在右上角「作战时间」处修改。

@@ -198,8 +222,13 @@
- - +
+ + {{ deductionWarnings[0] }} + + 等 {{ deductionWarnings.length }} 条 + +
@@ -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); + // 明确构造后端需要的字段,确保 startTime(相对K时)一定会被提交并更新到数据库 + 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; // 1小时30分钟 = 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; diff --git a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue index 6bb0006..d03dd53 100644 --- a/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue +++ b/ruoyi-ui/src/views/dialogs/WaypointEditDialog.vue @@ -47,14 +47,16 @@ - - + - + /> +
正数=K 之后,负数=K 之前。例:40 表示 K+00:40,-15 表示 K-00:15
@@ -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; +}