<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import draggable from 'vuedraggable'
import { useTabStore } from '@/pinia/tab'
import { Button } from '@/components/ui/button'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
import localforage from 'localforage'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  AlertDialogTitle,
  AlertDialogTrigger,
} from 'reka-ui'
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'

interface DataEntry {
  key: string
  value: any
}

interface DataPackage {
  version: number
  exportedAt: string
  localStorage: DataEntry[]
  sessionStorage: DataEntry[]
  localforageDefault: DataEntry[]
}

interface UserGuideStep {
  title: string
  description: string
  points: string[]
}

type XmInfoLike = {
  projectName?: unknown
}

const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
const userGuideSteps: UserGuideStep[] = [
  {
    title: '欢迎使用',
    description: '这个引导会覆盖系统主要功能和完整使用路径，建议按顺序走一遍。',
    points: [
      '顶部是标签页区，可在项目、合同段、服务计算页之间快速切换。',
      '页面里的表格与表单会自动保存到本地，无需手动点击保存。',
      '你可以随时点击右上角“使用引导”重新打开本教程。'
    ]
  },
  {
    title: '项目卡片与四个模块',
    description: '默认标签“项目卡片”是入口，左侧流程线包含四个项目级功能。',
    points: [
      '基础信息：填写项目名称与项目规模明细。',
      '合同段管理：新建、排序、搜索、导入/导出合同段。',
      '咨询分类系数 / 工程专业系数：维护系数预算取值和备注。'
    ]
  },
  {
    title: '基础信息填写',
    description: '先在“基础信息”中补齐项目级数据，再进入后续合同段计算。',
    points: [
      '项目名称会用于导出文件名和页面展示。',
      '项目明细表支持直接编辑、复制粘贴、撤销重做。',
      '分组行自动汇总，顶部固定行显示总合计。'
    ]
  },
  {
    title: '合同段管理',
    description: '在“合同段管理”中完成合同段生命周期操作。',
    points: [
      '“添加合同段”用于新增，卡片右上角可编辑或删除。',
      '支持搜索、网格/列表切换，非搜索状态可拖拽排序。',
      '更多菜单可导入/导出合同段；点击卡片进入该合同段详情。'
    ]
  },
  {
    title: '合同段详情',
    description: '进入合同段后，左侧同样是流程线，包含规模信息和咨询服务两部分。',
    points: [
      '规模信息：按工程专业填写当前合同段的规模数据。',
      '咨询服务：选择服务词典并生成服务费用明细。',
      '合同段页面会独立缓存，不同合同段互不干扰。'
    ]
  },
  {
    title: '咨询服务与计算页',
    description: '咨询服务页面用于管理服务明细，并进入具体计费方法页面。',
    points: [
      '先点击“浏览”选择服务，再确认生成明细行。',
      '明细表“编辑”可打开服务计算页，“清空”会重置该服务计算结果。',
      '服务计算页包含投资规模法、用地规模法、工作量法、工时法四种方法。'
    ]
  },
  {
    title: '系数维护',
    description: '项目级系数用于调节预算取值，可在两个系数页分别维护。',
    points: [
      '咨询分类系数页：按咨询分类维护预算取值与说明。',
      '工程专业系数页：按专业树维护预算取值与说明。',
      '支持批量粘贴、撤销重做，便于一次性维护多行数据。'
    ]
  },
  {
    title: '数据管理与恢复',
    description: '顶部工具栏负责全量数据导入导出与初始化重置。',
    points: [
      '“导入/导出”是整项目级别的数据包操作。',
      '“重置”会清空本地全部数据并恢复默认页面。',
      '建议在重要调整前先导出备份。'
    ]
  }
]

const componentMap: Record<string, any> = {
  XmView: markRaw(defineAsyncComponent(() => import('@/components/views/Xm.vue'))),
  ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/ContractDetailView.vue'))),
  ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
}

const tabStore = useTabStore()



const tabContextOpen = ref(false)
const tabContextX = ref(0)
const tabContextY = ref(0)
const contextTabId = ref<string>('XmView')
const tabContextRef = ref<HTMLElement | null>(null)

const dataMenuOpen = ref(false)
const dataMenuRef = ref<HTMLElement | null>(null)
const importFileRef = ref<HTMLInputElement | null>(null)
const userGuideOpen = ref(false)
const userGuideStepIndex = ref(0)
const tabItemElMap = new Map<string, HTMLElement>()
const tabPanelElMap = new Map<string, HTMLElement>()

const tabsModel = computed({
  get: () => tabStore.tabs,
  set: (value) => {
    tabStore.tabs = value
  }
})

const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value))

const hasClosableTabs = computed(() => tabStore.tabs.some(t => t.id !== 'XmView'))
const activeGuideStep = computed(
  () => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0]
)
const isFirstGuideStep = computed(() => userGuideStepIndex.value === 0)
const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideSteps.length - 1)
const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`)
const canCloseLeft = computed(() => {
  if (contextTabIndex.value <= 0) return false
  return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView')
})
const canCloseRight = computed(() => {
  if (contextTabIndex.value < 0) return false
  return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => t.id !== 'XmView')
})
const canCloseOther = computed(() =>
  tabStore.tabs.some(t => t.id !== 'XmView' && t.id !== contextTabId.value)
)

const closeMenus = () => {
  tabContextOpen.value = false
  dataMenuOpen.value = false
}

const markGuideCompleted = () => {
  try {
    localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
  } catch (error) {
    console.error('mark guide completed failed:', error)
  }
}

const hasGuideCompleted = () => {
  try {
    return localStorage.getItem(USER_GUIDE_COMPLETED_KEY) === '1'
  } catch (error) {
    console.error('read guide completion failed:', error)
    return false
  }
}

const hasNonDefaultTabState = () => {
  try {
    const raw = localStorage.getItem('tabs')
    if (!raw) return false
    const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string }
    const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : []
    const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'XmView')
    const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
    return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView')
  } catch (error) {
    console.error('parse tabs cache failed:', error)
    return false
  }
}

const shouldAutoOpenGuide = async () => {
  if (hasGuideCompleted()) return false
  if (hasNonDefaultTabState()) return false
  try {
    const keys = await localforage.keys()
    return keys.length === 0
  } catch (error) {
    console.error('read localforage keys failed:', error)
    return false
  }
}

const openUserGuide = (startAt = 0) => {
  closeMenus()
  userGuideStepIndex.value = Math.min(Math.max(startAt, 0), userGuideSteps.length - 1)
  userGuideOpen.value = true
}

const closeUserGuide = (completed = false) => {
  userGuideOpen.value = false
  if (completed) markGuideCompleted()
}

const prevUserGuideStep = () => {
  if (isFirstGuideStep.value) return
  userGuideStepIndex.value -= 1
}

const nextUserGuideStep = () => {
  if (isLastGuideStep.value) {
    closeUserGuide(true)
    return
  }
  userGuideStepIndex.value += 1
}

const jumpToGuideStep = (stepIndex: number) => {
  userGuideStepIndex.value = Math.min(Math.max(stepIndex, 0), userGuideSteps.length - 1)
}

const openTabContextMenu = (event: MouseEvent, tabId: string) => {
  contextTabId.value = tabId
  tabContextX.value = event.clientX
  tabContextY.value = event.clientY
  tabContextOpen.value = true
  void nextTick(() => {
    if (!tabContextRef.value) return
    const gap = 8
    const rect = tabContextRef.value.getBoundingClientRect()
    if (tabContextX.value + rect.width + gap > window.innerWidth) {
      tabContextX.value = Math.max(gap, window.innerWidth - rect.width - gap)
    }
    if (tabContextY.value + rect.height + gap > window.innerHeight) {
      tabContextY.value = Math.max(gap, window.innerHeight - rect.height - gap)
    }
    if (tabContextX.value < gap) tabContextX.value = gap
    if (tabContextY.value < gap) tabContextY.value = gap
  })
}

const handleGlobalMouseDown = (event: MouseEvent) => {
  const target = event.target as Node
  if (tabContextOpen.value && tabContextRef.value && !tabContextRef.value.contains(target)) {
    tabContextOpen.value = false
  }
  if (dataMenuOpen.value && dataMenuRef.value && !dataMenuRef.value.contains(target)) {
    dataMenuOpen.value = false
  }
}

const handleGlobalKeyDown = (event: KeyboardEvent) => {
  if (!userGuideOpen.value) return
  if (event.key === 'Escape') {
    event.preventDefault()
    closeUserGuide(false)
    return
  }
  if (event.key === 'ArrowLeft') {
    event.preventDefault()
    prevUserGuideStep()
    return
  }
  if (event.key === 'ArrowRight') {
    event.preventDefault()
    nextUserGuideStep()
  }
}

const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => {
  if (action === 'all') tabStore.closeAllTabs()
  if (action === 'left') tabStore.closeLeftTabs(contextTabId.value)
  if (action === 'right') tabStore.closeRightTabs(contextTabId.value)
  if (action === 'other') tabStore.closeOtherTabs(contextTabId.value)
  tabContextOpen.value = false
}

const canMoveTab = (event: any) => {
  const draggedId = event?.draggedContext?.element?.id
  const targetIndex = event?.relatedContext?.index
  if (draggedId === 'XmView') return false
  if (typeof targetIndex === 'number' && targetIndex === 0) return false
  return true
}

const setTabItemRef = (id: string, el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabItemElMap.set(id, el)
    return
  }
  tabItemElMap.delete(id)
}

const setTabPanelRef = (id: string, el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabPanelElMap.set(id, el)
    return
  }
  tabPanelElMap.delete(id)
}

const ensureActiveTabVisible = () => {
  const activeId = tabStore.activeTabId
  const el = tabItemElMap.get(activeId)
  if (!el) return
  el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}

const getActivePanelScrollViewport = (tabId?: string | null) => {
  if (!tabId) return null
  const panelEl = tabPanelElMap.get(tabId)
  if (!panelEl) return null
  return panelEl.querySelector<HTMLElement>('[data-slot="scroll-area-viewport"]')
}

const getTabScrollSessionKey = (tabId: string) => `tab-scroll-top:${tabId}`

const saveTabInnerScrollTop = (tabId?: string | null) => {
  if (!tabId) return
  const viewport = getActivePanelScrollViewport(tabId)
  if (!viewport) return
  const top = viewport.scrollTop || 0
  try {
    sessionStorage.setItem(getTabScrollSessionKey(tabId), String(top))
  } catch (error) {
    console.error('save tab scroll failed:', error)
  }
}

const restoreTabInnerScrollTop = (tabId?: string | null) => {
  if (!tabId) return
  const viewport = getActivePanelScrollViewport(tabId)
  if (!viewport) return
  let top = 0
  try {
    top = Number(sessionStorage.getItem(getTabScrollSessionKey(tabId)) || '0') || 0
  } catch (error) {
    console.error('restore tab scroll failed:', error)
  }
  viewport.scrollTop = top
}

const scheduleRestoreTabInnerScrollTop = (tabId?: string | null) => {
  if (!tabId) return
  requestAnimationFrame(() => {
    restoreTabInnerScrollTop(tabId)
    requestAnimationFrame(() => {
      restoreTabInnerScrollTop(tabId)
    })
  })
}

const readWebStorage = (storageObj: Storage): DataEntry[] => {
  const entries: DataEntry[] = []
  for (let i = 0; i < storageObj.length; i++) {
    const key = storageObj.key(i)
    if (!key) continue
    const raw = storageObj.getItem(key)
    let value: any = raw
    if (raw != null) {
      try {
        value = JSON.parse(raw)
      } catch {
        value = raw
      }
    }
    entries.push({ key, value })
  }
  return entries
}

const writeWebStorage = (storageObj: Storage, entries: DataEntry[]) => {
  storageObj.clear()
  for (const entry of entries || []) {
    const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value)
    storageObj.setItem(entry.key, value)
  }
}

const readForage = async (store: typeof localforage): Promise<DataEntry[]> => {
  const keys = await store.keys()
  const entries: DataEntry[] = []
  for (const key of keys) {
    const value = await store.getItem(key)
    entries.push({ key, value })
  }
  return entries
}

const writeForage = async (store: typeof localforage, entries: DataEntry[]) => {
  await store.clear()
  for (const entry of entries || []) {
    await store.setItem(entry.key, entry.value)
  }
}

const normalizeEntries = (value: unknown): DataEntry[] => {
  if (!Array.isArray(value)) return []
  return value
    .filter(item => item && typeof item === 'object' && typeof (item as any).key === 'string')
    .map(item => ({ key: String((item as any).key), value: (item as any).value }))
}

const sanitizeFileNamePart = (value: string): string => {
  const cleaned = value
    .replace(/[\\/:*?"<>|]/g, '_')
    .replace(/\s+/g, ' ')
    .trim()
  return cleaned || '造价项目'
}

const formatExportTimestamp = (date: Date): string => {
  const yyyy = date.getFullYear()
  const mm = String(date.getMonth() + 1).padStart(2, '0')
  const dd = String(date.getDate()).padStart(2, '0')
  const hh = String(date.getHours()).padStart(2, '0')
  const mi = String(date.getMinutes()).padStart(2, '0')
  return `${yyyy}${mm}${dd}-${hh}${mi}`
}

const getExportProjectName = (entries: DataEntry[]): string => {
  const target = entries.find(item => item.key === 'xm-info-v3')
  const data = (target?.value || {}) as XmInfoLike
  return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
}

const exportData = async () => {
  try {
    const now = new Date()
    const payload: DataPackage = {
      version: 1,
      exportedAt: now.toISOString(),
      localStorage: readWebStorage(localStorage),
      sessionStorage: readWebStorage(sessionStorage),
      localforageDefault: await readForage(localforage),
    }

    const content = await encodeZwArchive(payload)
    const binary = new Uint8Array(content.length)
    binary.set(content)
    const blob = new Blob([binary], { type: 'application/octet-stream' })
    const url = URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = url
    const projectName = getExportProjectName(payload.localforageDefault)
    const timestamp = formatExportTimestamp(now)
    link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}`
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    URL.revokeObjectURL(url)
  } catch (error) {
    console.error('export failed:', error)
    window.alert('导出失败，请重试。')
  } finally {
    dataMenuOpen.value = false
  }
}
const exportReport = async ()=>{
  
}
const triggerImport = () => {
  importFileRef.value?.click()
}

const importData = async (event: Event) => {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return

  try {
    if (!file.name.toLowerCase().endsWith(ZW_FILE_EXTENSION)) {
      throw new Error('INVALID_FILE_EXT')
    }
    const buffer = await file.arrayBuffer()
    const payload = await decodeZwArchive<DataPackage>(buffer)

    writeWebStorage(localStorage, normalizeEntries(payload.localStorage))
    writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage))
    await writeForage(localforage, normalizeEntries(payload.localforageDefault))

    tabStore.resetTabs()
    dataMenuOpen.value = false
    window.location.reload()
  } catch (error) {
    console.error('import failed:', error)
    window.alert('导入失败：文件无效、已损坏或被修改。')
  } finally {
    input.value = ''
  }
}

const handleReset = async () => {
  try {
    localStorage.clear()
    sessionStorage.clear()
    await localforage.clear()
  } catch (error) {
    console.error('reset failed:', error)
  } finally {
    tabStore.resetTabs()
    window.location.reload()
  }
}

onMounted(() => {
  window.addEventListener('mousedown', handleGlobalMouseDown)
  window.addEventListener('keydown', handleGlobalKeyDown)
  void nextTick(() => {
    ensureActiveTabVisible()
    scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
  })
  void (async () => {
    if (await shouldAutoOpenGuide()) {
      openUserGuide(0)
    }
  })()
})

onBeforeUnmount(() => {
  window.removeEventListener('mousedown', handleGlobalMouseDown)
  window.removeEventListener('keydown', handleGlobalKeyDown)
})

watch(
  () => tabStore.activeTabId,
  (nextId, prevId) => {
    saveTabInnerScrollTop(prevId)
    void nextTick(() => {
      ensureActiveTabVisible()
      scheduleRestoreTabInnerScrollTop(nextId)
    })
  }
)

watch(
  () => tabStore.tabs.map(t => t.id),
  (ids) => {
    const idSet = new Set(ids)
    try {
      for (let i = sessionStorage.length - 1; i >= 0; i--) {
        const key = sessionStorage.key(i)
        if (!key || !key.startsWith('tab-scroll-top:')) continue
        const tabId = key.slice('tab-scroll-top:'.length)
        if (!idSet.has(tabId)) sessionStorage.removeItem(key)
      }
    } catch (error) {
      console.error('cleanup tab scroll cache failed:', error)
    }
  }
)
</script>

<template>
  <div class="flex flex-col w-full h-screen bg-background overflow-hidden">
    <div class="flex items-start gap-2 border-b bg-muted/30 px-2 pt-2 flex-none">
      <ScrollArea type="auto" class="min-w-0 flex-1 whitespace-nowrap pb-2">
        <draggable
          v-model="tabsModel"
          item-key="id"
          tag="div"
          class="flex w-max gap-1"
          :animation="180"
          :move="canMoveTab"
        >
          <template #item="{ element: tab }">
            <div
              :ref="el => setTabItemRef(tab.id, el)"
              @click="tabStore.activeTabId = tab.id"
              @contextmenu.prevent="openTabContextMenu($event, tab.id)"
              :class="[
                'group relative flex items-center h-9 px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border-x border-t transition-all text-sm',
                tabStore.activeTabId === tab.id
                  ? 'bg-background border-border font-medium'
                  : 'border-transparent hover:bg-muted text-muted-foreground',
                tab.id !== 'XmView' ? 'cursor-move' : ''
              ]"
            >
              <span class="truncate mr-2">{{ tab.title }}</span>

              <Button
                v-if="tab.id !== 'XmView'"
                variant="ghost"
                size="icon"
                class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
                @click.stop="tabStore.removeTab(tab.id)"
              >
                <X class="h-3 w-3" />
              </Button>
            </div>
          </template>
        </draggable>
        <ScrollBar orientation="horizontal" />
      </ScrollArea>

      <div ref="dataMenuRef" class="relative mb-2 shrink-0">
        <Button variant="outline" size="sm" class="cursor-pointer" @click="dataMenuOpen = !dataMenuOpen">
          <ChevronDown class="h-4 w-4 mr-1" />
          导入/导出
        </Button>
        <div
          v-if="dataMenuOpen"
          class="absolute right-0 top-full mt-1 z-50 min-w-[108px] rounded-md border bg-background p-1 shadow-md"
        >
        <button
            class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
            @click="triggerImport"
          >
            导入
          </button>
          <button
            class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
            @click="exportData"
          >
            导出
          </button>
            <button
            class="w-full cursor-pointer rounded px-3 py-1.5 text-left text-sm hover:bg-muted"
            @click="exportReport"
          >
            导出报表
          </button>
        </div>
        <input
          ref="importFileRef"
          type="file"
          accept=".zw"
          class="hidden"
          @change="importData"
        />
      </div>

      <Button variant="outline" size="sm" class="mb-2 shrink-0 cursor-pointer" @click="openUserGuide(0)">
        <CircleHelp class="h-4 w-4 mr-1" />
        使用引导
      </Button>

      <AlertDialogRoot>
        <AlertDialogTrigger as-child>
          <Button variant="destructive" size="sm" class="mb-2 shrink-0">
            <RotateCcw class="h-4 w-4 mr-1" />
            重置
          </Button>
        </AlertDialogTrigger>
        <AlertDialogPortal>
          <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
          <AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
            <AlertDialogTitle class="text-base font-semibold">确认重置</AlertDialogTitle>
            <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
              将清空所有项目数据，并恢复默认页面，确认继续吗？
            </AlertDialogDescription>
            <div class="mt-4 flex items-center justify-end gap-2">
              <AlertDialogCancel as-child>
                <Button variant="outline">取消</Button>
              </AlertDialogCancel>
              <AlertDialogAction as-child>
                <Button variant="destructive" @click="handleReset">确认重置</Button>
              </AlertDialogAction>
            </div>
          </AlertDialogContent>
        </AlertDialogPortal>
      </AlertDialogRoot>
    </div>

    <div class="flex-1 overflow-auto relative">
      <div
        v-for="tab in tabStore.tabs"
        :key="tab.id"
        :ref="el => setTabPanelRef(tab.id, el)"
        v-show="tabStore.activeTabId === tab.id"
        class="h-full w-full p-6 animate-in fade-in duration-300"
      >
        <component :is="componentMap[tab.componentName]" v-bind="tab.props || {}" />
      </div>
    </div>

    <div
      v-if="tabContextOpen"
      ref="tabContextRef"
      class="fixed z-[70] w-max rounded-lg border border-border/80 bg-background/95 p-1.5 shadow-xl ring-1 ring-black/5 backdrop-blur-sm"
      :style="{ left: `${tabContextX}px`, top: `${tabContextY}px` }"
    >
      <button
        class="flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
        :disabled="!hasClosableTabs"
        @click="runTabMenuAction('all')"
      >
        删除所有
      </button>
      <button
        class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
        :disabled="!canCloseLeft"
        @click="runTabMenuAction('left')"
      >
        删除左侧
      </button>
      <button
        class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
        :disabled="!canCloseRight"
        @click="runTabMenuAction('right')"
      >
        删除右侧
      </button>
      <button
        class="mt-0.5 flex cursor-pointer items-center whitespace-nowrap rounded-md px-3 py-1.5 text-left text-sm transition-colors hover:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
        :disabled="!canCloseOther"
        @click="runTabMenuAction('other')"
      >
        删除其他
      </button>
    </div>

    <div
      v-if="userGuideOpen"
      class="fixed inset-0 z-[120] flex items-center justify-center bg-black/45 p-4"
      @click.self="closeUserGuide(false)"
    >
      <div class="w-full max-w-3xl rounded-xl border bg-background shadow-2xl">
        <div class="flex items-start justify-between border-b px-6 py-5">
          <div>
            <p class="text-xs text-muted-foreground">新用户引导 · {{ guideProgressText }}</p>
            <h3 class="mt-1 text-lg font-semibold">{{ activeGuideStep.title }}</h3>
          </div>
          <Button variant="ghost" size="icon" class="h-8 w-8" @click="closeUserGuide(false)">
            <X class="h-4 w-4" />
          </Button>
        </div>

        <div class="space-y-4 px-6 py-5">
          <p class="text-sm leading-6 text-foreground">{{ activeGuideStep.description }}</p>
          <ul class="list-disc space-y-2 pl-5 text-sm text-muted-foreground">
            <li v-for="(point, index) in activeGuideStep.points" :key="`${activeGuideStep.title}-${index}`">
              {{ point }}
            </li>
          </ul>
        </div>

        <div class="flex flex-col gap-3 border-t px-6 py-4 sm:flex-row sm:items-center sm:justify-between">
          <div class="flex items-center gap-1.5">
            <button
              v-for="(_step, index) in userGuideSteps"
              :key="`guide-dot-${index}`"
              class="h-2.5 w-2.5 cursor-pointer rounded-full transition-colors"
              :class="index === userGuideStepIndex ? 'bg-primary' : 'bg-muted'"
              :aria-label="`跳转到第 ${index + 1} 步`"
              @click="jumpToGuideStep(index)"
            />
          </div>
          <div class="flex items-center justify-end gap-2">
            <Button variant="ghost" @click="closeUserGuide(false)">稍后再看</Button>
            <Button variant="outline" :disabled="isFirstGuideStep" @click="prevUserGuideStep">上一步</Button>
            <Button @click="nextUserGuideStep">{{ isLastGuideStep ? '完成并不再自动弹出' : '下一步' }}</Button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
