<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import draggable from 'vuedraggable'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
import localforage from 'localforage'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  AlertDialogTitle,
  AlertDialogTrigger,
  ToastDescription,
  ToastProvider,
  ToastRoot,
  ToastTitle,
  ToastViewport,
} from 'reka-ui'
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
import { addNumbers, roundTo } from '@/lib/decimal'
import { exportFile, serviceList, additionalWorkList, reserveList } from '@/sql'

interface DataEntry {
  key: string
  value: any
}

interface ForageStoreSnapshot {
  storeName: string
  entries: DataEntry[]
}

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

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

type XmInfoLike = {
  projectName?: unknown
  preparedBy?: unknown
  reviewedBy?: unknown
  preparedDate?: unknown
  projectIndustry?: unknown
}

interface ScaleRowLike {
  id: string
  amount: number | null
  landArea: number | null
  isGroupRow?: boolean
}

interface XmInfoStorageLike extends XmInfoLike {
  detailRows?: ScaleRowLike[],
  totalAmount?: number,
  roughCalcEnabled?: boolean
}
interface XmScaleStorageLike {
  detailRows?: ScaleRowLike[]
}

interface ContractCardItem {
  id: string
  name?: string
  order?: number
}

interface ZxFwRowLike {
  id: string
  process?: unknown
  subtotal?: unknown
  investScale?: unknown
  landScale?: unknown
  workload?: unknown
  hourly?: unknown
}

interface ZxFwStorageLike {
  selectedIds?: string[]
  selectedCodes?: string[]
  detailRows?: ZxFwRowLike[]
}

interface ScaleMethodRowLike extends ScaleRowLike {
  basicFormula?: unknown
  optionalFormula?: unknown
  benchmarkBudget?: unknown
  benchmarkBudgetBasic?: unknown
  benchmarkBudgetOptional?: unknown
  budgetFee?: unknown
  budgetFeeBasic?: unknown
  budgetFeeOptional?: unknown
  consultCategoryFactor?: unknown
  majorFactor?: unknown
  workStageFactor?: unknown
  workRatio?: unknown
  remark?: unknown
}

interface HtFeeMainRowLike {
  id?: unknown
  name?: unknown
}

interface RateMethodRowLike {
  rate?: unknown
  budgetFee?: unknown
}

interface QuantityMethodRowLike {
  id?: unknown
  feeItem?: unknown
  unit?: unknown
  quantity?: unknown
  unitPrice?: unknown
  budgetFee?: number|null
  remark?: unknown
}

interface WorkloadMethodRowLike {
  id: string
  budgetAdoptedUnitPrice?: unknown
  workload?: unknown
  basicFee?: unknown
  consultCategoryFactor?: unknown
  serviceFee?: unknown
  remark?: unknown
}

interface HourlyMethodRowLike {
  id: string
  adoptedBudgetUnitPrice?: unknown
  personnelCount?: unknown
  workdayCount?: unknown
  serviceBudget?: unknown
  remark?: unknown
}

interface DetailRowsStorageLike<T> {
  detailRows?: T[],
  roughCalcEnabled?: boolean,
  totalAmount?: number,


}

interface FactorRowLike {
  id: string
  standardFactor?: unknown
  budgetValue?: unknown
  remark?: unknown
}

interface ExportScaleRow {
  major: number
  cost: number | null
  area: number | null
}

interface ExportMethod1Detail {
  major: number
  cost: number
  basicFee: number
  basicFormula: string
  basicFee_basic: number
  optionalFormula: string
  basicFee_optional: number
  serviceCoe: number
  majorCoe: number
  processCoe: number
  proportion: number
  fee: number
  remark: string
}

interface ExportMethod1 {
  cost: number
  basicFee: number
  basicFee_basic: number
  basicFee_optional: number
  fee: number
  det: ExportMethod1Detail[]
}

interface ExportMethod2Detail {
  major: number
  area: number
  basicFee: number
  basicFormula: string
  basicFee_basic: number
  optionalFormula: string
  basicFee_optional: number
  serviceCoe: number
  majorCoe: number
  processCoe: number
  proportion: number
  fee: number
  remark: string
}

interface ExportMethod2 {
  area: number
  basicFee: number
  basicFee_basic: number
  basicFee_optional: number
  fee: number
  det: ExportMethod2Detail[]
}

interface ExportMethod3Detail {
  task: number
  price: number
  amount: number
  basicFee: number
  serviceCoe: number
  fee: number
  remark: string
}

interface ExportMethod3 {
  basicFee: number
  fee: number
  det: ExportMethod3Detail[]
}

interface ExportMethod4Detail {
  expert: number
  price: number
  person_num: number
  work_day: number
  fee: number
  remark: string
}

interface ExportMethod4 {
  person_num: number
  work_day: number
  fee: number
  det: ExportMethod4Detail[]
}

interface ExportService {
  id: number
  fee: number
  process: number
  method1?: ExportMethod1
  method2?: ExportMethod2
  method3?: ExportMethod3
  method4?: ExportMethod4
}

interface ExportServiceCoe {
  serviceid: number
  coe: number
  remark: string
}

interface ExportMajorCoe {
  majorid: number
  coe: number
  remark: string
}

interface ExportContract {
  name: string
  serviceFee: number
  addtionalFee: number
  reserveFee: number
  fee: number
  scale: ExportScaleRow[]
  serviceCoes: ExportServiceCoe[]
  majorCoes: ExportMajorCoe[]
  services: ExportService[]
  addtional: ExportAdditional | null
  reserve: ExportReserve | null
}



interface ExportMethod0 {
  coe: number
  fee: number
}

interface ExportMethod5Detail {
  name: string
  unit: string
  amount: number
  price: number
  fee: number
  remark: string
}

interface ExportMethod5 {
  fee: number
  det: ExportMethod5Detail[]
}

interface ExportAdditionalDetail {
  id: number
  code?: unknown
  name: string
  fee: number
  m0?: ExportMethod0
  m4?: ExportMethod4
  m5?: ExportMethod5
}

interface ExportAdditional {
  code?: unknown
  name: string
  fee: number
  det: ExportAdditionalDetail[]
}

interface ExportReserve {
  code?: unknown
  name: string
  fee: number
  m0?: ExportMethod0
  m4?: ExportMethod4
  m5?: ExportMethod5
}

interface ExportReportPayload {
  name: string
  writer: string
  reviewer: string
  date: string
  industry: number
  fee: number
  scaleCost: number
  scale: ExportScaleRow[]
  serviceCoes: ExportServiceCoe[]
  majorCoes: ExportMajorCoe[]
  contracts: ExportContract[]
}

const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
const CONSULT_CATEGORY_FACTOR_DB_KEY = 'xm-consult-category-factor-v1'
const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1'
const PINIA_PERSIST_DB_NAME = 'DB'
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const
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/xmCard.vue'))),
  ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/htCard.vue'))),
  ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))),
  HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))),
}

const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()



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 importConfirmOpen = ref(false)
const pendingImportPayload = shallowRef<DataPackage | null>(null)
const pendingImportFileName = ref('')
const userGuideOpen = ref(false)
const userGuideStepIndex = ref(0)
const reportExportToastOpen = ref(false)
const reportExportProgress = ref(0)
const reportExportStatus = ref<'running' | 'success' | 'error'>('running')
const reportExportText = ref('')
const tabItemElMap = new Map<string, HTMLElement>()
const tabTitleElMap = new Map<string, HTMLElement>()
const tabPanelElMap = new Map<string, HTMLElement>()
const tabScrollAreaRef = ref<HTMLElement | null>(null)
const showTabScrollLeft = ref(false)
const showTabScrollRight = ref(false)
const isTabStripHover = ref(false)
const isTabDragging = ref(false)
const tabTitleOverflowMap = ref<Record<string, boolean>>({})
let tabStripViewportEl: HTMLElement | null = null
let tabTitleOverflowRafId: number | null = null
let reportExportToastTimer: ReturnType<typeof setTimeout> | null = null

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 clearReportExportToastTimer = () => {
  if (!reportExportToastTimer) return
  clearTimeout(reportExportToastTimer)
  reportExportToastTimer = null
}

const showReportExportProgress = (progress: number, text: string) => {
  clearReportExportToastTimer()
  reportExportStatus.value = 'running'
  reportExportProgress.value = Math.max(0, Math.min(100, progress))
  reportExportText.value = text
  reportExportToastOpen.value = true
}

const finishReportExportProgress = (success: boolean, text: string) => {
  clearReportExportToastTimer()
  reportExportStatus.value = success ? 'success' : 'error'
  reportExportProgress.value = 100
  reportExportText.value = text
  reportExportToastOpen.value = true
  reportExportToastTimer = setTimeout(() => {
    reportExportToastOpen.value = false
  }, success ? 1200 : 1800)
}

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 kvStore.keys()
    return keys.length === 0
  } catch (error) {
    console.error('read kv 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 handleTabDragStart = () => {
  isTabDragging.value = true
}

const handleTabDragEnd = () => {
  isTabDragging.value = false
}

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

const setTabTitleRef = (id: string, el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabTitleElMap.set(id, el)
    scheduleUpdateTabTitleOverflow()
    return
  }
  tabTitleElMap.delete(id)
  delete tabTitleOverflowMap.value[id]
  scheduleUpdateTabTitleOverflow()
}

const updateTabTitleOverflow = () => {
  const nextMap: Record<string, boolean> = {}
  for (const [id, el] of tabTitleElMap.entries()) {
    nextMap[id] = el.scrollWidth > el.clientWidth + 1
  }
  tabTitleOverflowMap.value = nextMap
}

const scheduleUpdateTabTitleOverflow = () => {
  if (tabTitleOverflowRafId != null) return
  tabTitleOverflowRafId = requestAnimationFrame(() => {
    tabTitleOverflowRafId = null
    updateTabTitleOverflow()
  })
}

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

const setTabScrollAreaRef = (el: Element | ComponentPublicInstance | null) => {
  if (el instanceof HTMLElement) {
    tabScrollAreaRef.value = el
    return
  }
  const rootEl =
    el && typeof el === 'object' && '$el' in el ? (el as ComponentPublicInstance).$el : null
  tabScrollAreaRef.value = rootEl instanceof HTMLElement ? rootEl : null
}

const getTabStripViewport = () =>
  tabScrollAreaRef.value?.querySelector<HTMLElement>('[data-slot="scroll-area-viewport"]') || null

const updateTabScrollButtons = () => {
  const viewport = getTabStripViewport()
  if (!viewport) {
    showTabScrollLeft.value = false
    showTabScrollRight.value = false
    return
  }
  const maxLeft = Math.max(0, viewport.scrollWidth - viewport.clientWidth)
  showTabScrollLeft.value = viewport.scrollLeft > 1
  showTabScrollRight.value = viewport.scrollLeft < maxLeft - 1
}

const handleTabStripScroll = () => {
  updateTabScrollButtons()
}

const bindTabStripScroll = () => {
  const viewport = getTabStripViewport()
  if (tabStripViewportEl === viewport) {
    updateTabScrollButtons()
    return
  }
  if (tabStripViewportEl) {
    tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
  }
  tabStripViewportEl = viewport
  if (tabStripViewportEl) {
    tabStripViewportEl.addEventListener('scroll', handleTabStripScroll, { passive: true })
  }
  updateTabScrollButtons()
}

const scrollTabStripBy = (delta: number) => {
  const viewport = getTabStripViewport()
  if (!viewport) return
  viewport.scrollBy({ left: delta, behavior: 'smooth' })
  requestAnimationFrame(updateTabScrollButtons)
}

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)
  }
}

type ForageInstance = ReturnType<typeof localforage.createInstance>
type ForageStore = Pick<ForageInstance, 'keys' | 'getItem' | 'setItem' | 'clear'>

const createForageStore = (storeName: string): ForageInstance =>
  localforage.createInstance({
    name: PINIA_PERSIST_DB_NAME,
    storeName
  })

const getPiniaPersistStoreName = (storeId: string) => `${PINIA_PERSIST_BASE_STORE_NAME}-${storeId}`

const getPiniaPersistStores = () =>
  PINIA_PERSIST_STORE_IDS.map(storeId => {
    const storeName = getPiniaPersistStoreName(storeId)
    return {
      storeName,
      store: createForageStore(storeName)
    }
  })

const flushPiniaPersistNow = async () => {
  await Promise.all([
    tabStore.$persistNow?.(),
    zxFwPricingStore.$persistNow?.(),
    kvStore.$persistNow?.()
  ])
}

const readForage = async (store: ForageStore): 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: toPersistableValue(value) })
  }
  return entries
}

const toPersistableValue = (value: unknown) => {
  try {
    return JSON.parse(JSON.stringify(value))
  } catch (error) {
    console.error('normalize persist value failed, fallback to null:', error)
    return null
  }
}

const writeForage = async (store: ForageStore, entries: DataEntry[]) => {
  await store.clear()
  for (const entry of entries || []) {
    await store.setItem(entry.key, toPersistableValue(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 normalizeForageStoreSnapshots = (value: unknown): ForageStoreSnapshot[] => {
  if (!Array.isArray(value)) return []
  return value
    .filter(item =>
      item
      && typeof item === 'object'
      && typeof (item as any).storeName === 'string'
      && Array.isArray((item as any).entries)
    )
    .map(item => ({
      storeName: String((item as any).storeName),
      entries: normalizeEntries((item as any).entries)
    }))
}

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 === PROJECT_INFO_DB_KEY) ||
    entries.find(item => item.key === LEGACY_PROJECT_DB_KEY)
  const data = (target?.value || {}) as XmInfoLike
  return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目'
}

const toFiniteNumber = (value: unknown): number | null => {
  const num = Number(value)
  return Number.isFinite(num) ? num : null
}

const toFiniteNumberOrZero = (value: unknown): number => toFiniteNumber(value) ?? 0

const toSafeInteger = (value: unknown): number | null => {
  const num = Number(value)
  if (!Number.isInteger(num)) return null
  if (!Number.isSafeInteger(num)) return null
  return num
}

const sumNumbers = (values: Array<number | null | undefined>): number =>
  values.reduce<number>(
    (sum, value) => sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0),
    0
  )

const isNonEmptyString = (value: unknown): value is string =>
  typeof value === 'string' && value.trim().length > 0

const getTaskIdFromRowId = (value: string): number | null => {
  const match = /^task-(\d+)-\d+$/.exec(value)
  return match ? toSafeInteger(match[1]) : null
}

const getExpertIdFromRowId = (value: string): number | null => {
  const match = /^expert-(\d+)$/.exec(value)
  return match ? toSafeInteger(match[1]) : null
}

const hasServiceId = (serviceId: string) =>
  Object.prototype.hasOwnProperty.call(serviceList as Record<string, unknown>, serviceId)

const sortServiceIdsByDict = (ids: string[]) =>
  [...ids].sort((left, right) => {
    const leftOrder = Number((serviceList as Record<string, any>)[left]?.order)
    const rightOrder = Number((serviceList as Record<string, any>)[right]?.order)
    const safeLeft = Number.isFinite(leftOrder) ? leftOrder : Number.MAX_SAFE_INTEGER
    const safeRight = Number.isFinite(rightOrder) ? rightOrder : Number.MAX_SAFE_INTEGER
    return safeLeft - safeRight
  })

const mapIndustryCodeToExportIndustry = (value: unknown): number => {
  const raw = typeof value === 'string' ? value.trim().toUpperCase() : ''
  if (!raw) return 0
  if (raw === '0' || raw === 'E2') return 0
  if (raw === '1' || raw === 'E3') return 1
  if (raw === '2' || raw === 'E4') return 2
  return 0
}

const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined): ExportServiceCoe[] => {
  if (!Array.isArray(rows)) return []
  return rows
    .map(row => {
      const serviceid = toSafeInteger(row.id)
      if (serviceid == null || row.budgetValue == null) return null
      const coe = toFiniteNumber(row.budgetValue)
      return {
        serviceid,
        coe,
        remark: row.remark
      }
    })
    .filter((item): item is ExportServiceCoe => Boolean(item))
}

const buildProjectMajorCoes = (rows: FactorRowLike[] | undefined): ExportMajorCoe[] => {
  if (!Array.isArray(rows)) return []
  return rows
    .map(row => {
      const majorid = toSafeInteger(row.id)
      if (majorid == null || row.budgetValue == null) return null
      const coe = toFiniteNumber(row.budgetValue)
      return {
        majorid,
        coe,
        remark: row.remark
      }
    })
    .filter((item): item is ExportMajorCoe => Boolean(item))
}

const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] => {
  if (!Array.isArray(rows)) return []
  return rows
    .map(row => {

      if (row.id == null || (row.amount == null && row.landArea == null)) return null
      return {
        major: toSafeInteger(row.id),
        cost: row.amount,
        area: row.landArea
      }
    })
    .filter((item): item is ExportScaleRow => Boolean(item))
}

const sumLeafScaleCost = (rows: ScaleRowLike[] | undefined) => {
  if (!Array.isArray(rows)) return 0
  return sumNumbers(
    rows.map(row => {
      if (row?.isGroupRow === true) return null
      return toFiniteNumber(row?.amount)
    })
  )
}



const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
  if (!Array.isArray(rows)) return null
  let hasTotalValue = false
  const det = rows
    .map(row => {
      const major = toSafeInteger(row.id)
      if (major == null) return null
      const cost = toFiniteNumber(row.amount)
      const basicFee = toFiniteNumber(row.benchmarkBudget)
      const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
      const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
      const fee = toFiniteNumber(row.budgetFee)
      if (basicFee != null || fee != null) hasTotalValue = true
      const remark = typeof row.remark === 'string' ? row.remark : ''
      const hasValue =
        cost != null ||
        basicFee != null ||
        basicFeeBasic != null ||
        basicFeeOptional != null ||
        fee != null ||
        isNonEmptyString(remark)
      if (!hasValue) return null
      return {
        major,
        cost: cost ?? 0,
        basicFee: basicFee ?? 0,
        basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
        basicFee_basic: basicFeeBasic ?? 0,
        optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
        basicFee_optional: basicFeeOptional ?? 0,
        serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
        majorCoe: toFiniteNumberOrZero(row.majorFactor),
        processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
        proportion: toFiniteNumber(row.workRatio) ?? 1,
        fee: fee ?? 0,
        remark
      }
    })
    .filter((item): item is ExportMethod1Detail => Boolean(item))

  if (det.length === 0 || !hasTotalValue) return null
  return {
    cost: sumNumbers(det.map(item => item.cost)),
    basicFee: sumNumbers(det.map(item => item.basicFee)),
    basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)),
    basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)),
    fee: sumNumbers(det.map(item => item.fee)),
    det
  }
}

const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | null => {
  if (!Array.isArray(rows)) return null
  let hasTotalValue = false
  const det = rows
    .map(row => {
      const major = toSafeInteger(row.id)
      if (major == null) return null
      const area = toFiniteNumber(row.landArea)
      const basicFee = toFiniteNumber(row.benchmarkBudget)
      const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic)
      const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional)
      const fee = toFiniteNumber(row.budgetFee)
      if (basicFee != null || fee != null) hasTotalValue = true
      const remark = typeof row.remark === 'string' ? row.remark : ''
      const hasValue =
        area != null ||
        basicFee != null ||
        basicFeeBasic != null ||
        basicFeeOptional != null ||
        fee != null ||
        isNonEmptyString(remark)
      if (!hasValue) return null
      return {
        major,
        area: area ?? 0,
        basicFee: basicFee ?? 0,
        basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '',
        basicFee_basic: basicFeeBasic ?? 0,
        optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '',
        basicFee_optional: basicFeeOptional ?? 0,
        serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
        majorCoe: toFiniteNumberOrZero(row.majorFactor),
        processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
        proportion: toFiniteNumber(row.workRatio) ?? 1,
        fee: fee ?? 0,
        remark
      }
    })
    .filter((item): item is ExportMethod2Detail => Boolean(item))

  if (det.length === 0 || !hasTotalValue) return null
  return {
    area: sumNumbers(det.map(item => item.area)),
    basicFee: sumNumbers(det.map(item => item.basicFee)),
    basicFee_basic: sumNumbers(det.map(item => item.basicFee_basic)),
    basicFee_optional: sumNumbers(det.map(item => item.basicFee_optional)),
    fee: sumNumbers(det.map(item => item.fee)),
    det
  }
}

const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3 | null => {
  if (!Array.isArray(rows)) return null
  let hasTotalValue = false
  const det = rows
    .map(row => {
      const task = getTaskIdFromRowId(row.id)
      if (task == null || row.basicFee == null) return null
      const amount = toFiniteNumber(row.workload)
      const basicFee = toFiniteNumber(row.basicFee)
      const fee = toFiniteNumber(row.serviceFee)
      if (fee != null) hasTotalValue = true
      const remark = typeof row.remark === 'string' ? row.remark : ''
      const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
      if (!hasValue) return null
      return {
        task,
        price: toFiniteNumberOrZero(row.budgetAdoptedUnitPrice),
        amount: amount ?? 0,
        basicFee: basicFee ?? 0,
        serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
        fee: fee ?? 0,
        remark
      }
    })
    .filter((item): item is ExportMethod3Detail => Boolean(item))

  if (det.length === 0 || !hasTotalValue) return null
  return {
    basicFee: sumNumbers(det.map(item => item.basicFee)),
    fee: sumNumbers(det.map(item => item.fee)),
    det
  }
}

const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 | null => {
  if (!Array.isArray(rows)) return null
  let hasTotalValue = false
  const det = rows
    .map(row => {
      const expert = getExpertIdFromRowId(row.id)
      if (expert == null || row.serviceBudget == null) return null
      const personNum = toFiniteNumber(row.personnelCount)
      const workDay = toFiniteNumber(row.workdayCount)
      const fee = toFiniteNumber(row.serviceBudget)
      if (fee != null) hasTotalValue = true
      const remark = typeof row.remark === 'string' ? row.remark : ''
      const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
      if (!hasValue) return null
      return {
        expert,
        price: toFiniteNumberOrZero(row.adoptedBudgetUnitPrice),
        person_num: personNum ?? 0,
        work_day: workDay ?? 0,
        fee: fee ?? 0,
        remark
      }
    })
    .filter((item): item is ExportMethod4Detail => Boolean(item))

  if (det.length === 0 || !hasTotalValue) return null
  return {
    person_num: sumNumbers(det.map(item => item.person_num)),
    work_day: sumNumbers(det.map(item => item.work_day)),
    fee: sumNumbers(det.map(item => item.fee)),
    det
  }
}

const buildServiceFee = (
  row: ZxFwRowLike | null | undefined,
  method1: ExportMethod1 | null,
  method2: ExportMethod2 | null,
  method3: ExportMethod3 | null,
  method4: ExportMethod4 | null
) => {
  const subtotal = toFiniteNumber(row?.subtotal)
  if (subtotal != null) return subtotal

  const methodSum = sumNumbers([method1?.fee, method2?.fee, method3?.fee, method4?.fee])
  if (methodSum !== 0) return methodSum

  return sumNumbers([
    toFiniteNumber(row?.investScale),
    toFiniteNumber(row?.landScale),
    toFiniteNumber(row?.workload),
    toFiniteNumber(row?.hourly)
  ])
}

const createRichTextCode = (...parts: string[]): unknown => ({
  richText: parts
    .map(item => String(item || '').trim())
    .filter(Boolean)
    .map(text => ({ text }))
})

const buildMethod0 = (payload: RateMethodRowLike | null | undefined): ExportMethod0 | null => {
  const coe = toFiniteNumber(payload?.rate)
  const fee = toFiniteNumber(payload?.budgetFee)
  if (fee == null) return null
  return {
    coe: coe ?? 0,
    fee
  }
}

const buildMethod5 = (rows: QuantityMethodRowLike[] | undefined): ExportMethod5 | null => {
  if (!Array.isArray(rows) || rows.length === 0) return null
  const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
  const subtotalFee = toFiniteNumber(subtotalRow?.budgetFee)
  if (subtotalFee == null ) return null
  const det = rows
    .filter(row => String(row?.id || '') !== 'fee-subtotal-fixed')
    .map(row => {
      const quantity = toFiniteNumber(row.quantity)
      const unitPrice = toFiniteNumber(row.unitPrice)
      const fee = toFiniteNumber(row.budgetFee)
      const name = typeof row.feeItem === 'string' ? row.feeItem : ''
      const unit = typeof row.unit === 'string' ? row.unit : ''
      const remark = typeof row.remark === 'string' ? row.remark : ''

      if (row.budgetFee==null) return null
      return {
        name,
        unit,
        amount: quantity ,
        price: unitPrice ,
        fee: fee ,
        remark
      }
    })
    .filter((item): item is ExportMethod5Detail => Boolean(item))

  if (det.length === 0) return null
  return {
    fee: subtotalFee,
    det
  }
}

const normalizeHtFeeMainRows = (rows: HtFeeMainRowLike[] | undefined) => {
  if (!Array.isArray(rows)) return [] as Array<{ id: string; name: string }>
  return rows
    .map(row => {
      const id = String(row?.id || '').trim()
      if (!id) return null
      return {
        id,
        name: typeof row?.name === 'string' ? row.name : ''
      }
    })
    .filter((item): item is { id: string; name: string } => Boolean(item))
}

const loadHtFeeMethodsByRow = async (mainStorageKey: string, rowId: string) => {
  const [rateState, hourlyState, quantityState] = await Promise.all([
    zxFwPricingStore.loadHtFeeMethodState<RateMethodRowLike>(mainStorageKey, rowId, 'rate-fee'),
    zxFwPricingStore.loadHtFeeMethodState<DetailRowsStorageLike<HourlyMethodRowLike>>(mainStorageKey, rowId, 'hourly-fee'),
    zxFwPricingStore.loadHtFeeMethodState<DetailRowsStorageLike<QuantityMethodRowLike>>(mainStorageKey, rowId, 'quantity-unit-price-fee')
  ])
  const m0 = buildMethod0(rateState)


  const m4 = buildMethod4(Array.isArray(hourlyState?.detailRows) ? hourlyState?.detailRows : undefined)
  const m5 = buildMethod5(Array.isArray(quantityState?.detailRows) ? quantityState?.detailRows : undefined)
  if (!m0 && !m4 && !m5) return null
  return {
    fee: sumNumbers([m0?.fee, m4?.fee, m5?.fee]),
    m0,
    m4,
    m5
  }
}

const buildAdditionalDetailCode = (index: number, name: string): unknown => {
  const dictItem = additionalWorkList.find(item => String(item?.name || '').trim() === String(name || '').trim())
  if (dictItem?.code) return dictItem.code
  const useIndex = index
  const suffix = useIndex === 0 ? 'F' : useIndex === 1 ? 'X' : String(useIndex + 1)
  return createRichTextCode('C', suffix)
}

const buildAdditionalExport = async (contractId: string): Promise<ExportAdditional | null> => {
  const storageKey = `htExtraFee-${contractId}-additional-work`
  const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(storageKey)
  const rows = normalizeHtFeeMainRows(mainState?.detailRows)
  if (rows.length === 0) return null

  const det = (
    await Promise.all(
      rows.map(async (row, index) => {
        const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
        if (!methodPayload) return null
        const item: ExportAdditionalDetail = {
          id: index,
          code: buildAdditionalDetailCode(index, row.name),
          name: row.name,
          fee: methodPayload.fee
        }
        if (methodPayload.m0) item.m0 = methodPayload.m0
        if (methodPayload.m4) item.m4 = methodPayload.m4
        if (methodPayload.m5) item.m5 = methodPayload.m5
        return item
      })
    )
  ).filter((item): item is ExportAdditionalDetail => Boolean(item))

  if (det.length === 0) return null
  return {
    code: createRichTextCode('C', 'C'),
    name: '附加工作',
    fee: sumNumbers(det.map(item => item.fee)),
    det
  }
}

const buildReserveExport = async (contractId: string): Promise<ExportReserve | null> => {
  const storageKey = `htExtraFee-${contractId}-reserve`
  const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(storageKey)
  const rows = normalizeHtFeeMainRows(mainState?.detailRows)
  if (rows.length === 0) return null

  for (const row of rows) {
    const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
    if (!methodPayload) continue
    const reserve: ExportReserve = {
      code: reserveList[0]?.code || createRichTextCode('Y', 'B'),
      name: row.name || '预备费',
      fee: methodPayload.fee
    }
    if (methodPayload.m0) reserve.m0 = methodPayload.m0
    if (methodPayload.m4) reserve.m4 = methodPayload.m4
    if (methodPayload.m5) reserve.m5 = methodPayload.m5
    return reserve
  }
  return null
}

const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
  const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([
    kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
    kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(CONSULT_CATEGORY_FACTOR_DB_KEY),
    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(MAJOR_FACTOR_DB_KEY),
    kvStore.getItem<ContractCardItem[]>('ht-card-v1')
  ])

  const projectInfo = projectInfoRaw || {}
  const projectScaleSource = projectScaleRaw || {}
  const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
  const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumLeafScaleCost(projectScaleSource.detailRows)
  projectScale.push({
    major: -1, cost: projectScaleCost,
    area: null
  })

  const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows)
  const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows)
  const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目'
  const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
  const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
  const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
  const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)

  const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : [])
    .filter(item => item && typeof item.id === 'string')
    .sort((a, b) => (typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER) - (typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER))

  const contracts: ExportContract[] = []

  for (let index = 0; index < contractCards.length; index++) {
    const contract = contractCards[index]
    const contractId = contract.id
    await zxFwPricingStore.loadContract(contractId)
    const [htInfoRaw, zxFwRaw, htConsultCategoryFactorRaw, htMajorFactorRaw] = await Promise.all([
      kvStore.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
      kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`),
      kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-consult-category-factor-v1-${contractId}`),
      kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-major-factor-v1-${contractId}`)
    ])

    const contractState = zxFwPricingStore.getContractState(contractId)
    const zxRowsFromStore: ZxFwRowLike[] = Array.isArray(contractState?.detailRows)
      ? contractState.detailRows.map(row => ({
        id: String(row.id || ''),
        process: row.process,
        subtotal: row.subtotal,
        investScale: row.investScale,
        landScale: row.landScale,
        workload: row.workload,
        hourly: row.hourly
      }))
      : []
    const zxRowsFromKv = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : []
    const zxRows = zxRowsFromStore.length > 0 ? zxRowsFromStore : zxRowsFromKv
    const selectedIdsFromStore = Array.isArray(contractState?.selectedIds)
      ? contractState.selectedIds.map(id => String(id || '').trim()).filter(Boolean)
      : []
    const selectedIdsFromKv = Array.isArray(zxFwRaw?.selectedIds)
      ? zxFwRaw.selectedIds.map(id => String(id || '').trim()).filter(Boolean)
      : []
    const selectedIds = Array.from(new Set([...selectedIdsFromStore, ...selectedIdsFromKv])).filter(hasServiceId)

    let fixedRow: ZxFwRowLike | undefined
    const serviceRowMap = new Map<string, ZxFwRowLike>()
    for (const row of zxRows) {
      const rowId = String(row?.id || '').trim()
      if (!rowId) continue
      if (rowId === 'fixed-budget-c') {
        fixedRow = row
        continue
      }
      if (!hasServiceId(rowId)) continue
      serviceRowMap.set(rowId, row)
    }
    const fallbackServiceIds = Array.from(serviceRowMap.keys())
    const serviceIdTexts = sortServiceIdsByDict(
      (selectedIds.length > 0 ? selectedIds : fallbackServiceIds).filter(hasServiceId)
    )

    const services = (
      await Promise.all(
        serviceIdTexts.map(async serviceIdText => {
          const serviceId = toSafeInteger(serviceIdText)
          if (serviceId == null) return null
          const sourceRow = serviceRowMap.get(serviceIdText)

          const [method1State, method2State, method3State, method4State] = await Promise.all([
            zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale'),
            zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'landScale'),
            zxFwPricingStore.loadServicePricingMethodState<WorkloadMethodRowLike>(contractId, serviceIdText, 'workload'),
            zxFwPricingStore.loadServicePricingMethodState<HourlyMethodRowLike>(contractId, serviceIdText, 'hourly')
          ])

          const method1Raw = method1State ? { detailRows: method1State.detailRows } : null
          const method2Raw = method2State ? { detailRows: method2State.detailRows } : null
          const method3Raw = method3State ? { detailRows: method3State.detailRows } : null
          const method4Raw = method4State ? { detailRows: method4State.detailRows } : null
          const method1 = buildMethod1(method1Raw?.detailRows)
          const method2 = buildMethod2(method2Raw?.detailRows)
          const method3 = buildMethod3(method3Raw?.detailRows)
          const method4 = buildMethod4(method4Raw?.detailRows)
          const fee = buildServiceFee(sourceRow, method1, method2, method3, method4)
          const process = Number(sourceRow?.process) === 1 ? 1 : 0
          const service: ExportService = {
            id: serviceId,
            process,
            fee
          }
          if (method1) service.method1 = method1
          if (method2) service.method2 = method2
          if (method3) service.method3 = method3
          if (method4) service.method4 = method4
          return service
        })
      )
    ).filter((item): item is ExportService => Boolean(item))

    const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal)
    const serviceFeeSum = sumNumbers(services.map(item => item.fee))
    const fixedMethodSum = sumNumbers([
      toFiniteNumber(fixedRow?.investScale),
      toFiniteNumber(fixedRow?.landScale),
      toFiniteNumber(fixedRow?.workload),
      toFiniteNumber(fixedRow?.hourly)
    ])
    const serviceFee = fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum)
    const [addtional, reserve] = await Promise.all([
      buildAdditionalExport(contractId),
      buildReserveExport(contractId)
    ])
    const addtionalFee = addtional ? addtional.fee : 0
    const reserveFee = reserve ? reserve.fee : 0
    const contractFee = roundTo(addNumbers(serviceFee, addtionalFee, reserveFee), 3)
    const contractScale = htInfoRaw?.roughCalcEnabled ? [] : toExportScaleRows(htInfoRaw?.detailRows)
    contractScale.push({
      major: -1, cost: contractFee,
      area: null
    })
    const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorRaw?.detailRows)
    const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorRaw?.detailRows)


    contracts.push({
      name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`,
      serviceFee,
      addtionalFee,
      reserveFee,
      fee: contractFee,
      scale: contractScale,
      serviceCoes: contractServiceCoesRaw,
      majorCoes: contractMajorCoesRaw,
      services,
      addtional,
      reserve
    })
  }

  return {
    name: projectName,
    writer,
    reviewer,
    date,
    industry,
    fee: sumNumbers(contracts.map(item => item.fee)),
    scaleCost: projectScaleCost,
    scale: projectScale,
    serviceCoes: projectServiceCoes,
    majorCoes: projectMajorCoes,
    contracts
  }
}

const exportData = async () => {
  try {
    // 先把 Pinia 内存态强制落盘，避免导出快照缺失附加工作费/预备费最新值。
    await flushPiniaPersistNow()
    const now = new Date()
    const piniaForageStores = await Promise.all(
      getPiniaPersistStores().map(async ({ storeName, store }) => ({
        storeName,
        entries: await readForage(store)
      }))
    )
    const payload: DataPackage = {
      version: 2,
      exportedAt: now.toISOString(),
      localStorage: readWebStorage(localStorage),
      sessionStorage: readWebStorage(sessionStorage),
      localforageDefault: await readForage(localforage),
      localforageStores: piniaForageStores
    }

    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 () => {
  try {
    showReportExportProgress(10, '正在准备报表导出...')
    const now = new Date()
    showReportExportProgress(40, '正在汇总报表数据...')
    const payload = await buildExportReportPayload()
    showReportExportProgress(80, '正在生成并写出报表文件...')
    const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
    console.log(payload)
    await exportFile(fileName, payload)
    finishReportExportProgress(true, '报表导出完成')
  } catch (error) {
    console.error('export report failed:', error)
    finishReportExportProgress(false, '报表导出失败，请重试')
  } finally {
    dataMenuOpen.value = false
  }
}
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)
    pendingImportPayload.value = payload
    pendingImportFileName.value = file.name
    importConfirmOpen.value = true
  } catch (error) {
    console.error('import failed:', error)
    window.alert('导入失败：文件无效、已损坏或被修改。')
  } finally {
    input.value = ''
  }
}

const cancelImportConfirm = () => {
  importConfirmOpen.value = false
  pendingImportPayload.value = null
  pendingImportFileName.value = ''
}

const confirmImportOverride = async () => {
  const payload = pendingImportPayload.value
  if (!payload) return
  try {
    writeWebStorage(localStorage, normalizeEntries(payload.localStorage))
    writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage))
    await writeForage(localforage, normalizeEntries(payload.localforageDefault))
    const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores)
    const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
    await Promise.all(
      getPiniaPersistStores().map(async ({ storeName, store }) => {
        const entries = snapshotMap.get(storeName) || []
        await writeForage(store, entries)
      })
    )

    const readPersistedState = (storeId: string) => {
      const storeName = getPiniaPersistStoreName(storeId)
      const entries = snapshotMap.get(storeName) || []
      const hit =
        entries.find(entry => entry.key === storeName) ||
        entries.find(entry => entry.key === storeId) ||
        entries.find(entry => entry.value && typeof entry.value === 'object')
      return hit && hit.value && typeof hit.value === 'object' ? hit.value : null
    }

    const tabsState = readPersistedState('tabs')
    if (tabsState) {
      tabStore.$patch(tabsState as any)
    } else {
      tabStore.resetTabs()
    }

    const zxFwPricingState = readPersistedState('zxFwPricing')
    if (zxFwPricingState) {
      zxFwPricingStore.$patch(zxFwPricingState as any)
    }

    const kvState = readPersistedState('kv')
    if (kvState) {
      kvStore.$patch(kvState as any)
    }

    await Promise.all([
      tabStore.$persistNow?.(),
      zxFwPricingStore.$persistNow?.(),
      kvStore.$persistNow?.()
    ])
    dataMenuOpen.value = false
    window.location.reload()
  } catch (error) {
    console.error('import apply failed:', error)
    window.alert('导入失败：写入本地数据时发生错误。')
  } finally {
    cancelImportConfirm()
  }
}

const handleReset = async () => {
  try {
    localStorage.clear()
    sessionStorage.clear()
    await localforage.clear()
    await Promise.all(
      getPiniaPersistStores().map(async ({ store }) => {
        await store.clear()
      })
    )
    localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
  } catch (error) {
    console.error('reset failed:', error)
  } finally {
    tabStore.resetTabs()
    await tabStore.$persistNow?.()
    window.location.reload()
  }
}

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

onBeforeUnmount(() => {
  clearReportExportToastTimer()
  window.removeEventListener('mousedown', handleGlobalMouseDown)
  window.removeEventListener('keydown', handleGlobalKeyDown)
  window.removeEventListener('resize', scheduleUpdateTabTitleOverflow)
  if (tabStripViewportEl) {
    tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll)
    tabStripViewportEl = null
  }
  if (tabTitleOverflowRafId != null) {
    cancelAnimationFrame(tabTitleOverflowRafId)
    tabTitleOverflowRafId = null
  }
})

watch(
  () => tabStore.activeTabId,
  (nextId, prevId) => {
    saveTabInnerScrollTop(prevId)
    void nextTick(() => {
      bindTabStripScroll()
      ensureActiveTabVisible()
      updateTabScrollButtons()
      scheduleUpdateTabTitleOverflow()
      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)
    }
    void nextTick(() => {
      bindTabStripScroll()
      updateTabScrollButtons()
      scheduleUpdateTabTitleOverflow()
    })
  }
)
</script>

<template>
  <ToastProvider>
  <TooltipProvider>
    <div class="flex flex-col w-full h-screen bg-background overflow-hidden">
      <div class="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2 border-b bg-muted/30 px-2 pt-1 h-15 flex-none">
        <div class="flex min-w-0 items-start gap-1 h-full" @mouseenter="isTabStripHover = true"
          @mouseleave="isTabStripHover = false">
          <button type="button" :class="[
            'h-9 w-8  self-center  shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted',
            isTabStripHover && showTabScrollLeft ? 'opacity-100' : 'pointer-events-none opacity-0'
          ]" @click="scrollTabStripBy(-260)">
            &lt;
          </button>
          <ScrollArea :ref="setTabScrollAreaRef" type="auto"
            class="h-full tab-strip-scroll-area min-w-0 flex-1 whitespace-nowrap">
            <draggable v-model="tabsModel" item-key="id" tag="div"
              :class="['tab-strip-sortable h-[calc(3.50rem)] flex w-max gap-0', isTabDragging ? 'is-dragging' : '']"
              :animation="260" easing="cubic-bezier(0.22, 1, 0.36, 1)" ghost-class="tab-drag-ghost"
              chosen-class="tab-drag-chosen" drag-class="tab-drag-active" :move="canMoveTab" @start="handleTabDragStart"
              @end="handleTabDragEnd">
              <template #item="{ element: tab }">
                <div :ref="el => setTabItemRef(tab.id, el)" @mousedown.left="tabStore.activeTabId = tab.id"
                  @contextmenu.prevent="openTabContextMenu($event, tab.id)" :class="[
                    'tab-item group relative -mb-px -ml-px first:ml-0 flex items-center h-full px-4 min-w-[120px] max-w-[220px] cursor-pointer rounded-t-md border border-transparent transition-[background-color,border-color,color,box-shadow,transform] duration-200 text-sm',
                    tabStore.activeTabId === tab.id && !isTabDragging
                      ? 'z-10 bg-background text-foreground !border-border !border-b-0 font-medium'
                      : 'bg-muted/25 text-muted-foreground hover:bg-muted/40 hover:text-foreground hover:border-border/70',
                    tab.id !== 'XmView' ? 'cursor-move' : ''
                  ]">
                  <TooltipRoot>
                    <TooltipTrigger as-child>
                      <span :ref="el => setTabTitleRef(tab.id, el)" class="truncate mr-2">
                        {{ tab.title }}
                      </span>
                    </TooltipTrigger>
                    <TooltipContent v-if="tabTitleOverflowMap[tab.id]" side="bottom">{{ tab.title }}</TooltipContent>
                  </TooltipRoot>

                  <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>
          </ScrollArea>
          <button type="button" :class="[
            ' self-center  h-9 w-8 shrink-0 cursor-pointer rounded border bg-background text-sm text-muted-foreground transition hover:bg-muted ',
            isTabStripHover && showTabScrollRight ? 'opacity-100' : 'pointer-events-none opacity-0'
          ]" @click="scrollTabStripBy(260)">
            &gt;
          </button>
        </div>
        <div class="flex shrink-0 self-center items-center gap-1">
          <div ref="dataMenuRef" class="relative shrink-0">
            <Button variant="outline" size="sm"
              class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none 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  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="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer"
            @click="openUserGuide(0)">
            <CircleHelp class="h-4 w-4 mr-1" />
            使用引导
          </Button>

          <AlertDialogRoot>
            <AlertDialogTrigger as-child>
              <Button variant="destructive" size="sm"
                class="h-9 min-h-9 shrink-0 px-3 py-0 text-sm leading-none cursor-pointer">
                <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>

          <AlertDialogRoot :open="importConfirmOpen" @update:open="importConfirmOpen = $event">
            <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">
                  将使用“{{ pendingImportFileName || '所选文件' }}”覆盖当前本地全部数据，是否继续？
                </AlertDialogDescription>
                <div class="mt-4 flex items-center justify-end gap-2">
                  <AlertDialogCancel as-child>
                    <Button variant="outline" @click="cancelImportConfirm">取消</Button>
                  </AlertDialogCancel>
                  <AlertDialogAction as-child>
                    <Button variant="destructive" @click="confirmImportOverride">确认覆盖</Button>
                  </AlertDialogAction>
                </div>
              </AlertDialogContent>
            </AlertDialogPortal>
          </AlertDialogRoot>
        </div>

      </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-4 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>

      <ToastRoot
        :open="reportExportToastOpen"
        :duration="0"
        class="pointer-events-auto rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
      >
        <ToastTitle class="text-sm font-semibold text-foreground">
          {{ reportExportStatus === 'running' ? '导出报表' : (reportExportStatus === 'success' ? '导出成功' : '导出失败') }}
        </ToastTitle>
        <ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription>
        <div class="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted">
          <div
            class="h-full transition-all duration-300"
            :class="reportExportStatus === 'error'
              ? 'bg-red-500'
              : (reportExportStatus === 'success' ? 'bg-foreground/70' : 'bg-foreground')"
            :style="{ width: `${reportExportProgress}%` }"
          />
        </div>
      </ToastRoot>
      <ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none" />
    </div>
  </TooltipProvider>
  </ToastProvider>
</template>

<style scoped>
.tab-strip-sortable>.tab-item {
  transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}

.tab-strip-sortable.is-dragging>.tab-item {
  will-change: transform;
}

.tab-drag-ghost {
  opacity: 0.32;
}

.tab-drag-chosen {
  transform: scale(1.015);
  box-shadow: 0 10px 24px rgb(0 0 0 / 18%);
}

.tab-drag-active {
  cursor: grabbing;
}

.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]) {
  scrollbar-width: none;
  overflow-y: hidden !important;
}

.tab-strip-scroll-area :deep([data-slot="scroll-area-viewport"]::-webkit-scrollbar) {
  display: none;
}

.tab-strip-scroll-area :deep([data-slot="scroll-area-scrollbar"][data-orientation="vertical"]),
.tab-strip-scroll-area :deep([data-slot="scroll-area-corner"]) {
  display: none !important;
}
</style>
