<template>
  <div class="map-monitor-page" :class="[`layout-${layoutMode}`, { 'sheet-collapsed': sheetCollapsed }]">
    <header class="mm-command-bar" aria-label="监控指挥条">
      <div class="mm-kpi-pills" aria-label="关键指标">
        <div class="mm-pill">
          <span class="mm-pill-label">在线</span>
          <span class="mm-pill-val"><b>{{ onlineCars }}</b><small>/{{ cars.length }}</small></span>
        </div>
        <div class="mm-pill">
          <span class="mm-pill-label">运行</span>
          <span class="mm-pill-val"><b>{{ runningCars }}</b></span>
        </div>
        <div class="mm-pill">
          <span class="mm-pill-label">空闲</span>
          <span class="mm-pill-val"><b>{{ idleCars }}</b></span>
        </div>
        <div class="mm-pill">
          <span class="mm-pill-label">离线</span>
          <span class="mm-pill-val"><b>{{ offlineCars }}</b></span>
        </div>
        <span class="mm-pill-sep" aria-hidden="true" />
        <div class="mm-pill">
          <span class="mm-pill-label">任务</span>
          <span class="mm-pill-val"><b>{{ totalTasks }}</b></span>
        </div>
        <div class="mm-pill">
          <span class="mm-pill-label">进行中</span>
          <span class="mm-pill-val"><b>{{ runningMissions }}</b></span>
        </div>
        <div class="mm-pill">
          <span class="mm-pill-label">排队</span>
          <span class="mm-pill-val mm-warn"><b>{{ queuedMissions }}</b></span>
        </div>
        <div class="mm-pill">
          <span class="mm-pill-label">异常</span>
          <span class="mm-pill-val" :class="{ 'mm-danger': abnormalTasks > 0 }"><b>{{ abnormalTasks }}</b></span>
        </div>
        <span class="mm-pill-sep" aria-hidden="true" />
        <div class="mm-pill">
          <span class="mm-pill-label">报警</span>
          <span class="mm-pill-val" :class="{ 'mm-danger': activeAlarmCount > 0 }"><b>{{ activeAlarmCount }}</b></span>
        </div>
        <div class="mm-pill mm-pill--muted">
          <span class="mm-pill-label">完成</span>
          <span class="mm-pill-val"><b>{{ completedTasks }}</b></span>
        </div>
      </div>

      <div class="mm-command-actions">
        <span class="mm-clock">{{ clockText }}</span>
        <span class="mm-live" :class="{ 'is-live': streamConnected }">
          {{ streamConnected ? '实时' : '轮询' }}
        </span>
        <div class="mm-layout-toggle" role="group" aria-label="布局切换">
          <button
            type="button"
            class="mm-toggle-btn"
            :class="{ 'is-active': layoutMode === 'dual' }"
            title="双栏：地图 + 右侧列表/详情"
            @click="setLayout('dual')"
          >双栏</button>
          <button
            type="button"
            class="mm-toggle-btn"
            :class="{ 'is-active': layoutMode === 'immersive' }"
            title="沉浸：全幅地图 + 底部抽屉"
            @click="setLayout('immersive')"
          >沉浸</button>
        </div>
        <button type="button" class="mm-icon-btn" :disabled="refreshing" @click="refreshAll">
          {{ refreshing ? '刷新中' : '刷新' }}
        </button>
      </div>
    </header>

    <div class="mm-workspace">
      <section class="mm-canvas" aria-label="三维地图">
        <Workspace3D
          ref="workspaceRef"
          :host="vrHost"
          :scope="auth.scope ?? 'Platform'"
          :token="auth.token ?? ''"
          :read-only="!!readOnly"
          :canvas-only="true"
          :hide-toolbar="true"
          @pick="onPick"
          @select="onSelect"
          @ready="onWorkspaceReady"
        />
        <FloatingAlarmStack :alarms="alarms" @locate="onAlarmLocate" @ack="onAlarmAck" />

        <aside
          v-if="selection && layoutMode === 'immersive'"
          ref="floatPanelRef"
          class="mm-float-detail"
          :class="{ 'is-dragging': floatDragging }"
          :style="floatPanelStyle"
          aria-label="选中详情"
        >
          <header class="mm-float-head" @mousedown.prevent="onFloatDragStart">
            <div class="mm-float-title">
              <span class="mm-float-grip" aria-hidden="true" />
              <span class="mm-float-id">{{ floatTitle }}</span>
              <span v-if="selectionKindLabel" class="mm-float-kind">{{ selectionKindLabel }}</span>
            </div>
            <button type="button" class="mm-float-close" aria-label="关闭" @mousedown.stop @click="clearSelection">×</button>
          </header>
          <div class="mm-float-body">
            <MonitorSelectionPanel
              :selection="selection"
              :refresh-key="tick"
              :cars="cars"
              :missions="missions"
              :read-only="!!readOnly"
            />
          </div>
        </aside>
      </section>

      <aside class="mm-panel" aria-label="工作列表面板">
        <div v-if="layoutMode === 'immersive'" class="mm-sheet-handle">
          <button type="button" class="mm-sheet-toggle" @click="toggleSheet">
            {{ sheetCollapsed ? '展开列表' : '收起列表' }}
          </button>
          <span class="mm-sheet-hint">沉浸模式 · 底栏</span>
        </div>

        <div v-show="!sheetCollapsed || layoutMode === 'dual'" class="mm-panel-list">
          <div class="mm-rail-head">
            <div class="mm-segment" role="tablist">
              <button
                type="button"
                role="tab"
                class="mm-segment-btn"
                :class="{ 'is-active': workbenchTab === 'vehicle' }"
                :aria-selected="workbenchTab === 'vehicle'"
                @click="setNav('vehicle')"
              >车辆</button>
              <button
                type="button"
                role="tab"
                class="mm-segment-btn"
                :class="{ 'is-active': workbenchTab === 'mission' }"
                :aria-selected="workbenchTab === 'mission'"
                @click="setNav('mission')"
              >任务</button>
              <button
                type="button"
                role="tab"
                class="mm-segment-btn"
                :class="{ 'is-active': workbenchTab === 'alarm' }"
                :aria-selected="workbenchTab === 'alarm'"
                @click="setNav('alarm')"
              >
                告警
                <span v-if="activeAlarmCount" class="mm-seg-badge">{{ activeAlarmCount }}</span>
              </button>
            </div>
            <span class="mm-rail-count">
              {{
                workbenchTab === 'vehicle'
                  ? `${cars.length}`
                  : workbenchTab === 'mission'
                    ? `${deliveries.length}`
                    : `${activeAlarmCount}`
              }}
            </span>
          </div>

          <div class="mm-rail-list">
            <VehicleMonitorPanel
              v-show="workbenchTab === 'vehicle'"
              table
              :cars="cars"
              :missions="missions"
              :selected-id="selectedVehicleId"
              @select="onVehicleSelect"
            />
            <MissionListPanel
              v-show="workbenchTab === 'mission'"
              :deliveries="deliveries"
              :selected-id="selectedDeliveryId"
              @select="onDeliverySelect"
              @refresh="onDeliveryListRefresh"
              @action-done="refreshDeliveries"
            />
            <div v-show="workbenchTab === 'alarm'" class="mm-alarm-panel">
              <div class="mm-alarm-head" aria-hidden="true">
                <span class="col-car">车辆编号</span>
                <span class="col-info">信息</span>
              </div>
              <div class="mm-alarm-body">
                <button
                  v-for="a in activeAlarms"
                  :key="`${a.carId}-${a.timestamp}`"
                  type="button"
                  class="mm-alarm-row"
                  @click="onAlarmLocate(a)"
                >
                  <span class="col-car">{{ a.carId }}</span>
                  <span class="col-info" :title="a.info || a.carName">{{ a.info || a.carName || '-' }}</span>
                </button>
                <div v-if="!activeAlarms.length" class="mm-alarm-empty">暂无报警</div>
              </div>
            </div>
          </div>
        </div>

        <div v-if="layoutMode === 'dual'" class="mm-panel-detail">
          <header class="mm-detail-head">
            <span>选中信息</span>
            <button
              v-if="selection"
              type="button"
              class="mm-detail-clear"
              @click="clearSelection"
            >清除</button>
          </header>
          <div class="mm-detail-body">
            <MonitorSelectionPanel
              v-if="selection"
              :selection="selection"
              :refresh-key="tick"
              :cars="cars"
              :missions="missions"
              :read-only="!!readOnly"
            />
            <div v-else class="mm-detail-empty">在地图或列表中选择车辆 / 站点 / 路径</div>
          </div>
        </div>
      </aside>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Workspace3D from '@/components/Workspace3D.vue'
import VehicleMonitorPanel from '@/components/workbench/VehicleMonitorPanel.vue'
import MissionListPanel from '@/components/workbench/MissionListPanel.vue'
import MonitorSelectionPanel from '@/components/workbench/MonitorSelectionPanel.vue'
import FloatingAlarmStack from '@/components/map-monitor/FloatingAlarmStack.vue'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { listCars, listMissions } from '@/api/projection'
import { fetchCdmTaskFeed } from '@/api/delivery'
import { useProjectionStream, type StreamEvent } from '@/composables/useProjectionStream'
import type { AlarmEvent, SelectionDetailEvent } from '@/composables/useMapEditStream'
import { reflectionApi } from '@/api/reflection'
import { workspaceToolbarApi } from '@/api/workspaceToolbar'
import type { Car } from '@/types/car'
import type { Mission } from '@/types/mission'
import type { DeliveryTask } from '@/types/delivery'
import type { SelectedObjectRef } from '@/types/workbench'
import { fetchMonitorConfigCached, invalidateMonitorConfigCache } from '@/utils/monitorConfigCache'
import type { MapFocusKind } from '@/utils/mapObjectFocus'
import { defaultVrHost } from '@/utils/vrender'
defineProps<{
  /** 只读模式（运营端复用 MapMonitorView 时传 true）：3D 不可编辑，选中信息面板动作改用运维白名单。 */
  readOnly?: boolean
}>()

const auth = useAuthStore()
const route = useRoute()
const router = useRouter()
const vrHost = defaultVrHost()

const cars = ref<Car[]>([])
const missions = ref<Mission[]>([])
const deliveries = ref<DeliveryTask[]>([])
const deliveryListOpts = ref({ includeFinished: true, includeAborted: true })
const selection = ref<SelectedObjectRef | null>(null)
const tick = ref(0)
const refreshing = ref(false)
const workbenchTab = ref<'vehicle' | 'mission' | 'alarm'>('vehicle')
const selectedDeliveryId = ref<string | null>(null)
const workspaceRef = ref<InstanceType<typeof Workspace3D> | null>(null)
const workspaceReady = ref(false)
const pendingMapFocus = ref<{ kind: MapFocusKind; id: number } | null>(null)
// 调度回放：PlaybackView「查看」跳转携带 ?playback=<fileName>，待 webVRender 就绪后触发回放。
const pendingPlayback = ref<string | null>(null)

/**
 * 浮动报警：订阅后端 AlarmStreamService 推送的 `alarm` 事件，
 * 维持一个滚动列表；超过 30 条丢最旧。clear 事件由 FloatingAlarmStack 自动过滤。
 *
 * 注意：本页只持有一个 useProjectionStream 实例，所有 SSE 事件（alarm / car-state /
 * mission-status / selection-detail / ...）共用同一条 EventSource，避免历史上「自己 + useMapEditStream
 * 各开一条」造成的两路 SSE 占用。
 */
const alarms = ref<AlarmEvent[]>([])

async function onAlarmLocate(a: AlarmEvent) {
  // 点击浮动报警的「定位」：同步 3D 高亮 + 立即把相机定位到该报警车辆（与车辆监控台选中一致）。
  // a.carId 已经是 number（AlarmEvent 类型保证），无须额外字符串处理；如果将来后端
  // 改成字符串，这里 Number(...) 会返回 NaN 并由下面的 isFinite 拦下来。
  const numId = Number(a.carId)
  if (!Number.isFinite(numId)) {
    ElMessage.warning(`报警车辆 id 无法解析：${a.carId}`)
    return
  }
  selection.value = { kind: 'vehicle', id: String(numId), name: a.carName ?? String(a.carId) }
  await selectAndLocateVehicle(numId)
}
function onAlarmAck(_a: AlarmEvent) {
  // 当前后端无 ACK 接口，前端本地标记即可（FloatingAlarmStack 已处理）
}

let pollTimer: ReturnType<typeof setInterval> | null = null

const onlineCars = computed(() => cars.value.filter((c) => c.state !== 'offline').length)
const runningMissions = computed(() =>
  deliveries.value.length > 0
    ? deliveries.value.filter((d) => d.statusCode === 'Fetching' || d.statusCode === 'Putting').length
    : missions.value.filter((m) => m.status === 'running').length
)
const queuedMissions = computed(() =>
  deliveries.value.length > 0
    ? deliveries.value.filter((d) => d.statusCode === 'Waiting').length
    : missions.value.filter((m) => m.status === 'queued').length
)
const activeAlarms = computed(() =>
  alarms.value.filter((a) => a.action !== 'clear')
)
const activeAlarmCount = computed(() => activeAlarms.value.length)

const clockText = ref('')
let clockTimer: ReturnType<typeof setInterval> | null = null

/** 选中详情浮动窗：可拖拽，默认右下角 */
const floatPanelRef = ref<HTMLElement | null>(null)
const floatDragging = ref(false)
const floatPos = ref<{ left?: number; top?: number; right?: number; bottom?: number }>({
  right: 14,
  bottom: 14
})
const floatPanelStyle = computed(() => ({
  left: floatPos.value.left !== undefined ? `${floatPos.value.left}px` : undefined,
  top: floatPos.value.top !== undefined ? `${floatPos.value.top}px` : undefined,
  right: floatPos.value.right !== undefined ? `${floatPos.value.right}px` : undefined,
  bottom: floatPos.value.bottom !== undefined ? `${floatPos.value.bottom}px` : undefined
}))

type MonitorLayout = 'dual' | 'immersive'
const LAYOUT_KEY = 'migu.map-monitor.layout'

function readStoredLayout(): MonitorLayout {
  try {
    const v = localStorage.getItem(LAYOUT_KEY)
    if (v === 'dual' || v === 'immersive') return v
  } catch { /* ignore */ }
  return 'dual'
}

const layoutMode = ref<MonitorLayout>(readStoredLayout())
const sheetCollapsed = ref(false)

function setLayout(mode: MonitorLayout) {
  if (layoutMode.value === mode) return
  layoutMode.value = mode
  try { localStorage.setItem(LAYOUT_KEY, mode) } catch { /* ignore */ }
  if (mode === 'immersive') {
    floatPos.value = { right: 14, bottom: sheetCollapsed.value ? 56 : 280 }
  } else {
    floatPos.value = { right: 14, bottom: 14 }
    sheetCollapsed.value = false
  }
}

function toggleSheet() {
  sheetCollapsed.value = !sheetCollapsed.value
  if (layoutMode.value === 'immersive' && floatPos.value.left === undefined) {
    floatPos.value = { right: 14, bottom: sheetCollapsed.value ? 56 : 280 }
  }
}

let floatDragStartX = 0
let floatDragStartY = 0
let floatOriginLeft = 0
let floatOriginTop = 0

function onFloatDragStart(ev: MouseEvent) {
  const el = floatPanelRef.value
  if (!el) return
  const rect = el.getBoundingClientRect()
  const parent = el.offsetParent as HTMLElement | null
  const parentRect = parent?.getBoundingClientRect()
  floatOriginLeft = parentRect ? rect.left - parentRect.left : rect.left
  floatOriginTop = parentRect ? rect.top - parentRect.top : rect.top
  floatDragStartX = ev.clientX
  floatDragStartY = ev.clientY
  floatDragging.value = true
  floatPos.value = { left: floatOriginLeft, top: floatOriginTop }
  window.addEventListener('mousemove', onFloatDragMove)
  window.addEventListener('mouseup', onFloatDragEnd)
}

function onFloatDragMove(ev: MouseEvent) {
  const el = floatPanelRef.value
  const parent = el?.offsetParent as HTMLElement | null
  let left = floatOriginLeft + (ev.clientX - floatDragStartX)
  let top = floatOriginTop + (ev.clientY - floatDragStartY)
  if (el && parent) {
    const maxL = Math.max(0, parent.clientWidth - el.offsetWidth)
    const maxT = Math.max(0, parent.clientHeight - el.offsetHeight)
    left = Math.min(Math.max(0, left), maxL)
    top = Math.min(Math.max(0, top), maxT)
  }
  floatPos.value = { left, top }
}

function onFloatDragEnd() {
  floatDragging.value = false
  window.removeEventListener('mousemove', onFloatDragMove)
  window.removeEventListener('mouseup', onFloatDragEnd)
}

watch(selection, (next, prev) => {
  // 新开选中时若尚未拖过，回到默认右下角（沉浸模式抬高避开底栏）
  if (next && !prev && floatPos.value.left === undefined) {
    floatPos.value = {
      right: 14,
      bottom: layoutMode.value === 'immersive' ? (sheetCollapsed.value ? 56 : 280) : 14
    }
  }
})

const runningCars = computed(() => cars.value.filter((c) => c.state === 'running').length)
const idleCars = computed(() => cars.value.filter((c) => c.state === 'idle').length)
const offlineCars = computed(() => cars.value.filter((c) => c.state === 'offline').length)
const totalTasks = computed(() =>
  deliveries.value.length > 0 ? deliveries.value.length : missions.value.length
)
const completedTasks = computed(() =>
  deliveries.value.length > 0
    ? deliveries.value.filter((d) => d.statusCode === 'Finished' || d.statusCode === 'Completed').length
    : missions.value.filter((m) => m.status === 'completed').length
)
const abnormalTasks = computed(() =>
  deliveries.value.length > 0
    ? deliveries.value.filter((d) => d.statusCode === 'Aborted' || d.statusCode === 'Error' || d.statusCode === 'Failed').length
    : missions.value.filter((m) => m.status === 'failed' || m.status === 'cancelled').length
)

const selectedCar = computed(() => {
  if (!selection.value || selection.value.kind !== 'vehicle') return null
  const sel = selection.value
  return cars.value.find((c) => c.id === sel.id || detailIdForCar(c) === sel.id) ?? null
})

const floatTitle = computed(() => {
  if (!selection.value) return ''
  if (selection.value.kind === 'vehicle') {
    const car = selectedCar.value
    const id = car ? (car.rawId != null ? String(car.rawId) : car.id) : selection.value.id
    const state = car ? stateLabelOf(car.state) : ''
    return state ? `${id}  ${state}` : String(id)
  }
  return selection.value.name || `${selectionKindLabel.value} ${selection.value.id}`
})

function stateLabelOf(s: string): string {
  switch (s) {
    case 'running': return '运行中'
    case 'idle': return '空闲'
    case 'charging': return '充电中'
    case 'paused': return '已暂停'
    case 'fault': return '异常'
    case 'offline': return '离线'
    default: return s
  }
}

function setNav(mode: 'vehicle' | 'mission' | 'alarm') {
  workbenchTab.value = mode
}

function clearSelection() {
  selection.value = null
  selectedDeliveryId.value = null
  floatPos.value = { right: 14, bottom: 14 }
}

function tickClock() {
  const d = new Date()
  const p = (n: number) => String(n).padStart(2, '0')
  clockText.value = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
}

const selectionKindLabel = computed(() => {
  switch (selection.value?.kind) {
    case 'vehicle':
      return '车辆'
    case 'site':
      return '站点'
    case 'track':
      return '路径'
    case 'delivery':
      return '任务'
    case 'special':
      return '装饰'
    default:
      return ''
  }
})

const selectedVehicleId = computed(() => {
  if (!selection.value || selection.value.kind !== 'vehicle') return null
  const sel = selection.value
  const match = cars.value.find((c) => c.id === sel.id || detailIdForCar(c) === sel.id)
  return match?.id ?? null
})

function onPick(_p: { x: number; y: number }) {
  // Workspace3D pick 坐标事件；监控页侧栏选中走 SSE selection-detail / select
}

// SimpleLite 3D 端发送的对象命名规则（见 SimpleUI.cs / UISite.cs / TrackUiHelper）：
//   - 车辆：`Car-{id}` 或 `UICar-{id}`（不同入口不同前缀，正则同时兼容）
//   - 站点：`UISite-{id}`
//   - 路径：`UITrack-{id}` / `UICircularArcTrack-{id}` / `UIBezierTrack-{id}` / `UINurbsTrack-{id}`
const RX_CAR = /^(?:UI)?(?:Car|Vehicle)-(\d+)$/i
const RX_SITE = /^UISite-(\d+)$/i
const RX_TRACK = /^(?:UITrack|UICircularArcTrack|UIBezierTrack|UINurbsTrack)-(\d+)$/i

function parseSceneObjectName(name: string): { kind: 'vehicle' | 'site' | 'track'; id: string } | null {
  const car = RX_CAR.exec(name)
  if (car) return { kind: 'vehicle', id: car[1]! }
  const site = RX_SITE.exec(name)
  if (site) return { kind: 'site', id: site[1]! }
  const track = RX_TRACK.exec(name)
  if (track) return { kind: 'track', id: track[1]! }
  // 与地图编辑页一致：贪婪吃掉 UI 前缀字母段，兼容 UIBezierTrack-3 等
  const m = /^UI([A-Za-z]+)-(\d+)$/.exec(name) ?? /^([A-Za-z]+)-(\d+)$/.exec(name)
  if (!m) return null
  const tn = m[1]!.toLowerCase()
  const id = m[2]!
  if (tn === 'site') return { kind: 'site', id }
  if (tn === 'car' || tn === 'vehicle') return { kind: 'vehicle', id }
  if (tn.includes('track')) return { kind: 'track', id }
  return null
}

function sameSelection(a: SelectedObjectRef | null, b: SelectedObjectRef | null): boolean {
  if (!a && !b) return true
  if (!a || !b) return false
  return a.kind === b.kind && a.id === b.id
}

/** 把解析结果写入侧栏 selection；仅当对象变化时刷新详情，避免选中闪烁。 */
function applyParsedSelection(parsed: { kind: 'vehicle' | 'site' | 'track'; id: string }, rawName?: string) {
  selectedDeliveryId.value = null
  let next: SelectedObjectRef
  if (parsed.kind === 'vehicle') {
    const car = (rawName ? findSelectedCar(rawName) : undefined)
      ?? cars.value.find((c) => detailIdForCar(c) === parsed.id)
    next = car
      ? { kind: 'vehicle', id: detailIdForCar(car), name: car.name }
      : { kind: 'vehicle', id: parsed.id, name: `Vehicle ${parsed.id}` }
  } else {
    next = {
      kind: parsed.kind,
      id: parsed.id,
      name: rawName || `${parsed.kind === 'site' ? 'Site' : 'Track'} ${parsed.id}`
    }
  }
  if (sameSelection(selection.value, next)) return
  selection.value = next
}

function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, names?: readonly string[]) {
  selectedDeliveryId.value = null
  if (kind === 'car') {
    const sid = String(id)
    const rawName = names?.find((n) => RX_CAR.test(n)) ?? `Car-${id}`
    const car = findSelectedCar(rawName) ?? cars.value.find((c) => detailIdForCar(c) === sid || c.id === sid)
    const next = car
      ? { kind: 'vehicle' as const, id: detailIdForCar(car), name: car.name }
      : { kind: 'vehicle' as const, id: sid, name: `Vehicle ${sid}` }
    if (!sameSelection(selection.value, next)) {
      selection.value = next
    }
    return
  }
  const sid = String(id)
  const rawName = names?.find((n) => {
    const p = parseSceneObjectName(n)
    return p?.kind === kind && p.id === sid
  })
  const next = {
    kind,
    id: sid,
    name: rawName || `${kind === 'site' ? 'Site' : 'Track'} ${sid}`
  }
  if (!sameSelection(selection.value, next)) {
    selection.value = next
  }
}

/**
 * SimpleLite 画布点选后通过 SSE `selection-detail` 广播（见 SimpleUI.BroadcastSelectionToWeb）。
 * embed 模式通常不会 postMessage `workspace.select`，因此必须在此同步侧栏。
 */
function onSelectionDetailFromStream(e: SelectionDetailEvent) {
  const siteN = e.siteIds?.length ?? 0
  const trackN = e.trackIds?.length ?? 0
  const carN = e.carIds?.length ?? 0
  const nameN = Array.isArray(e.names) ? e.names.length : 0
  if (siteN + trackN + carN + nameN === 0) {
    selection.value = null
    return
  }

  if (Array.isArray(e.names)) {
    for (const n of e.names) {
      const parsed = parseSceneObjectName(n)
      if (parsed) {
        applyParsedSelection(parsed, n)
        return
      }
    }
  }

  if (e.kind && e.id) {
    applySelectionFromKindId(e.kind, e.id, e.names)
    return
  }

  if (siteN > 0) {
    applySelectionFromKindId('site', e.siteIds![0]!, e.names)
    return
  }
  if (trackN > 0) {
    applySelectionFromKindId('track', e.trackIds![0]!, e.names)
    return
  }
  if (carN > 0) {
    applySelectionFromKindId('car', e.carIds![0]!, e.names)
  }
}

function onSelect(names: string[]) {
  if (!names.length) {
    // embed 画布可能不发 select 或发空数组；勿清空，改读后端当前选中。
    void fallbackSelectionFromBackend()
    return
  }

  for (const n of names) {
    const parsed = parseSceneObjectName(n)
    if (!parsed) continue
    applyParsedSelection(parsed, n)
    return
  }

  void fallbackSelectionFromBackend()
}

function pullFocusFromRoute(): boolean {
  const kind = route.query.focusKind
  const idRaw = route.query.focusId
  if (typeof kind !== 'string' || typeof idRaw !== 'string') return false
  if (kind !== 'car' && kind !== 'site' && kind !== 'track' && kind !== 'special') return false
  const id = Number(idRaw)
  if (!Number.isFinite(id)) return false

  pendingMapFocus.value = { kind, id }
  const q = { ...route.query }
  delete q.focusKind
  delete q.focusId
  void router.replace({ path: route.path, query: q })
  return true
}

async function applyPendingMapFocus() {
  const focus = pendingMapFocus.value
  if (!focus || !workspaceReady.value) return
  pendingMapFocus.value = null

  const { kind, id } = focus
  if (kind === 'car') {
    const car = cars.value.find((c) => detailIdForCar(c) === String(id) || c.rawId === id)
    selection.value = car
      ? { kind: 'vehicle', id: detailIdForCar(car), name: car.name }
      : { kind: 'vehicle', id: String(id), name: `Vehicle ${id}` }
    await selectAndLocateVehicle(id)
    return
  }

  if (kind === 'site' || kind === 'track') {
    applySelectionFromKindId(kind, id)
    try {
      await reflectionApi.setSelection(kind, id)
    } catch (err) {
      ElMessage.error({ message: `定位对象失败：${(err as Error).message}`, grouping: true })
    }
    return
  }

  try {
    await reflectionApi.setSelection('special', id)
    selection.value = { kind: 'special', id: String(id), name: `装饰物 ${id}` }
  } catch (err) {
    ElMessage.error({ message: `定位对象失败：${(err as Error).message}`, grouping: true })
  }
}

function scheduleMapFocusFromRoute() {
  if (!pullFocusFromRoute()) return
  void applyPendingMapFocus()
}

/**
 * 读取 ?playback=<fileName>：暂存待回放文件名并从 URL 移除（避免刷新重复触发），
 * 真正的 startPlayback 在 webVRender iframe 就绪后由 applyPendingPlayback 执行。
 */
function pullPlaybackFromRoute(): boolean {
  const fileName = route.query.playback
  if (typeof fileName !== 'string' || !fileName) return false
  pendingPlayback.value = fileName
  const q = { ...route.query }
  delete q.playback
  void router.replace({ path: route.path, query: q })
  return true
}

async function applyPendingPlayback() {
  const fileName = pendingPlayback.value
  if (!fileName || !workspaceReady.value) return
  pendingPlayback.value = null
  try {
    // iframe 终端就绪后再启动：SimpleLite StartPlayback 会把回放控制条投影到该 webVRender 终端。
    await workspaceToolbarApi.startPlayback(fileName)
    ElMessage.success({ message: `已开始回放：${fileName}`, grouping: true })
  } catch (err) {
    ElMessage.error({ message: `启动回放失败：${(err as Error).message}`, grouping: true })
  }
}

function schedulePlaybackFromRoute() {
  if (!pullPlaybackFromRoute()) return
  void applyPendingPlayback()
}

function onWorkspaceReady() {
  workspaceReady.value = true
  void fallbackSelectionFromBackend()
  void applyPendingMapFocus()
  void applyPendingPlayback()
}

async function fallbackSelectionFromBackend() {
  try {
    const sel = await reflectionApi.getSelection()
    if (!sel?.kind || sel.id == null) return
    if (sel.kind !== 'car' && sel.kind !== 'site' && sel.kind !== 'track') return
    applySelectionFromKindId(sel.kind, Number(sel.id), sel.name ? [sel.name] : undefined)
  } catch {
    // 仅作为命名兼容兜底，失败时保持当前 selection，不打断主流程。
  }
}

/**
 * 选中并定位到指定车辆：① 同步 SimpleLite 3D 场景高亮；② 立即把相机定位（居中 + 2D 俯视）
 * 到该车，与原生「双击车辆行 = 选中+定位」一致。两个请求互不阻塞，各自独立提示错误
 * （例如 iframe 未连接时相机定位会 503，但高亮仍可成功）。
 */
async function selectAndLocateVehicle(id: number) {
  if (!Number.isFinite(id)) return
  const [selRes, locRes] = await Promise.allSettled([
    reflectionApi.setSelection('car', id),
    workspaceToolbarApi.locateCamera(id)
  ])
  if (selRes.status === 'rejected') {
    ElMessage.error({ message: `同步 3D 选中失败：${(selRes.reason as Error).message}`, grouping: true })
  }
  if (locRes.status === 'rejected') {
    ElMessage.error({ message: `定位车辆失败：${(locRes.reason as Error).message}`, grouping: true })
  }
}

async function onVehicleSelect(ref: SelectedObjectRef) {
  workbenchTab.value = 'vehicle'
  selectedDeliveryId.value = null
  if (!sameSelection(selection.value, ref)) {
    selection.value = ref
  }
  await selectAndLocateVehicle(Number(ref.id))
}

async function onDeliverySelect(task: DeliveryTask) {
  selectedDeliveryId.value = task.id
  if (task.carId != null) {
    const car = cars.value.find((c) => c.rawId === task.carId || detailIdForCar(c) === String(task.carId))
    if (car) {
      await onVehicleSelect({ kind: 'vehicle', id: detailIdForCar(car), name: car.name })
      return
    }
    selection.value = {
      kind: 'vehicle',
      id: String(task.carId),
      name: task.carName ?? `Vehicle ${task.carId}`
    }
    await selectAndLocateVehicle(task.carId)
    return
  }
  selection.value = {
    kind: 'delivery',
    id: String(task.id),
    name: `${task.srcLabel} → ${task.dstLabel}`
  }
}

function onDeliveryListRefresh(opts: { includeFinished: boolean; includeAborted: boolean }) {
  deliveryListOpts.value = opts
  void refreshDeliveries()
}

async function refreshDeliveries() {
  try {
    // 与任务管理页统一：读平台快照库（离线可读 + 历史保留）；含已完成/取消异常改为本地筛选
    const feed = await fetchCdmTaskFeed(1000)
    const opts = deliveryListOpts.value
    deliveries.value = feed.tasks.filter((d) => {
      if (!opts.includeFinished && d.statusCode === 'Finished') return false
      if (!opts.includeAborted && (d.statusCode === 'Canceled' || d.statusCode === 'Terminated' || d.statusCode === 'Error')) return false
      return true
    })
  } catch (err) {
    console.warn('[MapMonitor] refreshDeliveries failed', err)
    deliveries.value = []
  }
}

function findSelectedCar(name: string): Car | undefined {
  const parsedId = parseWorkspaceCarId(name)
  return cars.value.find((c) =>
    c.name === name ||
    c.id === name ||
    (parsedId != null && detailIdForCar(c) === parsedId)
  )
}

function parseWorkspaceCarId(name: string): string | null {
  const m = /^(?:UI)?(?:Car|Vehicle)-(\d+)$/i.exec(name)
  return m?.[1] ?? null
}

function detailIdForCar(car: Car): string {
  if (car.rawId != null) return String(car.rawId)
  return car.id.replace(/^C/i, '').replace(/^0+/, '') || car.id
}

async function refreshAll() {
  refreshing.value = true
  try {
    const [carList, missionList] = await Promise.all([listCars(), listMissions()])
    cars.value = carList
    missions.value = missionList
    await refreshDeliveries()
  } catch (err) {
    console.warn('[MapMonitor] refreshAll failed', err)
  } finally {
    refreshing.value = false
  }
}

// SSE 实时通道：本页只起一条流，所有事件都从这里分发。
//
// 刷新策略：
// - car-state / mission-status：后端真正的"车辆/任务状态变更"事件 → 立刻 refreshAll（目前后端
//   还没真正实现这两条，但为未来事件型推送预留）。
// - alarm：直接更新 alarms 列表，不需要 refreshAll。
// - snapshot-tick：后端 1Hz 心跳，仅含 cars/missions/sites/tracks 数字，不带实际状态。**不要**
//   在这里触发 refreshAll，否则会等价"1s 全量轮询 listCars+listMissions"，比原来的 3s 轮询还
//   重。snapshot-tick 仅用于 `stream.connected` 心跳判定。
// - selection-detail：画布点选后 SimpleLite 广播，驱动右侧「选中信息」面板（embed 模式主路径）。
// - 其它 object-* / pick-result / project-*：本页不处理，由地图编辑页订阅。
//
// 实时通道未连接（旧 SimpleLite / 离线）时退回 3s 轮询保底。
const stream = useProjectionStream({ autoConnect: true })
const streamConnected = computed(() => stream.connected.value)

function onStreamEvent(e: StreamEvent) {
  if (!e?.kind) return
  switch (e.kind) {
    case 'alarm': {
      const a = e.payload as AlarmEvent | undefined
      if (!a) return
      // clear 事件 carId 同样有，但 info 为空；FloatingAlarmStack 内部按 info 是否为空过滤。
      alarms.value = [a, ...alarms.value.filter((x) => x.carId !== a.carId)].slice(0, 30)
      break
    }
    case 'car-state':
    case 'mission-status':
      void refreshAll()
      break
    case 'selection-detail':
      if (e.payload && typeof e.payload === 'object') {
        onSelectionDetailFromStream(e.payload as SelectionDetailEvent)
      }
      break
    case 'monitor-config-updated':
      // 运营维护保存后刷新动作白名单（勿在 refreshAll 里 tick++，避免轮询导致选中面板反复重载）
      invalidateMonitorConfigCache()
      tick.value++
      break
    // 'snapshot-tick' 故意不触发 refreshAll —— 见上面注释。
  }
}
stream.on(onStreamEvent)

watch(
  () => [route.query.focusKind, route.query.focusId] as const,
  () => scheduleMapFocusFromRoute()
)

watch(
  () => route.query.playback,
  () => schedulePlaybackFromRoute()
)

onMounted(async () => {
  tickClock()
  clockTimer = setInterval(tickClock, 1000)
  scheduleMapFocusFromRoute()
  schedulePlaybackFromRoute()
  void fetchMonitorConfigCached()
  await refreshAll()
  void applyPendingMapFocus()
  pollTimer = setInterval(() => {
    // SSE 已连但 projection/cars 曾 502 时 connected 仍为 true，需在车列表为空时继续轮询
    if (!stream.connected.value || cars.value.length === 0) {
      void refreshAll()
    } else if (workbenchTab.value === 'mission') {
      void refreshDeliveries()
    }
  }, 3000)
})

onUnmounted(() => {
  if (pollTimer) clearInterval(pollTimer)
  if (clockTimer) clearInterval(clockTimer)
  stream.off(onStreamEvent)
  onFloatDragEnd()
})
</script>
<style scoped>
/* Option A：Outpost 浅色 chrome + 品牌紫；画布/浮层保持深色指挥视口 */
.map-monitor-page {
  --mm-rail-w: 380px;
  --mm-sheet-h: min(34vh, 340px);
  --mm-radius: 14px;
  --mm-ease: cubic-bezier(0.16, 1, 0.3, 1);
  --mm-accent: var(--mg-primary, #7543e8);
  --mm-accent-soft: rgba(var(--mg-primary-rgb, 117, 67, 232), 0.14);
  --mm-accent-hi: var(--mg-accent, #9b7cff);
  --mm-surface: #fffefd;
  --mm-surface-2: #f6f3fb;
  --mm-border: rgba(40, 33, 58, 0.1);
  --mm-ink: #28213a;
  --mm-muted: #756d85;
  --mg-text-light: #28213a;
  --mg-text-muted: #756d85;
  --mg-text-dim: #9a91ad;
  --mg-text-faint: #b8b0c8;
  display: flex;
  flex-direction: column;
  gap: 10px;
  height: calc(100vh - 56px - 36px - 32px);
  min-height: 0;
  margin: -8px -8px 0;
  color: var(--mm-ink);
  position: relative;
}

.map-monitor-page :deep(.vehicle-monitor),
.map-monitor-page :deep(.mission-list-panel),
.map-monitor-page :deep(.monitor-selection) {
  --mg-text-light: #28213a;
  --mg-text-muted: #756d85;
  --mg-text-dim: #9a91ad;
  color: #28213a;
}
.map-monitor-page :deep(.table-head),
.map-monitor-page :deep(.col-pos),
.map-monitor-page :deep(.muted),
.map-monitor-page :deep(.muted-line),
.map-monitor-page :deep(.kv-row .k) {
  color: #756d85 !important;
}
.map-monitor-page :deep(.table-row),
.map-monitor-page :deep(.col-id),
.map-monitor-page :deep(.col-state),
.map-monitor-page :deep(.kv-row .v) {
  color: #28213a !important;
}
.map-monitor-page :deep(.el-input__wrapper) {
  background: #f6f3fb !important;
  box-shadow: 0 0 0 1px rgba(40, 33, 58, 0.1) inset !important;
}
.map-monitor-page :deep(.el-input__inner) {
  color: #28213a !important;
}
.map-monitor-page :deep(.el-input__inner::placeholder) {
  color: #b8b0c8 !important;
}

.mm-command-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 8px 12px;
  border-radius: var(--mm-radius);
  background: var(--mm-surface);
  border: 1px solid var(--mm-border);
  box-shadow:
    0 1px 2px rgba(40, 33, 58, 0.04),
    0 8px 22px rgba(54, 35, 78, 0.07);
  overflow-x: auto;
  z-index: 12;
}

.mm-kpi-pills {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  overflow-x: auto;
}
.mm-pill {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px 11px;
  border-radius: 10px;
  background: var(--mm-surface-2);
  border: 1px solid var(--mm-border);
  flex: none;
}
.mm-pill--muted { opacity: 0.85; }
.mm-pill-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--mm-muted);
  white-space: nowrap;
  letter-spacing: 0.02em;
}
.mm-pill-val {
  font-family: var(--mg-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 16px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--mm-ink);
}
.mm-pill-val small {
  margin-left: 2px;
  font-size: 12px;
  font-weight: 500;
  color: var(--mm-muted);
}
.mm-pill-val.mm-warn b { color: var(--mg-status-warning, #b45309); }
.mm-pill-val.mm-danger b { color: var(--mg-status-danger, #b91c1c); }
.mm-pill-sep {
  width: 1px;
  height: 30px;
  background: rgba(40, 33, 58, 0.12);
  flex: none;
  margin: 0 4px;
}

.mm-command-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}
.mm-clock {
  font-family: var(--mg-font-mono);
  font-size: 12px;
  color: var(--mm-muted);
  font-variant-numeric: tabular-nums;
}
.mm-live {
  font-size: 12px;
  font-weight: 650;
  color: var(--mm-muted);
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--mm-surface-2);
  border: 1px solid var(--mm-border);
}
.mm-live.is-live {
  color: var(--mg-status-success, #15803d);
  background: rgba(var(--mg-status-success-rgb, 21, 128, 61), 0.12);
  border-color: rgba(var(--mg-status-success-rgb, 21, 128, 61), 0.28);
}

.mm-layout-toggle {
  display: inline-flex;
  padding: 2px;
  border-radius: 10px;
  background: var(--mm-surface-2);
  border: 1px solid var(--mm-border);
}
.mm-toggle-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--mm-muted);
  font-size: 12px;
  font-weight: 650;
  height: 28px;
  padding: 0 12px;
  border-radius: 8px;
  cursor: pointer;
}
.mm-toggle-btn.is-active {
  background: var(--mm-accent);
  color: #fff;
  box-shadow: 0 2px 8px rgba(var(--mg-primary-rgb, 117, 67, 232), 0.28);
}
.mm-icon-btn {
  height: 30px;
  padding: 0 12px;
  border-radius: 10px;
  border: 1px solid var(--mm-border);
  background: var(--mm-surface-2);
  color: var(--mm-ink);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.mm-icon-btn:hover:not(:disabled) {
  background: var(--mm-accent-soft);
  border-color: rgba(var(--mg-primary-rgb, 117, 67, 232), 0.35);
  color: var(--mm-accent);
}
.mm-icon-btn:disabled { opacity: 0.55; cursor: default; }

.mm-workspace {
  flex: 1;
  min-height: 0;
  display: flex;
  position: relative;
  gap: 8px;
}

.mm-canvas {
  position: relative;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--mm-radius);
  border: 1px solid rgba(40, 33, 58, 0.12);
  background: #12121a;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}
.mm-canvas :deep(.workspace-3d-wrap) {
  flex: 1;
  min-height: 0;
}

/* —— B 双栏 —— */
.layout-dual .mm-workspace { flex-direction: row; align-items: stretch; }
.layout-dual .mm-canvas { flex: 1 1 auto; }
.layout-dual .mm-panel {
  flex: 0 0 var(--mm-rail-w);
  width: var(--mm-rail-w);
  min-width: 300px;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: var(--mm-radius);
  border: 1px solid var(--mm-border);
  background: var(--mm-surface);
  box-shadow:
    0 1px 2px rgba(40, 33, 58, 0.04),
    0 10px 26px rgba(54, 35, 78, 0.08);
}
.layout-dual .mm-panel-list {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.layout-dual .mm-panel-detail {
  flex: 1 1 0;
  min-height: 160px;
  max-height: 48%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-top: 1px solid var(--mm-border);
}

/* —— C 沉浸 —— */
.layout-immersive .mm-command-bar {
  position: absolute;
  top: 8px;
  left: 8px;
  right: 8px;
  background: rgba(255, 254, 253, 0.94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow:
    0 1px 2px rgba(40, 33, 58, 0.06),
    0 12px 28px rgba(54, 35, 78, 0.14);
}
.layout-immersive .mm-workspace {
  flex: 1;
  gap: 0;
}
.layout-immersive .mm-canvas {
  position: absolute;
  inset: 0;
  border-radius: var(--mm-radius);
  z-index: 1;
}
.layout-immersive .mm-panel {
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 8px;
  height: var(--mm-sheet-h);
  z-index: 8;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 14px;
  border: 1px solid var(--mm-border);
  background: rgba(255, 254, 253, 0.96);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow:
    0 -4px 20px rgba(54, 35, 78, 0.16),
    0 1px 0 rgba(255, 255, 255, 0.6) inset;
  transition: height 0.22s var(--mm-ease);
}
.layout-immersive.sheet-collapsed .mm-panel {
  height: 44px;
}
.layout-immersive .mm-panel-list {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.mm-sheet-handle {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--mm-border);
}
.mm-sheet-toggle {
  appearance: none;
  border: 1px solid rgba(var(--mg-primary-rgb, 117, 67, 232), 0.28);
  background: var(--mm-accent-soft);
  color: var(--mm-accent);
  font-size: 12px;
  font-weight: 650;
  height: 28px;
  padding: 0 12px;
  border-radius: 8px;
  cursor: pointer;
}
.mm-sheet-hint {
  font-size: 12px;
  color: var(--mm-muted);
}

.mm-rail-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 12px 8px;
  border-bottom: 1px solid var(--mm-border);
}
.mm-segment {
  display: inline-flex;
  padding: 3px;
  border-radius: 10px;
  background: var(--mm-surface-2);
  border: 1px solid var(--mm-border);
  gap: 2px;
}
.mm-segment-btn {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--mm-muted);
  font-size: 12px;
  font-weight: 600;
  height: 28px;
  padding: 0 14px;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.15s var(--mm-ease), color 0.15s var(--mm-ease);
}
.mm-segment-btn.is-active {
  background: var(--mm-accent);
  color: #fff;
  box-shadow: 0 2px 8px rgba(var(--mg-primary-rgb, 117, 67, 232), 0.25);
}
.mm-seg-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  margin-left: 4px;
  border-radius: 8px;
  background: rgba(239, 68, 68, 0.2);
  color: var(--mg-status-danger);
  font-size: 10px;
  font-weight: 700;
  font-family: var(--mg-font-mono);
}
.mm-rail-count {
  font-family: var(--mg-font-mono);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--mg-text-muted);
  font-weight: 600;
}
.mm-rail-list {
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  padding: 8px 10px 10px;
}
.mm-rail-list > * {
  flex: 1;
  min-height: 0;
}

.mm-detail-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  font-size: 12px;
  font-weight: 600;
  color: var(--mg-text-muted);
  border-bottom: 1px solid var(--mm-border);
}
.mm-detail-clear {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--mm-accent);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.mm-detail-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 4px 8px 10px;
}
.mm-detail-empty {
  padding: 28px 12px;
  text-align: center;
  font-size: 12px;
  color: var(--mg-text-muted);
}

/* 浮层叠在深色画布上：保持深玻璃，accent 跟主题紫 */
.mm-float-detail {
  position: absolute;
  z-index: 6;
  width: min(400px, calc(100% - 28px));
  max-height: min(42%, 320px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 12px;
  --mg-text-light: #f4eeff;
  --mg-text-muted: rgba(244, 238, 255, 0.72);
  background: rgba(24, 18, 40, 0.94);
  border: 1px solid rgba(155, 124, 255, 0.22);
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: #f4eeff;
}
.mm-float-detail.is-dragging {
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55);
  user-select: none;
}
.mm-float-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 10px 12px 8px;
  border-bottom: 1px solid rgba(155, 124, 255, 0.16);
  cursor: grab;
}
.mm-float-detail.is-dragging .mm-float-head { cursor: grabbing; }
.mm-float-title {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.mm-float-grip {
  width: 10px;
  height: 14px;
  flex: none;
  background:
    radial-gradient(circle, rgba(244, 238, 255, 0.55) 1.2px, transparent 1.3px) 0 0 / 5px 5px,
    radial-gradient(circle, rgba(244, 238, 255, 0.55) 1.2px, transparent 1.3px) 5px 2.5px / 5px 5px;
  opacity: 0.55;
}
.mm-float-id {
  font-size: 13px;
  font-weight: 650;
  color: #f4eeff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mm-float-kind {
  font-size: 10px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 5px;
  color: #ddd4ff;
  background: rgba(117, 67, 232, 0.35);
}
.mm-float-close {
  appearance: none;
  border: 0;
  background: transparent;
  color: rgba(244, 238, 255, 0.65);
  width: 28px;
  height: 28px;
  border-radius: 6px;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.mm-float-close:hover {
  background: rgba(155, 124, 255, 0.18);
  color: #fff;
}
.mm-float-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 4px 8px 10px;
}
.mm-float-detail :deep(.monitor-selection),
.mm-float-detail :deep(.vehicle-monitor),
.mm-float-detail :deep(.mission-list-panel) {
  --mg-text-light: #f4eeff;
  --mg-text-muted: rgba(244, 238, 255, 0.72);
  --mg-text-dim: rgba(244, 238, 255, 0.55);
  color: #f4eeff;
}
.mm-float-detail :deep(.table-head),
.mm-float-detail :deep(.col-pos),
.mm-float-detail :deep(.muted),
.mm-float-detail :deep(.muted-line),
.mm-float-detail :deep(.kv-row .k) {
  color: rgba(244, 238, 255, 0.7) !important;
}
.mm-float-detail :deep(.table-row),
.mm-float-detail :deep(.col-id),
.mm-float-detail :deep(.col-state),
.mm-float-detail :deep(.kv-row .v) {
  color: #f4eeff !important;
}
.mm-float-detail :deep(.el-input__wrapper) {
  background: rgba(12, 8, 24, 0.65) !important;
  box-shadow: 0 0 0 1px rgba(155, 124, 255, 0.22) inset !important;
}
.mm-float-detail :deep(.el-input__inner) {
  color: #f4eeff !important;
}

.mm-alarm-panel {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.mm-alarm-head,
.mm-alarm-row {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: 8px;
  align-items: center;
  padding: 0 8px;
}
.mm-alarm-head {
  flex: none;
  height: 28px;
  font-size: 11px;
  font-weight: 600;
  color: var(--mg-text-muted);
  border-bottom: 1px solid var(--mm-border);
}
.mm-alarm-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}
.mm-alarm-row {
  appearance: none;
  width: 100%;
  border: 0;
  border-bottom: 1px solid var(--mm-border);
  background: transparent;
  color: var(--mg-text-light);
  text-align: left;
  min-height: 40px;
  padding-top: 8px;
  padding-bottom: 8px;
  font-size: 12px;
  cursor: pointer;
}
.mm-alarm-row:hover { background: var(--mm-accent-soft); }
.mm-alarm-row .col-car {
  font-family: var(--mg-font-mono);
  font-variant-numeric: tabular-nums;
  font-weight: 650;
}
.mm-alarm-row .col-info {
  color: var(--mg-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.mm-alarm-empty {
  text-align: center;
  padding: 28px 8px;
  font-size: 12px;
  color: var(--mg-text-muted);
}

@media (max-width: 1100px) {
  .map-monitor-page { --mm-rail-w: 320px; }
  .mm-clock { display: none; }
  .mm-pill--muted { display: none; }
}

@media (max-width: 820px) {
  .layout-dual .mm-workspace { flex-wrap: wrap; }
  .layout-dual .mm-canvas {
    flex: 1 1 100%;
    min-height: 42vh;
  }
  .layout-dual .mm-panel {
    flex: 1 1 100%;
    width: 100%;
    min-height: 36vh;
  }
}

@media (prefers-reduced-motion: reduce) {
  .mm-segment-btn,
  .layout-immersive .mm-panel { transition: none; }
}
</style>
