|
|
|
@ -15,6 +15,7 @@ |
|
|
|
:coordinateFormat="coordinateFormat" |
|
|
|
:bottomPanelVisible="bottomPanelVisible" |
|
|
|
:route-locked="routeLocked" |
|
|
|
:route-locked-by-other-ids="routeLockedByOtherRouteIds" |
|
|
|
:deduction-time-minutes="deductionMinutesFromK" |
|
|
|
:room-id="currentRoomId" |
|
|
|
@draw-complete="handleMapDrawComplete" |
|
|
|
@ -176,6 +177,8 @@ |
|
|
|
:routes="routes" |
|
|
|
:active-route-ids="activeRouteIds" |
|
|
|
:route-locked="routeLocked" |
|
|
|
:route-locked-by="routeLockedBy" |
|
|
|
:current-user-id="currentUserId" |
|
|
|
:selected-route-details="selectedRouteDetails" |
|
|
|
:conflicts="conflicts" |
|
|
|
:conflict-count="conflictCount" |
|
|
|
@ -511,6 +514,10 @@ export default { |
|
|
|
screenshotDataUrl: '', |
|
|
|
screenshotFileName: '', |
|
|
|
|
|
|
|
// 协作:谁正在编辑(锁定)哪条航线(来自 WebSocket 广播) |
|
|
|
routeLockedBy: {}, // routeId -> { userId, userName, nickName, sessionId } |
|
|
|
routeEditLockedId: null, // 本端当前编辑锁定的航线 ID,关闭弹窗时发送解锁 |
|
|
|
|
|
|
|
// 作战信息 |
|
|
|
roomCode: 'JTF-7-ALPHA', |
|
|
|
onlineCount: 0, |
|
|
|
@ -616,6 +623,8 @@ export default { |
|
|
|
plans: [], |
|
|
|
activeRightTab: 'plan', |
|
|
|
activeRouteIds: [], // 存储当前所有选中的航线ID |
|
|
|
/** 新加入时收到的房间可见航线 ID,若 routes 未加载则暂存,getList 完成后应用 */ |
|
|
|
roomStatePendingVisibleRouteIds: [], |
|
|
|
/** 航线上锁状态:routeId -> true 上锁,与地图右键及右侧列表锁图标同步 */ |
|
|
|
routeLocked: {}, |
|
|
|
// 冲突数据(由 runConflictCheck 根据当前航线与时间轴计算真实问题) |
|
|
|
@ -679,6 +688,14 @@ export default { |
|
|
|
this.timeProgress = progress; |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
/** 航线编辑弹窗关闭时:发送编辑解锁,让其他成员可编辑 */ |
|
|
|
showRouteDialog(visible) { |
|
|
|
if (visible) return; |
|
|
|
if (this.routeEditLockedId != null && this.wsConnection && this.wsConnection.sendObjectEditUnlock) { |
|
|
|
this.wsConnection.sendObjectEditUnlock('route', this.routeEditLockedId); |
|
|
|
this.routeEditLockedId = null; |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
@ -720,6 +737,17 @@ export default { |
|
|
|
if (!this.addHoldContext) return ''; |
|
|
|
if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋,保存航线时生效。'; |
|
|
|
return `在 ${this.addHoldContext.fromName} 与 ${this.addHoldContext.toName} 之间添加盘旋,到计划时间后沿切线飞往下一航点(原「下一格」航点将被移除)。`; |
|
|
|
}, |
|
|
|
/** 被其他成员编辑锁定的航线 ID 列表,供地图禁止拖拽等 */ |
|
|
|
routeLockedByOtherRouteIds() { |
|
|
|
const myId = this.currentUserId; |
|
|
|
if (myId == null) return Object.keys(this.routeLockedBy).map(id => Number(id)); |
|
|
|
const ids = []; |
|
|
|
Object.keys(this.routeLockedBy).forEach(rid => { |
|
|
|
const editor = this.routeLockedBy[rid]; |
|
|
|
if (editor && Number(editor.userId) !== Number(myId)) ids.push(Number(rid)); |
|
|
|
}); |
|
|
|
return ids; |
|
|
|
} |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
@ -760,6 +788,10 @@ export default { |
|
|
|
}, |
|
|
|
// 处理从地图点击传来的编辑请求(支持 wpId 或 waypointIndex,如右键盘旋弧时仅有 waypointIndex) |
|
|
|
async handleOpenWaypointEdit(wpId, routeId, waypointIndex) { |
|
|
|
if (routeId != null && this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法修改航点'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (waypointIndex != null && (wpId == null || wpId === undefined)) { |
|
|
|
try { |
|
|
|
const response = await getRoutes(routeId); |
|
|
|
@ -842,9 +874,23 @@ export default { |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
/** 该航线是否被其他成员编辑锁定(非自己) */ |
|
|
|
isRouteLockedByOther(routeId) { |
|
|
|
const lockedBy = this.routeLockedBy[routeId]; |
|
|
|
if (!lockedBy) return false; |
|
|
|
const myId = this.currentUserId != null ? Number(this.currentUserId) : null; |
|
|
|
const uid = lockedBy.userId != null ? Number(lockedBy.userId) : null; |
|
|
|
return myId !== uid; |
|
|
|
}, |
|
|
|
// 处理从地图点击传来的航线编辑请求 |
|
|
|
async handleOpenRouteEdit(routeId) { |
|
|
|
console.log(`>>> [父组件接收] 航线 ID: ${routeId}`); |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
const lockedBy = this.routeLockedBy[routeId]; |
|
|
|
const name = lockedBy && (lockedBy.nickName || lockedBy.userName) || '其他成员'; |
|
|
|
this.$message.warning(`${name} 正在编辑该航线,请稍后再试`); |
|
|
|
return; |
|
|
|
} |
|
|
|
try { |
|
|
|
const response = await getRoutes(routeId); |
|
|
|
if (response.code === 200 && response.data) { |
|
|
|
@ -871,6 +917,10 @@ export default { |
|
|
|
|
|
|
|
/** 右键航点“向前/向后增加航点”:进入放置模式,传入 waypoints 给地图预览 */ |
|
|
|
handleAddWaypointAt({ routeId, waypointIndex, mode }) { |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法添加航点'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (this.routeLocked[routeId]) { |
|
|
|
this.$message.info('该航线已上锁,请先解锁'); |
|
|
|
return; |
|
|
|
@ -998,6 +1048,10 @@ export default { |
|
|
|
|
|
|
|
/** 右键航点“切换盘旋航点”:普通航点设为盘旋(圆形默认),盘旋航点设为普通;支持首尾航点 */ |
|
|
|
async handleToggleWaypointHold({ routeId, dbId, waypointIndex }) { |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法修改'); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (this.routeLocked[routeId]) { |
|
|
|
this.$message.info('该航线已上锁,请先解锁'); |
|
|
|
return; |
|
|
|
@ -1134,6 +1188,10 @@ export default { |
|
|
|
|
|
|
|
/** 地图上拖拽航点结束:将新位置写回数据库并刷新显示 */ |
|
|
|
async handleWaypointPositionChanged({ dbId, routeId, lat, lng, alt }) { |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法拖拽修改'); |
|
|
|
return; |
|
|
|
} |
|
|
|
let waypoints = null; |
|
|
|
let route = null; |
|
|
|
if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) { |
|
|
|
@ -1303,6 +1361,21 @@ export default { |
|
|
|
this.wsOnlineMembers = this.wsOnlineMembers.filter(m => m.id !== sessionId && m.id !== member.sessionId); |
|
|
|
this.onlineCount = this.wsOnlineMembers.length; |
|
|
|
this.mySyncSessionIds = this.mySyncSessionIds.filter(s => s !== sessionId && s !== member.sessionId); |
|
|
|
// 该成员离开后清除其编辑锁定状态 |
|
|
|
Object.keys(this.routeLockedBy).forEach(rid => { |
|
|
|
if (this.routeLockedBy[rid] && this.routeLockedBy[rid].sessionId === sessionId) { |
|
|
|
this.$delete(this.routeLockedBy, rid); |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
onObjectEditLock: (msg) => { |
|
|
|
if (msg.objectType !== 'route' || msg.objectId == null) return; |
|
|
|
this.$set(this.routeLockedBy, msg.objectId, msg.editor); |
|
|
|
}, |
|
|
|
onObjectEditUnlock: (msg) => { |
|
|
|
if (msg.objectType !== 'route' || msg.objectId == null) return; |
|
|
|
const cur = this.routeLockedBy[msg.objectId]; |
|
|
|
if (cur && cur.sessionId === msg.sessionId) this.$delete(this.routeLockedBy, msg.objectId); |
|
|
|
}, |
|
|
|
onChatMessage: (msg) => { |
|
|
|
this.chatMessages = [...this.chatMessages, msg]; |
|
|
|
@ -1346,6 +1419,9 @@ export default { |
|
|
|
if (this.isMySyncSession(senderSessionId)) return; |
|
|
|
this.applySyncPlatformStyles(); |
|
|
|
}, |
|
|
|
onRoomState: (visibleRouteIds) => { |
|
|
|
this.applyRoomStateVisibleRoutes(visibleRouteIds); |
|
|
|
}, |
|
|
|
onConnected: () => {}, |
|
|
|
onDisconnected: () => { |
|
|
|
this.onlineCount = 0; |
|
|
|
@ -1353,6 +1429,7 @@ export default { |
|
|
|
this.mySyncSessionIds = []; |
|
|
|
this.chatMessages = []; |
|
|
|
this.privateChatMessages = {}; |
|
|
|
this.routeLockedBy = {}; |
|
|
|
}, |
|
|
|
onError: (err) => { |
|
|
|
console.warn('[WebSocket]', err); |
|
|
|
@ -1364,12 +1441,31 @@ export default { |
|
|
|
this.wsConnection.disconnect(); |
|
|
|
this.wsConnection = null; |
|
|
|
} |
|
|
|
this.roomStatePendingVisibleRouteIds = []; |
|
|
|
this.wsOnlineMembers = []; |
|
|
|
this.mySyncSessionIds = []; |
|
|
|
this.onlineCount = 0; |
|
|
|
this.chatMessages = []; |
|
|
|
this.privateChatMessages = {}; |
|
|
|
}, |
|
|
|
/** 应用房间状态中的可见航线(新加入用户同步;若 routes 未加载则暂存) */ |
|
|
|
async applyRoomStateVisibleRoutes(visibleRouteIds) { |
|
|
|
if (!visibleRouteIds || !Array.isArray(visibleRouteIds) || visibleRouteIds.length === 0) return; |
|
|
|
if (!this.routes || this.routes.length === 0) { |
|
|
|
this.roomStatePendingVisibleRouteIds = visibleRouteIds; |
|
|
|
return; |
|
|
|
} |
|
|
|
for (const routeId of visibleRouteIds) { |
|
|
|
await this.applySyncRouteVisibility(routeId, true); |
|
|
|
} |
|
|
|
}, |
|
|
|
/** 应用暂存的房间可见航线(getList 完成后调用) */ |
|
|
|
async applyRoomStatePending() { |
|
|
|
if (this.roomStatePendingVisibleRouteIds.length === 0) return; |
|
|
|
const pending = [...this.roomStatePendingVisibleRouteIds]; |
|
|
|
this.roomStatePendingVisibleRouteIds = []; |
|
|
|
await this.applyRoomStateVisibleRoutes(pending); |
|
|
|
}, |
|
|
|
/** 判断是否为当前连接发出的同步消息(避免自己发的消息再应用一次) */ |
|
|
|
isMySyncSession(senderSessionId) { |
|
|
|
if (!senderSessionId) return false; |
|
|
|
@ -1621,9 +1717,18 @@ export default { |
|
|
|
openRouteDialog(route) { |
|
|
|
this.selectedRoute = route; |
|
|
|
this.showRouteDialog = true; |
|
|
|
// 进入编辑即锁定该航线,广播给其他成员 |
|
|
|
if (this.wsConnection && this.wsConnection.sendObjectEditLock && route && route.id != null) { |
|
|
|
this.wsConnection.sendObjectEditLock('route', route.id); |
|
|
|
this.routeEditLockedId = route.id; |
|
|
|
} |
|
|
|
}, |
|
|
|
// 更新航线数据(含航点表编辑后的批量持久化) |
|
|
|
async updateRoute(updatedRoute) { |
|
|
|
if (this.isRouteLockedByOther(updatedRoute.id)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法保存'); |
|
|
|
return; |
|
|
|
} |
|
|
|
const index = this.routes.findIndex(r => r.id === updatedRoute.id); |
|
|
|
if (index === -1) return; |
|
|
|
try { |
|
|
|
@ -1732,6 +1837,10 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
async handleDeleteRoute(route) { |
|
|
|
if (this.isRouteLockedByOther(route.id)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法删除'); |
|
|
|
return; |
|
|
|
} |
|
|
|
try { |
|
|
|
// 二次确认,防止误删 |
|
|
|
await this.$confirm(`确定要彻底删除航线 "${route.name}" 吗?`, '提示', { |
|
|
|
@ -1835,6 +1944,7 @@ export default { |
|
|
|
}).catch(() => {}); |
|
|
|
} |
|
|
|
} |
|
|
|
this.$nextTick(() => this.applyRoomStatePending()); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
console.error("数据加载失败:", error); |
|
|
|
@ -1989,6 +2099,10 @@ export default { |
|
|
|
// 航点编辑弹窗相关方法 |
|
|
|
openWaypointDialog(data) { |
|
|
|
console.log(">>> [父组件接收] 编辑航点详情:", data); |
|
|
|
if (data && data.routeId != null && this.isRouteLockedByOther(data.routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法修改'); |
|
|
|
return; |
|
|
|
} |
|
|
|
// 将整个对象赋值给弹窗绑定的变量 |
|
|
|
this.selectedWaypoint = data; |
|
|
|
this.showWaypointDialog = true; |
|
|
|
@ -1996,6 +2110,11 @@ export default { |
|
|
|
/** 航点编辑保存:更新数据库并同步地图显示 */ |
|
|
|
async updateWaypoint(updatedWaypoint) { |
|
|
|
if (!this.selectedRouteDetails || !this.selectedRouteDetails.waypoints) return; |
|
|
|
const routeId = updatedWaypoint.routeId != null ? updatedWaypoint.routeId : this.selectedRouteDetails.id; |
|
|
|
if (this.isRouteLockedByOther(routeId)) { |
|
|
|
this.$message.warning('该航线正被其他成员编辑,无法保存'); |
|
|
|
return; |
|
|
|
} |
|
|
|
try { |
|
|
|
if (this.$refs.cesiumMap && updatedWaypoint.turnAngle > 0) { |
|
|
|
updatedWaypoint.turnRadius = this.$refs.cesiumMap.getWaypointRadius(updatedWaypoint); |
|
|
|
@ -3519,25 +3638,72 @@ export default { |
|
|
|
const minutes = absMin % 60; |
|
|
|
return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`; |
|
|
|
}, |
|
|
|
selectPlan(plan) { |
|
|
|
async selectPlan(plan) { |
|
|
|
if (plan && plan.id) { |
|
|
|
this.selectedPlanId = plan.id; |
|
|
|
this.selectedPlanDetails = plan; |
|
|
|
} else { |
|
|
|
this.selectedPlanId = null; |
|
|
|
this.selectedPlanDetails = null; |
|
|
|
this.selectedRouteId = null; |
|
|
|
this.selectedRouteDetails = null; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
this.selectedRouteId = null; |
|
|
|
this.selectedRouteDetails = null; |
|
|
|
this.activeRouteIds = []; |
|
|
|
// 清空地图上的航点/航线及探测区、威力区(按 routeId 逐个移除,避免误删其他实体) |
|
|
|
if (this.$refs.cesiumMap) { |
|
|
|
[...this.activeRouteIds].forEach(routeId => { |
|
|
|
this.$refs.cesiumMap.removeRouteById(routeId); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 物理清场(仅航点/航线;空域图形与房间绑定,不随方案切换) |
|
|
|
if (this.$refs.cesiumMap && this.$refs.cesiumMap.clearAllWaypoints) { |
|
|
|
this.$refs.cesiumMap.clearAllWaypoints(); |
|
|
|
// 保留 activeRouteIds,恢复显示该方案下已打开的航线(含联网同步的) |
|
|
|
const planRouteIds = this.activeRouteIds.filter(id => { |
|
|
|
const r = this.routes.find(x => x.id === id); |
|
|
|
return r && r.scenarioId === plan.id; |
|
|
|
}); |
|
|
|
if (planRouteIds.length > 0) { |
|
|
|
for (const routeId of planRouteIds) { |
|
|
|
const route = this.routes.find(r => r.id === routeId); |
|
|
|
if (!route) continue; |
|
|
|
try { |
|
|
|
const res = await getRoutes(route.id); |
|
|
|
if (res.code !== 200 || !res.data) continue; |
|
|
|
const waypoints = res.data.waypoints || []; |
|
|
|
const routeIndex = this.routes.findIndex(r => r.id === route.id); |
|
|
|
if (routeIndex > -1) { |
|
|
|
this.$set(this.routes, routeIndex, { ...this.routes[routeIndex], waypoints }); |
|
|
|
} |
|
|
|
if (waypoints.length > 0 && this.$refs.cesiumMap) { |
|
|
|
const roomId = this.currentRoomId; |
|
|
|
if (roomId && route.platformId) { |
|
|
|
try { |
|
|
|
const styleRes = await getPlatformStyle({ roomId, routeId: route.id, platformId: route.platformId }); |
|
|
|
if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(route.id, styleRes.data); |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes)); |
|
|
|
} |
|
|
|
} catch (_) {} |
|
|
|
} |
|
|
|
const firstId = planRouteIds[0]; |
|
|
|
const firstRoute = this.routes.find(r => r.id === firstId); |
|
|
|
if (firstRoute && firstRoute.waypoints) { |
|
|
|
this.selectedRouteId = firstRoute.id; |
|
|
|
this.selectedRouteDetails = { |
|
|
|
id: firstRoute.id, |
|
|
|
name: firstRoute.callSign || firstRoute.name, |
|
|
|
waypoints: firstRoute.waypoints, |
|
|
|
platformId: firstRoute.platformId, |
|
|
|
platform: firstRoute.platform, |
|
|
|
attributes: firstRoute.attributes |
|
|
|
}; |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.selectedRouteId = null; |
|
|
|
this.selectedRouteDetails = null; |
|
|
|
} |
|
|
|
console.log(`>>> [切换成功] 已进入方案: ${plan && plan.name},地图已清空,列表已展开。`); |
|
|
|
console.log(`>>> [切换成功] 已进入方案: ${plan.name},已恢复显示 ${planRouteIds.length} 条航线。`); |
|
|
|
}, |
|
|
|
/** 切换航线:实现多选/开关逻辑 */ |
|
|
|
async selectRoute(route) { |
|
|
|
|