<template>
  <!-- 以地图为绝对定位背景，所有组件浮动其上 -->
  <div class="mission-planning-container" :class="{ 'screenshot-mode': screenshotMode }">
    <!-- 地图背景：支持从右侧平台列表拖拽图标到地图 -->
    <div
      id="gis-map-background"
      class="map-background"
      @dragover.prevent="handleMapDragover"
      @drop="handleMapDrop"
    >
      <!-- cesiummap组件 -->
      <cesiumMap ref="cesiumMap" :drawDomClick="drawDom || airspaceDrawDom"
                 :tool-mode="drawDom ? 'ranging' : (airspaceDrawDom ? 'airspace' : 'airspace')"
                 :scaleConfig="scaleConfig"
                 :coordinateFormat="coordinateFormat"
                 :bottomPanelVisible="bottomPanelVisible"
                 :map-drag-enabled="mapDragEnabled"
                 :hide-map-info="sixStepsOverlayVisible"
                 :route-locked="routeLocked"
                 :route-locked-by-other-ids="routeLockedByOtherRouteIds"
                 :deduction-time-minutes="deductionMinutesFromK"
                 :room-id="currentRoomId"
                 @draw-complete="handleMapDrawComplete"
                 @route-lock-changed="handleRouteLockChanged"
                 @drawing-points-update="missionDrawingPointsCount = $event"
                 @platform-route-drawing-started="missionDrawingActive = true"
                 @drawing-cancelled="missionDrawingActive = false"
                 @open-waypoint-dialog="handleOpenWaypointEdit"
                 @open-route-dialog="handleOpenRouteEdit"
                 @copy-route="handleCopyRoute"
                 @route-copy-placed="handleRouteCopyPlaced"
                 @add-waypoint-at="handleAddWaypointAt"
                 @add-waypoint-placed="handleAddWaypointPlaced"
                 @toggle-waypoint-hold="handleToggleWaypointHold"
                 @waypoint-position-changed="handleWaypointPositionChanged"
                 @missile-deleted="handleMissileDeleted"
                 @scale-click="handleScaleClick"
                 @platform-icon-updated="onPlatformIconUpdated"
                 @platform-icon-removed="onPlatformIconRemoved"
                 @viewer-ready="onViewerReady"
                 @drawing-entities-changed="onDrawingEntitiesChanged"
                 @platform-style-saved="onPlatformStyleSaved" />
      <div v-show="!screenshotMode" class="map-overlay-text">
        <!-- <i class="el-icon-location-outline text-3xl mb-2 block"></i> -->
        <!-- <p>二维GIS地图区域</p>
        <p class="text-sm mt-1">支持标绘/航线/空域/实时态势</p> -->
      </div>
      <div v-if="missionDrawingActive && missionDrawingPointsCount >= 2 && !screenshotMode" class="mission-drawing-actions" style="position:absolute; bottom:16px; left:50%; transform:translateX(-50%); z-index:10; display:flex; gap:8px; align-items:center;">
        <span class="text-white text-sm">已 {{ missionDrawingPointsCount }} 个航点，右键结束</span>
        <el-button type="primary" size="small" @click="openAddHoldDuringDrawing">插入盘旋</el-button>
      </div>

      <!-- 地图中间的浮动红点（触发左侧菜单） -->
      <div
        v-show="!screenshotMode"
        class="floating-red-dot left-red-dot"
        :class="{ hidden: !isMenuHidden }"
        @click="showMenu"
        title="显示左侧菜单"
      >
        <div class="red-dot"></div>
        <i class="el-icon-s-unfold icon-inside"></i>
      </div>
      <el-dialog title="保存新航线" :visible.sync="showNameDialog" width="30%" :append-to-body="true" @open="onSaveRouteDialogOpen" @close="tempMapPlatform = null; saveDialogScenarioId = null">
        <el-form label-width="80px">
          <el-form-item label="航线名称">
            <el-input v-model="newRouteName" placeholder="例如：航线一"></el-input>
          </el-form-item>
          <el-form-item label="所属方案">
            <el-select v-model="saveDialogScenarioId" placeholder="请选择方案" style="width:100%" clearable>
              <el-option v-for="p in plans" :key="p.id" :label="p.name" :value="p.id" />
            </el-select>
            <div v-if="plans.length === 0" class="el-form-item__error" style="margin-top:4px;">暂无方案，请先点击地图左侧红点展开菜单，选择「方案」并新建方案后再保存。</div>
          </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
          <el-button @click="showNameDialog = false">取 消</el-button>
          <el-button type="primary" @click="confirmSaveNewRoute">确 定</el-button>
        </div>
      </el-dialog>

      <!-- 设定/修改 K 时弹窗（与 4T 一致：可拖动、记录位置、不阻挡地图） -->
      <k-time-set-dialog
        v-model="showKTimeSetDialog"
        :date-time.sync="kTimeForm.dateTime"
        :room-id="currentRoomId"
        @save="saveKTime"
      />
    </div>
    <!-- 顶部导航栏 -->
    <top-header
      v-show="!screenshotMode"
      :room-code="roomCode"
      :online-count="onlineCount"
      :combat-time="combatTime"
      :k-time-display="kTimeDisplay"
      :astro-time="astroTime"
      :room-detail="roomDetail"
      :can-set-k-time="canSetKTime"
      :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"
      @import-acd="importACD"
      @import-ato="importATO"
      @import-layer="importLayer"
      @import-route="importRoute"
      @export-plan="exportPlan"
      @route-edit="routeEdit"
      @military-marking="militaryMarking"
      @icon-edit="iconEdit"
      @toggle-icon-edit="toggleIconEditMode"
      @attribute-edit="attributeEdit"
      @time-settings="timeSettings"
      @aircraft-settings="aircraftSettings"
      @key-event-edit="keyEventEdit"
      @missile-launch="missileLaunch"
      @toggle-2d-3d="toggle2D3D"
      @toggle-ruler="toggleRuler"
      @toggle-grid="toggleGrid"
      @save-scale="saveScale"
      @load-terrain="loadTerrain"
      @change-projection="changeProjection"
      @load-aero-chart="loadAeroChart"
      @start-power-zone-drawing="startPowerZoneDrawing"
      @threat-zone="threatZone"
      @route-calculation="routeCalculation"
      @conflict-display="conflictDisplay"
      @data-materials="dataMaterials"
      @coordinate-conversion="coordinateConversion"
      @page-layout="pageLayout"
      @data-storage-path="dataStoragePath"
      @save-external-params="saveExternalParams"
      @import-airport="importAirport"
      @import-route-data="importRouteData"
      @import-landmark="importLandmark"
      @toggle-airport="toggleAirport"
      @toggle-landmark="toggleLandmark"
      @toggle-route="toggleRoute"
      @generate-gantt-chart="generateGanttChart"
      @system-description="systemDescription"
      @layer-favorites="layerFavorites"
      @route-favorites="routeFavorites"
      @show-online-members="showOnlineMembersDialog"
    />
    <!-- 左侧折叠菜单栏 - 蓝色主题 -->
    <left-menu
      v-show="!screenshotMode"
      :is-hidden="isMenuHidden"
      :menu-items="menuItems"
      :active-menu="activeMenu"
      :is-edit-mode="isIconEditMode"
      :available-icons="topNavItems"
      :position="menuPosition"
      @hide="hideMenu"
      @select="selectMenu"
      @menu-action="handleMenuAction"
      @update:menu-items="updateMenuItems"
      @drag-end="handleMenuDragEnd"
      @add-items="handleAddMenuItems"
      @delete="handleDeleteMenuItem"
      @save-menu-items="handleSaveMenuItems"
      @exit-icon-edit="exitIconEdit"
      @reset-menu-items="handleResetMenuItems"
    />
    <!-- 右侧实体列表（浮动）- 蓝色主题 -->
    <right-panel
      v-show="!screenshotMode"
      ref="rightPanel"
      :is-hidden="isRightPanelHidden"
      :active-tab="activeRightTab"
      :plans="plans"
      :selected-plan-id="selectedPlanId"
      :selected-plan-details="selectedPlanDetails"
      :selected-route-id="selectedRouteId"
      :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"
      :air-platforms="airPlatforms"
      :sea-platforms="seaPlatforms"
      :ground-platforms="groundPlatforms"
      @hide="hideRightPanel"
      @select-route="selectRoute"
      @create-route="createRoute"
      @delete-route="handleDeleteRoute"
      @select-plan="selectPlan"
      @create-plan="createPlan"
      @delete-plan="executeDeletePlan"
      @open-plan-dialog="openPlanDialog"
      @open-route-dialog="openRouteDialog"
      @open-waypoint-dialog="openWaypointDialog"
      @add-waypoint="addWaypoint"
      @cancel-route="cancelRoute"
      @toggle-route-visibility="(route, opts) => toggleRouteVisibility(route, opts)"
      @toggle-route-lock="handleToggleRouteLockFromPanel"
      @view-conflict="viewConflict"
      @resolve-conflict="resolveConflict"
      @run-conflict-check="runConflictCheck"
      @open-platform-dialog="openPlatformDialog"
      @delete-platform="handleDeletePlatform"
      @open-import-dialog="showImportDialog = true"
    />
    <!-- 左下角工具面板 -->
    <bottom-left-panel v-show="!screenshotMode" @bottom-panel-visible="handleBottomPanelVisible" @six-steps-overlay-visible="sixStepsOverlayVisible = $event" :room-id="currentRoomId" />
    <!-- 底部时间轴（最初版本的样式）- 蓝色主题 -->
    <div
      v-show="!screenshotMode"
      class="floating-timeline blue-theme"
      :class="{ 'show': showKTimePopup }"
    >
      <!-- 隐藏按钮（向下箭头） -->
      <div class="popup-hide-btn" @click="hideKTimePopup" title="隐藏K时">
        <i class="el-icon-arrow-down"></i>
      </div>
      <div class="timeline-controls">
        <div class="current-time blue-time">
          <i class="el-icon-time"></i>
          <span class="time-text">{{ currentTime }}</span>
        </div>
        <div
          ref="timelineSliderWrap"
          class="timeline-slider"
          @mousemove="onTimelineHover"
          @mouseleave="onTimelineLeave"
        >
          <div
            v-show="timelineHoverVisible"
            class="timeline-hover-time"
            :style="{ left: timelineHoverPercent + '%' }"
          >
            {{ timelineHoverTime }}
          </div>
          <el-slider
            v-model="timeProgress"
            :max="100"
            :format-tooltip="formatTimeTooltip"
            class="compact-slider blue-slider"
          />
        </div>
        <div class="playback-controls">
          <button
            class="control-btn blue-control-btn"
            @click="togglePlay"
            :title="isPlaying ? '暂停' : '播放'"
          >
            <i :class="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'"></i>
          </button>
          <el-dropdown trigger="click" @command="setPlaybackSpeed" class="speed-control">
            <span class="speed-text clickable">{{ playbackSpeed }}x<i class="el-icon-arrow-down el-icon--right"></i></span>
            <el-dropdown-menu slot="dropdown">
              <el-dropdown-item v-for="s in speedOptions" :key="s" :command="s">{{ s }}x</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      </div>
      <el-dialog :title="addHoldDialogTitle" :visible.sync="showAddHoldDialog" width="420px" append-to-body>
        <div v-if="addHoldContext" class="add-hold-tip">{{ addHoldDialogTip }}</div>
        <el-form :model="addHoldForm" label-width="100px" size="small">
          <el-form-item label="盘旋类型">
            <el-radio-group v-model="addHoldForm.holdType">
              <el-radio label="hold_circle">圆形</el-radio>
              <el-radio label="hold_ellipse">椭圆</el-radio>
            </el-radio-group>
          </el-form-item>
          <el-form-item v-if="addHoldForm.holdType === 'hold_circle'" label="半径(米)">
            <el-input-number v-model="addHoldForm.radius" :min="100" :max="50000" style="width:100%" />
          </el-form-item>
          <template v-if="addHoldForm.holdType === 'hold_ellipse'">
            <el-form-item label="跑道边长(km)">
              <el-input-number v-model="addHoldForm.edgeLengthKm" :min="1" :max="200" :step="1" style="width:100%" />
            </el-form-item>
          </template>
          <el-form-item label="盘旋方向">
            <el-radio-group v-model="addHoldForm.clockwise">
              <el-radio :label="true">顺时针</el-radio>
              <el-radio :label="false">逆时针</el-radio>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="计划离开(K时)">
            <el-input
              v-model.number="addHoldForm.startTimeMinutes"
              type="number"
              placeholder="正数=K+多少分钟，负数=K-多少分钟，留空用下一航点时间"
            />
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="showAddHoldDialog = false">取消</el-button>
          <el-button type="primary" @click="saveAddHold">确定添加</el-button>
        </span>
      </el-dialog>
    </div>

    <!-- 在线成员弹窗（与 4T 一致：可拖动、记录位置、不阻挡地图） -->
    <online-members-dialog
      v-model="showOnlineMembers"
      :online-members="wsOnlineMembers"
      :room-id="currentRoomId"
      :chat-messages="chatMessages"
      :private-chat-messages="privateChatMessages"
      :send-chat="sendChat"
      :send-private-chat="sendPrivateChat"
      :send-private-chat-history-request="sendPrivateChatHistoryRequest"
      :current-user-id="currentUserId"
    />

    <!-- 平台编辑弹窗（与 4T 一致：可拖动、记录位置、不阻挡地图） -->
    <platform-edit-dialog
      v-model="showPlatformDialog"
      :platform="selectedPlatform"
      :room-id="currentRoomId"
      @save="updatePlatform"
    />

    <!-- 航线编辑弹窗（与 4T 一致：可拖动、记录位置、不阻挡地图） -->
    <route-edit-dialog
      v-model="showRouteDialog"
      :route="selectedRoute"
      :room-id="currentRoomId"
      @save="updateRoute"
    />

    <!-- 航点编辑弹窗 -->
    <waypoint-edit-dialog
      v-model="showWaypointDialog"
      :waypoint="selectedWaypoint"
      @save="updateWaypoint"
    />



    <!-- 比例尺弹窗 -->
    <scale-dialog
      v-model="showScaleDialog"
      :scale="currentScale"
      @save="saveScale"
      @unit-change="handleScaleUnitChange"
    />

    <!-- 外部参数弹窗 -->
    <external-params-dialog
      v-model="showExternalParamsDialog"
      :external-params="currentExternalParams"
      @save="saveExternalParams"
      @import-airport="importAirport"
      @import-route-data="importRoute"
      @import-landmark="importLandmark"
    />

    <!-- 页面布局弹窗 -->
    <page-layout-dialog
      v-model="showPageLayoutDialog"
      @save="savePageLayout"
    />

    <!-- 导入平台弹窗  -->
    <PlatformImportDialog
      :visible.sync="showImportDialog"
      @confirm="handleImportConfirm"
    />

    <!-- 4T悬浮窗（THREAT/TASK/TARGET/TACTIC）- 仅点击4T图标时打开 -->
    <four-t-panel
      v-if="show4TPanel && !screenshotMode"
      :visible.sync="show4TPanel"
      :room-id="currentRoomId"
    />

    <el-dialog
      title="新建方案"
      :visible.sync="showPlanNameDialog"
      width="400px"
      append-to-body
    >
      <el-form :model="newPlanForm" ref="newPlanForm" :rules="planRules" label-width="80px">
        <el-form-item label="方案名称" prop="name">
          <el-input v-model="newPlanForm.name" placeholder="请输入方案名称"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="showPlanNameDialog = false">取 消</el-button>
        <el-button type="primary" @click="confirmCreatePlan">确 定</el-button>
      </div>
    </el-dialog>

    <!-- 截图保存弹窗：选择文件名后保存到本地（浏览器会弹出保存位置对话框） -->
    <el-dialog
      title="保存地图截图"
      :visible.sync="showScreenshotDialog"
      width="520px"
      append-to-body
      class="screenshot-save-dialog"
    >
      <div class="screenshot-preview" v-if="screenshotDataUrl">
        <img :src="screenshotDataUrl" alt="截图预览" />
      </div>
      <el-form label-width="90px">
        <el-form-item label="文件名">
          <el-input v-model="screenshotFileName" placeholder="例如：地图截图.png" />
        </el-form-item>
        <p class="screenshot-tip">点击「保存」后，浏览器将弹出保存对话框，您可选择保存路径。</p>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="showScreenshotDialog = false">取 消</el-button>
        <el-button type="primary" @click="confirmSaveScreenshot">
          <i class="el-icon-download"></i> 保 存
        </el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import cesiumMap from '@/views/cesiumMap'
import OnlineMembersDialog from '@/views/dialogs/OnlineMembersDialog'
import PlatformEditDialog from '@/views/dialogs/PlatformEditDialog'
import RouteEditDialog from '@/views/dialogs/RouteEditDialog'
import WaypointEditDialog from '@/views/dialogs/WaypointEditDialog'
import ScaleDialog from '@/views/dialogs/ScaleDialog'
import ExternalParamsDialog from '@/views/dialogs/ExternalParamsDialog'
import PageLayoutDialog from '@/views/dialogs/PageLayoutDialog'
import KTimeSetDialog from '@/views/dialogs/KTimeSetDialog'
import LeftMenu from './LeftMenu'
import RightPanel from './RightPanel'
import BottomLeftPanel from './BottomLeftPanel'
import TopHeader from './TopHeader'
import FourTPanel from './FourTPanel'
import { createRoomWebSocket } from '@/utils/websocket';
import { listScenario, addScenario, delScenario } from "@/api/system/scenario";
import { listRoutes, getRoutes, addRoutes, updateRoutes, delRoutes, getPlatformStyle, getMissileParams, updateMissilePositions } from "@/api/system/routes";
import { updateWaypoints, addWaypoints, delWaypoints } from "@/api/system/waypoints";
import { listLib,addLib,delLib} from "@/api/system/lib";
import { getRooms, updateRooms } from "@/api/system/rooms";
import { getMenuConfig, saveMenuConfig } from "@/api/system/userMenuConfig";
import { listByRoomId as listRoomPlatformIcons, addRoomPlatformIcon, updateRoomPlatformIcon, delRoomPlatformIcon } from "@/api/system/roomPlatformIcon";
import PlatformImportDialog from "@/views/dialogs/PlatformImportDialog.vue";
export default {
  name: 'MissionPlanningView',
  components: {
    PlatformImportDialog,
    cesiumMap,
    OnlineMembersDialog,
    PlatformEditDialog,
    RouteEditDialog,
    WaypointEditDialog,
    ScaleDialog,
    ExternalParamsDialog,
    PageLayoutDialog,
    KTimeSetDialog,
    LeftMenu,
    RightPanel,
    BottomLeftPanel,
    TopHeader,
    FourTPanel
  },
  data() {
    return {
      drawDom:false,
      airspaceDrawDom:false,
      /** 是否允许地图拖动（由顶部小手图标切换，默认关闭） */
      mapDragEnabled: false,
      // 在线成员弹窗
      showOnlineMembers: false,
      // 编辑弹窗控制
      showPlatformDialog: false,
      selectedPlatform: null,
      showRouteDialog: false,
      selectedRoute: null,
      showWaypointDialog: false,
      selectedWaypoint: null,
      // 比例尺、外部参数弹窗
      showScaleDialog: false,
      currentScale: {
        scaleNumerator: 1,
        scaleDenominator: 1000,
        unit: 'm'
      },
      scaleConfig: {
        scaleNumerator: 1,
        scaleDenominator: 1000,
        unit: 'm'
      },
      showExternalParamsDialog: false,
      currentExternalParams: {},
      showPageLayoutDialog: false,
      menuPosition: 'left',
      showNameDialog: false,
      newRouteName: '',
      tempMapPoints: [],
      /** 从平台右键「在此之前/在此之后插入航线」完成时传入，保存航线时用作 platformId */
      tempMapPlatform: null,
      /** 保存新航线弹窗内选择的方案 ID（弹窗内可直接选方案，无需先展开侧边） */
      saveDialogScenarioId: null,
      platformIconSaveTimer: null,
      //导入平台弹窗
      showImportDialog: false,
      // 底部面板可见状态（时间轴/六步法）
      bottomPanelVisible: false,
      // 六步法弹窗可见时隐藏地图右下角坐标和比例尺
      sixStepsOverlayVisible: false,

      // 地图截图
      screenshotMode: false,
      showScreenshotDialog: false,
      screenshotDataUrl: '',
      screenshotFileName: '',

      // 协作：谁正在编辑（锁定）哪条航线（来自 WebSocket 广播）
      routeLockedBy: {},   // routeId -> { userId, userName, nickName, sessionId }
      routeEditLockedId: null, // 本端当前编辑锁定的航线 ID，关闭弹窗时发送解锁

      // 作战信息
      roomCode: 'JTF-7-ALPHA',
      onlineCount: 0,
      wsOnlineMembers: [],
      wsConnection: null,
      chatMessages: [],
      privateChatMessages: {},
      combatTime: 'K+00:00:00', // 进入房间时固定作战时间，不随真实时间走
      astroTime: '',
      roomDetail: null,
      showKTimeSetDialog: false,
      kTimeForm: { dateTime: null },
      saveRoomDrawingsTimer: null,

      // 左侧菜单栏
      isMenuHidden: true,      // 是否完全隐藏左侧菜单
      activeMenu: 'file',
      isIconEditMode: false,   // 图标编辑模式

      // 默认菜单项配置
      defaultMenuItems: [
        { id: 'file', name: '方案', icon: 'plan' },
        { id: '4t', name: '4T', icon: 'T' },
        { id: 'start', name: '冲突', icon: 'chongtu' },
        { id: 'insert', name: '平台', icon: 'el-icon-s-platform' },
        { id: 'pattern', name: '空域', icon: 'ky' },
        { id: 'deduction', name: '推演', icon: 'el-icon-video-play' },
        { id: 'modify', name: '测距', icon: 'cj' },
        { id: 'refresh', name: '截图', icon: 'screenshot', action: 'refresh' },
        { id: 'basemap', name: '底图', icon: 'dt' },
        { id: 'datacard', name: '数据卡', icon: 'shujukapian' },
        { id: 'save', name: '保存', icon: 'el-icon-document-checked' },
        { id: 'import', name: '导入', icon: 'el-icon-upload2' },
        { id: 'export', name: '导出', icon: 'el-icon-download' }
      ],

      // 方案-航线-航点数据
      selectedPlanId: null,
      selectedPlanDetails: null,
      routes: [],
      selectedRouteId: null,
      selectedRouteDetails: null,

      showPlanNameDialog: false, // 控制新建方案弹窗
      newPlanForm: {
        name: ''
      },
      planRules: {
        name: [{ required: true, message: '请输入方案名称', trigger: 'blur' }]
      },
      // 顶部导航菜单项（用于图标选择）- 只显示指定的菜单项
      topNavItems: [
        { id: 'routeEdit', name: '航线编辑', icon: 'el-icon-edit-outline' },
        { id: 'militaryMarking', name: '军事标绘', icon: 'el-icon-crop' },
        { id: 'attributeEdit', name: '属性修改', icon: 'el-icon-setting' },
        { id: 'timeSettings', name: '时间设置', icon: 'el-icon-timer' },
        { id: 'aircraftSettings', name: '机型设置', icon: 'el-icon-s-custom' },
        { id: 'keyEventEdit', name: '关键事件编辑', icon: 'el-icon-edit' },
        { id: 'missileLaunch', name: '导弹发射', icon: 'el-icon-s-promotion' },
        { id: 'toggle2D3D', name: '2D/3D切换', icon: 'el-icon-picture-outline' },
        { id: 'toggleRuler', name: '显示/隐藏标尺', icon: 'el-icon-set-up' },
        { id: 'toggleGrid', name: '网格', icon: 'el-icon-s-grid' },
        { id: 'toggleScale', name: '比例尺', icon: 'el-icon-data-line' },
        { id: 'loadTerrain', name: '加载/切换地形', icon: 'el-icon-picture-outline-round' },
        { id: 'changeProjection', name: '投影', icon: 'el-icon-aim' },
        { id: 'loadAeroChart', name: '航空图', icon: 'el-icon-map-location' },
        { id: 'powerZone', name: '威力区', icon: 'el-icon-warning-outline' },
        { id: 'threatZone', name: '威胁区', icon: 'el-icon-warning' },
        { id: 'routeCalculation', name: '航线计算', icon: 'el-icon-s-data' },
        { id: 'conflictDisplay', name: '冲突显示', icon: 'el-icon-error' },
        { id: 'dataMaterials', name: '数据资料', icon: 'el-icon-folder' },
        { id: 'coordinateConversion', name: '坐标换算', icon: 'el-icon-coordinate' },
        { id: 'pageLayout', name: '页面布局', icon: 'el-icon-menu' },
        { id: 'dataStoragePath', name: '数据存储路径', icon: 'el-icon-folder-opened' },
        { id: 'externalParams', name: '外部参数', icon: 'el-icon-s-tools' },
        { id: 'toggleAirport', name: '显示/隐藏机场', icon: 'el-icon-s-flag' },
        { id: 'toggleLandmark', name: '显示/隐藏地标', icon: 'el-icon-location-outline' },
        { id: 'toggleRoute', name: '显示/隐藏航线', icon: 'el-icon-position' },
        { id: 'systemDescription', name: '系统说明', icon: 'el-icon-info' },
        { id: 'layerFavorites', name: '图层收藏', icon: 'el-icon-star-off' },
        { id: 'routeFavorites', name: '航线收藏', icon: 'el-icon-collection' }
      ],

      // 右侧面板控制
      isRightPanelHidden: true, // 是否完全隐藏右侧面板
      // K时弹出框控制
      showKTimePopup: false,
      // 4T悬浮窗显示控制（仅点击4T图标时打开/关闭）
      show4TPanel: false,

      // 显示/隐藏控制
      showAirport: true,
      showLandmark: true,
      showRoute: true,

      // 坐标格式控制
      coordinateFormat: 'dms', // 'decimal' 或 'dms'

      menuItems: [],

      // 右侧面板
      currentRoomId: null,
      plans: [],
      activeRightTab: 'plan',
      activeRouteIds: [], // 存储当前所有选中的航线ID
      /** 新加入时收到的房间可见航线 ID，若 routes 未加载则暂存，getList 完成后应用 */
      roomStatePendingVisibleRouteIds: [],
      /** 航线上锁状态：routeId -> true 上锁，与地图右键及右侧列表锁图标同步 */
      routeLocked: {},
      // 冲突数据（由 runConflictCheck 根据当前航线与时间轴计算真实问题）
      conflictCount: 0,
      conflicts: [],

      // 平台数据
      activePlatformTab: 'air',
      airPlatforms: [],
      seaPlatforms: [],
      groundPlatforms: [],

      // 时间控制（与进入房间固定作战时间 K+00:00:00 一致）
      timeProgress: 0,
      currentTime: 'K+00:00:00',
      deductionMinutesFromK: 0,
      deductionWarnings: [],
      deductionEarlyArrivalByRoute: {}, // routeId -> earlyArrivalLegs
      showAddHoldDialog: false,
      addHoldContext: null, // { routeId, routeName, legIndex, fromName, toName }
      addHoldForm: { holdType: 'hold_circle', radius: 15000, edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: null },
      missionDrawingActive: false,
      missionDrawingPointsCount: 0,
      isPlaying: false,
      playbackSpeed: 1,
      speedOptions: [64, 32, 16, 8, 4, 2, 1],
      playbackInterval: null,
      /** 播放时上一帧时间戳（毫秒），用于按真实经过时间推进，避免 setInterval 不准导致时间轴变慢 */
      _playbackLastTickTime: null,
      timelineHoverTime: '',
      timelineHoverVisible: false,
      timelineHoverPercent: 0,
      /** 导弹从 Redis 加载过的房间+航线组合 key，避免重复加载 */
      _missilesLoadKey: null,

      // 用户
      userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
      /** 当前连接的 WebSocket sessionId 集合（用于过滤自己发出的同步消息，避免重复应用） */
      mySyncSessionIds: [],
    };
  },
  watch: {
    '$route.query.roomId': {
      handler(newRoomId) {
        if (newRoomId != null && String(newRoomId) !== String(this.currentRoomId)) {
          this.currentRoomId = newRoomId;
          this.connectRoomWebSocket();
          if (newRoomId) this.getRoomDetail();
        }
      }
    },
    timeProgress: {
      handler() {
        this.updateTimeFromProgress();
      },
      immediate: true
    },
    /** 点开航线时：用当前固定作战时间同步时间轴与图标位置 */
    activeRouteIds: {
      handler(ids) {
        if (!ids || ids.length === 0) return;
        this.$nextTick(() => {
          const minutes = this.combatTimeToMinutes(this.combatTime);
          const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
          const span = Math.max(0, maxMinutes - minMinutes) || 120;
          const progress = Math.max(0, Math.min(100, ((minutes - minMinutes) / span) * 100));
          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: {
    currentUserId() {
      const id = this.$store.getters.id;
      return id != null ? Number(id) : null;
    },
    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;
    },
    /** 格式化的 K 时（基准时刻），供右上角显示 */
    kTimeDisplay() {
      if (!this.roomDetail || !this.roomDetail.kAnchorTime) return '';
      return this.formatKTimeForPicker(this.roomDetail.kAnchorTime) || '';
    },
    hasEarlyArrivalLegs() {
      return this.activeRouteIds.some(rid => (this.deductionEarlyArrivalByRoute[rid] || []).length > 0);
    },
    addHoldDialogTitle() {
      if (!this.addHoldContext) return '添加盘旋';
      if (this.addHoldContext.mode === 'drawing') return '在最后两航点之间插入盘旋';
      return '在此航段添加盘旋';
    },
    addHoldDialogTip() {
      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() {
    this.getList();
    this.connectRoomWebSocket();
    // 初始化时左侧菜单隐藏
    this.isMenuHidden = true;
    // 初始化时右侧面板隐藏
    this.isRightPanelHidden = true;
    // 初始化菜单项为默认配置，再尝试加载当前用户已保存的配置
    this.menuItems = [...this.defaultMenuItems];
    this.loadUserMenuConfig();

    // 更新时间
    this.updateTime();
    setInterval(this.updateTime, 1000);
    // 作战时间也需要实时更新
    setInterval(this.updateCombatTime, 1000);
  },
  beforeDestroy() {
    this.disconnectRoomWebSocket();
    // 清除播放定时器
    if (this.playbackInterval) {
      clearInterval(this.playbackInterval);
      this.playbackInterval = null;
    }
  },
  created() {
    this.currentRoomId = this.$route.query.roomId;
    console.log("从路由接收到的真实房间 ID:", this.currentRoomId);
    this.getList();
    this.getPlatformList();
    if (this.currentRoomId) this.getRoomDetail();
  },
  methods: {
    handleBottomPanelVisible(visible) {
      this.bottomPanelVisible = visible
    },
    // 处理从地图点击传来的编辑请求（支持 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);
          if (response.code === 200 && response.data && response.data.waypoints) {
            const wp = response.data.waypoints[waypointIndex];
            if (wp) wpId = wp.id;
          }
        } catch (e) {}
      }
      if (wpId == null || wpId === undefined) {
        this.$message.info('未找到该航点');
        return;
      }
      console.log(`>>> [父组件接收] 航点 ID: ${wpId}, 所属航线 ID: ${routeId}`);
      // 如果点击的点不属于当前选中的航线
      if (this.selectedRouteId != routeId) {
        console.log(">>> [自动切换] 正在获取新航线详情...");
        try {
          const response = await getRoutes(routeId);
          if (response.code === 200 && response.data) {
            const fullRouteData = response.data;
            const fromList = this.routes.find(r => r.id === routeId);
            // 同步更新父组件状态，合并 list 中的 platform 以便拖拽后重绘平台不丢失
            this.selectedRouteId = fullRouteData.id;
            this.selectedRouteDetails = {
              id: fullRouteData.id,
              name: fullRouteData.callSign,
              waypoints: fullRouteData.waypoints || [],
              platformId: fromList?.platformId,
              platform: fromList?.platform,
              attributes: fromList?.attributes
            };
          }
        } catch (error) {
          this.$message.error('切换航线数据失败');
          return;
        }
      }
      // 此时账本已确保是最新且正确的，开始找点
      const wpData = this.selectedRouteDetails.waypoints.find(item => item.id == wpId);
      if (wpData) {
        // 成功找到业务数据，深拷贝后打开弹窗
        this.selectedWaypoint = JSON.parse(JSON.stringify(wpData));
        this.showWaypointDialog = true;
      } else {
        this.$message.info('未找到该航点的业务数据');
        console.error("查找失败！账本内IDs:", this.selectedRouteDetails.waypoints.map(w => w.id));
      }
      // 获取当前最新的航点列表
      const waypointsList = this.selectedRouteDetails.waypoints;
      // 找到点在数组里的索引
      const index = waypointsList.findIndex(item => item.id == wpId);
      const total = waypointsList.length;
      if (index !== -1) {
        const wpData = waypointsList[index];
        this.selectedWaypoint = {
          ...JSON.parse(JSON.stringify(wpData)),
          currentIndex: index,
          totalPoints: total
        };
        console.log(`>>> [地图触发锁定] 序号: ${index}, 总数: ${total}, 数据已装载`);
        this.showWaypointDialog = true;
      } else {
        this.$message.info('未找到该航点的业务数据');
        console.error("查找失败！账本内IDs:", waypointsList.map(w => w.id));
      }
    },
    /** 删除平台库数据 */
    handleDeletePlatform(platform) {
      // 安全确认：防止误操作
      this.$modal.confirm('是否确认删除名称为 "' + platform.name + '" 的平台数据？').then(() => {
        // 调用后端接口删除
        return delLib(platform.id);
      }).then(() => {
        // 删除成功后的反馈
        this.$modal.msgSuccess("删除成功");
        //重新分拣列表，让页面上的卡片消失
        this.getPlatformList();
      }).catch(() => {
      });
    },

    /** 该航线是否被其他成员编辑锁定（非自己） */
    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) {
          const routeData = response.data;
          // 构造航线对象，保持和右侧面板编辑时一致的格式
          const route = {
            id: routeData.id,
            name: routeData.callSign,
            scenarioId: routeData.scenarioId,
            platformId: routeData.platformId,
            platform: routeData.platform,
            attributes: routeData.attributes,
            points: routeData.waypoints ? routeData.waypoints.length : 0,
            waypoints: routeData.waypoints || []
          };
          // 打开航线编辑弹窗
          this.openRouteDialog(route);
        }
      } catch (error) {
        this.$message.error('获取航线数据失败');
        console.error('获取航线数据失败:', error);
      }
    },

    /** 右键航点“向前/向后增加航点”：进入放置模式，传入 waypoints 给地图预览 */
    handleAddWaypointAt({ routeId, waypointIndex, mode }) {
      if (this.isRouteLockedByOther(routeId)) {
        this.$message.warning('该航线正被其他成员编辑，无法添加航点');
        return;
      }
      if (this.routeLocked[routeId]) {
        this.$message.info('该航线已上锁，请先解锁');
        return;
      }
      const route = this.routes.find(r => r.id === routeId);
      if (!route || !route.waypoints || route.waypoints.length === 0) {
        this.$message.warning('航线无航点数据');
        return;
      }
      if (!this.$refs.cesiumMap || typeof this.$refs.cesiumMap.startAddWaypointAt !== 'function') return;
      this.$refs.cesiumMap.startAddWaypointAt(routeId, waypointIndex, mode, route.waypoints);
    },

    /** 地图放置新航点后：调用 addWaypoints 插入，再按插入位置重排 seq 并重绘。向后添加时：当前第 K 个，新航点为第 K+1 个，原第 K+1 个及以后依次后移。 */
    async handleAddWaypointPlaced({ routeId, waypointIndex, mode, position }) {
      const route = this.routes.find(r => r.id === routeId);
      if (!route || !route.waypoints) {
        this.$message.warning('航线不存在或无航点');
        return;
      }
      const waypoints = route.waypoints;
      const refWp = waypoints[waypointIndex];
      // 向后添加：insertIndex = waypointIndex+1，新航点成为第 (waypointIndex+2) 个，原第 waypointIndex+2 个变为第 waypointIndex+3 个
      const insertIndex = mode === 'before' ? waypointIndex : waypointIndex + 1;
      const prevWp = insertIndex > 0 ? waypoints[insertIndex - 1] : null;
      const startTime = prevWp && prevWp.startTime ? prevWp.startTime : 'K+00:00:00';
      const count = waypoints.length + 1;
      const newName = `WP${insertIndex + 1}`;
      try {
        const addRes = await addWaypoints({
          routeId,
          name: newName,
          seq: insertIndex + 1,
          lat: position.lat,
          lng: position.lng,
          alt: position.alt,
          speed: (refWp && refWp.speed != null) ? refWp.speed : 800,
          startTime,
          turnAngle: (refWp && refWp.turnAngle != null) ? refWp.turnAngle : (insertIndex === 0 || insertIndex === count - 1 ? 0 : 45)
        });
        await this.getList();
        let updated = this.routes.find(r => r.id === routeId);
        if (!updated || !updated.waypoints || updated.waypoints.length !== count) {
          this.$message.warning('添加航点后数据未刷新');
          return;
        }
        const list = updated.waypoints;
        const prevIds = new Set(waypoints.map(w => w.id));
        const newWp = (addRes && addRes.data && addRes.data.id != null && list.find(w => w.id === addRes.data.id))
          ? list.find(w => w.id === addRes.data.id)
          : list.find(w => !prevIds.has(w.id)) || list[list.length - 1];
        if (!newWp) {
          this.$message.warning('未找到新插入的航点');
          return;
        }
        const others = list.filter(w => w.id !== newWp.id);
        others.sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0));
        const reordered = [...others.slice(0, insertIndex), newWp, ...others.slice(insertIndex)];
        const routeInListFirst = this.routes.find(r => r.id === routeId);
        if (routeInListFirst) routeInListFirst.waypoints = reordered;
        if (this.selectedRouteId === routeId) this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints: reordered };
        if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
          this.$refs.cesiumMap.removeRouteById(routeId);
          this.$refs.cesiumMap.renderRouteWaypoints(reordered, routeId, routeInListFirst?.platformId, routeInListFirst?.platform, this.parseRouteStyle(routeInListFirst?.attributes || route?.attributes));
          this.$nextTick(() => this.updateDeductionPositions());
        }
        const isHold = (w) => (w.pointType || w.point_type) === 'hold_circle' || (w.pointType || w.point_type) === 'hold_ellipse';
        for (let i = 0; i < reordered.length; i++) {
          const wp = reordered[i];
          const newSeq = i + 1;
          const isNewlyInserted = wp.id === newWp.id;
          const nameToUse = isNewlyInserted ? (isHold(wp) ? `HOLD${newSeq}` : `WP${newSeq}`) : (wp.name || (isHold(wp) ? `HOLD${newSeq}` : `WP${newSeq}`));
          await updateWaypoints({
            id: wp.id,
            routeId,
            name: nameToUse,
            seq: newSeq,
            lat: wp.lat,
            lng: wp.lng,
            alt: wp.alt,
            speed: wp.speed,
            startTime: wp.startTime,
            turnAngle: wp.turnAngle,
            ...(wp.pointType != null && { pointType: wp.pointType }),
            ...(wp.holdParams != null && { holdParams: wp.holdParams }),
            ...(wp.labelFontSize != null && { labelFontSize: wp.labelFontSize }),
            ...(wp.labelColor != null && { labelColor: wp.labelColor })
          });
        }
        await this.getList();
        updated = this.routes.find(r => r.id === routeId);
        if (!updated || !updated.waypoints) {
          this.$message.warning('刷新后未拿到航线航点');
          return;
        }
        const sortedWaypoints = updated.waypoints.slice().sort((a, b) => (Number(a.seq) || 0) - (Number(b.seq) || 0));
        updated.waypoints = sortedWaypoints;
        const routeInList = this.routes.find(r => r.id === routeId);
        if (routeInList) routeInList.waypoints = sortedWaypoints;
        if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
          const roomId = this.currentRoomId;
          if (roomId && updated.platformId) {
            try {
              const styleRes = await getPlatformStyle({ roomId, routeId, platformId: updated.platformId });
              if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
            } catch (_) {}
          }
          this.$refs.cesiumMap.removeRouteById(routeId);
          this.$refs.cesiumMap.renderRouteWaypoints(sortedWaypoints, routeId, updated.platformId, updated.platform, this.parseRouteStyle(updated.attributes));
          this.$nextTick(() => this.updateDeductionPositions());
          if (roomId && sortedWaypoints.length > 0) {
            this.updateMissilePositionsAfterRouteEdit(roomId, routeId, updated.platformId != null ? updated.platformId : 0, sortedWaypoints);
          }
        }
        if (this.selectedRouteId === routeId) {
          this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints: sortedWaypoints };
        }
        this.$message.success('已添加航点');
        this.wsConnection?.sendSyncWaypoints?.(routeId);
      } catch (e) {
        this.$message.error(e.msg || e.message || '添加航点失败');
        console.error(e);
      }
    },

    /** 右键航点“切换盘旋航点”：普通航点设为盘旋（圆形默认），盘旋航点设为普通；支持首尾航点 */
    async handleToggleWaypointHold({ routeId, dbId, waypointIndex }) {
      if (this.isRouteLockedByOther(routeId)) {
        this.$message.warning('该航线正被其他成员编辑，无法修改');
        return;
      }
      if (this.routeLocked[routeId]) {
        this.$message.info('该航线已上锁，请先解锁');
        return;
      }
      let route = this.routes.find(r => r.id === routeId);
      let waypoints = route && route.waypoints;
      if (!waypoints || waypoints.length === 0) {
        try {
          const res = await getRoutes(routeId);
          if (res.code === 200 && res.data && res.data.waypoints) {
            waypoints = res.data.waypoints;
            route = { ...route, waypoints };
          }
        } catch (e) {
          this.$message.error('获取航线失败');
          return;
        }
      }
      if (!waypoints || waypoints.length === 0) {
        this.$message.warning('航线无航点');
        return;
      }
      const wp = dbId != null ? waypoints.find(w => w.id === dbId) : waypoints[waypointIndex];
      if (!wp) {
        this.$message.warning('未找到该航点');
        return;
      }
      const index = waypoints.indexOf(wp);
      const total = waypoints.length;
      const isFirstOrLast = index === 0 || index === total - 1;
      const isHold = this.isHoldWaypoint(wp);
      let pointType;
      let holdParams;
      let turnAngle;
      const preserveTurnAngle = () => isFirstOrLast ? 0 : (wp.turnAngle != null && wp.turnAngle !== '' ? Number(wp.turnAngle) : 0);
      if (isHold) {
        pointType = 'normal';
        holdParams = null;
        turnAngle = preserveTurnAngle();
      } else {
        pointType = 'hold_circle';
        holdParams = JSON.stringify({ radius: 15000, clockwise: true });
        turnAngle = preserveTurnAngle();
      }
      try {
        const payload = {
          id: wp.id,
          routeId,
          name: wp.name,
          seq: wp.seq,
          lat: wp.lat,
          lng: wp.lng,
          alt: wp.alt,
          speed: wp.speed,
          startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00',
          turnAngle,
          pointType
        };
        if (holdParams != null) payload.holdParams = holdParams;
        else payload.holdParams = '';
        if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
        if (wp.labelColor != null) payload.labelColor = wp.labelColor;
        if (turnAngle > 0 && this.$refs.cesiumMap) {
          payload.turnRadius = this.$refs.cesiumMap.getWaypointRadius(payload);
        } else {
          payload.turnRadius = 0;
        }
        const response = await updateWaypoints(payload);
        if (response.code !== 200) throw new Error(response.msg || '更新失败');
        const merged = { ...wp, ...payload };
        const routeInList = this.routes.find(r => r.id === routeId);
        if (routeInList && routeInList.waypoints) {
          const idx = routeInList.waypoints.findIndex(p => p.id === wp.id);
          if (idx !== -1) routeInList.waypoints.splice(idx, 1, merged);
        }
        if (this.selectedRouteId === routeId && this.selectedRouteDetails && this.selectedRouteDetails.waypoints) {
          const idxS = this.selectedRouteDetails.waypoints.findIndex(p => p.id === wp.id);
          if (idxS !== -1) this.selectedRouteDetails.waypoints.splice(idxS, 1, merged);
        }
        if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
          const r = this.routes.find(rr => rr.id === routeId);
          if (r && r.waypoints) {
            const roomId = this.currentRoomId;
            if (roomId && r.platformId) {
              try {
                const styleRes = await getPlatformStyle({ roomId, routeId, platformId: r.platformId });
                if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
              } catch (_) {}
            }
            const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
            if (r.waypoints.some(wp => this.isHoldWaypoint(wp))) {
              this.getPositionAtMinutesFromK(r.waypoints, minMinutes, minMinutes, maxMinutes, routeId);
            }
            this.$refs.cesiumMap.removeRouteById(routeId);
            this.$refs.cesiumMap.renderRouteWaypoints(r.waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes));
            this.$nextTick(() => this.updateDeductionPositions());
            if (roomId && r.waypoints && r.waypoints.length > 0) {
              this.updateMissilePositionsAfterRouteEdit(roomId, routeId, r.platformId != null ? r.platformId : 0, r.waypoints);
            }
          }
        }
        this.$message.success(isHold ? '已设为普通航点' : '已设为盘旋航点');
        this.wsConnection?.sendSyncWaypoints?.(routeId);
      } catch (e) {
        this.$message.error(e.msg || e.message || '切换失败');
        console.error(e);
      }
    },

    /** 右键「复制航线」：拉取航点后进入复制预览，左键放置后弹窗保存 */
    async handleCopyRoute(routeId) {
      try {
        const res = await getRoutes(routeId);
        if (res.code !== 200 || !res.data) {
          this.$message.error('获取航线数据失败');
          return;
        }
        const waypoints = res.data.waypoints || [];
        if (waypoints.length < 2) {
          this.$message.warning('航线航点不足，无法复制');
          return;
        }
        if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.startRouteCopyPreview === 'function') {
          this.$refs.cesiumMap.startRouteCopyPreview(waypoints);
          this.$message.info('移动鼠标到目标位置，左键放置复制航线；右键取消');
        }
      } catch (e) {
        this.$message.error('获取航线数据失败');
        console.error(e);
      }
    },

    /** 复制航线已放置：用当前偏移后的航点打开「保存新航线」弹窗 */
    handleRouteCopyPlaced(points) {
      this.tempMapPoints = points || [];
      this.tempMapPlatform = null;
      this.showNameDialog = true;
    },

    /** 地图上拖拽航点结束：将新位置写回数据库并刷新显示 */
    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) {
        waypoints = this.selectedRouteDetails.waypoints;
        route = this.selectedRouteDetails;
      }
      if (!waypoints) {
        const r = this.routes.find(r => r.id === routeId);
        if (r && r.waypoints) {
          waypoints = r.waypoints;
          route = r;
        }
      }
      if (!waypoints || !route) {
        this.$message.error('未找到对应航线数据');
        return;
      }
      const wp = waypoints.find(p => p.id === dbId);
      if (!wp) {
        this.$message.error('未找到对应航点');
        return;
      }
      const payload = {
        id: wp.id,
        routeId: wp.routeId != null ? wp.routeId : routeId,
        name: wp.name,
        seq: wp.seq,
        lat: Number(lat),
        lng: Number(lng),
        alt: Number(alt),
        speed: wp.speed,
        startTime: (wp.startTime != null && wp.startTime !== '') ? wp.startTime : 'K+00:00:00',
        turnAngle: wp.turnAngle
      };
      if (wp.pointType != null) payload.pointType = wp.pointType;
      if (wp.holdParams != null) payload.holdParams = wp.holdParams;
      if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
      if (wp.labelColor != null) payload.labelColor = wp.labelColor;
      if (wp.segmentMode != null) payload.segmentMode = wp.segmentMode;
      if (wp.color != null) payload.color = wp.color;
      if (wp.pixelSize != null) payload.pixelSize = wp.pixelSize;
      if (wp.outlineColor != null) payload.outlineColor = wp.outlineColor;
      try {
        const response = await updateWaypoints(payload);
        if (response.code === 200) {
          const merged = { ...wp, ...payload };
          const idx = waypoints.findIndex(p => p.id === dbId);
          if (idx !== -1) waypoints.splice(idx, 1, merged);
          if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
            const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId);
            if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged);
          }
          // 定速/定时：根据拖拽后的新位置重算相对K时或上一航点速度
          const routeForPlatform = this.routes.find(r => r.id === routeId) || route;
          if (idx > 0) {
            const prev = waypoints[idx - 1];
            const distM = this.segmentDistance(
              { lat: prev.lat, lng: prev.lng, alt: prev.alt },
              { lat: merged.lat, lng: merged.lng, alt: merged.alt }
            );
            const prevMinutes = this.waypointStartTimeToMinutesDecimal(prev.startTime);
            if (prev.segmentMode === 'fixed_speed') {
              const speedKmh = Number(prev.segmentTargetSpeed ?? prev.speed) || 800;
              const newMinutesFromK = prevMinutes + (distM / 1000) / speedKmh * 60;
              const newStartTime = this.minutesToStartTimeWithSeconds(newMinutesFromK);
              const startPayload = { ...merged, startTime: newStartTime };
              if (merged.segmentMode != null) startPayload.segmentMode = merged.segmentMode;
              try {
                const r2 = await updateWaypoints(startPayload);
                if (r2.code === 200) {
                  Object.assign(merged, { startTime: newStartTime });
                  if (idx !== -1) waypoints.splice(idx, 1, merged);
                  if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
                    const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId);
                    if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged);
                  }
                }
              } catch (e) {
                console.warn('定速重算相对K时失败', e);
              }
            } else if (merged.segmentMode === 'fixed_time') {
              const currMinutes = (merged.segmentTargetMinutes != null && merged.segmentTargetMinutes !== '') ? Number(merged.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(merged.startTime);
              const deltaMin = currMinutes - prevMinutes;
              if (deltaMin > 0.001) {
                const newSpeedKmh = (distM / 1000) / (deltaMin / 60);
                const speedVal = Math.round(newSpeedKmh * 10) / 10;
                const speedPayload = { ...prev, speed: speedVal };
                if (prev.segmentMode != null) speedPayload.segmentMode = prev.segmentMode;
                try {
                  const r2 = await updateWaypoints(speedPayload);
                  if (r2.code === 200) {
                    Object.assign(prev, { speed: speedVal });
                    const prevIdx = idx - 1;
                    waypoints.splice(prevIdx, 1, prev);
                    if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
                      const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === prev.id);
                      if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, prev);
                    }
                  }
                } catch (e) {
                  console.warn('定时重算上一航点速度失败', e);
                }
              }
            }
          }
          // 下一航点是定时点时：重算本航点（被拖动的）速度，使平台能在下一航点的 K 时到达
          if (idx >= 0 && idx < waypoints.length - 1) {
            const next = waypoints[idx + 1];
            if (next.segmentMode === 'fixed_time') {
              const distToNextM = this.segmentDistance(
                { lat: merged.lat, lng: merged.lng, alt: merged.alt },
                { lat: next.lat, lng: next.lng, alt: next.alt }
              );
              const currMinutes = this.waypointStartTimeToMinutesDecimal(merged.startTime);
              const nextMinutes = (next.segmentTargetMinutes != null && next.segmentTargetMinutes !== '') ? Number(next.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(next.startTime);
              const deltaMin = nextMinutes - currMinutes;
              if (deltaMin > 0.001) {
                const newSpeedKmh = (distToNextM / 1000) / (deltaMin / 60);
                const speedVal = Math.round(newSpeedKmh * 10) / 10;
                const currPayload = { ...merged, speed: speedVal };
                if (merged.segmentMode != null) currPayload.segmentMode = merged.segmentMode;
                if (merged.labelFontSize != null) currPayload.labelFontSize = merged.labelFontSize;
                if (merged.labelColor != null) currPayload.labelColor = merged.labelColor;
                if (merged.color != null) currPayload.color = merged.color;
                if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize;
                if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor;
                try {
                  const r2 = await updateWaypoints(currPayload);
                  if (r2.code === 200) {
                    Object.assign(merged, { speed: speedVal });
                    waypoints.splice(idx, 1, merged);
                    if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
                      const i = this.selectedRouteDetails.waypoints.findIndex(p => p.id === dbId);
                      if (i !== -1) this.selectedRouteDetails.waypoints.splice(i, 1, merged);
                    }
                  }
                } catch (e) {
                  console.warn('下一航点为定时点重算本航点速度失败', e);
                }
              }
            }
          }
          if (this.$refs.cesiumMap) {
            const roomId = this.currentRoomId;
            if (roomId && routeForPlatform.platformId) {
              try {
                const styleRes = await getPlatformStyle({ roomId, routeId, platformId: routeForPlatform.platformId });
                if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
              } catch (_) {}
            }
            this.$refs.cesiumMap.renderRouteWaypoints(
              waypoints,
              routeId,
              routeForPlatform.platformId,
              routeForPlatform.platform,
              this.parseRouteStyle(route.attributes)
            );
          }
          this.$message.success('航点位置已更新');
          this.$nextTick(() => this.updateDeductionPositions());
          this.wsConnection?.sendSyncWaypoints?.(routeId);
          // 航点拖拽后，根据新位置重算导弹发射位置并更新 Redis
          if (this.currentRoomId && routeForPlatform && waypoints.length > 0) {
            this.updateMissilePositionsAfterRouteEdit(this.currentRoomId, routeId, routeForPlatform.platformId != null ? routeForPlatform.platformId : 0, waypoints);
          }
        } else {
          throw new Error(response.msg || '更新失败');
        }
      } catch (error) {
        console.error('更新航点位置失败:', error);
        this.$message.error(error.message || '更新失败，请重试');
      }
    },

    // 显示在线成员弹窗
    showOnlineMembersDialog() {
      this.showOnlineMembers = true;
    },
    sendChat(content) {
      if (this.wsConnection && this.wsConnection.sendChat) {
        this.wsConnection.sendChat(content);
      }
    },
    sendPrivateChatHistoryRequest(targetUserId) {
      if (this.wsConnection && this.wsConnection.sendPrivateChatHistoryRequest) {
        this.wsConnection.sendPrivateChatHistoryRequest(targetUserId);
      }
    },
    normalizeChatMessages(messages) {
      if (!messages || !Array.isArray(messages)) return [];
      return messages.map(m => {
        if (typeof m === 'string') {
          try {
            return JSON.parse(m);
          } catch (e) {
            return null;
          }
        }
        return m;
      }).filter(Boolean);
    },
    sendPrivateChat(targetUserId, targetUserName, content) {
      if (!this.wsConnection || !this.wsConnection.sendPrivateChat) return;
      this.wsConnection.sendPrivateChat(targetUserId, targetUserName, content);
      const sender = {
        userId: this.currentUserId,
        userName: this.$store.getters.name,
        nickName: this.$store.getters.nickName || this.$store.getters.name,
        avatar: this.$store.getters.avatar || ''
      };
      const msg = { type: 'PRIVATE_CHAT', sender, targetUserId, targetUserName, content, timestamp: Date.now() };
      const list = this.privateChatMessages[targetUserId] || [];
      this.$set(this.privateChatMessages, targetUserId, [...list, msg]);
    },
    connectRoomWebSocket() {
      if (!this.currentRoomId) return;
      this.disconnectRoomWebSocket();
      const baseUrl = process.env.VUE_APP_BACKEND_URL || (window.location.origin + (process.env.VUE_APP_BASE_API || ''));
      this.wsConnection = createRoomWebSocket({
        roomId: this.currentRoomId,
        onMembers: (members) => {
          this.wsOnlineMembers = members.map(m => ({
            id: m.sessionId || m.userId,
            userId: m.userId,
            userName: m.userName,
            name: m.nickName || m.userName,
            role: m.role === 'owner' ? '房主' : (m.role === 'admin' ? '管理员' : '成员'),
            status: '在线',
            isEditing: false,
            avatar: m.avatar ? (m.avatar.startsWith('http') ? m.avatar : (baseUrl + m.avatar)) : ''
          }));
          this.onlineCount = this.wsOnlineMembers.length;
          const myId = this.$store.getters.id;
          this.mySyncSessionIds = (members || []).filter(m => myId != null && String(m.userId) === String(myId)).map(m => m.sessionId).filter(Boolean);
        },
        onMemberJoined: (member) => {
          const baseUrl = (process.env.VUE_APP_BACKEND_URL || (window.location.origin + (process.env.VUE_APP_BASE_API || '')));
          const m = {
            id: member.sessionId || member.userId,
            userId: member.userId,
            userName: member.userName,
            name: member.nickName || member.userName,
            role: member.role === 'owner' ? '房主' : (member.role === 'admin' ? '管理员' : '成员'),
            status: '在线',
            isEditing: false,
            avatar: member.avatar ? (member.avatar.startsWith('http') ? member.avatar : (baseUrl + member.avatar)) : ''
          };
          if (!this.wsOnlineMembers.find(x => x.id === m.id)) {
            this.wsOnlineMembers = [...this.wsOnlineMembers, m];
            this.onlineCount = this.wsOnlineMembers.length;
          }
          const myId = this.$store.getters.id;
          if (myId != null && String(member.userId) === String(myId) && member.sessionId) {
            if (!this.mySyncSessionIds.includes(member.sessionId)) {
              this.mySyncSessionIds = [...this.mySyncSessionIds, member.sessionId];
            }
          }
        },
        onMemberLeft: (member, sessionId) => {
          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];
        },
        onPrivateChat: (msg) => {
          const senderId = msg.sender && msg.sender.userId;
          if (!senderId) return;
          const list = this.privateChatMessages[senderId] || [];
          this.$set(this.privateChatMessages, senderId, [...list, msg]);
        },
        onChatHistory: (messages) => {
          const history = this.normalizeChatMessages(messages);
          const maxTs = history.length ? Math.max(...history.map(m => m.timestamp || 0)) : 0;
          const newer = this.chatMessages.filter(m => (m.timestamp || 0) > maxTs);
          this.chatMessages = [...history, ...newer];
        },
        onPrivateChatHistory: (targetUserId, messages) => {
          const history = this.normalizeChatMessages(messages);
          const existing = this.privateChatMessages[targetUserId] || [];
          const maxTs = history.length ? Math.max(...history.map(m => m.timestamp || 0)) : 0;
          const newer = existing.filter(m => (m.timestamp || 0) > maxTs);
          this.$set(this.privateChatMessages, targetUserId, [...history, ...newer]);
        },
        onSyncRouteVisibility: (routeId, visible, senderSessionId) => {
          if (this.isMySyncSession(senderSessionId)) return;
          this.applySyncRouteVisibility(routeId, visible);
        },
        onSyncWaypoints: (routeId, senderSessionId) => {
          if (this.isMySyncSession(senderSessionId)) return;
          this.applySyncWaypoints(routeId);
        },
        onSyncPlatformIcons: (senderSessionId) => {
          if (this.isMySyncSession(senderSessionId)) return;
          this.applySyncPlatformIcons();
        },
        onSyncRoomDrawings: (senderSessionId) => {
          if (this.isMySyncSession(senderSessionId)) return;
          this.applySyncRoomDrawings();
        },
        onSyncPlatformStyles: (senderSessionId) => {
          if (this.isMySyncSession(senderSessionId)) return;
          this.applySyncPlatformStyles();
        },
        onRoomState: (visibleRouteIds) => {
          this.applyRoomStateVisibleRoutes(visibleRouteIds);
        },
        onConnected: () => {},
        onDisconnected: () => {
          this.onlineCount = 0;
          this.wsOnlineMembers = [];
          this.mySyncSessionIds = [];
          this.chatMessages = [];
          this.privateChatMessages = {};
          this.routeLockedBy = {};
        },
        onError: (err) => {
          console.warn('[WebSocket]', err);
        }
      });
    },
    disconnectRoomWebSocket() {
      if (this.wsConnection) {
        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;
      return Array.isArray(this.mySyncSessionIds) && this.mySyncSessionIds.includes(senderSessionId);
    },
    /** 收到其他设备的航线显隐同步：直接应用变更，不经过 selectRoute 的 toggle 逻辑，避免互相干扰 */
    async applySyncRouteVisibility(routeId, visible) {
      const route = this.routes.find(r => r.id === routeId);
      if (!route) return;
      if (visible) {
        if (this.activeRouteIds.includes(routeId)) return;
        try {
          const res = await getRoutes(route.id);
          if (res.code !== 200 || !res.data) return;
          const waypoints = res.data.waypoints || [];
          this.activeRouteIds = [...this.activeRouteIds, route.id];
          this.selectedRouteId = res.data.id;
          this.selectedRouteDetails = {
            id: res.data.id,
            name: res.data.callSign,
            waypoints: waypoints,
            platformId: route.platformId,
            platform: route.platform,
            attributes: route.attributes
          };
          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));
            this.$nextTick(() => this.updateDeductionPositions());
          }
        } catch (_) {}
      } else {
        const idx = this.activeRouteIds.indexOf(routeId);
        if (idx > -1) {
          this.activeRouteIds = this.activeRouteIds.filter(id => id !== routeId);
          if (this.$refs.cesiumMap) this.$refs.cesiumMap.removeRouteById(routeId);
          if (this.selectedRouteDetails && this.selectedRouteDetails.id === routeId) {
            if (this.activeRouteIds.length > 0) {
              const lastId = this.activeRouteIds[this.activeRouteIds.length - 1];
              const res = await getRoutes(lastId);
              if (res.code === 200 && res.data) {
                const fromList = this.routes.find(r => r.id === lastId);
                this.selectedRouteId = res.data.id;
                this.selectedRouteDetails = {
                  id: res.data.id,
                  name: res.data.callSign,
                  waypoints: res.data.waypoints || [],
                  platformId: fromList?.platformId,
                  platform: fromList?.platform,
                  attributes: fromList?.attributes
                };
              }
            } else {
              this.selectedRouteId = null;
              this.selectedRouteDetails = null;
            }
          }
        }
      }
    },
    /** 收到其他设备的航点变更同步：拉取最新数据并重绘 */
    async applySyncWaypoints(routeId) {
      try {
        const res = await getRoutes(routeId);
        if (res.code !== 200 || !res.data) return;
        const waypoints = res.data.waypoints || [];
        const route = this.routes.find(r => r.id === routeId);
        if (route) {
          this.$set(route, 'waypoints', waypoints);
        }
        if (this.selectedRouteId === routeId && this.selectedRouteDetails) {
          this.selectedRouteDetails = { ...this.selectedRouteDetails, waypoints };
        }
        if (this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap && waypoints.length > 0) {
          const r = this.routes.find(rr => rr.id === routeId);
          if (r) {
            if (waypoints.some(wp => this.isHoldWaypoint(wp))) {
              const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
              this.getPositionAtMinutesFromK(waypoints, minMinutes, minMinutes, maxMinutes, routeId);
            }
            this.$refs.cesiumMap.removeRouteById(routeId);
            this.$refs.cesiumMap.renderRouteWaypoints(waypoints, routeId, r.platformId, r.platform, this.parseRouteStyle(r.attributes));
            this.$nextTick(() => this.updateDeductionPositions());
          }
        }
      } catch (e) {
        console.warn('applySyncWaypoints failed', e);
      }
    },
    /** 收到其他设备的平台图标变更同步：拉取最新数据并重绘 */
    applySyncPlatformIcons() {
      const rId = this.currentRoomId;
      if (!rId || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadRoomPlatformIcons !== 'function') return;
      listRoomPlatformIcons(rId).then(res => {
        if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data);
      }).catch(() => {});
    },
    /** 收到其他设备的空域图形变更同步：拉取最新房间数据并重绘 */
    applySyncRoomDrawings() {
      if (!this.currentRoomId || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadFrontendDrawings !== 'function') return;
      getRooms(this.currentRoomId).then(res => {
        if (res.code === 200 && res.data) {
          this.roomDetail = this.roomDetail ? { ...this.roomDetail, frontendDrawings: res.data.frontendDrawings } : { ...res.data };
          if (res.data.frontendDrawings) {
            this.$refs.cesiumMap.loadFrontendDrawings(res.data.frontendDrawings);
          } else {
            this.$refs.cesiumMap.clearDrawingEntities();
          }
        }
      }).catch(() => {});
    },
    /** 收到其他设备的探测区/威力区样式变更同步：刷新平台图标与航线样式 */
    async applySyncPlatformStyles() {
      const rId = this.currentRoomId;
      if (!rId || !this.$refs.cesiumMap) return;
      if (typeof this.$refs.cesiumMap.loadRoomPlatformIcons === 'function') {
        try {
          const res = await listRoomPlatformIcons(rId);
          if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data);
        } catch (_) {}
      }
      for (const routeId of this.activeRouteIds || []) {
        const route = this.routes.find(r => r.id === routeId);
        if (!route || !route.platformId) continue;
        try {
          const styleRes = await getPlatformStyle({ roomId: rId, routeId, platformId: route.platformId });
          if (styleRes.data && this.$refs.cesiumMap) {
            this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
            if (styleRes.data.detectionZoneVisible !== false && styleRes.data.detectionZoneRadius != null && Number(styleRes.data.detectionZoneRadius) > 0) {
              this.$refs.cesiumMap.ensureDetectionZoneForRoute(routeId, styleRes.data.detectionZoneRadius, styleRes.data.detectionZoneColor || 'rgba(0, 150, 255, 0.35)', styleRes.data.detectionZoneOpacity);
            }
            if (styleRes.data.powerZoneVisible !== false && styleRes.data.powerZoneRadius != null && Number(styleRes.data.powerZoneRadius) > 0) {
              this.$refs.cesiumMap.ensurePowerZoneForRoute(routeId, styleRes.data.powerZoneRadius, styleRes.data.powerZoneAngle ?? 120, styleRes.data.powerZoneColor || 'rgba(255, 0, 0, 0.3)', styleRes.data.powerZoneOpacity);
            }
          }
        } catch (_) {}
      }
    },
    // 平台编辑弹窗相关方法
    openPlatformDialog(platform) {
      this.selectedPlatform = JSON.parse(JSON.stringify(platform));
      this.showPlatformDialog = true;
    },
    /** 从数据库查询并分拣平台库数据 */
    getPlatformList() {
      listLib().then(res => {
        const allData = res.rows || [];
        this.airPlatforms = [];
        this.seaPlatforms = [];
        this.groundPlatforms = [];
        allData.forEach(item => {
          const platform = {
            id: item.id,
            name: item.name,
            type: item.type,
            specsJson: item.specsJson,
            imageUrl: item.iconUrl || '',
            icon: item.iconUrl ? '' : 'el-icon-picture-outline',
            status: 'ready'
          };
          if (item.type === 'Air') {
            this.airPlatforms.push(platform);
          } else if (item.type === 'Sea') {
            this.seaPlatforms.push(platform);
          } else if (item.type === 'Ground') {
            this.groundPlatforms.push(platform);
          }
        });

      });
    },
    /** 导入确认：将弹窗填写的模版数据存入数据库 */
    handleImportConfirm(formData) {
      if (!formData.name || !formData.type) {
        this.$modal.msgError("请填写完整的平台信息");
        return;
      }
      // 创建 FormData 对象，这是传输二进制文件的标准格式
      let data = new FormData();
      // 把文件塞进去
      if (formData.icon_file) {
        data.append("file", formData.icon_file);
      }
      // 把其他文本字段也塞进去
      data.append("name", formData.name);
      data.append("type", formData.type);
      data.append("specsJson", JSON.stringify({
        scenarioId: this.selectedPlanId,
        createTime: new Date().getTime()
      }));
      addLib(data).then(response => {
        this.$modal.msgSuccess("导入成功");
        this.showImportDialog = false;
        this.getPlatformList();
      }).catch(err => {
        // 增加错误提示，防止请求失败时没有任何反应
        console.error("上传出错：", err);
      });
    },
    updatePlatform() {
      // 刷新平台列表
      this.getPlatformList();
      // 提示成功
      this.$modal.msgSuccess('平台配置更新成功');
      // 关闭弹窗
      this.showPlatformDialog = false;
    },
    /** 新建航线时写入数据库的默认样式（简约风格：灰蓝配色、细描边） */
    getDefaultRouteAttributes() {
      const defaultAttrs = {
        waypointStyle: { pixelSize: 10, color: '#f1f5f9', outlineColor: '#64748b', outlineWidth: 1.5 },
        lineStyle: { style: 'solid', width: 2, color: '#64748b', gapColor: '#cbd5e1', dashLength: 20 }
      };
      return JSON.stringify(defaultAttrs);
    },
    /** 从航线 attributes JSON 解析出地图渲染用的样式对象 */
    parseRouteStyle(attributes) {
      if (attributes == null || attributes === '') return null;
      try {
        const attrs = typeof attributes === 'string' ? JSON.parse(attributes) : attributes;
        const wp = attrs.waypointStyle || {};
        const ln = attrs.lineStyle || {};
        if (!wp.pixelSize && !wp.color && !ln.width && !ln.color) return null;
        return {
          waypoint: {
            pixelSize: wp.pixelSize,
            color: wp.color,
            outlineColor: wp.outlineColor,
            outlineWidth: wp.outlineWidth
          },
          line: {
            style: ln.style,
            width: ln.width,
            color: ln.color,
            gapColor: ln.gapColor,
            dashLength: ln.dashLength
          }
        };
      } catch (_) {
        return null;
      }
    },
    // 航线编辑弹窗相关方法
    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 {
        const apiData = {
          id: updatedRoute.id,
          callSign: updatedRoute.name,
          platformId: updatedRoute.platformId || null,
          attributes: updatedRoute.attributes != null ? updatedRoute.attributes : undefined
        };
        const res = await updateRoutes(apiData);
        if (res.code === 200) {
          const newRouteData = {
            ...this.routes[index],
            name: updatedRoute.name,
            platformId: updatedRoute.platformId,
            platform: updatedRoute.platform,
            attributes: updatedRoute.attributes
          };
          // 若编辑航线弹窗中提交了航点表数据，逐条持久化到数据库并合并到本地
          if (updatedRoute.waypoints && updatedRoute.waypoints.length > 0) {
            for (const wp of updatedRoute.waypoints) {
              const payload = {
                id: wp.id,
                routeId: wp.routeId,
                name: wp.name,
                seq: wp.seq,
                lat: wp.lat,
                lng: wp.lng,
                alt: wp.alt,
                speed: wp.speed,
                startTime: wp.startTime != null && wp.startTime !== '' ? wp.startTime : 'K+00:00:00',
                turnAngle: wp.turnAngle
              };
              if (wp.pointType != null) payload.pointType = wp.pointType;
              if (wp.holdParams != null) payload.holdParams = wp.holdParams;
              if (wp.labelFontSize != null) payload.labelFontSize = wp.labelFontSize;
              if (wp.labelColor != null) payload.labelColor = wp.labelColor;
              if (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 {
                payload.turnRadius = 0;
              }
              await updateWaypoints(payload);
            }
            const mergedWaypoints = (newRouteData.waypoints || []).map((oldWp) => {
              const fromDialog = updatedRoute.waypoints.find((w) => w.id === oldWp.id);
              return fromDialog ? { ...oldWp, ...fromDialog } : oldWp;
            });
            newRouteData.waypoints = mergedWaypoints;
            if (this.selectedRouteDetails && this.selectedRouteId === updatedRoute.id) {
              this.selectedRouteDetails.waypoints = mergedWaypoints;
            }
          }
          this.routes.splice(index, 1, newRouteData);
          if (this.selectedRouteDetails && this.selectedRouteId === updatedRoute.id) {
            this.selectedRouteDetails.name = updatedRoute.name;
            this.selectedRouteDetails.platformId = updatedRoute.platformId;
            this.selectedRouteDetails.platform = updatedRoute.platform;
            this.selectedRouteDetails.attributes = updatedRoute.attributes;
          }
          this.$message.success(updatedRoute.waypoints && updatedRoute.waypoints.length > 0 ? '航线与航点已保存' : '航线更新成功');
          this.wsConnection?.sendSyncWaypoints?.(updatedRoute.id);
          const routeStyle = updatedRoute.routeStyle || this.parseRouteStyle(updatedRoute.attributes);
          if (this.$refs.cesiumMap && this.activeRouteIds.includes(updatedRoute.id)) {
            const route = this.routes.find(r => r.id === updatedRoute.id);
            if (route && route.waypoints && route.waypoints.length > 0) {
              const roomId = this.currentRoomId;
              if (roomId && route.platformId) {
                try {
                  const styleRes = await getPlatformStyle({ roomId, routeId: updatedRoute.id, platformId: route.platformId });
                  if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(updatedRoute.id, styleRes.data);
                } catch (_) {}
              }
              this.$refs.cesiumMap.removeRouteById(updatedRoute.id);
              this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, updatedRoute.id, route.platformId, route.platform, routeStyle);
              this.$nextTick(() => this.updateDeductionPositions());
              // 航线编辑后，根据新航点重算导弹发射位置并更新 Redis（使用合并后的 route.waypoints 确保最新位置）
              if (roomId && route.waypoints && route.waypoints.length > 0) {
                this.updateMissilePositionsAfterRouteEdit(roomId, updatedRoute.id, route.platformId != null ? route.platformId : 0, route.waypoints);
              }
            }
          }
        } else {
          this.$message.error(res.msg || '航线更新失败');
        }
      } catch (error) {
        this.$message.error('航线更新失败');
        console.error('航线更新失败:', error);
      }
    },
    // 新建航线
    createRoute(plan) {
      if (!plan || !plan.id) return;
      // 只有当目标方案和当前方案不同时，才执行“大扫除”
      if (this.selectedPlanId !== plan.id) {
        this.selectPlan(plan);
      } else {
        console.log(">>> 保持当前航线显示，直接进入规划模式");
      }
      // 进入 Cesium 绘图模式
      if (this.$refs.cesiumMap) {
        this.missionDrawingActive = true;
        this.missionDrawingPointsCount = 0;
        this.$refs.cesiumMap.startMissionRouteDrawing();
        this.$message.success(`${plan.name}开启航线规划`);
      }
    },
    async handleDeleteRoute(route) {
      if (this.isRouteLockedByOther(route.id)) {
        this.$message.warning('该航线正被其他成员编辑，无法删除');
        return;
      }
      try {
        // 二次确认，防止误删
        await this.$confirm(`确定要彻底删除航线 "${route.name}" 吗？`, '提示', {
          type: 'warning'
        });
        if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) {
          this.selectedRouteDetails = null;
        }
        const res = await delRoutes(route.id);
        if (res.code === 200) {
          this.$message.success('删除成功');
          // 同步地图：如果该航线正在显示，立即清除
          if (this.$refs.cesiumMap) {
            this.$refs.cesiumMap.removeRouteById(route.id);
            // 同时清除该航线的探测区、威力区（removeRouteById 已会移除，此处显式调用保持一致）
            this.$refs.cesiumMap.removeDetectionZoneByRouteId(route.id);
            this.$refs.cesiumMap.removePowerZoneByRouteId(route.id);
          }
          // 同步状态：从选中列表中移除该 ID
          const idx = this.activeRouteIds.indexOf(route.id);
          if (idx > -1) {
            this.activeRouteIds.splice(idx, 1);
          }
          await this.getList({ skipRoomPlatformIcons: true });
          // 删除航线后重新加载导弹（Redis 已清除该航线导弹，需同步清除地图上的导弹渲染）
          this.reloadMissilesAfterRouteChange();
        }
      } catch (e) {
        if (e !== 'cancel') {
          console.error("删除航线失败:", e);
          this.$message.error('删除操作失败');
        }
      }
    },
    /**
     * 从数据库拉取最新的航线列表数据
     * @param {Object} opts - 可选：{ skipRoomPlatformIcons: true } 时不再加载房间平台图标（用于删除航线等仅航线变化的场景，避免地图上的平台图标被清空再重画导致闪一下）
     */
    async getList(opts = {}) {
      const { skipRoomPlatformIcons = false } = opts;
      try {
        const roomId = this.$route.query.roomId || this.currentRoomId;
        const scenarioRes = await listScenario({ roomId: roomId });
        if (scenarioRes.code === 200) {
          this.plans = scenarioRes.rows.map(s => ({
            id: s.id,
            name: s.name,
            frontendDrawings: s.frontendDrawings || null,
            routes: []
          }));
        }
        // 获取所有航线
        const routeRes = await listRoutes({ pageNum: 1, pageSize: 9999 });
        if (routeRes.code === 200) {
          const allRoutes = routeRes.rows.map(item => ({
            id: item.id,
            name: item.callSign,
            platformId: item.platformId,
            platform: item.platform,
            attributes: item.attributes,
            points: item.waypoints ? item.waypoints.length : 0,
            waypoints: item.waypoints || [],
            conflict: false,
            scenarioId: item.scenarioId
          }));
          //分配航线到方案
          this.plans.forEach(plan => {
            plan.routes = allRoutes.filter(r => r.scenarioId === plan.id);
          });

          this.routes = allRoutes;
          // 先预取所有展示中航线的平台样式，再渲染，避免平台图标先黑后变色
          if (this.activeRouteIds.length > 0 && this.$refs.cesiumMap) {
            const roomId = this.currentRoomId;
            await Promise.all(this.activeRouteIds.map(async (id) => {
              const route = this.routes.find(r => r.id === id);
              if (!route || !route.waypoints || route.waypoints.length === 0) return;
              if (roomId && route.platformId) {
                try {
                  const res = await getPlatformStyle({ roomId, routeId: id, platformId: route.platformId });
                  if (res.data) this.$refs.cesiumMap.setPlatformStyle(id, res.data);
                } catch (_) {}
              }
            }));
            this.$nextTick(() => {
              const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
              this.activeRouteIds.forEach(id => {
                const route = this.routes.find(r => r.id === id);
                if (route && route.waypoints && route.waypoints.length > 0 && this.$refs.cesiumMap) {
                  // 含盘旋航点时先预计算半径/长短轴（根据相对K时与距离），再渲染，使圆/椭圆尺寸正确
                  if (route.waypoints.some(wp => this.isHoldWaypoint(wp))) {
                    this.getPositionAtMinutesFromK(route.waypoints, minMinutes, minMinutes, maxMinutes, id);
                  }
                  this.$refs.cesiumMap.removeRouteById(id);
                  this.$refs.cesiumMap.renderRouteWaypoints(route.waypoints, id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
                }
              });
            });
          }
          // 加载该房间下保存的地图平台图标（删除航线等操作时跳过，避免清空再重画导致平台图标闪一下）
          if (!skipRoomPlatformIcons) {
            const rId = roomId || this.currentRoomId;
            if (rId && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadRoomPlatformIcons === 'function') {
              listRoomPlatformIcons(rId).then(res => {
                if (res.code === 200 && res.data) this.$refs.cesiumMap.loadRoomPlatformIcons(rId, res.data);
              }).catch(() => {});
            }
          }
          this.$nextTick(() => this.applyRoomStatePending());
        }
      } catch (error) {
        console.error("数据加载失败:", error);
        this.$message.error("获取方案列表失败");
      }
    },
    handleMapDrawComplete(points, options) {
      this.missionDrawingActive = false;
      if (!points || points.length < 2) {
        this.$message.error('航点太少，无法生成航线');
        this.drawDom = false;
        return;
      }
      this.tempMapPoints = points;
      this.tempMapPlatform = (options && (options.platformId != null || options.platform)) ? options : null;
      this.showNameDialog = true;
    },
    /** 保存新航线弹窗打开时：若当前未选方案则用已选方案或第一个方案填充，便于在弹窗内直接选择 */
    onSaveRouteDialogOpen() {
      this.saveDialogScenarioId = this.selectedPlanId || (this.plans[0] && this.plans[0].id) || null;
    },

    openAddHoldDuringDrawing() {
      this.addHoldContext = { mode: 'drawing' };
      this.addHoldForm = { holdType: 'hold_circle', radius: 15000, edgeLengthKm: 20, clockwise: true, startTime: '', startTimeMinutes: 60 };
      this.showAddHoldDialog = true;
    },

    /** 弹窗点击“确定”：正式将数据保存到后端数据库 */
    async confirmSaveNewRoute() {
      // 1. 严格校验
      if (!this.newRouteName || this.newRouteName.trim() === '') {
        this.$message.error('新增航线未命名，请输入名称后保存！');
        return;
      }
      const currentScenarioId = this.saveDialogScenarioId != null ? this.saveDialogScenarioId : this.selectedPlanId;
      if (!currentScenarioId) {
        this.$message.warning(this.plans.length === 0 ? '暂无方案，请先点击地图左侧红点展开菜单，选择「方案」并新建方案后再保存。' : '请在上方选择所属方案后再保存。');
        return;
      }

      // 2. 构造数据（含盘旋航点的 pointType、holdParams；地图标签默认字号 14、颜色 #333333）
      // 默认相对 K 时：按“路程÷默认速度”累加；用向上取整避免因整数分钟舍去导致冲突检测误报“无法按时到达”
      const wpCount = this.tempMapPoints.length;
      let cumulativeMinutes = 0;
      const pointsWithStartTime = this.tempMapPoints.map((p, index) => {
        const startTime = this.minutesToStartTime(index === 0 ? 0 : Math.ceil(cumulativeMinutes));
        if (index < wpCount - 1) {
          const next = this.tempMapPoints[index + 1];
          const dist = this.segmentDistance(
            { lat: p.lat, lng: p.lng, alt: p.alt != null ? p.alt : 5000 },
            { lat: next.lat, lng: next.lng, alt: next.alt != null ? next.alt : 5000 }
          );
          const speedKmh = Number(p.speed) || 800;
          cumulativeMinutes += (dist / 1000) * (60 / speedKmh);
        }
        return { ...p, startTime };
      });
      // 新建航线时：首尾航点转弯坡度固定为 0，中间可编辑航点默认 45°
      const finalWaypoints = pointsWithStartTime.map((p, index) => {
        const isFirstOrLast = index === 0 || index === wpCount - 1;
        const defaultTurnAngle = isFirstOrLast ? 0.0 : 45.0;
        return {
        name: p.name || `WP${index + 1}`,
        lat: p.lat,
        lng: p.lng,
        alt: p.alt != null ? p.alt : 5000.0,
        speed: p.speed != null ? p.speed : 800.0,
        startTime: p.startTime,
        turnAngle: p.turnAngle != null ? p.turnAngle : defaultTurnAngle,
        labelFontSize: p.labelFontSize != null ? p.labelFontSize : 14,
        labelColor: p.labelColor || '#333333',
        ...(p.pointType && { pointType: p.pointType }),
        ...(p.holdParams != null && { holdParams: typeof p.holdParams === 'string' ? p.holdParams : JSON.stringify(p.holdParams) })
        };
      });

      const routeData = {
        callSign: this.newRouteName,
        scenarioId: currentScenarioId,
        platformId: (this.tempMapPlatform && this.tempMapPlatform.platformId != null) ? this.tempMapPlatform.platformId : 1,
        attributes: this.getDefaultRouteAttributes(),
        waypoints: finalWaypoints
      };

      try {
        const response = await addRoutes(routeData);
        if (response.code === 200) {
          const savedRoute = response.data;
          const newRouteId = savedRoute?.id;

          if (!newRouteId || !savedRoute.waypoints) {
            this.$message.error('保存成功但返回数据结构异常，请手动刷新');
            return;
          }

          // 1. 更新当前选中详情
          this.selectedRouteId = newRouteId;
          this.selectedRouteDetails = JSON.parse(JSON.stringify(savedRoute));

          // 2. 【修复关键 A】：使用 push 而不是赋值
          // 确保新 ID 进去的同时，activeRouteIds 里原有的老 ID（如 112）还在
          // 这样右侧列表里，老的航线依然会保持勾选状态
          if (!this.activeRouteIds.includes(newRouteId)) {
            this.activeRouteIds.push(newRouteId);
          }

          // 3. 先保存“用于创建航线的平台”引用，再清空（后续删除要用到）
          const platformToRemove = this.tempMapPlatform;

          // 4. UI 重置
          this.drawDom = false;
          this.showNameDialog = false;
          this.newRouteName = '';
          this.tempMapPoints = [];
          this.tempMapPlatform = null;

          // 5. 【修复关键 B】：只清理临时预览实体，不 clearAllWaypoints
          if (this.$refs.cesiumMap) {
            if (this.$refs.cesiumMap.tempPreviewEntity) {
              this.viewer.entities.remove(this.$refs.cesiumMap.tempPreviewEntity);
              this.$refs.cesiumMap.tempPreviewEntity = null;
            }
          }

          // 6. 若本条航线是从“地图上的平台图标”创建的：先删库再刷新列表，最后从地图移除该占位图标（避免 getList 重新拉取到未删记录又画上去）
          if (platformToRemove && this.$refs.cesiumMap) {
            if (platformToRemove.serverId) {
              await delRoomPlatformIcon(platformToRemove.serverId).catch(() => {});
              this.wsConnection?.sendSyncPlatformIcons?.();
              if (typeof this.$refs.cesiumMap.removePlatformIconByServerId === 'function') {
                this.$refs.cesiumMap.removePlatformIconByServerId(platformToRemove.serverId);
              }
            } else if (platformToRemove.mapEntityId && typeof this.$refs.cesiumMap.removeEntity === 'function') {
              this.$refs.cesiumMap.removeEntity(platformToRemove.mapEntityId);
            }
          }

          // 7. 刷新右侧列表（拉取最新数据，含新建航线）；getList 会重新加载房间平台图标，因已删记录，不会再画出该图标
          await this.getList();
          const routeFromList = this.routes.find(r => r.id === newRouteId);
          if (routeFromList) {
            this.selectedRouteDetails = { ...routeFromList };
          }
          this.$message.success('航线及其航点已成功保存并同步');
        }
      } catch (error) {
        console.error(">>> [保存异常]:", error);
        this.$message.error('保存失败，请检查网络或后端日志');
      }
    },
    // 航点编辑弹窗相关方法
    openWaypointDialog(data) {
      console.log(">>> [父组件接收] 编辑航点详情:", data);
      if (data && data.routeId != null && this.isRouteLockedByOther(data.routeId)) {
        this.$message.warning('该航线正被其他成员编辑，无法修改');
        return;
      }
      // 将整个对象赋值给弹窗绑定的变量
      this.selectedWaypoint = data;
      this.showWaypointDialog = true;
    },
    /** 航点编辑保存：更新数据库并同步地图显示 */
    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);
        } else {
          updatedWaypoint.turnRadius = 0;
        }
        // 明确构造后端需要的字段，确保 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
        };
        if (updatedWaypoint.pointType != null) payload.pointType = updatedWaypoint.pointType;
        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.segmentMode != null) payload.segmentMode = updatedWaypoint.segmentMode;
        if (updatedWaypoint.segmentTargetMinutes !== undefined) payload.segmentTargetMinutes = updatedWaypoint.segmentTargetMinutes;
        if (updatedWaypoint.segmentTargetSpeed !== undefined) payload.segmentTargetSpeed = updatedWaypoint.segmentTargetSpeed;
        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 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 });
            const merged = { ...updatedWaypoint, ...payload };
            const routeInList = this.routes.find(r => r.id === this.selectedRouteDetails.id);
            if (routeInList && routeInList.waypoints) {
              const idxInList = routeInList.waypoints.findIndex(p => p.id === updatedWaypoint.id);
              if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged);
            }
            // 定时点：根据“本航点相对K时”反算上一航点速度，使平台能在 K+本点时间 到达本点并出发（或提前到达后盘旋至该时刻）
            if (merged.segmentMode === 'fixed_time' && index > 0) {
              const prev = sd.waypoints[index - 1];
              const distM = this.segmentDistance(
                { lat: prev.lat, lng: prev.lng, alt: prev.alt },
                { lat: merged.lat, lng: merged.lng, alt: merged.alt }
              );
              const prevMinutes = this.waypointStartTimeToMinutesDecimal(prev.startTime);
              const currMinutes = (merged.segmentTargetMinutes != null && merged.segmentTargetMinutes !== '') ? Number(merged.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(merged.startTime);
              const deltaMin = currMinutes - prevMinutes;
              if (deltaMin > 0.001) {
                const newSpeedKmh = (distM / 1000) / (deltaMin / 60);
                const speedVal = Math.round(newSpeedKmh * 10) / 10;
                const prevPayload = { ...prev, speed: speedVal };
                if (prev.segmentMode != null) prevPayload.segmentMode = prev.segmentMode;
                if (prev.labelFontSize != null) prevPayload.labelFontSize = prev.labelFontSize;
                if (prev.labelColor != null) prevPayload.labelColor = prev.labelColor;
                if (prev.color != null) prevPayload.color = prev.color;
                if (prev.pixelSize != null) prevPayload.pixelSize = prev.pixelSize;
                if (prev.outlineColor != null) prevPayload.outlineColor = prev.outlineColor;
                try {
                  const r2 = await updateWaypoints(prevPayload);
                  if (r2.code === 200) {
                    Object.assign(prev, { speed: speedVal });
                    sd.waypoints.splice(index - 1, 1, prev);
                    if (routeInList && routeInList.waypoints) {
                      const prevIdxInList = routeInList.waypoints.findIndex(p => p.id === prev.id);
                      if (prevIdxInList !== -1) routeInList.waypoints.splice(prevIdxInList, 1, prev);
                    }
                  }
                } catch (e) {
                  console.warn('定时点重算上一航点速度失败', e);
                }
              }
            }
            // 下一航点是定时点时：重算本航点速度，使平台能在下一航点的 K 时到达
            if (index < sd.waypoints.length - 1) {
              const next = sd.waypoints[index + 1];
              if (next.segmentMode === 'fixed_time') {
                const distToNextM = this.segmentDistance(
                  { lat: merged.lat, lng: merged.lng, alt: merged.alt },
                  { lat: next.lat, lng: next.lng, alt: next.alt }
                );
                const currMinutes = this.waypointStartTimeToMinutesDecimal(merged.startTime);
                const nextMinutes = (next.segmentTargetMinutes != null && next.segmentTargetMinutes !== '') ? Number(next.segmentTargetMinutes) : this.waypointStartTimeToMinutesDecimal(next.startTime);
                const deltaMin = nextMinutes - currMinutes;
                if (deltaMin > 0.001) {
                  const newSpeedKmh = (distToNextM / 1000) / (deltaMin / 60);
                  const speedVal = Math.round(newSpeedKmh * 10) / 10;
                  const currPayload = { ...merged, speed: speedVal };
                  if (merged.segmentMode != null) currPayload.segmentMode = merged.segmentMode;
                  if (merged.labelFontSize != null) currPayload.labelFontSize = merged.labelFontSize;
                  if (merged.labelColor != null) currPayload.labelColor = merged.labelColor;
                  if (merged.pixelSize != null) currPayload.pixelSize = merged.pixelSize;
                  if (merged.color != null) currPayload.color = merged.color;
                  if (merged.outlineColor != null) currPayload.outlineColor = merged.outlineColor;
                  try {
                    const r2 = await updateWaypoints(currPayload);
                    if (r2.code === 200) {
                      Object.assign(merged, { speed: speedVal });
                      sd.waypoints.splice(index, 1, merged);
                      if (routeInList && routeInList.waypoints) {
                        const idxInList = routeInList.waypoints.findIndex(p => p.id === merged.id);
                        if (idxInList !== -1) routeInList.waypoints.splice(idxInList, 1, merged);
                      }
                    }
                  } catch (e) {
                    console.warn('下一航点为定时点重算本航点速度失败', e);
                  }
                }
              }
            }
            if (this.$refs.cesiumMap) {
              if (roomId && sd.platformId) {
                try {
                  const styleRes = await getPlatformStyle({ roomId, routeId: sd.id, platformId: sd.platformId });
                  if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(sd.id, styleRes.data);
                } catch (_) {}
              }
              this.$refs.cesiumMap.updateWaypointGraphicById(updatedWaypoint.id, updatedWaypoint.name);
              this.$refs.cesiumMap.renderRouteWaypoints(
                sd.waypoints,
                sd.id,
                sd.platformId,
                sd.platform,
                this.parseRouteStyle(sd.attributes)
              );
            }
            this.showWaypointDialog = false;
            this.$message.success('航点信息已持久化至数据库');
            if (this.$refs.cesiumMap && (this.$refs.cesiumMap.setRouteHoldRadii || this.$refs.cesiumMap.setRouteHoldEllipseParams)) {
              const rid = sd.id;
              if (this.$refs.cesiumMap.setRouteHoldRadii) this.$refs.cesiumMap.setRouteHoldRadii(rid, {});
              if (this.$refs.cesiumMap.setRouteHoldEllipseParams) this.$refs.cesiumMap.setRouteHoldEllipseParams(rid, {});
            }
            this.$nextTick(() => this.updateDeductionPositions());
            this.wsConnection?.sendSyncWaypoints?.(this.selectedRouteDetails.id);
            // 航点编辑后，根据新位置重算导弹发射位置并更新 Redis
            if (roomId && sd.waypoints && sd.waypoints.length > 0) {
              this.updateMissilePositionsAfterRouteEdit(roomId, sd.id, sd.platformId != null ? sd.platformId : 0, sd.waypoints);
            }
          }
        } else {
          // 如果 code 不是 200，手动抛出错误进入catch
          throw new Error(response.msg || '后端业务逻辑校验失败');
        }
      } catch (error) {
        console.error("更新航点失败:", error);
        this.$message.error(error.message || '数据库更新失败，请重试');
      }
    },
    updateTime() {
      const now = new Date();
      const year = now.getFullYear();
      const month = (now.getMonth() + 1).toString().padStart(2, '0');
      const day = now.getDate().toString().padStart(2, '0');
      const hours = now.getHours().toString().padStart(2, '0');
      const minutes = now.getMinutes().toString().padStart(2, '0');
      const seconds = now.getSeconds().toString().padStart(2, '0');
      this.astroTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    },

    /** 是否有选中的航线用于推演（有则作战时间随推演时间轴变化） */
    hasDeductionRange() {
      return this.activeRouteIds && this.activeRouteIds.length > 0;
    },
    updateCombatTime() {
      // 作战时间始终与推演时间轴同步：有航线时用 deductionMinutesFromK，无航线时用 currentTime（由时间轴 updateTimeFromProgress 计算）
      if (this.hasDeductionRange()) {
        const sign = this.deductionMinutesFromK >= 0 ? '+' : '-';
        const absMin = Math.abs(Math.floor(this.deductionMinutesFromK));
        const hours = Math.floor(absMin / 60);
        const minutes = absMin % 60;
        this.combatTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
        return;
      }
      // 无展示航线时：作战时间仍随推演时间轴变化，用 currentTime
      this.combatTime = this.currentTime || 'K+00:00:00';
    },
    /** 将作战时间字符串（如 K+01:30:00）解析为相对 K 的分钟数 */
    combatTimeToMinutes(str) {
      if (!str || str === '未设定') return 0;
      const m = String(str).match(/K([+-])(\d{1,2}):(\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);
    },
    getRoomDetail() {
      if (!this.currentRoomId) return;
      getRooms(this.currentRoomId).then(res => {
        if (res.code === 200 && res.data) {
          this.roomDetail = res.data;
          this.$nextTick(() => this.loadRoomDrawings());
        }
      }).catch(() => {});
    },
    /** 加载当前房间的空域/威力区图形（与房间 ID 绑定，进入该房间即显示） */
    loadRoomDrawings() {
      if (!this.roomDetail || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadFrontendDrawings !== 'function') return;
      if (this.roomDetail.frontendDrawings) {
        this.$refs.cesiumMap.loadFrontendDrawings(this.roomDetail.frontendDrawings);
      } else {
        this.$refs.cesiumMap.clearDrawingEntities();
      }
    },
    /** 地图 viewer 就绪时加载当前房间图形（可能 getRoomDetail 尚未返回，此处再试一次） */
    onViewerReady() {
      this.loadRoomDrawings();
    },
    /** 空域/威力区图形增删时防抖自动保存到当前房间 */
    onDrawingEntitiesChanged() {
      if (!this.currentRoomId) return;
      if (this.saveRoomDrawingsTimer) clearTimeout(this.saveRoomDrawingsTimer);
      this.saveRoomDrawingsTimer = setTimeout(() => {
        this.saveRoomDrawingsTimer = null;
        this.saveRoomDrawingsToServer();
      }, 600);
    },
    /** 将当前地图上的空域图形写入当前房间（静默保存，不弹成功提示；失败时抛出供调用方处理） */
    async saveRoomDrawingsToServer() {
      if (!this.currentRoomId || !this.$refs.cesiumMap) return;
      if (typeof this.$refs.cesiumMap.getFrontendDrawingsData !== 'function') return;
      const drawingsData = this.$refs.cesiumMap.getFrontendDrawingsData();
      const frontendDrawingsStr = JSON.stringify(drawingsData);
      await updateRooms({ id: this.currentRoomId, frontendDrawings: frontendDrawingsStr });
      if (this.roomDetail) this.roomDetail.frontendDrawings = frontendDrawingsStr;
      this.wsConnection?.sendSyncRoomDrawings?.();
    },
    /** 将任意日期字符串格式化为 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() {
      console.log("当前登录 ID (myId):", this.$store.getters.id);
      console.log("当前房间 ownerId:", this.roomDetail ? this.roomDetail.ownerId : '无房间信息');
      console.log("当前角色 roles:", this.$store.getters.roles);
      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('设定失败'));
    },

    // 左侧菜单栏操作
    showMenu() {
      this.isMenuHidden = false;
    },

    hideMenu() {
      this.isMenuHidden = true;
    },


    // 右侧面板操作
    showRightPanel() {
      this.isRightPanelHidden = false;
      this.$message.info('显示右侧面板');
    },

    // 顶部导航栏操作
    selectTopNav(item) {
      this.activeMenu = item.id;
    },

    // 文件下拉菜单方法：立即保存当前房间的空域/威力区图形（与自动保存共用逻辑）
    async savePlan() {
      if (!this.currentRoomId) {
        this.$message.warning('请先进入任务房间');
        return;
      }
      try {
        await this.saveRoomDrawingsToServer();
        this.$message.success('房间空域图形已保存');
      } catch (e) {
        this.$message.error('保存失败，请检查网络');
      }
    },

    importPlanFile() {
      this.$message.success('导入计划');
      // 这里可以添加导入计划文件的逻辑
    },

    importACD() {
      this.$message.success('导入ACD');
      // 这里可以添加导入ACD文件的逻辑
    },

    importATO() {
      this.$message.success('导入ATO');
      // 这里可以添加导入ATO文件的逻辑
    },

    importLayer() {
      this.$message.success('导入图层');
      // 这里可以添加导入图层的逻辑
    },

    importRoute() {
      this.$message.success('导入航线');
      // 这里可以添加导入航线的逻辑
    },

    exportPlan() {
      this.$message.success('导出计划');
      // 这里可以添加导出计划的逻辑
    },

    // 编辑下拉菜单方法
    routeEdit() {
      this.$message.success('航线编辑');
      // 这里可以添加航线编辑的逻辑
    },

    militaryMarking() {
      this.$message.success('军事标绘');
      // 这里可以添加军事标绘的逻辑
    },

    iconEdit() {
      // 这个方法现在由 TopHeader 组件处理状态切换
      // 只需要在这里处理其他逻辑（如果有的话）
    },

    toggleIconEditMode(isEditMode) {
      this.isIconEditMode = isEditMode
    },

    exitIconEdit() {
      this.isIconEditMode = false
    },

    async handleResetMenuItems() {
      this.menuItems = [...this.defaultMenuItems]
      try {
        await saveMenuConfig({
          menuItems: JSON.stringify(this.menuItems),
          position: this.menuPosition || 'left'
        })
      } catch (e) { /* 未登录时仅本地恢复默认 */ }
    },

    updateMenuItems(newItems) {
      this.menuItems = newItems
    },

    handleMenuDragEnd(newItems) {
      this.menuItems = newItems
    },

    handleAddMenuItems(selectedItems) {
      selectedItems.forEach(item => {
        const newId = Date.now().toString() + Math.random().toString(36).substr(2, 9)
        const newMenuItem = {
          id: newId,
          name: item.name,
          icon: item.icon,
          action: item.id
        }
        this.menuItems.push(newMenuItem)
      })
    },

    handleMenuAction(actionId) {
      const actionMap = {
        'savePlan': () => this.savePlan(),
        'routeEdit': () => this.routeEdit(),
        'militaryMarking': () => this.militaryMarking(),
        'attributeEdit': () => this.attributeEdit(),
        'timeSettings': () => this.timeSettings(),
        'aircraftSettings': () => this.aircraftSettings(),
        'keyEventEdit': () => this.keyEventEdit(),
        'missileLaunch': () => this.missileLaunch(),
        'toggle2D3D': () => this.toggle2D3D(),
        'toggleRuler': () => this.toggleRuler(),
        'toggleGrid': () => this.toggleGrid(),
        'toggleScale': () => {
          this.showScaleDialog = true
          this.currentScale = {}
        },
        'loadTerrain': () => this.loadTerrain(),
        'changeProjection': () => this.changeProjection(),
        'loadAeroChart': () => this.loadAeroChart(),
        'powerZone': () => this.startPowerZoneDrawing(),
        'threatZone': () => this.threatZone(),
        'routeCalculation': () => this.routeCalculation(),
        'conflictDisplay': () => this.conflictDisplay(),
        'dataMaterials': () => this.dataMaterials(),
        'coordinateConversion': () => this.coordinateConversion(),
        'pageLayout': () => this.pageLayout(),
        'dataStoragePath': () => this.dataStoragePath(),
        'externalParams': () => {
          this.showExternalParamsDialog = true
          this.currentExternalParams = {}
        },
        'toggleAirport': () => this.toggleAirport(),
        'toggleLandmark': () => this.toggleLandmark(),
        'toggleRoute': () => this.toggleRoute(),
        'layerFavorites': () => this.layerFavorites(),
        'routeFavorites': () => this.routeFavorites(),
        'refresh': () => this.captureMapScreenshot()
      }

      if (actionMap[actionId]) {
        actionMap[actionId]()
      }
    },

    /** 截图：隐藏上下左右菜单只保留地图，用 postRender + readPixels 避免 WebGL 缓冲被清空导致黑屏 */
    async captureMapScreenshot() {
      const cm = this.$refs.cesiumMap
      if (!cm || !cm.viewer || !cm.viewer.scene || !cm.viewer.scene.canvas) {
        this.$message.warning('地图未就绪，请稍后再试')
        return
      }
      this.screenshotMode = true
      await this.$nextTick()
      await new Promise(r => setTimeout(r, 350))
      const viewer = cm.viewer
      const canvas = viewer.scene.canvas
      const self = this
      const removeListener = viewer.scene.postRender.addEventListener(function captureFrame() {
        removeListener()
        self.screenshotMode = false
        try {
          const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')
          if (!gl) {
            self.$message.error('无法获取 WebGL 上下文')
            return
          }
          const width = canvas.width
          const height = canvas.height
          if (width === 0 || height === 0) {
            self.$message.error('画布尺寸为 0')
            return
          }
          const pixels = new Uint8Array(width * height * 4)
          gl.bindFramebuffer(gl.FRAMEBUFFER, null)
          gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
          const offscreen = document.createElement('canvas')
          offscreen.width = width
          offscreen.height = height
          const ctx = offscreen.getContext('2d')
          const imageData = ctx.createImageData(width, height)
          for (let y = 0; y < height; y++) {
            const srcRow = (height - 1 - y) * width * 4
            const dstRow = y * width * 4
            for (let i = 0; i < width * 4; i++) imageData.data[dstRow + i] = pixels[srcRow + i]
          }
          ctx.putImageData(imageData, 0, 0)
          const dataUrl = offscreen.toDataURL('image/png')
          const now = new Date()
          const timeStr = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + '_' + String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0') + String(now.getSeconds()).padStart(2, '0')
          self.screenshotFileName = `地图截图_${timeStr}.png`
          self.screenshotDataUrl = dataUrl
          self.showScreenshotDialog = true
        } catch (e) {
          self.$message.error('截图失败：' + (e && e.message ? e.message : '未知错误'))
        }
      })
      viewer.scene.requestRender()
    },

    /** 确认保存截图：触发浏览器下载（用户可在保存对话框中选择路径） */
    confirmSaveScreenshot() {
      if (!this.screenshotDataUrl) return
      let name = (this.screenshotFileName || '地图截图.png').trim()
      if (!name) name = '地图截图.png'
      if (!/\.(png|jpg|jpeg)$/i.test(name)) name = name + '.png'
      const a = document.createElement('a')
      a.href = this.screenshotDataUrl
      a.download = name
      a.click()
      this.showScreenshotDialog = false
      this.screenshotDataUrl = ''
      this.$message.success('已触发下载，请在浏览器保存对话框中选择保存位置')
    },

    handleDeleteMenuItem(deletedItem) {
      const index = this.menuItems.findIndex(item => item.id === deletedItem.id)
      if (index > -1) {
        this.menuItems.splice(index, 1)
      }
    },

    async handleSaveMenuItems(savedItems) {
      this.menuItems = [...savedItems]
      // 持久化到当前账号
      try {
        await saveMenuConfig({
          menuItems: JSON.stringify(this.menuItems),
          position: this.menuPosition || 'left'
        })
      } catch (e) {
        // 未登录或接口失败时仅本地生效，仍提示保存成功（LeftMenu 已提示）
        if (e && e.response && e.response.status === 401) {
          this.$message.info('当前未登录，配置仅在本页有效；登录后保存可同步到账号')
        }
      }
    },

    /** 加载当前用户的左侧菜单配置（登录且有过保存时生效） */
    async loadUserMenuConfig() {
      try {
        const res = await getMenuConfig()
        const data = res && res.data
        if (!data) return
        if (data.menuItems) {
          let arr = []
          try {
            arr = typeof data.menuItems === 'string' ? JSON.parse(data.menuItems) : data.menuItems
          } catch (e) { /* 解析失败保留默认 */ }
          if (Array.isArray(arr) && arr.length > 0) {
            const defaultMap = (this.defaultMenuItems || []).reduce((m, it) => { m[it.id] = it; return m }, {})
            const savedIds = new Set(arr.map(i => i.id))
            // 合并缺失的默认项（如新增的4T），按 defaultMenuItems 顺序插入
            const defaultOrder = (this.defaultMenuItems || []).map(d => d.id)
            defaultOrder.forEach(defId => {
              if (!savedIds.has(defId) && defaultMap[defId]) {
                const insertAfterId = defaultOrder[defaultOrder.indexOf(defId) - 1]
                const refIdx = insertAfterId ? arr.findIndex(i => i.id === insertAfterId) : -1
                const insertIdx = refIdx >= 0 ? refIdx + 1 : 0
                arr.splice(insertIdx, 0, { ...defaultMap[defId] })
                savedIds.add(defId)
              }
            })
            this.menuItems = arr.map(item => {
              const def = defaultMap[item.id]
              if (def) return { ...item, name: def.name, icon: def.icon, action: def.action }
              return item
            })
          }
        }
        if (data.position && ['left', 'top', 'bottom'].includes(data.position)) {
          this.menuPosition = data.position
        }
      } catch (e) {
        // 未登录或接口失败则使用默认菜单，不提示
      }
    },

    attributeEdit() {
      this.$message.success('属性修改');
    },

    timeSettings() {
      this.$message.success('时间设置');
      // 这里可以添加时间设置的逻辑
    },

    aircraftSettings() {
      this.$message.success('机型设置');
      // 这里可以添加机型设置的逻辑
    },

    keyEventEdit() {
      this.$message.success('关键事件编辑');
      // 这里可以添加关键事件编辑的逻辑
    },

    missileLaunch() {
      this.$message.success('导弹发射');
      // 这里可以添加导弹发射的逻辑
    },

    // 视图下拉菜单方法
    toggle2D3D() {
      this.$message.success('2D/3D切换');
      // 这里可以添加2D/3D切换的逻辑
    },

    toggleRuler() {
      this.$message.success('显示/隐藏标尺');
      // 这里可以添加标尺显示/隐藏的逻辑
    },

    toggleGrid() {
      this.$message.success('显示/隐藏网格');
      // 这里可以添加网格显示/隐藏的逻辑
    },

    saveScale(scale) {
      console.log('保存比例尺:', scale)
      this.scaleConfig = {
        scaleNumerator: scale.scaleNumerator,
        scaleDenominator: scale.scaleDenominator,
        unit: scale.unit
      }

      if (this.$refs.cesiumMap) {
        this.$refs.cesiumMap.updateScaleFromConfig(this.scaleConfig)
      }

      const scaleText = `${scale.scaleNumerator}:${scale.scaleDenominator}`
      this.$message.success(`比例尺 "${scaleText}" 保存成功`);
    },

    handleScaleClick(scaleInfo) {
      this.currentScale = {
        scaleNumerator: this.scaleConfig.scaleNumerator,
        scaleDenominator: this.scaleConfig.scaleDenominator,
        unit: this.scaleConfig.unit
      }
      this.showScaleDialog = true
    },

    /** 拖拽经过地图时允许放置 */
    handleMapDragover(ev) {
      ev.preventDefault()
      if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'copy'
    },

    /** 将平台图标放置到地图上，并保存到当前房间 */
    async handleMapDrop(ev) {
      ev.preventDefault()
      const raw = ev.dataTransfer && ev.dataTransfer.getData('application/json')
      if (!raw) return
      try {
        const platform = JSON.parse(raw)
        const map = this.$refs.cesiumMap
        if (!map || typeof map.addPlatformIconFromDrag !== 'function') return
        const entityData = map.addPlatformIconFromDrag(platform, ev.clientX, ev.clientY)
        if (!entityData || !this.currentRoomId) return
        const payload = {
          roomId: this.currentRoomId,
          platformId: platform.id,
          platformName: platform.name || '',
          platformType: platform.type || '',
          iconUrl: platform.imageUrl || platform.iconUrl || '',
          lng: entityData.lng,
          lat: entityData.lat,
          heading: entityData.heading != null ? entityData.heading : 0,
          iconScale: entityData.iconScale != null ? entityData.iconScale : 1
        }
        const res = await addRoomPlatformIcon(payload)
        if (res.code === 200 && res.data && res.data.id) {
          map.setPlatformIconServerId(entityData.id, res.data.id, this.currentRoomId)
          entityData.serverId = res.data.id
          entityData.roomId = this.currentRoomId
          this.$message.success('平台图标已保存到当前房间')
          this.wsConnection?.sendSyncPlatformIcons?.()
        }
      } catch (e) {
        console.warn('Parse platform drag data or save failed', e)
        this.$message && this.$message.error('保存平台图标失败')
      }
    },
    /** 平台图标移动/旋转/缩放结束：防抖更新到服务端 */
    onPlatformIconUpdated(entityData) {
      if (!entityData || !entityData.serverId) return
      if (this.platformIconSaveTimer) clearTimeout(this.platformIconSaveTimer)
      this.platformIconSaveTimer = setTimeout(() => {
        this.platformIconSaveTimer = null
        updateRoomPlatformIcon({
          id: entityData.serverId,
          lng: entityData.lng,
          lat: entityData.lat,
          heading: entityData.heading != null ? entityData.heading : 0,
          iconScale: entityData.iconScale != null ? entityData.iconScale : 1
        }).then(() => {
          this.wsConnection?.sendSyncPlatformIcons?.()
        }).catch(() => {})
      }, 500)
    },
    /** 平台图标从地图删除时同步删除服务端记录 */
    onPlatformIconRemoved({ serverId }) {
      if (!serverId) return
      delRoomPlatformIcon(serverId).then(() => {
        this.wsConnection?.sendSyncPlatformIcons?.()
      }).catch(() => {})
    },
    /** 探测区/威力区样式保存后通知其他设备同步 */
    onPlatformStyleSaved() {
      this.wsConnection?.sendSyncPlatformStyles?.()
    },

    handleScaleUnitChange(unit) {
      this.scaleConfig.unit = unit
      if (this.$refs.cesiumMap) {
        this.$refs.cesiumMap.currentScaleUnit = unit
      }
    },

    // 地图下拉菜单方法
    loadTerrain() {
      this.$message.success('加载/切换地形');
      // 这里可以添加地形加载/切换的逻辑
    },

    changeProjection() {
      this.$message.success('投影');
      // 这里可以添加投影切换的逻辑
    },

    loadAeroChart() {
      this.$message.success('航空图');
    },

    // 空域下拉菜单方法
    startPowerZoneDrawing() {
      this.airspaceDrawDom = true
      this.$refs.cesiumMap.startPowerZoneDrawing()
    },

    threatZone() {
      this.$message.success('威胁区');
    },

    // 工具下拉菜单方法
    routeCalculation() {
      this.$message.success('航线计算');
      // 这里可以添加航线计算的逻辑
    },

    conflictDisplay() {
      this.$message.success('冲突显示');
      // 这里可以添加冲突显示的逻辑
    },

    dataMaterials() {
      this.$message.success('数据资料');
      // 这里可以添加数据资料管理的逻辑
    },

    coordinateConversion() {
      this.coordinateFormat = this.coordinateFormat === 'decimal' ? 'dms' : 'decimal'
      this.$message.success(`坐标格式已切换为：${this.coordinateFormat === 'decimal' ? '十进制' : '度分秒'}`)
    },

    // 选项下拉菜单方法
    pageLayout() {
      this.showPageLayoutDialog = true;
    },

    dataStoragePath() {
      this.$message.success('数据存储路径');
    },

    saveExternalParams(externalParams) {
      console.log('保存外部参数:', externalParams)
      this.$message.success('外部参数保存成功');
    },

    async savePageLayout(position) {
      this.menuPosition = position;
      this.$message.success(`菜单位置已设置为：${this.getPositionLabel(position)}`);
      try {
        await saveMenuConfig({
          menuItems: JSON.stringify(this.menuItems),
          position: this.menuPosition || 'left'
        })
      } catch (e) { /* 未登录时仅本地生效 */ }
    },

    getPositionLabel(position) {
      const labels = {
        'top': '顶部',
        'bottom': '底部',
        'left': '左侧',
        'right': '右侧'
      };
      return labels[position] || position;
    },

    importAirport(path) {
      console.log('导入机场:', path)
      this.$message.success('机场数据导入成功');
    },

    importRouteData(path) {
      console.log('导入航路:', path)
      this.$message.success('航路数据导入成功');
    },

    importLandmark(path) {
      console.log('导入地标:', path)
      this.$message.success('地标数据导入成功');
    },

    toggleAirport() {
      this.showAirport = !this.showAirport;
      this.$message.success(this.showAirport ? '显示机场' : '隐藏机场');
    },

    toggleLandmark() {
      this.showLandmark = !this.showLandmark;
      this.$message.success(this.showLandmark ? '显示地标' : '隐藏地标');
    },

    toggleRoute() {
      this.showRoute = !this.showRoute;
      this.$message.success(this.showRoute ? '显示航线' : '隐藏航线');
    },

    generateGanttChart() {
      const url = this.$router.resolve('/ganttChart').href
      window.open(url, '_blank')
    },

    systemDescription() {
      this.$message.success('系统说明');
      // 这里可以添加系统说明的逻辑
    },

    // 收藏下拉菜单方法
    layerFavorites() {
      this.$message.success('图层收藏');
      // 这里可以添加图层收藏的逻辑
    },

    routeFavorites() {
      this.$message.success('航线收藏');
    },


    hideRightPanel() {
      this.isRightPanelHidden = true;
      this.$message.info('隐藏右侧面板');
    },

    selectMenu(item) {
      this.activeMenu = item.id;
      if (item.action) {
        this.handleMenuAction(item.action)
      }

      // 点击方案、平台、冲突等菜单项时，停止地图绘制状态（4T不停止，以便打开4T时仍可绘制航线）
      if (item.id === 'file' || item.id === 'start' || item.id === 'insert') {
        this.drawDom = false;
        this.airspaceDrawDom = false;
      }

      // 点击左侧的方案、冲突、平台时，切换右侧面板内容
      if (item.id === 'file') {
        // 方案：仅切换右侧面板，不打开4T悬浮窗
        if (this.activeRightTab === 'plan' && !this.isRightPanelHidden) {
          this.isRightPanelHidden = true;
        } else {
          this.activeRightTab = 'plan';
          this.isRightPanelHidden = false;
        }
      } else if (item.id === '4t') {
        // 4T：切换4T悬浮窗显示
        this.show4TPanel = !this.show4TPanel;
      } else if (item.id === 'start') {
        // 如果当前已经是冲突标签页，则关闭右侧面板
        if (this.activeRightTab === 'conflict' && !this.isRightPanelHidden) {
          this.isRightPanelHidden = true;
        } else {
          this.activeRightTab = 'conflict';
          this.isRightPanelHidden = false;
        }
      } else if (item.id === 'insert') {
        // 如果当前已经是平台标签页，则关闭右侧面板
        if (this.activeRightTab === 'platform' && !this.isRightPanelHidden) {
          this.isRightPanelHidden = true;
        } else {
          this.activeRightTab = 'platform';
          this.isRightPanelHidden = false;
        }
      } else if (item.id === 'modify') {
        // 点击测距时，启用测距绘制模式，关闭空域绘制模式
        this.drawDom = !this.drawDom
        this.airspaceDrawDom = false
        // 点击测距图标进行地图绘制时，自动收起右侧面板
        this.isRightPanelHidden = true;
        console.log('测距绘制模式:', this.drawDom, 999999)
      } else if (item.id === 'pattern') {
        // 点击空域时，启用空域绘制模式，关闭测距绘制模式
        this.airspaceDrawDom = !this.airspaceDrawDom
        this.drawDom = false
        // 点击空域图标进行地图绘制时，自动收起右侧面板
        this.isRightPanelHidden = true;
        console.log('空域绘制模式:', this.airspaceDrawDom, 999999)
      } else if (item.id === 'deduction') {
        // 点击推演按钮，显示/隐藏K时弹出框
        this.showKTimePopup = !this.showKTimePopup;
        if (this.showKTimePopup) {
          this.$nextTick(() => this.updateTimeFromProgress());
        }
        // 点击推演时，也停止地图绘制状态
        this.drawDom = false;
        this.airspaceDrawDom = false;
      } else if (item.id === 'datacard') {
        // 点击数据卡，在新标签页打开
        window.open('/dataCard.html', '_blank');
      } else {
        // 点击其他菜单项时，也自动收起右侧面板
        this.isRightPanelHidden = true;
        // 点击其他菜单项时，也停止地图绘制状态
        this.drawDom = false;
        this.airspaceDrawDom = false;
      }
    },

    // K时弹出框操作
    hideKTimePopup() {
      this.showKTimePopup = false;
      this.$message.info('隐藏推演时钟控制');
    },

    // 播放控制
    togglePlay() {
      this.isPlaying = !this.isPlaying;
      if (this.isPlaying) {
        this.startPlayback();
        this.$message.success('开始播放');
      } else {
        this.stopPlayback();
        this.$message.info('暂停播放');
      }
    },

    startPlayback() {
      if (this.playbackInterval) {
        clearInterval(this.playbackInterval);
      }
      this._playbackLastTickTime = Date.now();
      // 按真实经过时间推进：1 实机秒 = playbackSpeed 推演秒，避免 setInterval 不准导致“现实两秒对应时间轴一秒”
      this.playbackInterval = setInterval(() => {
        const now = Date.now();
        const elapsedSec = (now - (this._playbackLastTickTime || now)) / 1000;
        this._playbackLastTickTime = now;
        const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
        const span = Math.max(0, maxMinutes - minMinutes) || 120;
        const spanSeconds = span * 60;
        // 经过 elapsedSec 实机秒 → 推进 playbackSpeed * elapsedSec 推演秒 → 进度增量 (elapsedSec * playbackSpeed / spanSeconds) * 100
        this.timeProgress += (elapsedSec * this.playbackSpeed / spanSeconds) * 100;
        if (this.timeProgress >= 100) {
          this.timeProgress = 0;
        }
        // 时间显示与平台位置由 watch timeProgress 触发 updateTimeFromProgress
      }, 100);
    },

    stopPlayback() {
      if (this.playbackInterval) {
        clearInterval(this.playbackInterval);
        this.playbackInterval = null;
      }
    },

    setPlaybackSpeed(speed) {
      if (this.speedOptions.includes(speed)) {
        this.playbackSpeed = speed;
        if (this.isPlaying) {
          this.startPlayback();
        }
      }
    },

    updateTimeFromProgress() {
      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 totalSeconds = Math.floor(Math.abs(currentMinutesFromK) * 60);
      const hours = Math.floor(totalSeconds / 3600);
      const minutes = Math.floor((totalSeconds % 3600) / 60);
      const seconds = totalSeconds % 60;
      this.currentTime = `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
      // 右上角作战时间随时与推演时间轴同步（无论是否展示航线）
      this.combatTime = this.currentTime;
      this.updateDeductionPositions();
      // 有房间且已选航线时，按房间+航线组合从 Redis 加载导弹（仅变化时加载一次）
      const roomId = this.currentRoomId;
      const routeIds = (this.activeRouteIds || []).slice().sort((a, b) => (a - b));
      if (roomId != null && routeIds.length > 0 && this.$refs.cesiumMap && typeof this.$refs.cesiumMap.loadMissilesFromRedis === 'function') {
        const loadKey = roomId + '-' + routeIds.join(',');
        if (this._missilesLoadKey !== loadKey) {
          this._missilesLoadKey = loadKey;
          const routePlatforms = this.routes
            .filter(r => routeIds.includes(r.id))
            .map(r => ({ routeId: r.id, platformId: r.platformId != null ? r.platformId : 0 }));
          if (routePlatforms.length > 0) {
            this.$refs.cesiumMap.loadMissilesFromRedis(roomId, routePlatforms);
          }
        }
      }
      // 时间轴变化时显式通知地图更新导弹显隐与位置（保证与时间轴一致）
      if (this.$refs.cesiumMap && typeof this.$refs.cesiumMap.updateMissilesByTime === 'function') {
        this.$refs.cesiumMap.updateMissilesByTime(this.deductionMinutesFromK);
      }
    },

    /** 航线编辑后，根据新航点重算导弹发射位置并更新 Redis */
    async updateMissilePositionsAfterRouteEdit(roomId, routeId, platformId, waypoints) {
      try {
        const res = await getMissileParams({ roomId, routeId, platformId });
        let missiles = res.data;
        if (!missiles || (Array.isArray(missiles) && missiles.length === 0)) return;
        if (!Array.isArray(missiles)) missiles = [missiles];
        const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
        const updates = [];
        for (const m of missiles) {
          const launchK = m.launchTimeMinutesFromK != null ? Number(m.launchTimeMinutesFromK) : null;
          if (launchK == null) continue;
          const { position, nextPosition } = this.getPositionAtMinutesFromK(waypoints, launchK, minMinutes, maxMinutes);
          if (!position || position.lng == null || position.lat == null) continue;
          let platformHeadingDeg = 0;
          if (nextPosition && nextPosition.lng != null && nextPosition.lat != null) {
            platformHeadingDeg = this.headingDegFromPositions(position, nextPosition);
          }
          updates.push({
            launchTimeMinutesFromK: launchK,
            startLng: position.lng,
            startLat: position.lat,
            platformHeadingDeg
          });
        }
        if (updates.length > 0) {
          await updateMissilePositions({ roomId, routeId, platformId, updates });
          this.handleMissileDeleted();
        }
      } catch (_) {}
    },

    /** 导弹删除后重新加载导弹列表 */
    handleMissileDeleted() {
      this.reloadMissilesAfterRouteChange();
    },

    /** 根据当前选中的航线重新加载导弹（删除航线后需调用以清除地图上的导弹） */
    reloadMissilesAfterRouteChange() {
      const roomId = this.currentRoomId;
      if (roomId == null || !this.$refs.cesiumMap || typeof this.$refs.cesiumMap.loadMissilesFromRedis !== 'function') return;
      const routeIds = (this.activeRouteIds || []).slice().sort((a, b) => (a - b));
      const routePlatforms = this.routes
        .filter(r => routeIds.includes(r.id))
        .map(r => ({ routeId: r.id, platformId: r.platformId != null ? r.platformId : 0 }));
      this.$refs.cesiumMap.loadMissilesFromRedis(roomId, routePlatforms);
    },

    /** 仅针对当前展示的航线（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);
    },
    /** 将 startTime（如 K+00:19:30）转为相对 K 的分钟数（含秒，保留小数） */
    waypointStartTimeToMinutesDecimal(s) {
      if (!s || typeof s !== 'string') return 0;
      const m = s.match(/K([+-])(\d{2}):(\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);
      const sec = m[4] != null ? parseInt(m[4], 10) : 0;
      return sign * (h * 60 + min + sec / 60);
    },
    /** 将相对 K 的分钟数转为 startTime 字符串（如 K+01:00、K-00:30） */
    minutesToStartTime(minutes) {
      const m = Math.floor(Number(minutes));
      if (!Number.isFinite(m)) return 'K+00:00';
      const sign = m >= 0 ? '+' : '-';
      const abs = Math.abs(m);
      const h = Math.floor(abs / 60);
      const min = abs % 60;
      return `K${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}`;
    },
    /** 将相对 K 的分钟数（可含小数）转为 startTime，精确到秒（如 K+00:19:30） */
    minutesToStartTimeWithSeconds(minutes) {
      const num = Number(minutes);
      if (!Number.isFinite(num)) return 'K+00:00:00';
      const sign = num >= 0 ? '+' : '-';
      const abs = Math.abs(num);
      const totalSec = Math.round(abs * 60);
      const h = Math.floor(totalSec / 3600);
      const min = Math.floor((totalSec % 3600) / 60);
      const sec = totalSec % 60;
      return `K${sign}${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
    },

    isHoldWaypoint(wp) {
      const t = (wp && wp.pointType) || (wp && wp.point_type) || 'normal';
      return t === 'hold_circle' || t === 'hold_ellipse';
    },
    parseHoldParams(wp) {
      const raw = (wp && wp.holdParams) || (wp && wp.hold_params);
      if (!raw) return null;
      try {
        const p = typeof raw === 'string' ? JSON.parse(raw) : raw;
        return {
          radius: p.radius,
          semiMajor: p.semiMajor ?? p.semiMajorAxis,
          semiMinor: p.semiMinor ?? p.semiMinorAxis,
          headingDeg: p.headingDeg ?? 0,
          clockwise: p.clockwise !== false,
          edgeLength: p.edgeLength != null ? Number(p.edgeLength) : 20000
        };
      } catch (e) {
        return null;
      }
    },

    /** 路径片段总距离（米） */
    pathSliceDistance(pathSlice) {
      if (!pathSlice || pathSlice.length < 2) return 0;
      let d = 0;
      for (let i = 1; i < pathSlice.length; i++) d += this.segmentDistance(pathSlice[i - 1], pathSlice[i]);
      return d;
    },

    /** 圆周上按角度取点：圆心 lng/lat/alt，半径米。angleRad 为从北顺时针的角度弧度，0=北 */
    positionOnCircle(centerLng, centerLat, centerAlt, radiusM, angleRad) {
      const R = 6371000;
      const latRad = (centerLat * Math.PI) / 180;
      const dNorth = radiusM * Math.cos(angleRad);
      const dEast = radiusM * Math.sin(angleRad);
      const dLat = (dNorth / R) * (180 / Math.PI);
      const dLng = (dEast / (R * Math.cos(latRad))) * (180 / Math.PI);
      return {
        lng: centerLng + dLng,
        lat: centerLat + dLat,
        alt: centerAlt != null ? centerAlt : 0
      };
    },
    /** 从圆心到某点的角度（弧度，从北顺时针），用于盘旋入口基准角 */
    angleFromCenterToPoint(centerLng, centerLat, pointLng, pointLat) {
      const R = 6371000;
      const latRad = (centerLat * Math.PI) / 180;
      const dNorth = (pointLat - centerLat) * (R * Math.PI / 180);
      const dEast = (pointLng - centerLng) * (R * Math.cos(latRad) * Math.PI / 180);
      return Math.atan2(dEast, dNorth);
    },

    /** 两航点间近似距离（米），含高度差 */
    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);
    },

    /**
     * 按速度与计划时间构建航线时间轴：含飞行段、盘旋段与“提前到达则等待”的等待段。
     * pathData 可选：{ path, segmentEndIndices, holdArcRanges }，由 getRoutePathWithSegmentIndices 提供，用于输出 hold 段。
     * holdRadiusByLegIndex 可选：{ [legIndex]: number }，为盘旋段指定半径（用于推演时落点精准在切点）。
     */
    buildRouteTimeline(waypoints, globalMin, globalMax, pathData, holdRadiusByLegIndex) {
      const warnings = [];
      if (!waypoints || waypoints.length === 0) return { segments: [], warnings };
      const points = waypoints.map((wp, idx) => ({
        lng: parseFloat(wp.lng),
        lat: parseFloat(wp.lat),
        alt: Number(wp.alt) || 0,
        minutes: this.waypointStartTimeToMinutes(wp.startTime),
        speed: Number(wp.speed) || 800,
        isHold: this.isHoldWaypoint(wp)
      }));
      const hasHold = points.some(p => p.isHold);
      const allSame = points.every(p => p.minutes === points[0].minutes);
      if (allSame && points.length > 1 && !hasHold) {
        const span = Math.max(globalMax - globalMin, 1);
        points.forEach((p, i) => {
          p.minutes = globalMin + (span * i) / (points.length - 1);
        });
      } else if (!hasHold) {
        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,
          earlyArrivalLegs: [],
          lateArrivalLegs: []
        };
      }
      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 || {};
      for (let i = 0; i < points.length - 1; i++) {
        if (this.isHoldWaypoint(waypoints[i + 1]) && path && segmentEndIndices && holdArcRanges[i]) {
          const range = holdArcRanges[i];
          const startIdx = i === 0 ? 0 : segmentEndIndices[i - 1] + 1;
          const toEntrySlice = path.slice(startIdx, range.start + 1);
          const holdPathSlice = path.slice(range.start, range.end + 1);
          const exitIdx = segmentEndIndices[i];
          const toNextSlice = path.slice(exitIdx, (segmentEndIndices[i + 1] != null ? segmentEndIndices[i + 1] : path.length - 1) + 1);
          const distToEntry = this.pathSliceDistance(toEntrySlice);
          const holdWpForSegment = waypoints[i + 1];
          const segTarget = holdWpForSegment && (holdWpForSegment.segmentTargetMinutes ?? holdWpForSegment.displayStyle?.segmentTargetMinutes);
          const hasFixedTime = holdWpForSegment && holdWpForSegment.segmentMode === 'fixed_time' && (segTarget != null && segTarget !== '');
          let arrivalEntry;
          let speedKmhForLeg = points[i].speed || 800;
          if (hasFixedTime) {
            const targetMin = Number(segTarget);
            const deltaMin = targetMin - effectiveTime[i];
            if (deltaMin > 0.001 && distToEntry > 0) {
              arrivalEntry = targetMin;
              speedKmhForLeg = (distToEntry / 1000) / (deltaMin / 60);
              speedKmhForLeg = Math.round(speedKmhForLeg * 10) / 10;
            } else {
              arrivalEntry = effectiveTime[i] + (distToEntry / 1000) * (60 / speedKmhForLeg);
            }
          } else {
            const travelToEntryMin = (distToEntry / 1000) * (60 / speedKmhForLeg);
            arrivalEntry = effectiveTime[i] + travelToEntryMin;
          }
          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, 200); 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 = holdPathSlice.length - 1;
          let minD = 1e9;
          for (let k = 0; k < holdPathSlice.length; 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 holdSpeedKmh = points[i + 1].speed || 800;
          const HOLD_SPEED_KMH = 800;
          const speedMpMin = (HOLD_SPEED_KMH * 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 / holdSpeedKmh);
          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 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 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
            ? this.angleFromCenterToPoint(holdCenter.lng, holdCenter.lat, entryPos.lng, entryPos.lat)
            : null;
          segments.push({ startTime: effectiveTime[i], endTime: arrivalEntry, startPos: posCur, endPos: entryPos, type: 'fly', legIndex: i, pathSlice: toEntrySlice, speedKmh: speedKmhForLeg });
          const holdEntryToExitPath = holdClosedLoopPath.slice(0, exitIdxOnLoop + 1);
          segments.push({
            startTime: arrivalEntry,
            endTime: segmentEndTime,
            startPos: entryPos,
            endPos: exitPos,
            type: 'hold',
            legIndex: i,
            holdPath: holdPathSlice,
            holdClosedLoopPath,
            holdLoopLength,
            holdExitDistanceOnLoop,
            holdEntryToExitPath,
            holdN: n,
            speedKmh: HOLD_SPEED_KMH,
            holdEndTime,
            holdCenter,
            holdRadius,
            holdCircumference,
            holdClockwise,
            holdEntryAngle
          });
          segments.push({ startTime: segmentEndTime, endTime: arrivalNext, startPos: exitPos, endPos: toNextSlice.length ? toNextSlice[toNextSlice.length - 1] : exitPos, type: 'fly', legIndex: i, pathSlice: toNextSlice, speedKmh: holdSpeedKmh });
          continue; // 不 i++，让下次迭代处理下一航段（含连续盘旋点如 WP2→WP3 均为盘旋）
        }
        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时或速度。`
            );
            lateArrivalLegs.push({
              legIndex: i,
              fromName: waypoints[i].name,
              toName: waypoints[i + 1].name,
              requiredSpeedKmh: Math.ceil(requiredSpeedKmh),
              speedKmh
            });
          } 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', legIndex: i, speedKmh: speedKmh });
        if (actualArrival < effectiveTime[i + 1]) {
          segments.push({ startTime: actualArrival, endTime: effectiveTime[i + 1], startPos: posNext, endPos: posNext, type: 'wait', legIndex: i });
        }
      }
      const earlyArrivalLegs = [];
      for (let i = 0; i < points.length - 1; i++) {
        if (this.isHoldWaypoint(waypoints[i + 1])) continue;
        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 && actualArrival < scheduled - 0.5) {
          earlyArrivalLegs.push({ legIndex: i, scheduled, actualArrival, fromName: waypoints[i].name, toName: waypoints[i + 1].name });
        }
      }
      return { segments, warnings, earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts };
    },

    /** 从路径片段中按比例 t（0~1）取点：按弧长比例插值 */
    getPositionAlongPathSlice(pathSlice, t) {
      if (!pathSlice || pathSlice.length === 0) return null;
      if (pathSlice.length === 1 || t <= 0) return pathSlice[0];
      if (t >= 1) return pathSlice[pathSlice.length - 1];
      let totalLen = 0;
      const lengths = [0];
      for (let i = 1; i < pathSlice.length; i++) {
        totalLen += this.segmentDistance(pathSlice[i - 1], pathSlice[i]);
        lengths.push(totalLen);
      }
      const targetDist = t * totalLen;
      let idx = 0;
      while (idx < lengths.length - 1 && lengths[idx + 1] < targetDist) idx++;
      const a = pathSlice[idx];
      const b = pathSlice[idx + 1];
      const segLen = lengths[idx + 1] - lengths[idx];
      const segT = segLen > 0 ? (targetDist - lengths[idx]) / segLen : 0;
      return {
        lng: a.lng + (b.lng - a.lng) * segT,
        lat: a.lat + (b.lat - a.lat) * segT,
        alt: a.alt + (b.alt - a.alt) * segT
      };
    },

    /** 从时间轴中取当前推演时间对应的位置；支持 fly/wait/hold，hold 沿 holdPath 弧长插值 */
    getPositionFromTimeline(segments, minutesFromK, path, segmentEndIndices) {
      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) {
        if (last.type === 'wait' && path && segmentEndIndices && last.legIndex != null && last.legIndex < segmentEndIndices.length && path[segmentEndIndices[last.legIndex]]) {
          return path[segmentEndIndices[last.legIndex]];
        }
        if (last.type === 'hold' && last.holdPath && last.holdPath.length) return last.holdPath[last.holdPath.length - 1];
        return last.endPos;
      }
      for (let i = 0; i < segments.length; i++) {
        const s = segments[i];
        if (minutesFromK < s.endTime) {
          const t = Math.max(0, Math.min(1, (minutesFromK - s.startTime) / (s.endTime - s.startTime)));
          if (s.type === 'wait') {
            if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) {
              const endIdx = segmentEndIndices[s.legIndex];
              if (path[endIdx]) return path[endIdx];
            }
            return s.startPos;
          }
          if (s.type === 'hold' && s.holdPath && s.holdPath.length) {
            if (s.holdClosedLoopPath && s.holdClosedLoopPath.length >= 2 && s.holdLoopLength > 0 && s.speedKmh != null) {
              const distM = (minutesFromK - s.startTime) * (s.speedKmh * 1000 / 60);
              const n = Math.max(0, s.holdN != null ? s.holdN : 0);
              const totalFly = (s.holdExitDistanceOnLoop || 0) + n * s.holdLoopLength;
              if (totalFly <= 0) return s.startPos;
              if (distM >= totalFly) return s.endPos;
              if (distM < n * s.holdLoopLength) {
                const distOnLoop = ((distM % s.holdLoopLength) + s.holdLoopLength) % s.holdLoopLength;
                const tPath = distOnLoop / s.holdLoopLength;
                return this.getPositionAlongPathSlice(s.holdClosedLoopPath, tPath);
              }
              const distToExit = distM - n * s.holdLoopLength;
              const exitDist = s.holdExitDistanceOnLoop || 1;
              const tPath = Math.min(1, distToExit / exitDist);
              if (s.holdEntryToExitPath && s.holdEntryToExitPath.length > 0) {
                return this.getPositionAlongPathSlice(s.holdEntryToExitPath, tPath);
              }
              return this.getPositionAlongPathSlice(s.holdClosedLoopPath, distToExit / s.holdLoopLength);
            }
            return this.getPositionAlongPathSlice(s.holdPath, t);
          }
          if (s.type === 'fly' && s.pathSlice && s.pathSlice.length) {
            return this.getPositionAlongPathSlice(s.pathSlice, t);
          }
          if (path && segmentEndIndices && s.legIndex != null && s.legIndex < segmentEndIndices.length) {
            const startIdx = s.legIndex === 0 ? 0 : segmentEndIndices[s.legIndex - 1];
            const endIdx = segmentEndIndices[s.legIndex];
            const pathSlice = path.slice(startIdx, endIdx + 1);
            if (pathSlice.length > 0) return this.getPositionAlongPathSlice(pathSlice, t);
          }
          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 的分钟）得到平台位置，使用速度与等待逻辑；若有地图 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 (cesiumMap && cesiumMap.getRoutePathWithSegmentIndices) {
        const cachedRadii = (routeId != null && cesiumMap._routeHoldRadiiByRoute && cesiumMap._routeHoldRadiiByRoute[routeId]) ? cesiumMap._routeHoldRadiiByRoute[routeId] : {};
        const cachedEllipse = (routeId != null && cesiumMap._routeHoldEllipseParamsByRoute && cesiumMap._routeHoldEllipseParamsByRoute[routeId]) ? cesiumMap._routeHoldEllipseParamsByRoute[routeId] : {};
        const opts = (Object.keys(cachedRadii).length > 0 || Object.keys(cachedEllipse).length > 0) ? { holdRadiusByLegIndex: cachedRadii, holdEllipseParamsByLegIndex: cachedEllipse } : {};
        const ret = cesiumMap.getRoutePathWithSegmentIndices(waypoints, opts);
        if (ret.path && ret.path.length > 0 && ret.segmentEndIndices && ret.segmentEndIndices.length > 0) {
          pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
        }
      }
      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 holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(waypoints[i + 1]?.startTime);
          const holdWp = waypoints[i + 1];
          const segTarget = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes);
          const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTarget != null && segTarget !== '')
            ? Number(segTarget) : s.startTime;
          const holdDurationMin = Math.max(0, holdEndTime - arrivalAtHold);
          const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWp?.speed) || 800);
          const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
          const prevWp = waypoints[i];
          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 isHoldEllipse = waypoints[i + 1] && (waypoints[i + 1].pointType || waypoints[i + 1].point_type) === 'hold_ellipse';
          const isEllipse = isHoldEllipse || s.holdRadius == null;
          if (isEllipse && !isHoldEllipse && 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
              };
            }
          } else if (!isEllipse && cesiumMap.computeHoldRadiusForDuration) {
            let R = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, clockwise, totalHoldDistM);
            if (R == null || !Number.isFinite(R)) {
              R = totalHoldDistM / (2 * Math.PI);
            }
            if (R != null && Number.isFinite(R) && R > 0) {
              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 < 4; 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 holdWpCircle = waypoints[i + 1];
                const holdEndTimeCircle = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWpCircle?.startTime);
                const segTargetCircle = holdWpCircle && (holdWpCircle.segmentTargetMinutes ?? holdWpCircle.displayStyle?.segmentTargetMinutes);
                const arrivalAtHoldCircle = (holdWpCircle && holdWpCircle.segmentMode === 'fixed_time' && segTargetCircle != null && segTargetCircle !== '')
                  ? Number(segTargetCircle) : s.startTime;
                const holdDurationMin = Math.max(0, holdEndTimeCircle - arrivalAtHoldCircle);
                const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWpCircle?.speed) || 800);
                const totalHoldDistM = speedKmh * (holdDurationMin / 60) * 1000;
                const prevWp = waypoints[i];
                const holdWp = holdWpCircle;
                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;
                let Rnew = cesiumMap.computeHoldRadiusForDuration(centerCartesian, prevCartesian, nextCartesian, s.holdClockwise !== false, totalHoldDistM);
                if (Rnew == null || !Number.isFinite(Rnew)) Rnew = totalHoldDistM / (2 * Math.PI);
                if (Rnew != null && Number.isFinite(Rnew) && Rnew > 0) {
                  nextRadii[i] = Rnew;
                  if (holdRadiusByLegIndex[i] == null || Math.abs(nextRadii[i] - holdRadiusByLegIndex[i]) > 1) changed = true;
                }
              }
              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];
                if ((holdWp && (holdWp.pointType || holdWp.point_type) === 'hold_ellipse')) continue;
                const holdParams = this.parseHoldParams(holdWp);
                const holdEndTime = s.holdEndTime != null ? s.holdEndTime : this.waypointStartTimeToMinutesDecimal(holdWp?.startTime);
                const segTargetEllipse = holdWp && (holdWp.segmentTargetMinutes ?? holdWp.displayStyle?.segmentTargetMinutes);
                const arrivalAtHold = (holdWp && holdWp.segmentMode === 'fixed_time' && segTargetEllipse != null && segTargetEllipse !== '')
                  ? Number(segTargetEllipse) : s.startTime;
                const holdDurationMin = Math.max(0, holdEndTime - arrivalAtHold);
                const speedKmh = s.speedKmh != null ? s.speedKmh : (Number(holdWp?.speed) || 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 smj = out.semiMajor;
                  const smn = out.semiMinor;
                  const old = holdEllipseParamsByLegIndex[i];
                  if (!old || Math.abs(smj - old.semiMajor) > 1) changed = true;
                  holdEllipseParamsByLegIndex[i] = { semiMajor: smj, semiMinor: smn, headingDeg };
                }
              }
            }
            if (!changed || iter === 3) 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);
      const stepMin = 1 / 60;
      const nextPosition = this.getPositionFromTimeline(segments, minutesFromK + stepMin, path, segmentEndIndices);
      const previousPosition = this.getPositionFromTimeline(segments, minutesFromK - stepMin, path, segmentEndIndices);
      // 当前所在航段，用于标牌速度（与数据库航点 speed 对应）
      let currentSegment = null;
      if (segments && segments.length > 0) {
        if (minutesFromK <= segments[0].startTime) {
          const s = segments[0];
          currentSegment = { legIndex: s.legIndex, speedKmh: waypoints[s.legIndex] ? (Number(waypoints[s.legIndex].speed) || 800) : 800 };
        } else if (minutesFromK >= segments[segments.length - 1].endTime) {
          const s = segments[segments.length - 1];
          currentSegment = { legIndex: s.legIndex, speedKmh: s.speedKmh != null ? s.speedKmh : (waypoints[s.legIndex] ? (Number(waypoints[s.legIndex].speed) || 800) : 800) };
        } else {
          for (let i = 0; i < segments.length; i++) {
            const s = segments[i];
            if (minutesFromK >= s.startTime && minutesFromK < s.endTime) {
              currentSegment = { legIndex: s.legIndex, speedKmh: s.speedKmh != null ? s.speedKmh : (waypoints[s.legIndex] ? (Number(waypoints[s.legIndex].speed) || 800) : 800) };
              break;
            }
          }
        }
      }
      return { position, nextPosition, previousPosition, warnings, earlyArrivalLegs: earlyArrivalLegs || [], currentSegment };
    },

    /** 根据两点计算航向角（度），北为 0，顺时针为正，与数据库/标牌航向一致 */
    headingDegFromPositions(fromPos, toPos) {
      if (!fromPos || !toPos) return 0;
      const dLng = (toPos.lng != null ? Number(toPos.lng) : 0) - (fromPos.lng != null ? Number(fromPos.lng) : 0);
      const dLat = (toPos.lat != null ? Number(toPos.lat) : 0) - (fromPos.lat != null ? Number(fromPos.lat) : 0);
      if (Math.abs(dLng) < 1e-10 && Math.abs(dLat) < 1e-10) return 0;
      const rad = Math.atan2(dLng, dLat);
      let deg = (rad * 180 / Math.PI);
      return ((deg % 360) + 360) % 360;
    },

    /** 仅根据当前展示的航线（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, 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;
          const labelData = {
            name: (route.platform && route.platform.name) ? route.platform.name : '平台',
            altitude: position.alt != null ? Number(position.alt) : 0,
            speed: (currentSegment && currentSegment.speedKmh != null) ? currentSegment.speedKmh : 800,
            headingDeg: directionPoint ? this.headingDegFromPositions(position, directionPoint) : 0
          };
          this.$refs.cesiumMap.updatePlatformPosition(routeId, position, directionPoint, labelData);
        }
        this.deductionEarlyArrivalByRoute[routeId] = earlyArrivalLegs || [];
      });
      this.deductionWarnings = [...new Set(allWarnings)];
    },

    openAddHoldFromFirstEarly() {
      for (const routeId of this.activeRouteIds) {
        const legs = this.deductionEarlyArrivalByRoute[routeId] || [];
        if (legs.length === 0) continue;
        const route = this.routes.find(r => r.id === routeId);
        const leg = legs[0];
        const waypoints = route && route.waypoints ? route.waypoints : [];
        const nextWp = waypoints[leg.legIndex + 1];
        this.addHoldContext = {
          routeId,
          routeName: route ? route.name : '',
          legIndex: leg.legIndex,
          fromName: leg.fromName,
          toName: leg.toName
        };
        const defaultStart = nextWp && nextWp.startTime ? nextWp.startTime : 'K+01:00';
        this.addHoldForm.startTime = defaultStart;
        this.addHoldForm.startTimeMinutes = this.waypointStartTimeToMinutes(defaultStart);
        this.showAddHoldDialog = true;
        return;
      }
    },

    async saveAddHold() {
      if (!this.addHoldContext) return;
      if (this.addHoldContext.mode === 'drawing') {
        const holdParams = this.addHoldForm.holdType === 'hold_circle'
          ? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise }
          : { edgeLength: (this.addHoldForm.edgeLengthKm != null ? this.addHoldForm.edgeLengthKm : 20) * 1000, clockwise: this.addHoldForm.clockwise };
        if (this.$refs.cesiumMap && this.$refs.cesiumMap.insertHoldBetweenLastTwo) {
          this.$refs.cesiumMap.insertHoldBetweenLastTwo(holdParams);
        }
        this.showAddHoldDialog = false;
        this.addHoldContext = null;
        this.$message.success('已插入盘旋，继续点选航点后右键结束保存');
        return;
      }
      const { routeId, legIndex } = this.addHoldContext;
      const route = this.routes.find(r => r.id === routeId);
      if (!route || !route.waypoints || route.waypoints.length < 2) {
        this.$message.warning('航线数据异常');
        return;
      }
      const waypoints = route.waypoints;
      const prevWp = waypoints[legIndex];
      const nextWp = waypoints[legIndex + 1];
      const newSeq = (prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1) + 1;
      const baseSeq = prevWp.seq != null ? Number(prevWp.seq) : legIndex + 1;
      const holdParams = this.addHoldForm.holdType === 'hold_circle'
        ? { radius: this.addHoldForm.radius, clockwise: this.addHoldForm.clockwise }
        : { edgeLength: (this.addHoldForm.edgeLengthKm != null ? this.addHoldForm.edgeLengthKm : 20) * 1000, clockwise: this.addHoldForm.clockwise };
      const startTime = this.addHoldForm.startTimeMinutes !== '' && this.addHoldForm.startTimeMinutes != null && !Number.isNaN(Number(this.addHoldForm.startTimeMinutes))
          ? this.minutesToStartTime(Number(this.addHoldForm.startTimeMinutes))
          : (nextWp.startTime || 'K+01:00');
      try {
        await addWaypoints({
          routeId,
          name: 'HOLD',
          seq: newSeq,
          lat: nextWp.lat,
          lng: nextWp.lng,
          alt: nextWp.alt != null ? nextWp.alt : prevWp.alt,
          speed: prevWp.speed || 800,
          startTime,
          pointType: this.addHoldForm.holdType,
          holdParams: JSON.stringify(holdParams)
        });
        await delWaypoints(nextWp.id);
        for (let i = legIndex + 2; i < waypoints.length; i++) {
          const w = waypoints[i];
          if (w.id) {
            await updateWaypoints({ ...w, seq: baseSeq + (i - legIndex) });
          }
        }
        this.showAddHoldDialog = false;
        this.addHoldContext = null;
        await this.getList();
        const updated = this.routes.find(r => r.id === routeId);
        if (updated && updated.waypoints && this.activeRouteIds.includes(routeId) && this.$refs.cesiumMap) {
          const roomId = this.currentRoomId;
          if (roomId && updated.platformId) {
            try {
              const styleRes = await getPlatformStyle({ roomId, routeId, platformId: updated.platformId });
              if (styleRes.data) this.$refs.cesiumMap.setPlatformStyle(routeId, styleRes.data);
            } catch (_) {}
          }
          this.$refs.cesiumMap.removeRouteById(routeId);
          this.$refs.cesiumMap.renderRouteWaypoints(updated.waypoints, routeId, updated.platformId, updated.platform, this.parseRouteStyle(updated.attributes));
          this.$nextTick(() => this.updateDeductionPositions());
          if (roomId && updated.waypoints && updated.waypoints.length > 0) {
            this.updateMissilePositionsAfterRouteEdit(roomId, routeId, updated.platformId != null ? updated.platformId : 0, updated.waypoints);
          }
        }
        this.$message.success('已添加盘旋航点');
        this.wsConnection?.sendSyncWaypoints?.(routeId);
      } catch (e) {
        this.$message.error(e.msg || '添加盘旋失败');
        console.error(e);
      }
    },

    // 时间控制（保留用于底部时间轴）
    play() {
      this.$message.success('推演开始');
    },

    pause() {
      this.$message.info('推演暂停');
    },

    reset() {
      this.timeProgress = 0;
      this.currentTime = 'K+00:00:00';
      this.$message.info('推演已重置');
    },

    formatTimeTooltip(val) {
      const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
      const span = Math.max(0, maxMinutes - minMinutes) || 120;
      const minutesFromK = minMinutes + (val / 100) * span;
      const sign = minutesFromK >= 0 ? '+' : '-';
      const totalSeconds = Math.floor(Math.abs(minutesFromK) * 60);
      const hours = Math.floor(totalSeconds / 3600);
      const minutes = Math.floor((totalSeconds % 3600) / 60);
      const seconds = totalSeconds % 60;
      return `K${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
    },

    onTimelineHover(e) {
      const wrap = this.$refs.timelineSliderWrap;
      if (!wrap) return;
      const rect = wrap.getBoundingClientRect();
      const percent = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
      this.timelineHoverPercent = percent;
      this.timelineHoverTime = this.formatTimeTooltip(percent);
      this.timelineHoverVisible = true;
    },
    onTimelineLeave() {
      this.timelineHoverVisible = false;
    },
    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;
      }

      // 清空地图上的航点/航线及探测区、威力区（按 routeId 逐个移除，避免误删其他实体）
      if (this.$refs.cesiumMap) {
        [...this.activeRouteIds].forEach(routeId => {
          this.$refs.cesiumMap.removeRouteById(routeId);
        });
      }

      // 保留 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));
            }
            this.$nextTick(() => this.updateDeductionPositions());
          } 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.name}，已恢复显示 ${planRouteIds.length} 条航线。`);
    },
    /** 切换航线：实现多选/开关逻辑 */
    async selectRoute(route) {
      const index = this.activeRouteIds.indexOf(route.id);
      const isRouteExpanded = this.$refs.rightPanel ? this.$refs.rightPanel.expandedRoutes.includes(route.id) : false;
      // 航线已在选中列表中
      if (index > -1) {
        if (isRouteExpanded) {
          return;
        } else {
          // 航线未展开，点击则取消选中（从地图移除）
          this.activeRouteIds.splice(index, 1);
          if (this.$refs.cesiumMap) {
            this.$refs.cesiumMap.removeRouteById(route.id);
            // 隐藏航线时，同时移除关联的探测区、威力区
            this.$refs.cesiumMap.removeDetectionZoneByRouteId(route.id);
            this.$refs.cesiumMap.removePowerZoneByRouteId(route.id);
          }
          if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) {
            if (this.activeRouteIds.length > 0) {
              const lastId = this.activeRouteIds[this.activeRouteIds.length - 1];
              try {
                const res = await getRoutes(lastId);
                if (res.code === 200 && res.data) {
                  this.selectedRouteId = res.data.id;
                  this.selectedRouteDetails = {
                    id: res.data.id,
                    name: res.data.callSign,
                    waypoints: res.data.waypoints || []
                  };
                }
              } catch (e) {
                console.error("回显剩余航线失败", e);
              }
            } else {
              this.selectedRouteId = null;
              this.selectedRouteDetails = null;
            }
          }
          this.wsConnection?.sendSyncRouteVisibility?.(route.id, false);
          this.$message.info(`已取消航线: ${route.name}`);
          return;
        }
      }

      // 航线未被选中，点击则选中并显示航线和航点
      try {
        const response = await getRoutes(route.id);
        if (response.code === 200 && response.data) {
          const fullRouteData = response.data;
          const waypoints = fullRouteData.waypoints || [];
          this.activeRouteIds.push(route.id);
          this.selectedRouteId = fullRouteData.id;
          // 合并 list 中的 platformId/platform，以便拖拽航点后重绘时平台图标不丢失
          this.selectedRouteDetails = {
            id: fullRouteData.id,
            name: fullRouteData.callSign,
            waypoints: waypoints,
            platformId: route.platformId,
            platform: route.platform,
            attributes: route.attributes
          };

          // 更新 routes 数组中对应航线的 waypoints 字段
          const routeIndex = this.routes.findIndex(r => r.id === route.id);
          if (routeIndex > -1) {
            this.$set(this.routes, routeIndex, {
              ...this.routes[routeIndex],
              waypoints: waypoints
            });
          }

          if (waypoints.length > 0) {
            if (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 (_) {}
              }
              if (waypoints.some(wp => this.isHoldWaypoint(wp))) {
                const { minMinutes, maxMinutes } = this.getDeductionTimeRange();
                this.getPositionAtMinutesFromK(waypoints, minMinutes, minMinutes, maxMinutes, route.id);
              }
              this.$refs.cesiumMap.renderRouteWaypoints(waypoints, route.id, route.platformId, route.platform, this.parseRouteStyle(route.attributes));
              this.$nextTick(() => this.updateDeductionPositions());
            }
          } else {
            this.$message.warning('该航线暂无坐标数据，无法在地图展示');
          }
          this.wsConnection?.sendSyncRouteVisibility?.(route.id, true);
        }
      } catch (error) {
        console.error("获取航线详情失败:", error);
        this.$message.error('无法加载该航线的详细航点数据');
      }
    },

    createPlan() {
      this.newPlanForm.name = ''; // 重置名称
      this.showPlanNameDialog = true; // 打开对话框
    },
    async confirmCreatePlan() {
      this.$refs.newPlanForm.validate(async (valid) => {
        if (!valid) return;
        try {
          const postData = {
            roomId: this.currentRoomId,
            name: this.newPlanForm.name,
          };
          const res = await addScenario(postData);
          if (res.code === 200) {
            this.$message.success('方案创建成功');
            this.showPlanNameDialog = false;
            // 刷新列表，确保新方案带上后端 ID
            await this.getList();
            // 自动选中最新创建的方案
            if (res.data && res.data.id) {
              const newPlan = this.plans.find(p => p.id === res.data.id);
              if (newPlan) this.selectPlan(newPlan);
            }
          }
        } catch (error) {
          console.error("创建方案失败:", error);
          this.$message.error('保存方案失败，请检查网络');
        }
      });
    },
    openPlanDialog(plan) {
      this.$prompt('请输入方案名称', '编辑方案', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        inputValue: plan.name
      }).then(({value}) => {
        plan.name = value;
      }).catch(() => {
      });
    },
    async executeDeletePlan(plan) {
      try {
        const res = await delScenario(plan.id);
        if (res.code === 200) {
          this.$message.success('方案删除成功');
          const relatedRoutes = this.routes.filter(r => r.scenarioId === plan.id);
          relatedRoutes.forEach(route => {
            if (this.$refs.cesiumMap) {
              this.$refs.cesiumMap.removeRouteById(route.id);
            }
            const idx = this.activeRouteIds.indexOf(route.id);
            if (idx > -1) {
              this.activeRouteIds.splice(idx, 1);
            }
          });
          //重置方案选中状态
          if (this.selectedPlanId === plan.id) {
            this.selectedPlanId = null;
          }
          await this.getList(); // 刷新列表
        }
      } catch (e) {
        if (e !== 'cancel') console.error("删除方案失败:", e);
      }
    },
    addWaypoint() {
      if (this.selectedRouteDetails) {
        const count = this.selectedRouteDetails.waypoints.length + 1;
        this.selectedRouteDetails.waypoints.push({
          name: `WP${count}`,
          alt: 5000,
          speed: '800km/h',
          eta: `K+01:${(count * 15).toString().padStart(2, '0')}:00`
        });
        if (this.selectedPlanDetails) {
          const route = this.selectedPlanDetails.routes.find(r => r.id === this.selectedRouteId);
          if (route) {
            route.points = this.selectedRouteDetails.waypoints.length;
          }
        }
      }
    },
    cancelRoute() {
      // 清空所有选中的航线
      if (this.$refs.cesiumMap) {
        this.activeRouteIds.forEach(id => {
          this.$refs.cesiumMap.removeRouteById(id);
        });
      }
      this.activeRouteIds = [];
      this.selectedRouteDetails = null;
      this.$message.info('已清空所有选中航线');
    },

    // 切换航线显示/隐藏
    /** 地图右键上锁/解锁后同步到列表 */
    handleRouteLockChanged({ routeId, locked }) {
      this.$set(this.routeLocked, routeId, locked);
    },
    /** 右侧列表锁图标点击：切换该航线上锁状态，与地图右键状态同步 */
    handleToggleRouteLockFromPanel(route) {
      if (!route || route.id == null) return;
      const nextLocked = !this.routeLocked[route.id];
      this.$set(this.routeLocked, route.id, nextLocked);
      this.$message.success(nextLocked ? '航线已上锁，无法修改' : '航线已解锁，可以编辑');
    },

    async toggleRouteVisibility(route, opts) {
      const index = this.activeRouteIds.indexOf(route.id);
      const fromPlanSwitch = opts && opts.fromPlanSwitch;

      if (index > -1) {
        // 航线已显示，隐藏它
        this.activeRouteIds = this.activeRouteIds.filter(id => id !== route.id);
        if (this.$refs.cesiumMap) {
          this.$refs.cesiumMap.removeRouteById(route.id);
        }
        if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, false);
        if (this.selectedRouteDetails && this.selectedRouteDetails.id === route.id) {
          if (this.activeRouteIds.length > 0) {
            const lastId = this.activeRouteIds[this.activeRouteIds.length - 1];
            getRoutes(lastId).then(res => {
              if (res.code === 200 && res.data) {
                const fromList = this.routes.find(r => r.id === lastId);
                this.selectedRouteId = res.data.id;
                this.selectedRouteDetails = {
                  id: res.data.id,
                  name: res.data.callSign,
                  waypoints: res.data.waypoints || [],
                  platformId: fromList?.platformId,
                  platform: fromList?.platform,
                  attributes: fromList?.attributes
                };
              }
            }).catch(e => {
              console.error("获取航线详情失败", e);
            });
          } else {
            this.selectedRouteId = null;
            this.selectedRouteDetails = null;
          }
        }
      } else {
        // 航线已隐藏，显示它
        await this.selectRoute(route);
        if (!fromPlanSwitch) this.wsConnection?.sendSyncRouteVisibility?.(route.id, true);
      }
    },

    // 冲突操作：根据当前展示的航线与时间轴计算真实问题（提前到达、无法按时到达）
    runConflictCheck() {
      const list = [];
      let id = 1;
      const routeIds = this.activeRouteIds && this.activeRouteIds.length > 0 ? this.activeRouteIds : this.routes.map(r => r.id);
      const { minMinutes, maxMinutes } = this.getDeductionTimeRange();

      routeIds.forEach(routeId => {
        const route = this.routes.find(r => r.id === routeId);
        if (!route || !route.waypoints || route.waypoints.length < 2) return;
        let pathData = null;
        if (this.$refs.cesiumMap && this.$refs.cesiumMap.getRoutePathWithSegmentIndices) {
          const ret = this.$refs.cesiumMap.getRoutePathWithSegmentIndices(route.waypoints);
          if (ret.path && ret.path.length > 0 && ret.segmentEndIndices) {
            pathData = { path: ret.path, segmentEndIndices: ret.segmentEndIndices, holdArcRanges: ret.holdArcRanges || {} };
          }
        }
        const { earlyArrivalLegs, lateArrivalLegs, holdDelayConflicts } = this.buildRouteTimeline(route.waypoints, minMinutes, maxMinutes, pathData);

        const routeName = route.name || `航线${route.id}`;
        (earlyArrivalLegs || []).forEach(leg => {
          list.push({
            id: id++,
            title: '提前到达',
            routeName,
            fromWaypoint: leg.fromName,
            toWaypoint: leg.toName,
            time: this.minutesToStartTime(leg.actualArrival),
            suggestion: '该航段将提前到达下一航点，建议在此段加入盘旋或延后下一航点计划时间。',
            severity: 'high'
          });
        });
        (lateArrivalLegs || []).forEach(leg => {
          list.push({
            id: id++,
            title: '无法按时到达',
            routeName,
            fromWaypoint: leg.fromName,
            toWaypoint: leg.toName,
            suggestion: `当前速度不足，建议将本段速度提升至 ≥${leg.requiredSpeedKmh} km/h，或延后下一航点计划时间。`,
            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;
      this.conflictCount = list.length;
      if (list.length > 0) {
        this.$message.warning(`检测到 ${list.length} 处航线时间问题`);
      } else {
        this.$message.success('未发现航线时间冲突');
      }
    },

    viewConflict(conflict) {
      this.$message.info(`查看冲突：${conflict.title}`);
    },

    resolveConflict(conflict) {
      this.$message.success(`解决冲突：${conflict.title}`);
      // 移除已解决的冲突
      this.conflicts = this.conflicts.filter(c => c.id !== conflict.id);
      this.conflictCount = this.conflicts.length;
    },

    // 系统功能
    exportReport() {
      this.$message.success('作战报表导出成功');
    },

    // 新增导入功能
    importData() {
      this.$message.success('导入数据成功');
    }
  }
};
</script>

<style scoped>
.mission-planning-container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

/* 地图背景：使用相对路径便于 IDE 与构建解析；若需图片请将 map-background.png 放到 src/assets/ */
.map-background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, #1a2f4b 0%, #2c3e50 100%);
  /* 若已存在 src/assets/map-background.png，可改为：background: url('../../assets/map-background.png'); 并注释掉上一行 */
  background-size: cover;
  background-position: center;
  z-index: 1;
}

/* 截图保存弹窗 */
.screenshot-save-dialog .screenshot-preview {
  max-height: 320px;
  overflow: hidden;
  border-radius: 4px;
  background: #f5f5f5;
  margin-bottom: 12px;
}
.screenshot-save-dialog .screenshot-preview img {
  display: block;
  max-width: 100%;
  width: auto;
  height: auto;
  max-height: 320px;
  object-fit: contain;
}
.screenshot-save-dialog .screenshot-tip {
  font-size: 12px;
  color: #909399;
  margin-top: 8px;
  margin-bottom: 0;
}

/* ...其余样式省略，保持不变... */
.map-overlay-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: rgba(255, 255, 255, 0.3);
  text-align: center;
  font-size: 18px;
  pointer-events: none;
}

/* 地图中间的浮动红点 - 通用样式 */
.floating-red-dot {
  position: absolute;
  top: 50%;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  backdrop-filter: blur(5px);
  border: 2px solid rgba(255, 0, 0, 0.8);
  box-shadow: 0 0 20px rgba(255, 0, 0, 0.6);
  cursor: pointer;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease;
  animation: pulse-red 2s infinite;
}

.floating-red-dot:hover {
  transform: translateY(-50%) scale(1.1);
  box-shadow: 0 0 25px rgba(255, 0, 0, 0.8);
}

.floating-red-dot.hidden {
  display: none;
}

/* 左侧红点 */
.left-red-dot {
  left: 20px;
  transform: translateY(-50%);
}

.red-dot {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 30%, #ff4444, #cc0000);
  position: relative;
}

.icon-inside {
  position: absolute;
  color: white;
  font-size: 16px;
}

@keyframes pulse-red {
  0% {
    box-shadow: 0 0 20px rgba(255, 0, 0, 0.6);
  }
  50% {
    box-shadow: 0 0 25px rgba(255, 0, 0, 0.8);
  }
  100% {
    box-shadow: 0 0 20px rgba(255, 0, 0, 0.6);
  }
}

/* 蓝色主题通用类 */
.blue-theme {
  background: rgba(255, 255, 255, 0.3);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(0, 138, 255, 0.1);
  box-shadow: 0 4px 12px rgba(0, 138, 255, 0.2);
}

.blue-btn {
  background: rgba(0, 138, 255, 0.8);
  border: 1px solid rgba(0, 138, 255, 0.9);
  color: white;
  transition: all 0.3s;
}

.blue-btn:hover {
  background: rgba(0, 138, 255, 0.9);
  border-color: rgba(0, 138, 255, 1);
  transform: translateY(-1px);
  box-shadow: 0 2px 8px rgba(0, 138, 255, 0.3);
}

.blue-text-btn {
  color: #008aff;
}

.blue-text-btn:hover {
  color: #0066cc;
}

.blue-badge {
  background: rgba(0, 138, 255, 0.8);
  color: white;
}

.blue-tag {
  background: rgba(0, 138, 255, 0.2);
  color: #008aff;
  border: 1px solid rgba(0, 138, 255, 0.3);
}

.blue-time {
  background: rgba(0, 138, 255, 0.1);
  border: 1px solid rgba(0, 138, 255, 0.3);
  color: #008aff;
}

.blue-success {
  color: #008aff;
}

.blue-warning {
  color: #ff9900;
}

.blue-mark {
  color: #008aff;
}

.status-dot.operating {
  background: #008aff;
  animation: pulse 2s infinite;
  box-shadow: 0 0 10px rgba(0, 138, 255, 0.8);
}

/* 蓝色主题标签页 */
.blue-tabs >>> .el-tabs__item {
  color: #666;
  transition: all 0.3s;
}

.blue-tabs >>> .el-tabs__item:hover {
  color: #008aff;
}

.blue-tabs >>> .el-tabs__item.is-active {
  color: #008aff;
  font-weight: 600;
}

.blue-tabs >>> .el-tabs__active-bar {
  background-color: #008aff;
  box-shadow: 0 0 6px rgba(0, 138, 255, 0.5);
}

.blue-tabs >>> .el-tabs__nav-wrap::after {
  background-color: rgba(0, 138, 255, 0.3);
}

/* 底部时间轴（最初版本的样式）- 蓝色主题 */
.floating-timeline {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%) translateY(100%);
  width: 80%;
  border-radius: 12px;
  z-index: 95;
  padding: 10px 20px;
  color: #333;
  transition: all 0.3s ease;
  opacity: 0;
  pointer-events: none;
  backdrop-filter: blur(15px);
  background: rgba(255, 255, 255, 0.95);
  box-shadow: 0 8px 32px rgba(0, 138, 255, 0.25);
  border: 1px solid rgba(0, 138, 255, 0.3);
}

.floating-timeline.show {
  transform: translateX(-50%) translateY(0);
  opacity: 1;
  pointer-events: auto;
}

.k-time-tip {
  font-size: 12px;
  color: #909399;
  margin: 8px 0 0;
  line-height: 1.5;
}

.popup-hide-btn {
  position: absolute;
  top: -28px;
  left: 50%;
  transform: translateX(-50%);
  width: 56px;
  height: 28px;
  background: rgba(255, 255, 255, 0.3);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(0, 138, 255, 0.4);
  border-bottom: none;
  border-radius: 12px 12px 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: #008aff;
  font-size: 18px;
  transition: all 0.3s;
  z-index: 10;
}

.popup-hide-btn:hover {
  background: rgba(0, 138, 255, 0.2);
  color: #0066cc;
  height: 32px;
  top: -32px;
}

.timeline-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 5px;
}

.current-time {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: monospace;
  font-size: 14px;
  font-weight: 500;
  padding: 4px 12px;
  border-radius: 4px;
}

.timeline-slider {
  flex: 1;
  margin: 0 20px;
  position: relative;
}

.timeline-hover-time {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  transform: translateX(-50%);
  padding: 4px 10px;
  background: rgba(0, 0, 0, 0.85);
  color: #fff;
  font-size: 12px;
  font-weight: 500;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  z-index: 10;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.timeline-hover-time::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: rgba(0, 0, 0, 0.85);
}

.compact-slider {
  width: 100%;
}

.blue-slider >>> .el-slider__bar {
  background-color: rgba(0, 138, 255, 0.8);
}

.blue-slider >>> .el-slider__button {
  border-color: rgba(0, 138, 255, 0.8);
  background-color: rgba(0, 138, 255, 0.8);
}

.playback-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}

.control-btn {
  width: 26px;
  height: 26px;
  border: 1px solid rgba(0, 138, 255, 0.3);
  background: rgba(255, 255, 255, 0.9);
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
  color: #008aff;
}

.control-btn:hover:not(:disabled) {
  background: rgba(0, 138, 255, 0.1);
  border-color: rgba(0, 138, 255, 0.5);
}

.control-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.control-btn i {
  font-size: 14px;
}

.speed-control {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(0, 138, 255, 0.3);
  border-radius: 4px;
}

.speed-text {
  font-size: 11px;
  font-weight: 600;
  color: #008aff;
  min-width: 24px;
  text-align: center;
}
.speed-text.clickable {
  cursor: pointer;
  padding: 2px 4px;
  display: inline-flex;
  align-items: center;
}
.speed-text .el-icon--right {
  margin-left: 4px;
  font-size: 10px;
}

.system-status {
  display: flex;
  justify-content: center;
  gap: 30px;
  font-size: 12px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid rgba(0, 138, 255, 0.4);
}

.status-item {
  display: flex;
  align-items: center;
  gap: 5px;
}

.status-label {
  color: #666;
}

.status-value.success {
  font-weight: 500;
}

.status-value.warning {
  font-weight: 500;
}

/* 滚动条样式 */
::-webkit-scrollbar {
  width: 6px;
}

::-webkit-scrollbar-track {
  background: rgba(0, 138, 255, 0.1);
  border-radius: 3px;
  backdrop-filter: blur(5px);
}

::-webkit-scrollbar-thumb {
  background: linear-gradient(135deg, rgba(0, 138, 255, 0.5), rgba(0, 138, 255, 0.7));
  border-radius: 3px;
  box-shadow: 0 0 6px rgba(0, 138, 255, 0.4);
}

::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(135deg, rgba(0, 138, 255, 0.7), rgba(0, 138, 255, 0.9));
}

.ml-3 {
  margin-left: 10px;
}
</style>
