<script setup lang="ts">
import {
  computed,
  defineAsyncComponent,
  markRaw,
  nextTick,
  onBeforeUnmount,
  onMounted,
  ref,
  shallowRef,
  watch
} from 'vue'
import type {ComponentPublicInstance} from 'vue'
import {useI18n} from 'vue-i18n'
import draggable from 'vuedraggable'
import {useDark, useToggle} from '@vueuse/core'
import {useTabStore} from '@/pinia/tab'
import {useZxFwPricingStore} from '@/pinia/zxFwPricing'
import {useZxFwPricingKeysStore} from '@/pinia/zxFwPricingKeys'
import {useZxFwPricingHtFeeStore} from '@/pinia/zxFwPricingHtFee'
import {useKvStore} from '@/pinia/kv'
import {UI_PREFS_STORAGE_KEY, useUiPrefsStore} from '@/pinia/uiPrefs'
import {Button} from '@/components/ui/button'
import {ScrollArea} from '@/components/ui/scroll-area'
import {TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger} from '@/components/ui/tooltip'
import {Check, ChevronDown, CircleHelp, Loader2, Moon, Sun, X} from 'lucide-vue-next'
import localforage from 'localforage'
import {
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogRoot,
  AlertDialogTitle,
  ToastDescription,
  ToastProvider,
  ToastRoot,
  ToastTitle,
  ToastViewport,
  SelectContent,
  SelectIcon,
  SelectItem,
  SelectItemIndicator,
  SelectItemText,
  SelectPortal,
  SelectRoot,
  SelectTrigger,
  SelectViewport
} from 'reka-ui'
import {decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION} from '@/lib/zwArchive'
import {formatExportTimestamp} from '@/lib/contractSegment'
import {
  getExportProjectName,
  isDataPackageLike,
  normalizeEntries,
  normalizeForageStoreSnapshots,
  readForage,
  sanitizeFileNamePart,
  writeForage,
  type DataPackage,
  type ForageInstance,
  type ForageStore
} from '@/features/tab/importExport'
import type {
  ContractCardItem,
  DetailRowsStorageLike,
  ExportAdditional,
  ExportAdditionalDetail,
  ExportContract,
  ExportMajorCoe,
  ExportMethod0,
  ExportMethod1,
  ExportMethod1Detail,
  ExportMethod2,
  ExportMethod2Detail,
  ExportMethod3,
  ExportMethod3Detail,
  ExportMethod4,
  ExportMethod4Detail,
  ExportMethod5,
  ExportMethod5Detail,
  ExportReportPayload,
  ExportReserve,
  ExportScaleRow,
  ExportService,
  ExportServiceCoe,
  ExportTaskGroup,
  FactorRowLike,
  HourlyMethodRowLike,
  HtBaseInfoLike,
  HtFeeMainRowLike,
  QuantityMethodRowLike,
  RateMethodRowLike,
  ScaleMethodRowLike,
  ScaleRowLike,
  UserGuideStep,
  WorkContentStateLike,
  WorkloadMethodRowLike,
  XmInfoLike,
  XmInfoStorageLike,
  XmScaleStorageLike,
  ZxFwRowLike,
  ZxFwStorageLike
} from '@/features/tab/types'
import {
  buildProjectUrl,
  getProjectDbName,
  readCurrentProjectId,
  PROJECT_TAB_ID,
  QUICK_TAB_ID,
  consumePendingHomeImportFile,
  consumePendingHomeImportSkipConfirm,
  readWorkspaceMode,
  writeProjectIdToUrl,
  writeWorkspaceMode
} from '@/lib/workspace'
import {
  createProject,
  deleteProject,
  listProjects,
  upsertProject,
  type ProjectMeta
} from '@/lib/projectRegistry'
import {emitProjectDeleted, emitResetAll} from '@/lib/projectEvents'
import {collectActiveProjectSessionLocks} from '@/lib/projectSessionLock'
import {addNumbers} from '@/lib/decimal'
import {
  buildMethod0,
  buildMethod1,
  buildMethod2,
  buildMethod3,
  buildMethod4,
  buildMethod5,
  buildProjectMajorCoes,
  buildProjectServiceCoes,
  buildServiceFee,
  buildServiceFinalFee,
  getExpertIdFromRowId,
  getTaskIdFromRowId,
  groupWorkContentTasks,
  hasServiceId,
  isNonEmptyString,
  mapIndustryCodeToExportIndustry,
  sortServiceIdsByDict,
  sumNumbers,
  toExportScaleRows,
  toFiniteNumber,
  toFiniteNumberOrZero,
  toSafeInteger,
  toMoney
} from '@/lib/reportExportBuilders'
import {exportFile} from '@/sql'
import {
  getAdditionalWorkListEntries,
  getIndustryDisplayName,
  getReserveListEntries,
  getServiceDictItemById,
  industryTypeList
} from '@/sql'
import {initializeProjectFactorStates, initializeProjectScaleState} from '@/lib/projectWorkspace'
import {createProjectKvAdapter} from '@/lib/projectKvStore'
import {I18N_LOCALE_KEY, type AppLocale} from '@/i18n'
import ExcelJS from 'exceljs';
import {useDataStore} from "@/pinia/zx";
import type {ContractItem} from '@/features/ht/contracts';
import PizZip from 'pizzip';
import Docxtemplater from 'docxtemplater';
import { saveAs } from 'file-saver';
// 1. 定义合同列表响应式变量
const contractList = ref<ContractItem[]>([])

// 2. 加载合同列表的方法
const loadContractList = async () => {
  try {
    const saved = await kvStore.getItem<ContractItem[]>('ht-card-v1')
    if (saved && saved.length > 0) {
      contractList.value = [...saved].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
    } else {
      contractList.value = []
    }
  } catch (error) {
    console.error('load contract list failed:', error)
    contractList.value = []
  }
}


const {t, locale} = useI18n()

const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1'
const THEME_PREFERENCE_KEY = 'jgjs-theme-dark-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 DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName')
const MAX_PROJECT_COUNT = 10
const PINIA_PERSIST_DB_NAME = getProjectDbName(readCurrentProjectId())
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'zxFwPricingKeys', 'zxFwPricingHtFee', 'kv'] as const
const RESET_MIN_LOADING_MS = 1000
const userGuideSteps = computed<UserGuideStep[]>(() => {
  const stepKeys = ['step1', 'step2', 'step3', 'step4', 'step5', 'step6', 'step7', 'step8']
  return stepKeys.map(step => ({
    title: t(`tab.guide.steps.${step}.title`),
    description: t(`tab.guide.steps.${step}.description`),
    points: [
      t(`tab.guide.steps.${step}.point1`),
      t(`tab.guide.steps.${step}.point2`),
      t(`tab.guide.steps.${step}.point3`)
    ]
  }))
})

const componentMap: Record<string, any> = {
  ProjectCalcView: markRaw(defineAsyncComponent(() => import('@/features/xm/components/xmCard.vue'))),
  QuickCalcView: markRaw(defineAsyncComponent(() => import('@/features/ht/components/htCard.vue'))),
  QuickCalcWorkbenchView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/QuickCalcWorkbenchView.vue'))),
  ZxFwView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/ZxFwView.vue'))),
  HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/features/workbench/components/HtFeeMethodTypeLineView.vue'))),
}

const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const zxFwPricingKeysStore = useZxFwPricingKeysStore()
const zxFwPricingHtFeeStore = useZxFwPricingHtFeeStore()
const kvStore = useKvStore()
const uiPrefsStore = useUiPrefsStore()
const isDark = useDark({
  selector: 'html',
  attribute: 'class',
  valueDark: 'dark',
  valueLight: '',
  storageKey: 'jgjs-theme-dark-v1'
})
const toggleDark = useToggle(isDark)


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

const dataMenuOpen = ref(false)
const dataMenuRef = ref<HTMLElement | null>(null)
const projectMenuOpen = ref(false)
const projectMenuRef = ref<HTMLElement | null>(null)
const newProjectDialogOpen = ref(false)
const newProjectIndustry = ref(String(industryTypeList[0]?.id || ''))
const newProjectSubmitting = ref(false)
const projectLimitDialogOpen = ref(false)
const projectDeleteConfirmOpen = ref(false)
const pendingDeleteProject = shallowRef<ProjectMeta | null>(null)
const messageDialogOpen = ref(false)
const messageDialogTitle = ref('')
const messageDialogDesc = ref('')
const projectList = ref<ProjectMeta[]>([])
const openedProjectIds = ref<string[]>([])
const currentProjectId = ref(readCurrentProjectId())
const resetConfirmOpen = ref(false)
const isResetting = ref(false)
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 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

const reportExportToastOpen = ref(false)
const reportExportProgress = ref(0)
const reportExportStatus = ref<'running' | 'success' | 'error'>('running')
const reportExportText = ref('')
const reportExportBlobUrl = ref<string | null>(null)
let reportExportToastTimer: ReturnType<typeof setTimeout> | null = null

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, blobUrl?: string | null) => {
  clearReportExportToastTimer()
  reportExportStatus.value = success ? 'success' : 'error'
  reportExportProgress.value = 100
  reportExportText.value = text
  reportExportBlobUrl.value = success && blobUrl ? blobUrl : null
  reportExportToastOpen.value = true
  reportExportToastTimer = setTimeout(() => {
    reportExportToastOpen.value = false
  }, success ? 2000 : 1800)
}

const openExportedReport = () => {
  if (!reportExportBlobUrl.value) return
  window.open(reportExportBlobUrl.value, '_blank')
  reportExportToastOpen.value = false
}

const dismissReportToast = () => {
  if (reportExportBlobUrl.value) {
    URL.revokeObjectURL(reportExportBlobUrl.value)
    reportExportBlobUrl.value = null
  }
  reportExportToastOpen.value = false
}

const tabsModel = computed({
  get: () => tabStore.tabs,
  set: (value) => {
    tabStore.tabs = value
  }
})
const activeTab = computed(() => tabStore.tabs.find((t: any) => t.id === tabStore.activeTabId) || null)
const activeTabId = computed(() => (activeTab.value ? String(activeTab.value.id || '') : ''))
const resetUiWorkspaceMode = ref<string>('')
const quickModeThemePreferenceSnapshot = ref<string | null | undefined>(undefined)
const workspaceModeForUi = computed(() =>
    isResetting.value && resetUiWorkspaceMode.value
        ? resetUiWorkspaceMode.value
        : readWorkspaceMode()
)

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

const hasClosableTabs = computed(() => {
  const fixedId = workspaceModeForUi.value === 'quick' ? QUICK_TAB_ID : PROJECT_TAB_ID

  return tabStore.tabs.some((t: any) => t.componentName !== fixedId)
})
const activeGuideStep = computed(
    () => userGuideSteps.value[userGuideStepIndex.value] || userGuideSteps.value[0]
)
const isFirstGuideStep = computed(() => userGuideStepIndex.value === 0)
const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideSteps.value.length - 1)
const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.value.length}`)
const canCloseLeft = computed(() => {
  if (contextTabIndex.value <= 0) return false
  return tabStore.tabs.slice(1, contextTabIndex.value).length > 0
})
const canCloseRight = computed(() => {
  if (contextTabIndex.value < 0) return false
  return tabStore.tabs.slice(contextTabIndex.value + 1).length > 0
})
const canCloseOther = computed(() =>
    tabStore.tabs.slice(1, contextTabIndex.value).length > 1 && contextTabIndex.value !== 0
)
const localeLabel = computed(() => (locale.value === 'en-US' ? 'EN' : '中'))
const projectCountText = computed(() => t('tab.toolbar.projectCount', {count: `${projectList.value.length}/${MAX_PROJECT_COUNT}`}))
const newProjectIndustryLabel = computed(() => {
  const target = String(newProjectIndustry.value || '').trim()
  if (!target) return ''
  return getIndustryDisplayName(target, locale.value) || ''
})

const toggleLocale = () => {
  const nextLocale: AppLocale = locale.value === 'en-US' ? 'zh-CN' : 'en-US'
  uiPrefsStore.setLocale(nextLocale)
}

const resolveHtFeeMethodRowNameByDict = (
    storageKeyRaw: string,
    rowIdRaw: string,
    fallbackRaw: string
) => {
  const storageKey = String(storageKeyRaw || '').trim()
  const rowId = String(rowIdRaw || '').trim()
  const fallback = String(fallbackRaw || '').trim()
  if (!storageKey || !rowId) return fallback
  if (storageKey.includes('-additional-work')) {
    const item = getAdditionalWorkListEntries(locale.value).find(entry => String(entry?.id || '').trim() === rowId)
    return String(item?.name || '').trim() || fallback
  }
  if (storageKey.includes('-reserve')) {
    const item = getReserveListEntries(locale.value).find(entry => String(entry?.id || '').trim() === rowId)
    return String(item?.name || '').trim() || fallback
  }
  return fallback
}

const syncTabLabelsByLocale = () => {
  const nextProjectTitle = t('home.projectCalcTab')
  const nextQuickTitle = t('home.quickCalcTab')
  let changed = false
  tabStore.tabs = tabStore.tabs.map((tab: any) => {
    const currentProps = tab?.props && typeof tab.props === 'object' ? {...tab.props} : undefined
    if (tab.id === PROJECT_TAB_ID) {
      if (tab.title === nextProjectTitle) return tab
      changed = true
      return {...tab, title: nextProjectTitle}
    }
    if (tab.id === QUICK_TAB_ID) {
      if (tab.title === nextQuickTitle) return tab
      changed = true
      return {...tab, title: nextQuickTitle}
    }
    if (tab.componentName === 'QuickCalcView') {
      const contractName = String(currentProps?.contractName || '').trim()
      const nextTitle = t('ht.contractTabTitle', {name: contractName || '-'})
      if (tab.title === nextTitle) return tab
      changed = true
      return {...tab, title: nextTitle}
    }
    if (tab.componentName === 'ZxFwView') {
      const serviceId = String(currentProps?.serviceId || '').trim()
      const dict = serviceId ? (getServiceDictItemById(serviceId) as { code?: string; name?: string } | undefined) : undefined
      const serviceName = dict
          ? `${String(dict.code || '').trim()}${String(dict.name || '').trim()}`
          : String(currentProps?.fwName || '').trim()
      const nextTitle = t('htZxFw.editTabTitle', {name: serviceName || '-'})
      const nextProps = {
        ...(currentProps || {}),
        fwName: serviceName || String(currentProps?.fwName || '')
      }
      if (tab.title === nextTitle && String(currentProps?.fwName || '') === String(nextProps.fwName || '')) return tab
      changed = true
      return {...tab, title: nextTitle, props: nextProps}
    }
    if (tab.componentName === 'HtFeeMethodTypeLineView') {
      const storageKey = String(currentProps?.storageKey || '').trim()
      const rowId = String(currentProps?.rowId || '').trim()
      const nextSourceTitle = storageKey.includes('-reserve')
          ? t('htSummary.reservePrefix')
          : storageKey.includes('-additional-work')
              ? t('htSummary.additionalPrefix')
              : String(currentProps?.sourceTitle || '')
      const rows = storageKey
          ? (zxFwPricingStore.getHtFeeMainState<any>(storageKey)?.detailRows || [])
          : []
      const fromStoreName = Array.isArray(rows)
          ? String(rows.find((row: any) => String(row?.id || '') === rowId)?.name || '').trim()
          : ''
      const rowName = resolveHtFeeMethodRowNameByDict(
          storageKey,
          rowId,
          fromStoreName || String(currentProps?.rowName || '').trim()
      )
      const nextTitle = t('htFeeGrid.editTabTitle', {name: rowName || t('htFeeGrid.unnamed')})
      const nextProps = {
        ...(currentProps || {}),
        sourceTitle: nextSourceTitle,
        rowName
      }
      if (
          tab.title === nextTitle
          && String(currentProps?.rowName || '') === String(nextProps.rowName || '')
          && String(currentProps?.sourceTitle || '') === String(nextProps.sourceTitle || '')
      ) return tab
      changed = true
      return {...tab, title: nextTitle, props: nextProps}
    }
    return tab
  })
  if (!changed) return
  scheduleUpdateTabTitleOverflow()
}

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

const refreshProjectList = async () => {
  const baseProjects = listProjects()
  openedProjectIds.value = Array.from(collectActiveProjectSessionLocks(baseProjects.map(item => item.id)))
  const enriched = await Promise.all(
      baseProjects.map(async (project) => {
        try {
          const kvStoreInstance = localforage.createInstance({
            name: getProjectDbName(project.id),
            storeName: 'pinia-kv'
          })
          const kvState = await kvStoreInstance.getItem<any>('pinia-kv')
          const entries = kvState?.entries && typeof kvState.entries === 'object' ? kvState.entries : null
          const projectInfo = entries?.[PROJECT_INFO_DB_KEY]
          const projectName =
              projectInfo && typeof projectInfo === 'object' && typeof projectInfo.projectName === 'string'
                  ? projectInfo.projectName.trim()
                  : ''
          return {
            ...project,
            name: projectName || project.name
          }
        } catch {
          return project
        }
      })
  )
  projectList.value = enriched
}

const isProjectOpen = (projectId: string) => openedProjectIds.value.includes(projectId)

const openProjectInNewTab = (projectId: string) => {
  if (isProjectOpen(projectId)) return
  const href = buildProjectUrl(projectId)
  window.open(href, '_blank', 'noopener')
  projectMenuOpen.value = false
}

const getTodayDateString = () => {
  const now = new Date()
  const year = String(now.getFullYear())
  const month = String(now.getMonth() + 1).padStart(2, '0')
  const day = String(now.getDate()).padStart(2, '0')
  return `${year}-${month}-${day}`
}

const openCreateProjectDialog = () => {
  if (listProjects().length >= MAX_PROJECT_COUNT) {
    projectMenuOpen.value = false
    projectLimitDialogOpen.value = true
    return
  }
  projectMenuOpen.value = false
  newProjectIndustry.value = String(industryTypeList[0]?.id || '')
  newProjectDialogOpen.value = true
}

const closeCreateProjectDialog = () => {
  if (newProjectSubmitting.value) return
  newProjectDialogOpen.value = false
}

const createProjectAndOpen = async () => {
  if (newProjectSubmitting.value) return
  if (listProjects().length >= MAX_PROJECT_COUNT) {
    projectMenuOpen.value = false
    newProjectDialogOpen.value = false
    projectLimitDialogOpen.value = true
    return
  }
  const industry = String(newProjectIndustry.value || '').trim()
  if (!industry) return
  newProjectSubmitting.value = true
  try {
    const project = createProject(DEFAULT_PROJECT_NAME)
    const kvAdapter = createProjectKvAdapter(project.id)
    await kvAdapter.setItem(PROJECT_INFO_DB_KEY, {
      projectIndustry: industry,
      projectName: DEFAULT_PROJECT_NAME,
      preparedBy: '',
      reviewedBy: '',
      preparedCompany: '',
      preparedDate: getTodayDateString()
    })
    await initializeProjectFactorStates(
        kvAdapter,
        industry,
        CONSULT_CATEGORY_FACTOR_DB_KEY,
        MAJOR_FACTOR_DB_KEY
    )
    await initializeProjectScaleState(kvAdapter, industry, LEGACY_PROJECT_DB_KEY)
    void refreshProjectList()
    const href = buildProjectUrl(project.id)
    window.open(href, '_blank', 'noopener')
    newProjectDialogOpen.value = false
  } finally {
    newProjectSubmitting.value = false
  }
}

const handleCreateProjectDialogOpenChange = (open: boolean) => {
  if (newProjectSubmitting.value) return
  newProjectDialogOpen.value = open
}

const handleCreateProjectConfirm = () => {
  void createProjectAndOpen()
  projectMenuOpen.value = false
}

const returnToHome = () => {
  projectMenuOpen.value = false
  const href = buildProjectUrl(currentProjectId.value || readCurrentProjectId(), {
    forceHome: true
  })
  window.location.href = href
}

const formatProjectEditedTime = (value: string) => {
  const date = new Date(value)
  if (Number.isNaN(date.getTime())) return '-'
  const pad = (num: number) => String(num).padStart(2, '0')
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}

const removeProjectItem = async (project: ProjectMeta) => {
  const isCurrentProject = project.id === currentProjectId.value
  const deleteIndexedDBByName = (dbName: string) =>
      new Promise<void>((resolve) => {
        try {
          const request = window.indexedDB?.deleteDatabase(dbName)
          if (!request) {
            resolve()
            return
          }
          request.onsuccess = () => resolve()
          request.onerror = () => resolve()
          request.onblocked = () => resolve()
        } catch (_error) {
          resolve()
        }
      })
  const clearProjectPersistence = async () => {
    await projectDefaultForage.clear()
    await Promise.all(
        getPiniaPersistStores().map(async ({store}) => {
          await store.clear()
        })
    )
    const clearTasks: Promise<void>[] = []
    if (tabStore.$clearPersisted) clearTasks.push(tabStore.$clearPersisted())
    if (zxFwPricingStore.$clearPersisted) clearTasks.push(zxFwPricingStore.$clearPersisted())
    if (zxFwPricingKeysStore.$clearPersisted) clearTasks.push(zxFwPricingKeysStore.$clearPersisted())
    if (zxFwPricingHtFeeStore.$clearPersisted) clearTasks.push(zxFwPricingHtFeeStore.$clearPersisted())
    if (kvStore.$clearPersisted) clearTasks.push(kvStore.$clearPersisted())
    await Promise.all(clearTasks)
  }

  if (isCurrentProject) {
    window.dispatchEvent(new CustomEvent('jgjs-release-project-lock'))
    await clearProjectPersistence()
    await deleteIndexedDBByName(getProjectDbName(project.id))
    const removed = deleteProject(project.id)
    if (!removed) return
    emitProjectDeleted(project.id)
    writeWorkspaceMode('project')
    tabStore.resetTabs()
    tabStore.hasCompletedSetup = false
    window.location.href = buildProjectUrl('default', {forceHome: true})
    return
  }

  const removed = deleteProject(project.id)
  if (!removed) return
  emitProjectDeleted(project.id)
  try {
    await deleteIndexedDBByName(getProjectDbName(project.id))
  } catch (error) {
    console.error('delete project database failed:', error)
  }
  await refreshProjectList()
}

const requestRemoveProjectItem = (project: ProjectMeta) => {
  pendingDeleteProject.value = project
  projectDeleteConfirmOpen.value = true
}

const handleProjectDeleteDialogOpenChange = (open: boolean) => {
  projectDeleteConfirmOpen.value = open
}

const showMessageDialog = (title: string, description: string) => {
  messageDialogTitle.value = title
  messageDialogDesc.value = description
  messageDialogOpen.value = true
}

const handleToggleTheme = () => {
  toggleDark()
}

const cancelRemoveProjectItem = () => {
  projectDeleteConfirmOpen.value = false
  pendingDeleteProject.value = null
}

const confirmRemoveProjectItem = async () => {
  const project = pendingDeleteProject.value
  if (!project) return
  projectDeleteConfirmOpen.value = false
  pendingDeleteProject.value = null
  await removeProjectItem(project)
}

const switchToProject = (projectIdRaw: string) => {
  const projectId = String(projectIdRaw || '').trim()
  if (!projectId || projectId === currentProjectId.value) return
  upsertProject(projectId)
  writeWorkspaceMode('project')
  window.location.href = buildProjectUrl(projectId, {forceHome: false, newProject: 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 !== 'ProjectCalcView')
    const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : ''
    return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'ProjectCalcView')
  } 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.value.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.value.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
  }
  if (projectMenuOpen.value && projectMenuRef.value && !projectMenuRef.value.contains(target)) {
    projectMenuOpen.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 === tabStore.tabs[0]?.id) 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 createForageStore = (storeName: string): ForageInstance =>
    localforage.createInstance({
      name: PINIA_PERSIST_DB_NAME,
      storeName
    })

const projectDefaultForage = localforage.createInstance({
  name: PINIA_PERSIST_DB_NAME
})

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 loadFactorRowsState = async (storageKey: string) => {
  const [piniaData, kvData] = await Promise.all([
    zxFwPricingStore.loadKeyState<DetailRowsStorageLike<FactorRowLike>>(storageKey),
    kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(storageKey)
  ])
  return {
    piniaData,
    kvData,
    resolved: piniaData || kvData || null
  }
}

const resolveExportFactorRows = (
    primary: Awaited<ReturnType<typeof loadFactorRowsState>>,
    fallback?: Awaited<ReturnType<typeof loadFactorRowsState>>
) => {
  const primaryRows = Array.isArray(primary?.resolved?.detailRows) ? primary.resolved.detailRows : []
  if (primaryRows.length > 0) return primaryRows
  return Array.isArray(fallback?.resolved?.detailRows) ? fallback.resolved.detailRows : []
}

const resolveExportScaleState = <TState extends { detailRows?: unknown[]; roughCalcEnabled?: unknown; totalAmount?: unknown } | null | undefined>(
    primary: TState,
    fallback?: TState
) => {
  const primaryRows = Array.isArray(primary?.detailRows) ? primary.detailRows : []
  const primaryHasState =
      primaryRows.length > 0
      || primary?.roughCalcEnabled === true
      || (typeof primary?.totalAmount === 'number' && Number.isFinite(primary.totalAmount))
  if (primaryHasState) return primary
  return fallback ?? primary ?? null
}

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

const resolveMethodEnabled = (value: unknown, fallback: boolean) => {
  if (typeof value === 'boolean') return value
  if (typeof value === 'number') return value === 1
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase()
    if (normalized === 'true' || normalized === '1') return true
    if (normalized === 'false' || normalized === '0') return false
  }
  return fallback
}

const getServiceMethodAvailability = (serviceIdText: string) => {
  const dict = getServiceDictItemById(serviceIdText) as {
    scale?: boolean | null
    onlyCostScale?: boolean | null
    amount?: boolean | null
    workDay?: boolean | null
  } | null | undefined
  const scale = resolveMethodEnabled(dict?.scale, false)
  const onlyCostScale = resolveMethodEnabled(dict?.onlyCostScale, false)
  const amount = resolveMethodEnabled(dict?.amount, false)
  const workDay = resolveMethodEnabled(dict?.workDay, false)
  return {
    investmentScale: scale,
    landScale: scale && !onlyCostScale,
    workload: amount,
    hourly: workDay
  }
}

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

const getHtMainRowSubtotal = (row: { subtotal?: unknown } | null | undefined): number | null => {
  const subtotal = toFiniteNumber(row?.subtotal)
  if (subtotal == null) return null
  return toMoney(subtotal)
}

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 buildServiceTasks = async (
    contractId: string,
    serviceId: string
): Promise<ExportTaskGroup[]> => {
  const taskState = await zxFwPricingStore.loadKeyState<WorkContentStateLike>(`work-content-${contractId}-${serviceId}`)
  return groupWorkContentTasks(taskState?.detailRows)
}

const buildAdditionalRowTasks = async (contractId: string, rowId: string): Promise<ExportTaskGroup[]> => {
  const taskStorageKey = `work-content-htExtraFee-${contractId}-additional-work-${rowId}`
  const taskState = await zxFwPricingStore.loadKeyState<WorkContentStateLike>(taskStorageKey)
  return groupWorkContentTasks(taskState?.detailRows, {forceUngroup: true})
}

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 => {
            const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
            const rowSubtotal = getHtMainRowSubtotal(row)
            if (!methodPayload && rowSubtotal == null) return null
            const tasks = await buildAdditionalRowTasks(contractId, row.id)
            const item: ExportAdditionalDetail = {
              id: row.id,
              code: {
                richText: [{
                  font: {charset: 134, color: {theme: 1}, italic: true, name: '宋体', size: 10},
                  text: 'C'
                }, {
                  font: {
                    charset: 134,
                    color: {theme: 1},
                    italic: true,
                    name: 'Calibri',
                    size: 10,
                    vertAlign: 'subscript'
                  }, text: 'F'
                }]
              },

              name: row.name,
              fee: toMoney(rowSubtotal ?? methodPayload?.fee ?? 0),
              tasks
            }
            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: {
      richText: [{
        font: {charset: 134, color: {theme: 1}, italic: true, name: '宋体', size: 10},
        text: 'C'
      }, {
        font: {charset: 134, color: {theme: 1}, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript'},
        text: 'C'
      }]
    },

    name: t('htSummary.additionalPrefix'),
    fee: toMoney(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

  const reserveCodeTemplate = getReserveListEntries(locale.value)?.[0]?.code
      ?? {
        richText: [
          {font: {charset: 134, color: {theme: 1}, italic: true, name: '宋体', size: 10}, text: 'Y'},
          {
            font: {charset: 134, color: {theme: 1}, italic: true, name: 'Calibri', size: 10, vertAlign: 'subscript'},
            text: 'B'
          }
        ]
      }

  for (const row of rows) {
    const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
    const rowSubtotal = getHtMainRowSubtotal(row)
    if (!methodPayload && rowSubtotal == null) continue
    const reserve: ExportReserve = {
      code: reserveCodeTemplate,
      name: row.name || t('htSummary.reservePrefix'),
      fee: toMoney(rowSubtotal ?? methodPayload?.fee ?? 0),
      tasks: []
    }
    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, consultCategoryFactorState, majorFactorState, contractCardsRaw] = await Promise.all([
    kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
    kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
    loadFactorRowsState(CONSULT_CATEGORY_FACTOR_DB_KEY),
    loadFactorRowsState(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) ?? sumNumbers(projectScale.map(item => item.cost))


  const projectServiceCoes = buildProjectServiceCoes(resolveExportFactorRows(consultCategoryFactorState))
  const projectMajorCoes = buildProjectMajorCoes(resolveExportFactorRows(majorFactorState))
  const projectName = isNonEmptyString(projectInfo.projectName)
      ? projectInfo.projectName.trim()
      : t('tab.messages.defaultProjectName')
  const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : ''
  const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : ''
  const company = isNonEmptyString(projectInfo.preparedCompany) ? projectInfo.preparedCompany.trim() : ''
  const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : ''
  const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry)
  const overview = isNonEmptyString(projectInfo.overview) ? projectInfo.overview.trim() : ''
  const desc = isNonEmptyString(projectInfo.desc) ? projectInfo.desc.trim() : ''

  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, htConsultCategoryFactorState, htMajorFactorState, htBaseInfoRaw] = await Promise.all([
      kvStore.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
      kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`),
      loadFactorRowsState(`ht-consult-category-factor-v1-${contractId}`),
      loadFactorRowsState(`ht-major-factor-v1-${contractId}`),
      zxFwPricingStore.loadKeyState<HtBaseInfoLike>(`ht-base-info-${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,
          finalFee: row.finalFee,
          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 methodAvailability = getServiceMethodAvailability(serviceIdText)

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

              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 method2RawRows = Array.isArray(method2Raw?.detailRows) ? method2Raw.detailRows : []
              const method1 = methodAvailability.investmentScale ? buildMethod1(method1Raw?.detailRows) : null
              const method2 = methodAvailability.landScale ? buildMethod2(method2Raw?.detailRows) : null
              const method3 = methodAvailability.workload ? buildMethod3(method3Raw?.detailRows) : null
              const method4 = methodAvailability.hourly ? buildMethod4(method4Raw?.detailRows) : null

              if (methodAvailability.landScale && method2RawRows.length > 0 && method2?.det?.length != null) {
                const rawLen = method2RawRows.length
                const detLen = method2.det.length
                if (rawLen > detLen) {
                  console.warn('[export][landScale-duplicate-rows-deduped]', {
                    contractId,
                    serviceId: serviceIdText,
                    rawRows: rawLen,
                    exportedRows: detLen
                  })
                }
              }
              const sanitizedSourceRow = sourceRow
                  ? {
                    ...sourceRow,
                    investScale: methodAvailability.investmentScale ? sourceRow.investScale : null,
                    landScale: methodAvailability.landScale ? sourceRow.landScale : null,
                    workload: methodAvailability.workload ? sourceRow.workload : null,
                    hourly: methodAvailability.hourly ? sourceRow.hourly : null,
                    subtotal: sumNumbers([
                      methodAvailability.investmentScale ? toFiniteNumber(sourceRow.investScale) : 0,
                      methodAvailability.landScale ? toFiniteNumber(sourceRow.landScale) : 0,
                      methodAvailability.workload ? toFiniteNumber(sourceRow.workload) : 0,
                      methodAvailability.hourly ? toFiniteNumber(sourceRow.hourly) : 0
                    ])
                  }
                  : sourceRow
              const fee = buildServiceFee(sanitizedSourceRow, method1, method2, method3, method4)
              const finalFee = buildServiceFinalFee(sanitizedSourceRow, method1, method2, method3, method4)
              const tasks = await buildServiceTasks(contractId, serviceIdText)
              const process = Number(sourceRow?.process) === 1 ? 1 : 0

              const disabledMethodLeak = {
                investScale: !methodAvailability.investmentScale && (toFiniteNumber(sourceRow?.investScale) != null || Boolean(method1Raw)),
                landScale: !methodAvailability.landScale && (toFiniteNumber(sourceRow?.landScale) != null || Boolean(method2Raw)),
                workload: !methodAvailability.workload && (toFiniteNumber(sourceRow?.workload) != null || Boolean(method3Raw)),
                hourly: !methodAvailability.hourly && (toFiniteNumber(sourceRow?.hourly) != null || Boolean(method4Raw))
              }
              if (disabledMethodLeak.investScale || disabledMethodLeak.landScale || disabledMethodLeak.workload || disabledMethodLeak.hourly) {
                console.warn('[export][method-disabled-leak-detected]', {
                  contractId,
                  serviceId: serviceIdText,
                  methodAvailability,
                  sourceRow,
                  hasMethodPayload: {
                    method1: Boolean(method1Raw),
                    method2: Boolean(method2Raw),
                    method3: Boolean(method3Raw),
                    method4: Boolean(method4Raw)
                  },
                  disabledMethodLeak
                })
              }

              console.log('[export][service-methods]', {
                contractId,
                serviceId: serviceIdText,
                methodAvailability,
                exported: {
                  method1: Boolean(method1),
                  method2: Boolean(method2),
                  method3: Boolean(method3),
                  method4: Boolean(method4)
                },
                fee,
                finalFee
              })

              const service: ExportService = {
                id: serviceId,
                process,
                fee,
                finalFee,
                tasks
              }
              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 fixedFinalFee = toFiniteNumber(fixedRow?.finalFee)
    const fixedSubtotal = toFiniteNumber(fixedRow?.subtotal)
    const serviceFeeRaw = fixedFinalFee ?? fixedSubtotal ?? 0
    const serviceFee = toMoney(serviceFeeRaw)
    const [addtional, reserve] = await Promise.all([
      buildAdditionalExport(contractId),
      buildReserveExport(contractId)
    ])
    const addtionalFee = toMoney(addtional ? addtional.fee : 0)
    const reserveFee = toMoney(reserve ? reserve.fee : 0)
    const contractFee = toMoney(addNumbers(serviceFee, addtionalFee, reserveFee))
    const contractScaleState = resolveExportScaleState(htInfoRaw, projectScaleSource)
    const contractScale = contractScaleState?.roughCalcEnabled ? [] : toExportScaleRows(contractScaleState?.detailRows)

    const contractServiceCoesRaw = buildProjectServiceCoes(
        resolveExportFactorRows(htConsultCategoryFactorState, consultCategoryFactorState)
    )
    const contractMajorCoesRaw = buildProjectMajorCoes(
        resolveExportFactorRows(htMajorFactorState, majorFactorState)
    )
    console.log('[export][contract factor rows]', {
      contractId,
      consultFactorPinia: htConsultCategoryFactorState.piniaData,
      consultFactorKv: htConsultCategoryFactorState.kvData,
      consultFactorResolved: htConsultCategoryFactorState.resolved,
      majorFactorPinia: htMajorFactorState.piniaData,
      majorFactorKv: htMajorFactorState.kvData,
      majorFactorResolved: htMajorFactorState.resolved,
      contractServiceCoesRaw,
      contractMajorCoesRaw
    })

    contracts.push({
      name: isNonEmptyString(contract.name)
          ? contract.name
          : t('tab.messages.contractFallbackName', {index: index + 1}),
      serviceFee,
      addtionalFee,
      reserveFee,
      fee: contractFee,
      quality: isNonEmptyString(htBaseInfoRaw?.quality) ? htBaseInfoRaw.quality.trim() : '',
      duration: isNonEmptyString(htBaseInfoRaw?.duration) ? htBaseInfoRaw.duration.trim() : '',
      scale: contractScale,
      serviceCoes: contractServiceCoesRaw,
      majorCoes: contractMajorCoesRaw,
      services,
      addtional,
      reserve
    })
  }

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

const exportData = async () => {
  try {
    // TODO: 实现数据导出逻辑

    // 显示保存成功提示
    finishReportExportProgress(true, t('tab.toast.saveSuccess'))
  } catch (error) {
    console.error('exportData failed:', error)
    finishReportExportProgress(false, t('tab.toast.saveFailed'))
  }
}
const exportData2 = async () => {
  try {
    const now = new Date()
    const currentProjectId = readCurrentProjectId()
    const piniaForageStores = await Promise.all(
        getPiniaPersistStores().map(async ({storeName, store}) => ({
          storeName,
          entries: await readForage(store)
        }))
    )
    const payload: DataPackage = {
      version: 3,
      packageType: 'project-snapshot',
      exportedAt: now.toISOString(),
      projectId: currentProjectId,
      localStorage: [],
      sessionStorage: [],
      localforageDefault: await readForage(projectDefaultForage),
      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, PROJECT_INFO_DB_KEY, LEGACY_PROJECT_DB_KEY)
    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)
    showMessageDialog(t('tab.toast.failed'), t('ht.retry'))
  } finally {
    dataMenuOpen.value = false
  }
}

const exportContract = async (workbook, contract) => {
  // 生成文件名
  const fileName = `合同服务费用汇总表-${contract.contractId}-${contract.contractName}.xlsx`;

  // 生成buffer
  const buffer = await workbook.xlsx.writeBuffer();

  // 创建blob并下载
  const blob = new Blob([buffer], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  });

  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileName;
  a.click();
  window.URL.revokeObjectURL(url);
};

const exportReport2 = async () => {
  generateContracts();
}

/*const contracts = ref([
  {
    contractName: "XX城市综合体项目",
    items: [
      {code: "D1", name: "全过程造价咨询", amount: 111, finalFee: 1111, fee: 111},
      {code: "D2-1", name: "前期阶段造价咨询", amount: 112, finalFee: 1112, fee: 112},
      {code: "D2-2-1", name: "实施阶段造价咨询", amount: 113, finalFee: 1113, fee: 113},
      {code: "D3-1", name: "投资估算", amount: 114, finalFee: 1114, fee: 114},
      {code: "D3-2", name: "设计概算", amount: 115, finalFee: 1115, fee: 115}
      // 可继续添加更多项目...
    ],
    totalFinalFee: 5575 // 手动计算或用 reduce 计算
  },
  {
    contractName: "YY科技园区项目",
    items: [
      {code: "D1", name: "全过程造价咨询", amount: 222, finalFee: 2222, fee: 222},
      {code: "D2-1", name: "前期阶段造价咨询", amount: 223, finalFee: 2223, fee: 223},
      {code: "D2-2-1", name: "实施阶段造价咨询", amount: 224, finalFee: 2224, fee: 224},
      {code: "D3-1", name: "投资估算", amount: 225, finalFee: 2225, fee: 225},
      {code: "D3-2", name: "设计概算", amount: 226, finalFee: 2226, fee: 226}
    ],
    totalFinalFee: 11125
  },
  {
    contractName: "ZZ商业中心项目",
    items: [
      {code: "D1", name: "全过程造价咨询", amount: 333, finalFee: 3333, fee: 333},
      {code: "D2-1", name: "前期阶段造价咨询", amount: 334, finalFee: 3334, fee: 334},
      {code: "D2-2-1", name: "实施阶段造价咨询", amount: 335, finalFee: 3335, fee: 335},
      {code: "D3-1", name: "投资估算", amount: 336, finalFee: 3336, fee: 336},
      {code: "D3-2", name: "设计概算", amount: 337, finalFee: 3337, fee: 337}
    ],
    totalFinalFee: 16675
  }
]);

// 计算合计（可选：自动计算）
contracts.value.forEach(contract => {
  if (!contract.totalFinalFee) {
    contract.totalFinalFee = contract.items.reduce((sum, item) => sum + item.finalFee, 0);
  }
});*/

watch(
    () => useDataStore().items,
    async (newItems) => {
      if (Object.keys(newItems).length > 0) {
        await loadContractList()
        await getTemplateData();
      }
    },
    {immediate: true, deep: true}
)

const loading = ref(false);
const status = ref('准备就绪');
const getTemplateData = async () => {
  const projectInfoRaw = await kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY)
  // 准备数据
  let data = {
    projectName: '',
    projectScaleRaw: null,
    contracts: []
  };
  data.projectName = projectInfoRaw.projectName;
  const projectScaleRaw = await kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY)
  data.projectScaleRaw = projectScaleRaw;
  for (let i = 0; i < contractList.value.length; i++) {
    const contract = contractList.value[i];
    const checkedZx = await useDataStore().query([
      {field: 'id', value: `${contract.id}-checkedZx`, operator: 'eq'}
    ])
    const fallbackIds = new Set(
        checkedZx.length > 0
            ? (checkedZx[0]?.fallbackIds || checkedZx[0]?.ids || [])
            : []
    )
    const zxRows = await useDataStore().query([
      {field: 'type', value: `${contract.id}-zxFw`, operator: 'eq'}
    ]).then(rows => rows.filter(row => fallbackIds.has(String(row.id))))

    const investmentRows = await useDataStore().query([
      {field: 'type', value: `${contract.id}-investment`, operator: 'eq'}
    ]).then(rows => rows.filter(row => fallbackIds.has(String(row.id))))
    const hourlyRows = await useDataStore().query([
      {field: 'type', value: `${contract.id}-hourly`, operator: 'eq'}
    ]).then(rows => rows.filter(row => fallbackIds.has(String(row.id))))

    const taskRows = await useDataStore().query([
      {field: 'type', value: `${contract.id}-task`, operator: 'eq'}
    ])

    const contentRows = await useDataStore().query([
      {field: 'type', value: `${contract.id}-other`, operator: 'eq'}
    ])
    // 第 1216 行已经加载了项目级的规模数据
    const htScaleState = await kvStore.getItem<{ totalAmount?: number | null }>(`ht-info-v3-${contract.id}`)
    const obj = {
      contract: contract.name,
      htScaleState,
      investmentRows,
      hourlyRows,
      taskRows,
      zxRows,
      contentRows
    }
    data.contracts.push(obj)
  }
  return data;
}

const mergeCell = async (sourceRow, newWorksheet, newRow) => {
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    // 获取合并范围，例如 "A1:H1" 或 "A1:A2"（纵向合并）
    const masterAddress = cell.master.address;

    // 解析合并范围（支持 "A1:C3" 这种格式）
    const mergeRange = masterAddress.split(':');
    const startAddr = mergeRange[0];
    const endAddr = mergeRange[1] || startAddr;

    // 解析起始单元格地址（列字母 + 行号）
    const startColMatch = startAddr.match(/^([A-Z]+)/);
    const startRowMatch = startAddr.match(/(\d+)$/);
    const startColLetter = startColMatch ? startColMatch[1] : 'A';
    const startRowNum = startRowMatch ? parseInt(startRowMatch[1]) : 1;

    // 解析结束单元格地址
    const endColMatch = endAddr.match(/^([A-Z]+)/);
    const endRowMatch = endAddr.match(/(\d+)$/);
    const endColLetter = endColMatch ? endColMatch[1] : startColLetter;
    const endRowNum = endRowMatch ? parseInt(endRowMatch[1]) : startRowNum;

    // 计算合并的列数和行数
    const startColNum = startColLetter.split('').reduce((acc, char) => acc * 26 + (char.charCodeAt(0) - 64), 0);
    const endColNum = endColLetter.split('').reduce((acc, char) => acc * 26 + (char.charCodeAt(0) - 64), 0);
    const mergeColCount = endColNum - startColNum + 1;
    const mergeRowCount = endRowNum - startRowNum + 1;

    // 构建目标地址（使用 newRow 的行号）
    const targetStartAddr = `${startColLetter}${newRow.number}`;
    const targetEndAddr = mergeRowCount > 1
        ? `${endColLetter}${newRow.number + mergeRowCount - 1}`
        : `${endColLetter}${newRow.number}`;

    const targetAddress = mergeRowCount > 1 ? `${targetStartAddr}:${targetEndAddr}` : targetStartAddr;

    // 在目标表中执行合并（如果还没合并）
    if (!newWorksheet.getCell(targetStartAddr).isMerged) {
      // 执行合并操作
      if (mergeRowCount > 1 || mergeColCount > 1) {
        newWorksheet.mergeCells(targetAddress);
      }

      // 给第一个单元格赋值
      if (colNumber === startColNum && cell.row === startRowNum) {
        const targetCell = newWorksheet.getCell(targetStartAddr);
        targetCell.value = cell.value;
      }
    }

    // 复制样式（边框、字体、对齐）
    const targetCell = newRow.getCell(colNumber);
    targetCell.style = cell.style;
  })
}

const mergeCell2 = async (sourceRow, newWorksheet, newRow) => {
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    // 获取合并范围，例如 "A1:H1"
    const masterAddress = cell.master.address;
    const targetAddress = masterAddress.replace(/[A-Z]+/, (match) => {
      // 简单的列字母转换（仅适用于A-J列）
      return String.fromCharCode(match.charCodeAt(0) + colNumber - cell.col);
    }).replace(/[0-9]+/, 1);

    // 在目标表中执行合并（如果还没合并）
    if (!newWorksheet.getCell(targetAddress).isMerged) {
      // 计算合并范围
      const master = sourceRow.worksheet.getCell(masterAddress);
      const mergeCount = master.master.col - cell.col + 1; // 简化计算，实际需解析地址
      // 这里为了简单，直接赋值值给第一个单元格，或者您可以重新定义 merge
      if (colNumber === 1) { // 只给第一列赋值，避免重复
        newRow.getCell(1).value = cell.value;
      }
    }
    const targetCell = newRow.getCell(colNumber);
    // 2. 复制样式（边框、字体、对齐）
    targetCell.style = cell.style;
  })
}

const styleCell2 = async (newWorksheet) => {
  // 1. 计算并设置列宽
  calculateColumnWidths(newWorksheet)

  // 2. 计算并设置行高
  calculateRowHeights(newWorksheet)

  // 3. 应用单元格样式
  applyCellStyles(newWorksheet)
}

/**
 * 计算列宽（智能自适应）
 */
const calculateColumnWidths = (worksheet) => {
  worksheet.columns.forEach((column, colIndex) => {
    let maxLength = 8 // 默认最小宽度（给表头留空间）

    column.eachCell({includeEmpty: true}, (cell, rowNumber) => {
      if (cell.value === null || cell.value === undefined) return

      const cellWidth = calculateCellWidth(cell)
      if (cellWidth > maxLength) {
        maxLength = cellWidth
      }
    })

    // 设置列宽（添加缓冲空间）
    column.width = Math.min(maxLength + 2, 80) // 最大80字符宽
  })
}

/**
 * 计算单个单元格的视觉宽度
 */
const calculateCellWidth = (cell) => {
  if (!cell.value) return 0

  let valueStr = ''

  // 处理不同类型的内容
  if (typeof cell.value === 'object' && cell.value.text) {
    // 富文本
    valueStr = cell.value.text
  } else if (typeof cell.value === 'object' && cell.value.richText) {
    // 富文本数组
    valueStr = cell.value.richText.map(item => item.text || '').join('')
  } else {
    // 普通文本
    valueStr = String(cell.value)
  }

  // 处理换行符（按最长行计算）
  const lines = valueStr.split('\n')
  let maxWidth = 0

  lines.forEach(line => {
    // 中文字符算2个宽度，英文/数字算1个
    const chineseCount = (line.match(/[\u4e00-\u9fa5]/g) || []).length
    const otherCount = line.length - chineseCount
    const lineWidth = chineseCount * 2 + otherCount

    if (lineWidth > maxWidth) {
      maxWidth = lineWidth
    }
  })

  return maxWidth
}

/**
 * 计算行高（根据内容自动调整）
 */
const calculateRowHeights = (worksheet) => {
  worksheet.eachRow((row, rowNumber) => {
    let maxHeight = 15 // 默认行高（Excel默认15）

    row.eachCell((cell, colNumber) => {
      if (!cell.value) return

      const cellHeight = calculateCellHeight(cell)
      if (cellHeight > maxHeight) {
        maxHeight = cellHeight
      }
    })

    // 设置行高（最小15，最大200）
    row.height = Math.min(Math.max(maxHeight, 15), 200)
  })
}

/**
 * 计算单个单元格的视觉高度
 */
const calculateCellHeight = (cell) => {
  if (!cell.value) return 0

  let valueStr = ''

  if (typeof cell.value === 'object' && cell.value.text) {
    valueStr = cell.value.text
  } else if (typeof cell.value === 'object' && cell.value.richText) {
    valueStr = cell.value.richText.map(item => item.text || '').join('')
  } else {
    valueStr = String(cell.value)
  }

  // 按换行符分割，计算行数
  const lines = valueStr.split('\n').filter(line => line.trim() !== '')
  const lineCount = Math.max(lines.length, 1)

  // 每行高度约15（可根据字体大小调整）
  const lineHeight = 15
  return lineCount * lineHeight + 4 // 额外添加4像素内边距
}

/**
 * 应用单元格样式（边框、对齐等）
 */
const applyCellStyles = (worksheet) => {
  worksheet.eachRow((row, rowNumber) => {
    row.eachCell((cell, colNumber) => {
      // 设置边框
      cell.border = {
        top: {style: 'thin'},
        left: {style: 'thin'},
        bottom: {style: 'thin'},
        right: {style: 'thin'}
      }

      // 设置对齐方式
      cell.alignment = {
        vertical: 'middle', // 垂直居中
        horizontal: 'left', // 水平左对齐
        wrapText: true      // 自动换行
      }

      // 表头样式（第一行）
      if (rowNumber === 1) {
        cell.fill = {
          type: 'pattern',
          pattern: 'solid',
          fgColor: {argb: 'FF4A90E2'} // 蓝色背景
        }
        cell.font = {
          bold: true,
          color: {argb: 'FFFFFFFF'}, // 白色文字
          size: 11
        }
        cell.alignment = {
          vertical: 'middle',
          horizontal: 'center', // 表头居中
          wrapText: true
        }
      } else {
        // 数据行样式
        cell.font = {
          size: 10
        }
      }
    })
  })
}

const styleCell = async (newWorksheet) => {
  newWorksheet.columns.forEach((column) => {
    let maxLength = 5; // 默认最小宽度

    // 遍历该列的所有单元格（包括表头和数据）
    column.eachCell({includeEmpty: true}, (cell) => {
      if (cell.value) {
        // 将内容转为字符串
        const valueStr = cell.value.toString();
        // 计算长度（简单处理：中文字符算2个长度，英文算1个）
        // 这里用了一个简单的正则替换来估算视觉宽度
        const length = valueStr.replace(/[\u4e00-\u9fa5]/g, 'aa').length;

        if (length > maxLength) {
          maxLength = length;
        }
      }
    });

    // 设置列宽（加一点缓冲空间，比如 + 2）
    column.width = maxLength + 2;

    // 可选：限制最大宽度，防止某一行文字特别长导致列太宽
    if (column.width > 50) {
      column.width = 50;
    }
  });
}
// 生成目录
const importMl = async (data, templateWorkbook, finalWorkbook) => {
  let sheetName = `报告目录`;
  let templateSheet = templateWorkbook.getWorksheet('报告目录') || templateWorkbook.getWorksheet(0);
  const titleTemplate = templateSheet.getRow(6);
  const templateRow = templateSheet.getRow(7);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);

  for (let r = 1; r <= 5; r++) {
    const sourceRow = templateSheet.getRow(r);
    const targetRow = newWorksheet.getRow(r);

    if (r == 1 || r == 3 || r == 4) {
      mergeCell(sourceRow, newWorksheet, targetRow);
    } else {
      sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
        const targetCell = targetRow.getCell(colNumber);
        targetCell.value = cell.value;

        // 2. 复制样式（边框、字体、对齐）
        targetCell.style = cell.style;
      })
    }
  }
  let rowIndex = 6; // 数据从第3行开始写（因为表头占了1-2行）
  data.contracts.forEach((item, idx) => {
    const titleRow = newWorksheet.getRow(rowIndex);

    titleTemplate.eachCell({includeEmpty: true}, (cell, colNumber) => {
      const targetCell = titleRow.getCell(colNumber);
      // 替换占位符 {{title}} 为实际合同名称
      const newValue = replacePlaceholders(cell.value, {
        title: `三、合同服务费用表-${item.contract}合同`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
    newWorksheet.getCell(`A${rowIndex}`).alignment = {horizontal: 'center'};
    newWorksheet.mergeCells(rowIndex, 1, rowIndex, 4);
    rowIndex++;

    const newRow = newWorksheet.getRow(rowIndex);
    templateRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
      const targetCell = newRow.getCell(colNumber);

      // 1. 替换占位符并赋值
      const newValue = replacePlaceholders(cell.value, {
        num: 1,
        fileName: `合同服务费用汇总表－${item.contract}合同段`,
        fileCode: `${item.contract}表`,
      });
      targetCell.value = newValue;

      // 2. 复制样式（边框、字体、对齐）
      targetCell.style = cell.style;
    });
    rowIndex++;

    const newRow2 = newWorksheet.getRow(rowIndex);
    templateRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
      const targetCell = newRow2.getCell(colNumber);

      // 1. 替换占位符并赋值
      const newValue = replacePlaceholders(cell.value, {
        num: 2,
        fileName: `合同服务费用汇总明细表－${item.contract}合同段`,
        fileCode: `${item.contract}表`,
      });
      targetCell.value = newValue;

      // 2. 复制样式（边框、字体、对齐）
      targetCell.style = cell.style;
    });
    rowIndex++;

    const newRow3 = newWorksheet.getRow(rowIndex);

    templateRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
      const targetCell = newRow3.getCell(colNumber);

      // 1. 替换占位符并赋值
      const newValue = replacePlaceholders(cell.value, {
        num: 3,
        fileName: `投资规模法计算明细表－${item.contract}合同段`,
        fileCode: `${item.contract}表`,
      });
      targetCell.value = newValue;

      // 2. 复制样式（边框、字体、对齐）
      targetCell.style = cell.style;
    });
    rowIndex++;

    const newRow4 = newWorksheet.getRow(rowIndex);

    templateRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
      const targetCell = newRow4.getCell(colNumber);

      // 1. 替换占位符并赋值
      const newValue = replacePlaceholders(cell.value, {
        num: 4,
        fileName: `工作量法计算明细表－${item.contract}合同段`,
        fileCode: `${item.contract}表`,
      });
      targetCell.value = newValue;

      // 2. 复制样式（边框、字体、对齐）
      targetCell.style = cell.style;
    });
    rowIndex++;

    const newRow5 = newWorksheet.getRow(rowIndex);
    templateRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
      const targetCell = newRow5.getCell(colNumber);

      const newValue = replacePlaceholders(cell.value, {
        num: 5,
        fileName: `工时法计算明细表－${item.contract}合同段`,
        fileCode: `${item.contract}表`,
      });
      targetCell.value = newValue;

      // 2. 复制样式（边框、字体、对齐）
      targetCell.style = cell.style;
    });
    rowIndex++;
  });
  for (let r = 8; r <= 10; r++) {
    const sourceRow = templateSheet.getRow(r);
    const targetRow = newWorksheet.getRow(rowIndex);

    if (r == 8) {
      mergeCell(sourceRow, newWorksheet, targetRow);
      newWorksheet.getCell(`A${rowIndex}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${rowIndex}`, 1, `${rowIndex}`, 4);
    } else {
      sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
        const targetCell = targetRow.getCell(colNumber);
        targetCell.value = cell.value;

        // 2. 复制样式（边框、字体、对齐）
        targetCell.style = cell.style;
      })
    }
    rowIndex++;
  }

  styleCell(newWorksheet)
  newWorksheet.getCell('A1').alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 4);
  newWorksheet.getCell('A3').alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(3, 1, 3, 4);
  newWorksheet.getCell('A4').alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(4, 1, 4, 4);
}
// 生成项目费用汇总表
const importXmfyhz = async (data, templateWorkbook, finalWorkbook) => {
  let sheetName = `项目服务费用汇总表`;
  let templateSheet = templateWorkbook.getWorksheet('费总01表') || templateWorkbook.getWorksheet(1);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  let colIndex = 0;
  sourceRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    if (colNumber == 4) {
      data.contracts.forEach((item, idx) => {
        colIndex++;
        const targetCell = newRow3.getCell(idx + colNumber);

        // 1. 替换占位符并赋值
        const newValue = replacePlaceholders(cell.value, {
          title: `${item.contract}合同费用（元）`
        });
        targetCell.value = newValue;
        targetCell.style = cell.style;
      })
    } else {
      let targetCell = newRow3.getCell(colNumber);
      if (colNumber == 5) {
        targetCell = newRow3.getCell(colNumber + colIndex - 1);
      }
      targetCell.value = cell.value;
      targetCell.style = cell.style;
    }
  })

  const sourceRow4 = templateSheet.getRow(4);
  let rowIndex = 0;
  const mergedData = {};
  // 遍历每个contract
  data.contracts.forEach((contract, contractIndex) => {
    // 遍历当前contract的zxRows
    contract.zxRows.forEach(row => {
      // 用code和name作为唯一标识
      const key = `${row.code}-${row.name}`;

      // 如果这个key不存在，就创建一个新的条目
      if (!mergedData[key]) {
        mergedData[key] = {
          code: row.code,
          name: row.name,
          finalFee: new Array(data.contracts.length).fill(0),
          serviceFee: 0
        };
      }
      mergedData[key].serviceFee += row.finalFee ?? 0
      // 把当前的finalFee添加到数组中
      mergedData[key].finalFee[contractIndex] = row.finalFee ?? 0;
    });
  });
  const resultArray = Object.values(mergedData);

  const contractTotals = new Array(data.contracts.length).fill(0);
  let grandTotalServiceFee = 0;

  resultArray.forEach(item => {
    // 累加每个合同的 finalFee
    item.finalFee.forEach((fee, contractIndex) => {
      contractTotals[contractIndex] += fee ?? 0;
    });
    // 累加总 serviceFee
    grandTotalServiceFee += item.serviceFee ?? 0;
  });
  resultArray.forEach((item, idx) => {
    rowIndex++;
    const newRow4 = newWorksheet.getRow(4 + idx);
    sourceRow4.eachCell({includeEmpty: true}, (cell, colNumber) => {
      let targetCell = newRow4.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        num: `${idx + 1}`,
        code: item.code,
        name: item.name,
        finalFee: item.finalFee[idx],
        serviceFee: item.serviceFee
      });
      if (colNumber == 4) {
        for (let i = 0; i < data.contracts.length; i++) {
          targetCell = newRow4.getCell(colNumber + i);
          targetCell.value = replacePlaceholders(cell.value, {
            finalFee: item.finalFee[i]
          });
          targetCell.style = cell.style;
        }
      } else if (colNumber == 5) {
        targetCell = newRow4.getCell(colNumber + data.contracts.length - 1);
        targetCell.value = newValue;
        targetCell.style = cell.style;
      } else {
        targetCell.value = newValue;
        targetCell.style = cell.style;
      }
    })
  })

  const sourceRow5 = templateSheet.getRow(5);
  const newRow5 = newWorksheet.getRow(4 + rowIndex);
  sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
    if (colNumber == 1) {
      mergeCell(sourceRow5, newWorksheet, newRow5);
      newWorksheet.getCell(`A${4 + rowIndex}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${4 + rowIndex}`, 1, `${4 + rowIndex}`, 3);
    } else if (colNumber == 4) {
      for (let i = 0; i < data.contracts.length; i++) {
        let targetCell = newRow5.getCell(colNumber + i);
        let newValue = replacePlaceholders(cell.value, {
          finalFee2: contractTotals[i]
        });
        targetCell.value = newValue;
        targetCell.style = cell.style;
      }
    } else if (colNumber == 5) {
      // 填充 serviceFee 总和
      const targetCell = newRow5.getCell(colNumber + data.contracts.length - 1);
      targetCell.value = replacePlaceholders(cell.value, {
        serviceFee2: grandTotalServiceFee
      });
      targetCell.style = cell.style;
    } else {
      const targetCell = newRow5.getCell(colNumber);
      targetCell.value = cell.value;
      targetCell.style = cell.style;
    }
  })

  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  mergeCell(sourceRow, newWorksheet, newRow);
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, data.contracts.length + 4);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    const targetCell = newRow2.getCell(colNumber);
    // 1. 替换占位符并赋值
    if (colNumber == 1) {
      // 1. 替换占位符并赋值
      const newValue = replacePlaceholders(cell.value, {
        projectName: `项目名称：${data.projectName}`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    } else if (colNumber == 5) {
      targetCell.value = replacePlaceholders(cell.value, {
        projectName2: `预总01表`
      });
      targetCell.style = cell.style;
    }
  })

  styleCell(newWorksheet)

}
// 生成合同费用汇总表
const importHtfyhz = async (data, contract, templateWorkbook, finalWorkbook) => {
  let sheetName = `${contract.contract}（合同汇总表）`;
  let templateSheet = templateWorkbook.getWorksheet('费i-1（合同汇总表）') || templateWorkbook.getWorksheet(2);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);

  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  mergeCell(sourceRow, newWorksheet, newRow);
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    const targetCell = newRow.getCell(colNumber);
    const newValue = replacePlaceholders(cell.value, {
      title: `合同服务费用汇总表－${contract.contract}合同`
    });
    targetCell.value = newValue;
    targetCell.style = cell.style;
  })
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 8);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow2.getCell(colNumber);
    let newValue = null;
    if (colNumber == 1 || colNumber == 8) {
      newValue = replacePlaceholders(cell.value, {
        contractName: `合同名称：${contract.contract}`,
        contractName2: `${contract.contract}表`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    }
  })

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  mergeCell(sourceRow3, newWorksheet, newRow3)
  const sourceRow4 = templateSheet.getRow(4);
  const newRow4 = newWorksheet.getRow(4);
  mergeCell(sourceRow4, newWorksheet, newRow4);
  newRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    if (colNumber == 1 || colNumber == 2 || colNumber == 3 || colNumber == 7 || colNumber == 8) {
      newWorksheet.mergeCells(3, colNumber, 4, colNumber);
    }
    if (colNumber == 4) {
      newWorksheet.mergeCells(3, colNumber, 3, 6);
    }
  })

  const sourceRow5 = templateSheet.getRow(5);
  let totalFinalFee = 0;
  contract.zxRows.forEach((row, idx) => {
    const newRow5 = newWorksheet.getRow(idx + 5);
    let amount = contract.htScaleState.totalAmount ?? 0;
    let finalFee = row.finalFee ?? 0;
    let unitFee = (finalFee / amount) * 10000;
    totalFinalFee += finalFee;
    sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
      if (row.code == 'D5-1') {
        unitFee = (finalFee / amount);
        if (colNumber == 4) {
          cell.value = '报告份数';
        } else if (colNumber == 5) {
          cell.value = '份';
        }
      }
      let targetCell = newRow5.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        num: `${idx + 1}`,
        code: row.code,
        name: row.name,
        amount: amount,
        finalFee: finalFee,
        unitFee: unitFee
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
  });

  const sourceRow6 = templateSheet.getRow(6);
  const newRow6 = newWorksheet.getRow(5 + contract.zxRows.length);
  sourceRow6.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow6.getCell(colNumber);
    if (colNumber == 1) {
      mergeCell(sourceRow6, newWorksheet, newRow6);
      newWorksheet.getCell(`A${5 + contract.zxRows.length}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${5 + contract.zxRows.length}`, 1, `${5 + contract.zxRows.length}`, 3);
    } else if (colNumber == 4 || colNumber == 5 || colNumber == 6 || colNumber == 8) {
      cell.value = '/'
    } else if (colNumber == 7) {
      cell.value = replacePlaceholders(cell.value, {
        totalFinalFee: totalFinalFee
      });
    }
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })
  // styleCell(newWorksheet)
}
// 生成合同服务费用汇总明细表
const importHtfyhzmx = async (data, contract, templateWorkbook, finalWorkbook) => {
  let sheetName = `${contract.contract}（合同汇总明细表）`;
  let templateSheet = templateWorkbook.getWorksheet('费i-2（合同汇总明细表）') || templateWorkbook.getWorksheet(3);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);
  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow.getCell(colNumber);
    const newValue = replacePlaceholders(cell.value, {
      title: `合同服务费用汇总明细表－${contract.contract}合同`
    });
    targetCell.value = newValue;
    targetCell.style = cell.style;
  })
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 9);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow2.getCell(colNumber);
    let newValue = null;
    if (colNumber == 1 || colNumber == 9) {
      newValue = replacePlaceholders(cell.value, {
        contractName: `合同名称：${contract.contract}`,
        contractName2: `${contract.contract}表`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    }
  })

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  sourceRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow3.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  const sourceRow4 = templateSheet.getRow(4);
  let investScale2 = 0;
  let process2 = 0;
  let hourly2 = 0;
  let subtotal2 = 0;
  let finalFee2 = 0;
  contract.zxRows.forEach((row, idx) => {
    const newRow4 = newWorksheet.getRow(idx + 4);
    finalFee2 += row.finalFee ?? 0;
    investScale2 += row.investScale ?? 0;
    process2 += row.process ?? 0;
    hourly2 += row.hourly ?? 0;
    subtotal2 += row.subtotal ?? 0;
    sourceRow4.eachCell({includeEmpty: true}, (cell, colNumber) => {
      let targetCell = newRow4.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        code: row.code,
        name: row.name,
        investScale: row.investScale,
        process: row.process,
        hourly: row.hourly,
        subtotal: row.subtotal,
        finalFee: row.finalFee,
        remark: row.remark
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
  });

  const sourceRow5 = templateSheet.getRow(5);
  const newRow5 = newWorksheet.getRow(4 + contract.zxRows.length);
  sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow5.getCell(colNumber);
    if (colNumber == 1) {
      mergeCell(sourceRow5, newWorksheet, newRow5);
      newWorksheet.getCell(`A${4 + contract.zxRows.length}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${4 + contract.zxRows.length}`, 1, `${4 + contract.zxRows.length}`, 3);
    } else {
      cell.value = replacePlaceholders(cell.value, {
        investScale2: investScale2,
        process2: process2,
        hourly2: hourly2,
        subtotal2: subtotal2,
        finalFee2: finalFee2,
        remark2: ''
      });
      targetCell.value = cell.value;
      targetCell.style = cell.style;
    }
  })

  const sourceRow6 = templateSheet.getRow(6);
  const newRow6 = newWorksheet.getRow(5 + contract.investmentRows.length);
  sourceRow6.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow6.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  styleCell(newWorksheet)
}
// 生成投资规模法计算明细表
const importTzgmjsmx = async (data, contract, templateWorkbook, finalWorkbook) => {
  let sheetName = `${contract.contract}（规模法）`;
  let templateSheet = templateWorkbook.getWorksheet('费i-3（规模法）') || templateWorkbook.getWorksheet(4);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);
  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow.getCell(colNumber);
    const newValue = replacePlaceholders(cell.value, {
      title: `投资规模法计算明细表`
    });
    targetCell.value = newValue;
    targetCell.style = cell.style;
  })
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 10);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow2.getCell(colNumber);
    let newValue = null;
    if (colNumber == 1 || colNumber == 10) {
      newValue = replacePlaceholders(cell.value, {
        contractName: `合同名称：${contract.contract}`,
        contractName2: `${contract.contract}表`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    }
  })

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  mergeCell(sourceRow3, newWorksheet, newRow3)
  const sourceRow4 = templateSheet.getRow(4);
  const newRow4 = newWorksheet.getRow(4);
  mergeCell(sourceRow4, newWorksheet, newRow4);
  newRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    if (colNumber == 7) {
      newWorksheet.mergeCells(3, colNumber, 3, 9);
    } else if (colNumber == 1 || colNumber == 2 || colNumber == 3 || colNumber == 4 || colNumber == 5 || colNumber == 6 || colNumber == 10) {
      newWorksheet.mergeCells(3, colNumber, 4, colNumber);
    }
  })

  const sourceRow5 = templateSheet.getRow(5);
  let amount2 = '/';
  let base2 = '/';
  let optionalFormula2 = '/';
  let workStageFactor2 = '/';
  let remark2 = '';
  let workRatio2 = '/';
  let budgetFee2 = 0;
  let serviceFee2 = 0;
  contract.investmentRows.forEach((row, idx) => {
    const newRow5 = newWorksheet.getRow(idx + 5);
    budgetFee2 += row.budgetFee ?? 0;
    serviceFee2 += row.serviceFee ?? 0;
    sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
      let targetCell = newRow5.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        code: row.code,
        name: row.majorName,
        amount: row.amount,
        base: row.base,
        optionalFormula: row.optionalFormula,
        budgetFee: row.budgetFee,
        workStageFactor: row.workStageFactor,
        workRatio: row.workRatio,
        serviceFee: row.serviceFee,
        remark: row.remark
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
  });

  const sourceRow6 = templateSheet.getRow(6);
  const newRow6 = newWorksheet.getRow(5 + contract.investmentRows.length);
  sourceRow6.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow6.getCell(colNumber);
    if (colNumber == 1) {
      mergeCell(sourceRow6, newWorksheet, newRow6);
      newWorksheet.getCell(`A${5 + contract.investmentRows.length}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${5 + contract.investmentRows.length}`, 1, `${5 + contract.investmentRows.length}`, 2);
    } else {
      cell.value = replacePlaceholders(cell.value, {
        amount2: amount2,
        base2: base2,
        optionalFormula2: optionalFormula2,
        workStageFactor2: workStageFactor2,
        workRatio2: workRatio2,
        budgetFee2: budgetFee2,
        serviceFee2: serviceFee2,
        remark2: remark2
      });
      targetCell.value = cell.value;
      targetCell.style = cell.style;
    }
  })

  const sourceRow7 = templateSheet.getRow(7);
  const newRow7 = newWorksheet.getRow(6 + contract.investmentRows.length);
  sourceRow7.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow7.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  styleCell(newWorksheet)
}
// 生成工作量法计算明细表
const importGzlfjsmx = async (data, contract, templateWorkbook, finalWorkbook) => {
  let sheetName = `${contract.contract}（工作量法）`;
  let templateSheet = templateWorkbook.getWorksheet('费i-4（工作量法）') || templateWorkbook.getWorksheet(5);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);
  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow.getCell(colNumber);
    const newValue = replacePlaceholders(cell.value, {
      title: `工作量法计算明细表`
    });
    targetCell.value = newValue;
    targetCell.style = cell.style;
  })
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 11);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow2.getCell(colNumber);
    let newValue = null;
    if (colNumber == 1 || colNumber == 11) {
      newValue = replacePlaceholders(cell.value, {
        contractName: `合同名称：${contract.contract}`,
        contractName2: `${contract.contract}表`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    }
  })

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  sourceRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow3.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  const sourceRow4 = templateSheet.getRow(4);
  contract.taskRows.forEach((row, idx) => {
    const newRow4 = newWorksheet.getRow(idx + 4);
    sourceRow4.eachCell({includeEmpty: true}, (cell, colNumber) => {
      let targetCell = newRow4.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        code: row.taskCode,
        name: row.taskName,
        budgetBase: row.budgetBase,
        budgetReferenceUnitPrice: row.budgetReferenceUnitPrice,
        budgetAdoptedUnitPrice: row.budgetAdoptedUnitPrice,
        workload: row.workload,
        consultCategoryFactor: row.consultCategoryFactor,
        cLow: row.cLow,
        cMid: row.cMid,
        cHigh: row.cHigh,
        serviceFee: row.workload
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
  });

  const sourceRow5 = templateSheet.getRow(5);
  const newRow5 = newWorksheet.getRow(4 + contract.taskRows.length);
  sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow5.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  styleCell(newWorksheet)
}
// 生成工时法计算明细表
const importGsfjsmx = async (data, contract, templateWorkbook, finalWorkbook) => {
  let sheetName = `${contract.contract}（工时法）`;
  let templateSheet = templateWorkbook.getWorksheet('费i-5（工时法）') || templateWorkbook.getWorksheet(6);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);
  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow.getCell(colNumber);
    const newValue = replacePlaceholders(cell.value, {
      title: `工时法计算明细表`
    });
    targetCell.value = newValue;
    targetCell.style = cell.style;
  })
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 21);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow2.getCell(colNumber);
    let newValue = null;
    if (colNumber == 1 || colNumber == 21) {
      newValue = replacePlaceholders(cell.value, {
        contractName: `合同名称：${contract.contract}`,
        contractName2: `${contract.contract}表`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    }
  })

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  mergeCell(sourceRow3, newWorksheet, newRow3)
  const sourceRow4 = templateSheet.getRow(4);
  const newRow4 = newWorksheet.getRow(4);
  mergeCell(sourceRow4, newWorksheet, newRow4);
  newRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    if (colNumber == 3) {
      newWorksheet.mergeCells(3, colNumber, 3, 5);
    } else if (colNumber == 6) {
      newWorksheet.mergeCells(3, colNumber, 3, 8);
    } else if (colNumber == 9) {
      newWorksheet.mergeCells(3, colNumber, 3, 11);
    } else if (colNumber == 12) {
      newWorksheet.mergeCells(3, colNumber, 3, 14);
    } else if (colNumber == 15) {
      newWorksheet.mergeCells(3, colNumber, 3, 17);
    } else if (colNumber == 18) {
      newWorksheet.mergeCells(3, colNumber, 3, 20);
    } else if (colNumber == 1 || colNumber == 2 || colNumber == 21) {
      newWorksheet.mergeCells(3, colNumber, 4, colNumber);
    }
  })

  const sourceRow5 = templateSheet.getRow(5);
  let tUnitPrice = '/';
  let tUnitPrice2 = '/';
  let tUnitPrice3 = '/';
  let tUnitPrice4 = '/';
  let tUnitPrice5 = '/';
  let tWorkdayCount = 0;
  let tFeeSubtotal = 0;
  let tWorkdayCount2 = 0;
  let tFeeSubtotal2 = 0;
  let tWorkdayCount3 = 0;
  let tFeeSubtotal3 = 0;
  let tWorkdayCount4 = 0;
  let tFeeSubtotal4 = 0;
  let tWorkdayCount5 = 0;
  let tFeeSubtotal5 = 0;
  let tWorkdayCount6 = 0;
  let tFeeSubtotal6 = 0;
  let tAvgUnitPrice = 0;
  let remark2 = 0;
  contract.hourlyRows.forEach((row, idx) => {
    const newRow5 = newWorksheet.getRow(idx + 5);
    tWorkdayCount += row.workdayCount ?? 0;
    tFeeSubtotal += row.feeSubtotal ?? 0;
    tWorkdayCount2 += row.workdayCount2 ?? 0;
    tFeeSubtotal2 += row.feeSubtotal2 ?? 0;
    tWorkdayCount3 += row.workdayCount3 ?? 0;
    tFeeSubtotal3 += row.feeSubtotal3 ?? 0;
    tWorkdayCount4 += row.workdayCount4 ?? 0;
    tFeeSubtotal4 += row.feeSubtotal4 ?? 0;
    tWorkdayCount5 += row.workdayCount5 ?? 0;
    tFeeSubtotal5 += row.feeSubtotal5 ?? 0;
    tWorkdayCount6 += row.workdayCount6 ?? 0;
    tFeeSubtotal6 += row.feeSubtotal6 ?? 0;
    tAvgUnitPrice += row.avgUnitPrice ?? 0;
    sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
      let targetCell = newRow5.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        code: row.expertCode,
        name: row.expertName,
        unitPrice: row.unitPrice ?? 0,
        workdayCount: row.workdayCount ?? 0,
        feeSubtotal: row.feeSubtotal ?? 0,
        unitPrice2: row.unitPrice2 ?? 0,
        workdayCount2: row.workdayCount2 ?? 0,
        feeSubtotal2: row.feeSubtotal2 ?? 0,
        unitPrice3: row.unitPrice3 ?? 0,
        workdayCount3: row.workdayCount3 ?? 0,
        feeSubtotal3: row.feeSubtotal3 ?? 0,
        unitPrice4: row.unitPrice4 ?? 0,
        workdayCount4: row.workdayCount4 ?? 0,
        feeSubtotal4: row.feeSubtotal4 ?? 0,
        unitPrice5: row.unitPrice5 ?? 0,
        workdayCount5: row.workdayCount5 ?? 0,
        feeSubtotal5: row.feeSubtotal5 ?? 0,
        workdayCount6: row.workdayCount6 ?? 0,
        feeSubtotal6: row.feeSubtotal6 ?? 0,
        avgUnitPrice: row.avgUnitPrice ?? 0,
        remark: row.remark
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
  });

  const sourceRow6 = templateSheet.getRow(6);
  const newRow6 = newWorksheet.getRow(5 + contract.hourlyRows.length);
  sourceRow6.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow6.getCell(colNumber);
    if (colNumber == 1) {
      mergeCell(sourceRow6, newWorksheet, newRow6);
      newWorksheet.getCell(`A${5 + contract.hourlyRows.length}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${5 + contract.hourlyRows.length}`, 1, `${5 + contract.investmentRows.length}`, 2);
    } else {
      cell.value = replacePlaceholders(cell.value, {
        tUnitPrice: tUnitPrice,
        tWorkdayCount: tWorkdayCount,
        tFeeSubtotal: tFeeSubtotal,
        tUnitPrice2: tUnitPrice2,
        tWorkdayCount2: tWorkdayCount2,
        tFeeSubtotal2: tFeeSubtotal2,
        tUnitPrice3: tUnitPrice3,
        tWorkdayCount3: tWorkdayCount3,
        tFeeSubtotal3: tFeeSubtotal3,
        tUnitPrice4: tUnitPrice4,
        tWorkdayCount4: tWorkdayCount4,
        tFeeSubtotal4: tFeeSubtotal4,
        tUnitPrice5: tUnitPrice5,
        tWorkdayCount5: tWorkdayCount5,
        tFeeSubtotal5: tFeeSubtotal5,
        tWorkdayCount6: tWorkdayCount6,
        tFeeSubtotal6: tFeeSubtotal6,
        tAvgUnitPrice: tAvgUnitPrice,
        remark2: remark2
      });
      targetCell.value = cell.value;
      targetCell.style = cell.style;
    }
  })
  const sourceRow7 = templateSheet.getRow(7);
  const newRow7 = newWorksheet.getRow(6 + contract.hourlyRows.length);
  sourceRow7.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow7.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  styleCell(newWorksheet)
}

// 生成其他服务计算明细表
const importQtfwjsmx = async (data, contract, templateWorkbook, finalWorkbook) => {
  let sheetName = `${contract.contract}（其他）`;
  let templateSheet = templateWorkbook.getWorksheet('费i-6（其他）') || templateWorkbook.getWorksheet(7);
  const newWorksheet = finalWorkbook.addWorksheet(sheetName);
  const sourceRow = templateSheet.getRow(1);
  const newRow = newWorksheet.getRow(1);
  sourceRow.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow.getCell(colNumber);
    const newValue = replacePlaceholders(cell.value, {
      title: `其他服务计算明细表`
    });
    targetCell.value = newValue;
    targetCell.style = cell.style;
  })
  newWorksheet.getCell(`A1`).alignment = {horizontal: 'center'};
  newWorksheet.mergeCells(1, 1, 1, 7);

  const sourceRow2 = templateSheet.getRow(2);
  const newRow2 = newWorksheet.getRow(2);
  sourceRow2.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow2.getCell(colNumber);
    let newValue = null;
    if (colNumber == 1 || colNumber == 9) {
      newValue = replacePlaceholders(cell.value, {
        contractName: `合同名称：${contract.contract}`,
        contractName2: `${contract.contract}表`
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    }
  })

  const sourceRow3 = templateSheet.getRow(3);
  const newRow3 = newWorksheet.getRow(3);
  sourceRow3.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow3.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  const sourceRow4 = templateSheet.getRow(4);
  let serviceFee2 = 0;
  /*let contentArray = [];
  if (contract.contentRows.length > 0) {
    contentArray = [...contract.contentRows[0].value];
  }*/
  contract.contentRows.forEach((row, idx) => {
    const newRow4 = newWorksheet.getRow(idx + 4);
    serviceFee2 += row.serviceFee ?? 0;
    sourceRow4.eachCell({includeEmpty: true}, (cell, colNumber) => {
      let targetCell = newRow4.getCell(colNumber);
      let newValue = replacePlaceholders(cell.value, {
        num: row.num,
        feeItem: row.feeItem,
        unit: row.unit,
        quantity: row.quantity,
        unitPrice: row.unitPrice,
        serviceFee: row.serviceFee,
        remark: row.remark,
      });
      targetCell.value = newValue;
      targetCell.style = cell.style;
    });
  });

  const sourceRow5 = templateSheet.getRow(5);
  const newRow5 = newWorksheet.getRow(4 + contract.contentRows.length);
  sourceRow5.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow5.getCell(colNumber);
    if (colNumber == 1) {
      mergeCell(sourceRow5, newWorksheet, newRow5);
      newWorksheet.getCell(`A${4 + contract.contentRows.length}`).alignment = {horizontal: 'center'};
      newWorksheet.mergeCells(`${4 + contract.contentRows.length}`, 1, `${4 + contract.contentRows.length}`, 2);
    } else {
      cell.value = replacePlaceholders(cell.value, {
        serviceFee2: serviceFee2,
      });
      targetCell.value = cell.value;
      targetCell.style = cell.style;
    }
  })

  const sourceRow6 = templateSheet.getRow(6);
  const newRow6 = newWorksheet.getRow(5 + contract.contentRows.length);
  sourceRow6.eachCell({includeEmpty: true}, (cell, colNumber) => {
    let targetCell = newRow6.getCell(colNumber);
    targetCell.value = cell.value;
    targetCell.style = cell.style;
  })

  styleCell(newWorksheet)
}

const generateContracts = async () => {
  loading.value = true;
  status.value = '正在加载模板...';
  let data = await getTemplateData();
  try {
    // 1. 读取模板
    const res = await fetch('../../public/template-excel.xlsx');
    const buffer = await res.arrayBuffer();
    const templateWorkbook = new ExcelJS.Workbook();
    await templateWorkbook.xlsx.load(buffer);

    // ---------------------------------------------------------
    // ✅ 修改重点：在这里创建唯一的“总工作簿”，放在循环外面
    // ---------------------------------------------------------
    const finalWorkbook = new ExcelJS.Workbook();

    importMl(data, templateWorkbook, finalWorkbook)
    importXmfyhz(data, templateWorkbook, finalWorkbook)

    for (let i = 0; i < data.contracts.length; i++) {
      const contract = data.contracts[i];
      importHtfyhz(data, contract, templateWorkbook, finalWorkbook);
      importHtfyhzmx(data, contract, templateWorkbook, finalWorkbook);
      importTzgmjsmx(data, contract, templateWorkbook, finalWorkbook);
      importGzlfjsmx(data, contract, templateWorkbook, finalWorkbook);
      importGsfjsmx(data, contract, templateWorkbook, finalWorkbook);
      importQtfwjsmx(data, contract, templateWorkbook, finalWorkbook);
    }

    // ---------------------------------------------------------
    // ✅ 修改重点：循环结束后，统一导出这一个文件
    // ---------------------------------------------------------
    status.value = '正在导出文件...';
    const buffer2 = await finalWorkbook.xlsx.writeBuffer();
    const blob = new Blob([buffer2], {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = '所有合同汇总.xlsx'; // 统一文件名
    a.click();
    URL.revokeObjectURL(url);

    status.value = '✅ 生成成功！';
  } catch (err) {
    console.error(err);
    status.value = `❌ 错误：${err.message}`;
  } finally {
    loading.value = false;
  }
};

const generateContracts2 = async () => {
  loading.value = true;
  status.value = '正在加载模板...';

  try {
    // 1. 读取模板
    loading.value = true;
    status.value = '正在加载模板...';

    try {
      const res = await fetch('../../public/template-excel.xlsx');
      if (!res.ok) {
        throw new Error(`模板加载失败: ${res.status} ${res.statusText}`);
      }

      const buffer = await res.arrayBuffer();

      // ✅ 关键：检查 buffer 是否为空
      if (buffer.byteLength === 0) {
        throw new Error('模板文件为空，请确认 public/template-contract.xlsx 存在且非空');
      }

      const workbook = new ExcelJS.Workbook();
      await workbook.xlsx.load(buffer);

      // ✅ 调试：打印工作表信息
      console.log('Workbooks worksheets:', workbook.worksheets.map(ws => ws.name));
      console.log('Workbooks worksheet count:', workbook.worksheets.length);

      // ✅ 安全获取工作表：优先用名称 "Sheet1"，再用索引 1
      let worksheet = workbook.getWorksheet('Sheet2') || workbook.getWorksheet(4);

      if (!worksheet) {
        throw new Error(`未找到工作表！可用工作表: [${workbook.worksheets.map(ws => ws.name).join(', ')}]`);
      }

      // ✅ 再次确认 worksheet 有效
      if (!worksheet.getRow) {
        throw new Error('获取的工作表无效：缺少 getRow 方法');
      }

      // 2. 获取数据行模板（第4行）
      const templateRow = worksheet.getRow(3);
      if (!templateRow || !templateRow.getCell(2).value?.toString().includes('{{code}}')) {
        throw new Error('模板格式错误：第4行应为数据行占位符，含 {{code}} 等占位符');
      }

      // 3. 为每个合同生成
      for (let i = 0; i < contracts.value.length; i++) {
        const contract = contracts.value[i];
        status.value = `生成合同 ${i + 1}/${contracts.value.length}：${contract.contractName}`;

        // 创建新工作簿（避免复用污染）
        const newWorkbook = new ExcelJS.Workbook();
        const newWorksheet = newWorkbook.addWorksheet('Sheet' + i);

        // 复制表头（第1-3行）
        for (let r = 1; r <= 3; r++) {
          const row = worksheet.getRow(r);
          const newRow = newWorksheet.getRow(r);
          for (let c = 1; c <= 10; c++) {
            const cell = row.getCell(c);
            newRow.getCell(c).value = replacePlaceholders(cell.value, {
              contractName: contract.contractName
            });
          }
        }

        // 插入数据行（从第4行开始）
        let rowIndex = 4;
        contract.items.forEach((item, idx) => {
          const newRow = newWorksheet.getRow(rowIndex++);
          // 复制模板行格式（字体、边框等）
          templateRow.eachCell((cell, colNumber) => {
            const newValue = replacePlaceholders(cell.value, {
              rowIndex: idx + 1,
              code: item.code,
              name: item.name,
              amount: item.amount,
              finalFee: item.finalFee,
              fee: item.fee,
              contractName: contract.contractName
            });
            newRow.getCell(colNumber).value = newValue;

            // 保持样式（可选）
            if (cell.style) {
              newRow.getCell(colNumber).style = cell.style;
            }
          });
        });

        // 插入合计行（原第12行位置）
        const totalRow = newWorksheet.getRow(rowIndex);
        totalRow.getCell(1).value = '12';
        totalRow.getCell(2).value = '咨询服务费用合计';
        totalRow.getCell(7).value = contract.totalFinalFee; // 费用列
        totalRow.getCell(8).value = ''; // 折算单价留空

        // 4. 导出文件
        await exportFile(newWorkbook, contract.contractName, contract.contractName);
        status.value = `✅ 成功生成 ${contracts.value.length} 份合同！`;
      }
    } catch (err) {
      console.error('生成合同失败:', err);
      status.value = `❌ 错误：${err.message}`;
      loading.value = false;
    }
  } catch (err) {
    console.error(err);
    status.value = `❌ 错误：${err.message}`;
  } finally {
    loading.value = false;
  }
};

// 占位符替换函数
const replacePlaceholders = (value, data) => {
  if (typeof value !== 'string') return value;

  let result = value;
  Object.entries(data).forEach(([key, val]) => {
    const placeholder = `{{${key}}}`;
    result = result.replace(new RegExp(placeholder, 'g'), val);
  });
  return result;
};

// 导出单个文件
const exportFile = async (workbook, fileName, displayName) => {
  const buffer = await workbook.xlsx.writeBuffer();
  const blob = new Blob([buffer], {
    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  });
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `合同服务费用汇总表-${fileName}.xlsx`;
  a.click();
  window.URL.revokeObjectURL(url);
};


const exportReport = async () => {
  try {
    const now = new Date()
    const projectInfoRaw = await kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY)
    const projectName = isNonEmptyString(projectInfoRaw?.projectName)
        ? sanitizeFileNamePart(projectInfoRaw.projectName)
        : t('tab.messages.defaultProjectName')
    const fileName = `${formatExportTimestamp(now)}-${projectName}${t('tab.messages.reportFileSuffix')}`
    const blobUrl = await exportFile(fileName, () => buildExportReportPayload(), () => {
      showReportExportProgress(30, t('tab.messages.reportGenerating'))
    })
    finishReportExportProgress(true, t('tab.messages.reportExportDone'), blobUrl)
  } catch (error) {
    console.error('export report failed:', error)
    finishReportExportProgress(false, t('tab.messages.reportExportFailedRetry'))

  } finally {
    dataMenuOpen.value = false
  }
}

/**
 * 根据 groupCode 将扁平数据转换为父子层级结构
 * @param detailRows 原始数据数组
 * @returns 父子层级结构数组
 */
const buildTreeByGroupCode = (detailRows: any[]) => {
  if (!Array.isArray(detailRows)) return []

  // 1. 按 groupCode 分组
  const groupMap = new Map<string, any[]>()
  for (const row of detailRows) {
    const groupCode = row.groupCode || ''
    if (!groupCode) continue

    if (!groupMap.has(groupCode)) {
      groupMap.set(groupCode, [])
    }
    groupMap.get(groupCode)!.push(row)
  }

  // 2. 构建父子结构
  const treeData: any[] = []

  for (const [groupCode, items] of groupMap) {
    // 找到该组的父节点（isGroupRow: true 的项）
    let parentItem = items.find(item => item.isGroupRow === true)

    // 查找该组中 amount 有值的记录
    const itemWithValue = items.find(item => item.amount != null)

    // 如果父节点存在但 amount 为空，且组内有其他记录有值，则合并/覆盖
    if (parentItem && itemWithValue && parentItem.amount == null) {
      parentItem = { ...parentItem, ...itemWithValue }
    } else if (!parentItem && itemWithValue) {
      // 如果没有明确的父节点，则使用有值的那条作为父节点基础
      parentItem = { ...itemWithValue, isGroupRow: true }
    }

    if (parentItem) {
      // 找到该组的所有子节点（isGroupRow: false 的项，且排除父节点 ID）
      const children = items
          .filter(item => item.isGroupRow === false && item.id !== parentItem.id)
          .map(child => ({
            ...child,
            children: []
          }))

      treeData.push({
        ...parentItem,
        children: children.length > 0 ? children : undefined
      })
    }
  }


  return treeData
}

/**
 * 导出Word文档（基于模板）
 * 使用 docxtemplater + pizzip 进行模板渲染
 * 
 * 模板语法说明：
 * - 文本替换：{projectName}
 * - 循环：{#contracts}...{/contracts}
 * - 条件：{#hasData}...{/hasData}
 * - 图片：{%image}
 * - HTML内容：{^htmlContent}
 */
const exportWord = async () => {
  try {
    showReportExportProgress(10, t('tab.messages.reportGenerating') || '正在生成Word文档...')
    
    // 1. 加载项目数据
    const projectInfoRaw = await kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY)
    const projectScaleRaw = await kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY)
    const projectTreeArray = buildTreeByGroupCode(projectScaleRaw.detailRows)
    debugger
    console.log('父子结构数据:', projectTreeArray)
    // const saved = await kvStore.getItem<ContractItem[]>('ht-card-v1')
    const [year, month, day] = projectInfoRaw.preparedDate.split('-');
    const projectName = projectInfoRaw.projectName;
    const preparedDate = `${year}年${month}月${day}日`;

    const c2Item = projectTreeArray.find(item => item.groupCode === 'C2');
    const text1 = c2Item.children
        .filter(child => child.amount != null)
        .map(child =>
        `${child.majorName}：${child.amount}（万元）`
    );

    const c1Item = projectTreeArray.find(item => item.groupCode === 'C1');
    const text2 = c1Item.children
        .filter(child => child.amount != null)
        .map(child =>
        `${child.majorName}：${child.amount}（万元）`
    );
    const c3Item = projectTreeArray.find(item => item.groupCode === 'C3');
    const c4Item = projectTreeArray.find(item => item.groupCode === 'C4');
    const c5Item = projectTreeArray.find(item => item.groupCode === 'C5');
    // 2. 准备模板数据
    let templateData: Record<string, any> = {
      projectName: projectName,
      preparedBy: projectInfoRaw.preparedBy,
      reviewedBy: projectInfoRaw.reviewedBy,
      preparedCompany: projectInfoRaw.preparedCompany,
      preparedDate: preparedDate,
      overview: projectInfoRaw.overview,
      totalAmount: `${projectScaleRaw.totalAmount??0}万元`,
      text1: text1,
      text2: text2,
      text3: `${c3Item.majorName}：${c3Item.amount??0}（万元）`,
      text4: `${c4Item.majorName}：${c4Item.amount??0}（万元）`,
      text5: `${c5Item.majorName}：${c5Item.amount??0}（万元）`,
      contractCount: contractList.value.length,
      exportDate: new Date().toLocaleDateString('zh-CN'),
      contracts: [] as any[],
      desc: projectInfoRaw.desc
    }
    // const C2 = projectTreeArray

    // 3. 收集所有合同数据
    let projectBudget = 0;
    let contractFees = [];
    for (let i = 0; i < contractList.value.length; i++) {
      const contract = contractList.value[i]
      const baseInfo = zxFwPricingStore.getKeyState(`ht-base-info-${contract.id}`)

      // 查询咨询服务数据
      const checkedZx = await useDataStore().query([
        { field: 'id', value: `${contract.id}-checkedZx`, operator: 'eq' }
      ])
      
      const fallbackIds = new Set(
        checkedZx.length > 0
          ? (checkedZx[0]?.fallbackIds || checkedZx[0]?.ids || [])
          : []
      )
      
      const zxRows = await useDataStore().query([
        { field: 'type', value: `${contract.id}-zxFw`, operator: 'eq' }
      ])
      const zxRowCodes = [
        ...new Set(
            zxRows
                .map(row => row.code)
                .filter(Boolean)
                .flatMap(code => {
                  const mapping = {
                    'D1': ['D3-1', 'D3-2', 'D3-3', 'D3-4', 'D3-6-1', 'D3-7', 'D4-6', 'D5-1'],
                    'D2-1': ['D3-1', 'D3-2', 'D3-3'],
                    'D2-2': ['D3-4', 'D3-5', 'D3-6'],
                    'D3-1': ['D3-1'],
                    'D3-2': ['D3-2'],
                    'D3-3': ['D3-3'],
                    'D3-4': ['D3-4'],
                    'D3-6-1': ['D3-6-1'],
                    'D3-7': ['D3-7'],
                    'D4-6': ['D4-6'],
                    'D5-1': ['D5-1']
                  }
                  return mapping[code] || []
                })
        ),
        'D0-1',
        'D0'
      ]
      const contentRows = await useDataStore().query([
        { field: 'type', value: `${contract.id}-content`, operator: 'eq' }
      ]);
      let contentArray = contentRows.length > 0 ? contentRows[0].value : null;

      // 将 zxRowCodes 转换为 Set 提高查找效率
      const validCodeSet = new Set(zxRowCodes)

      // 过滤 contentArray，只保留 code 在 zxRowCodes 中的项
      if (Array.isArray(contentArray)) {
        contentArray = contentArray.filter(item => {
          const code = item?.code || ''
          return code && validCodeSet.has(code) && item.checked
        })
      }

      // 根据 code 和 path[0] 构建父子结构
      const contentTreeMap = new Map<string, any>()

      if (Array.isArray(contentArray)) {
        for (const item of contentArray) {
          const code = item?.code || ''
          if (!code) continue

          // 获取父类名称（path 的第一个元素）
          const parentName = Array.isArray(item.path) && item.path.length > 0
              ? item.path[0]
              : item.serviceGroup || code

          // 如果该分组还不存在，创建父节点
          if (!contentTreeMap.has(code)) {
            contentTreeMap.set(code, {
              code: code,
              groupName: parentName,
              children: []
            })
          }

          // 将当前项添加到对应分组的子节点中
          const group = contentTreeMap.get(code)
          if (group) {
            group.children.push({
              id: item.id || '',
              content: item.content || '',
              type: item.type || '',
              dictOrder: item.dictOrder || 0,
              checked: item.checked || false,
              remark: item.remark || ''
            })
          }
        }
      }

      // 转换为数组格式
      const contentTreeArray = Array.from(contentTreeMap.values())
      console.log('工作内容树形结构:', contentTreeArray)

      // 计算总费用
      let contractTotalFee = 0
      const serviceList = zxRows
        .filter(row => !row.isFixed)  // 排除固定行（小计行）
        .map(row => {
          const fee = row.finalFee || 0
          contractTotalFee += fee
          return {
            code: row.code || '',
            name: row.name || '',
            finalFee: fee,
            process: row.process === 1 ? '审查' : '编制',
            remark: row.remark || ''
          }
        })

      const contractType = zxRows.map(item =>
          `${item.name}`
      );
      let contractBudget = zxFwPricingStore.getBaseSubtotal(contract.id)
      contractFees.push({
        contractName: `${contract.name}`,
        contractBudget: `${contractBudget}元`,
      })
      projectBudget += contractBudget;
      templateData.contracts.push({
        contractName: `${contract.name}`,
        contractType: `${contract.name}：${contractType}`,
        contractId: contract.id,
        services: serviceList,
        totalFee: contractTotalFee,
        serviceCount: serviceList.length,
        quality: baseInfo == null ? '' : baseInfo.quality,
        duration: baseInfo == null ? '' : baseInfo.duration,
        work: contentTreeArray,
      })
    }
    const contractStr = contractFees.map((item, index) => {
      const contractNum = index + 1;
      const budget = item.contractBudget === "null元" ? "待定" : item.contractBudget;
      return `${item.contractName}合同${item.contractName}${budget}`;
    }).join('，');
    let text6 = `本项目造价咨询服务总费用为${projectBudget}元。其中，${contractStr}。计算过程详见相关计算表格。`;
    templateData = {...templateData, text6: text6}
    showReportExportProgress(30, '正在加载Word模板...')
    
    // 4. 尝试加载Word模板文件（.docx格式）
    let useTemplate = false
    let templateArrayBuffer: ArrayBuffer | null = null
    
    try {
      // 首先尝试加载 .docx 模板文件
      const response = await fetch('../../public/template-doc.docx')

      if (response.ok) {
        templateArrayBuffer = await response.arrayBuffer()
        useTemplate = true
        console.log('✅ 成功加载Word模板文件')
      } else {
        console.warn('⚠️ 未找到Word模板文件（template-report.docx），将生成基础文档')
      }
    } catch (error) {
      console.warn('⚠️ 加载Word模板失败，将生成基础文档', error)
    }
    
    // 5. 根据是否有模板选择不同的处理方式
    if (useTemplate && templateArrayBuffer) {
      // ===== 使用模板渲染 =====
      showReportExportProgress(50, '正在渲染模板数据...')
      
      try {
        // 使用 PizZip 加载模板
        const zip = new PizZip(templateArrayBuffer)
        
        // 创建 Docxtemplater 实例
        const doc = new Docxtemplater(zip, {
          paragraphLoop: true,
          linebreaks: true,
          delimiters: { start: '{', end: '}' }
        })
        
        showReportExportProgress(70, '正在填充数据...')
        
        // 渲染模板数据
        doc.render(templateData)
        
        showReportExportProgress(90, '正在生成文件...')
        
        // 生成 Blob
        const blob = doc.getZip().generate({
          type: 'blob',
          mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
          compression: 'DEFLATE'
        })
        
        // 下载文件
        const fileName = `${sanitizeFileNamePart(projectName)}-计价报告.docx`
        saveAs(blob, fileName)
        
        finishReportExportProgress(true, 'Word文档导出成功')
        dataMenuOpen.value = false
        return
      } catch (templateError) {
        console.error('❌ 模板渲染失败，使用基础方案', templateError)
        // 如果模板渲染失败，降级到基础方案
        useTemplate = false
      }
    }
    
    // ===== 基础方案：生成简单的Word文档 =====
    showReportExportProgress(50, '正在生成文档...')
    
    // 构建HTML内容（Word可以解析HTML格式）
    const htmlContent = `
<!DOCTYPE html>
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word'>
<head>
  <meta charset='utf-8'>
  <title>${projectName}-计价报告</title>
  <style>
    body { font-family: 'Microsoft YaHei', SimSun, sans-serif; font-size: 12pt; line-height: 1.5; }
    h1 { text-align: center; font-size: 18pt; margin: 20px 0; }
    h2 { font-size: 14pt; margin: 15px 0 10px 0; border-bottom: 1px solid #ccc; padding-bottom: 5px; }
    .info { margin: 10px 0; }
    .contract { margin: 20px 0; padding: 10px; border: 1px solid #ddd; }
    .service-list { margin: 10px 0; }
    .service-item { margin: 5px 0; padding-left: 20px; }
    table { border-collapse: collapse; width: 100%; margin: 10px 0; }
    th, td { border: 1px solid #999; padding: 8px; text-align: left; }
    th { background-color: #f0f0f0; font-weight: bold; }
    .total { font-weight: bold; color: #d32f2f; }
  </style>
</head>
<body>
  <h1>交通工程造价技术中介服务收费报告</h1>
  
  <div class="info">
    <p><strong>项目名称：</strong>${projectName}</p>
    <p><strong>导出日期：</strong>${templateData.exportDate}</p>
  </div>
  
  <hr />
  
  ${templateData.contracts.map(contract => `
    <div class="contract">
      <h2>合同段：${contract.contractName}</h2>
      <p><strong>合同ID：</strong>${contract.contractId}</p>
      <p><strong>服务项目数：</strong>${contract.serviceCount} 项</p>
      <p class="total"><strong>合同总费用：</strong>${(contract.totalFee / 10000).toFixed(2)} 万元（¥${contract.totalFee.toFixed(2)} 元）</p>
      
      <h3>服务明细</h3>
      ${contract.services.length > 0 ? `
        <table>
          <tr>
            <th>序号</th>
            <th>编码</th>
            <th>服务名称</th>
            <th>确认金额（元）</th>
            <th>工作环节</th>
            <th>备注</th>
          </tr>
          ${contract.services.map((service, index) => `
            <tr>
              <td>${index + 1}</td>
              <td>${service.code}</td>
              <td>${service.name}</td>
              <td>${service.finalFee.toFixed(2)}</td>
              <td>${service.process}</td>
              <td>${service.remark || '-'}</td>
            </tr>
          `).join('')}
        </table>
      ` : '<p>暂无服务项目</p>'}
    </div>
  `).join('')}
  
  <hr />
  <p style="text-align: center; color: #666; font-size: 10pt;">
    报告生成时间：${templateData.exportDate} | 交通工程造价技术中介服务收费项目及标准表
  </p>
</body>
</html>
    `
    
    // 创建 Blob 并下载
    const blob = new Blob(['\ufeff' + htmlContent], { 
      type: 'application/msword;charset=utf-8' 
    })
    const fileName = `${sanitizeFileNamePart(projectName)}-计价报告.doc`
    saveAs(blob, fileName)
    
    finishReportExportProgress(true, 'Word文档导出成功')
    dataMenuOpen.value = false
    
  } catch (error) {
    console.error('export word failed:', error)
    finishReportExportProgress(false, 'Word导出失败')
    showMessageDialog('错误', `Word导出失败：${error instanceof Error ? error.message : '未知错误'}`)
    dataMenuOpen.value = false
  }
}

const triggerImport = () => {
  importFileRef.value?.click()
}

const resolveProjectImportErrorMessage = (error: unknown) => {
  const message = error instanceof Error ? error.message : ''
  if (message === 'ZW_CRYPTO_UNAVAILABLE') {
    return t('tab.messages.importCryptoUnavailable')
  }
  return t('tab.messages.importInvalidFile')
}

const prepareImportPayloadFromFile = async (
    file: File,
    options?: { skipConfirm?: boolean }
) => {
  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)
  if (!isDataPackageLike(payload)) {
    throw new Error('INVALID_DATA_PACKAGE')
  }
  pendingImportPayload.value = payload
  pendingImportFileName.value = file.name
  if (options?.skipConfirm) {
    await confirmImportOverride()
    return
  }
  importConfirmOpen.value = true
}

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

  try {
    await prepareImportPayloadFromFile(file)
  } catch (error) {
    console.error('import failed:', error)
    showMessageDialog(t('tab.messages.importFailedTitle'), resolveProjectImportErrorMessage(error))
  } finally {
    input.value = ''
  }
}

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

const confirmImportOverride = async () => {
  const payload = pendingImportPayload.value
  if (!payload) return
  try {
    await writeForage(projectDefaultForage, 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)
      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 zxFwPricingKeysState = readPersistedState('zxFwPricingKeys')
    if (zxFwPricingKeysState) {
      zxFwPricingKeysStore.$patch(zxFwPricingKeysState as any)
    }

    const zxFwPricingHtFeeState = readPersistedState('zxFwPricingHtFee')
    if (zxFwPricingHtFeeState) {
      zxFwPricingHtFeeStore.$patch(zxFwPricingHtFeeState as any)
    }

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

    // 导入快照可能携带旧语言标题，先按当前 locale 归一化一次再持久化。
    syncTabLabelsByLocale()

    await Promise.all([
      tabStore.$persistNow?.(),
      zxFwPricingStore.$persistNow?.(),
      zxFwPricingKeysStore.$persistNow?.(),
      zxFwPricingHtFeeStore.$persistNow?.(),
      kvStore.$persistNow?.()
    ])
    dataMenuOpen.value = false
    window.location.reload()
  } catch (error) {
    console.error('import apply failed:', error)
    showMessageDialog(t('tab.messages.importFailedTitle'), t('tab.messages.importWriteError'))
  } finally {
    cancelImportConfirm()
  }
}

const handleReset = async () => {
  if (isResetting.value) return
  resetUiWorkspaceMode.value = readWorkspaceMode()
  const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
  const resetStartedAt = Date.now()
  const allProjectIds = Array.from(new Set(['default', 'quick', ...listProjects().map(item => item.id)]))
  const deleteIndexedDBByName = (dbName: string) =>
      new Promise<void>((resolve) => {
        try {
          const request = window.indexedDB?.deleteDatabase(dbName)
          if (!request) {
            resolve()
            return
          }
          request.onsuccess = () => resolve()
          request.onerror = () => resolve()
          request.onblocked = () => {
            // 被阻塞时也继续流程，刷新后浏览器会重试清理。
            resolve()
          }
        } catch (_error) {
          resolve()
        }
      })

  const purgeKnownIndexedDB = async () => {
    await Promise.all(allProjectIds.map(id => deleteIndexedDBByName(getProjectDbName(id))))
  }

  try {
    isResetting.value = true
    dataMenuOpen.value = false
    projectMenuOpen.value = false
    emitResetAll()
    window.dispatchEvent(new CustomEvent('jgjs-release-project-lock'))
    const themePreference =
        typeof localStorage !== 'undefined' ? localStorage.getItem(THEME_PREFERENCE_KEY) : null
    const localePreference =
        typeof localStorage !== 'undefined' ? localStorage.getItem(I18N_LOCALE_KEY) : null
    const uiPrefsSnapshot =
        typeof localStorage !== 'undefined' ? localStorage.getItem(UI_PREFS_STORAGE_KEY) : null

    // 1) 仅清持久化层，不先改当前页面内存态，避免“先切首页再刷新”的二次闪烁。
    localStorage.clear()
    if (themePreference != null) {
      localStorage.setItem(THEME_PREFERENCE_KEY, themePreference)
    }
    if (localePreference != null) {
      localStorage.setItem(I18N_LOCALE_KEY, localePreference)
    }
    if (uiPrefsSnapshot != null) {
      localStorage.setItem(UI_PREFS_STORAGE_KEY, uiPrefsSnapshot)
    }
    sessionStorage.clear()
    await projectDefaultForage.clear()

    // 2) 清 pinia 分库持久化
    await Promise.all(
        getPiniaPersistStores().map(async ({store}) => {
          await store.clear()
        })
    )

    // 3) 清插件按 store 维护的持久化 key（双保险）
    const clearTasks: Promise<void>[] = []
    if (tabStore.$clearPersisted) clearTasks.push(tabStore.$clearPersisted())
    if (zxFwPricingStore.$clearPersisted) clearTasks.push(zxFwPricingStore.$clearPersisted())
    if (kvStore.$clearPersisted) clearTasks.push(kvStore.$clearPersisted())
    await Promise.all(clearTasks)

    // 4) 强制删除已知 IndexedDB，避免页面卸载阶段旧组件回写脏数据
    await purgeKnownIndexedDB()

    // 5) 需要保留的最小标记恢复
    writeProjectIdToUrl('default')
    writeWorkspaceMode('project')
    localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')

    // 6) 保证重置按钮进度动画至少可见 2s，避免“刚点就消失”。
    const elapsed = Date.now() - resetStartedAt
    if (elapsed < RESET_MIN_LOADING_MS) {
      await wait(RESET_MIN_LOADING_MS - elapsed)
    }

    // 7) 直接刷新，交由默认初始化重建空状态
    window.location.reload()
  } catch (error) {
    console.error('reset failed:', error)
    isResetting.value = false
    resetUiWorkspaceMode.value = ''
  }
}

const handleResetConfirmOpenChange = (open: boolean) => {
  if (isResetting.value) return
  resetConfirmOpen.value = open
}

onMounted(async () => {
  await loadContractList()
  currentProjectId.value = readCurrentProjectId()
  upsertProject(currentProjectId.value)
  void refreshProjectList()
  window.addEventListener('mousedown', handleGlobalMouseDown)
  window.addEventListener('keydown', handleGlobalKeyDown)
  window.addEventListener('resize', scheduleUpdateTabTitleOverflow)
  const skipWorkspaceImportConfirm = consumePendingHomeImportSkipConfirm()
  void nextTick(() => {
    bindTabStripScroll()
    ensureActiveTabVisible()
    updateTabScrollButtons()
    scheduleUpdateTabTitleOverflow()
    scheduleRestoreTabInnerScrollTop(tabStore.activeTabId)
  })
  void (async () => {
    const pendingHomeImportFile = await consumePendingHomeImportFile()
    if (!pendingHomeImportFile) return
    await prepareImportPayloadFromFile(pendingHomeImportFile, {skipConfirm: skipWorkspaceImportConfirm}).catch(error => {
      console.error('home import failed:', error)
      showMessageDialog(t('tab.messages.importFailedTitle'), resolveProjectImportErrorMessage(error))
    })
  })()
  void (async () => {
    if (await shouldAutoOpenGuide()) {
      openUserGuide(0)
    }
  })()
})

onBeforeUnmount(() => {
  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(
    () => workspaceModeForUi.value,
    (mode, prevMode) => {
      if (typeof window === 'undefined') return
      if (mode === 'quick') {
        if (quickModeThemePreferenceSnapshot.value === undefined) {
          quickModeThemePreferenceSnapshot.value = localStorage.getItem(THEME_PREFERENCE_KEY)
        }
        isDark.value = false
        document.documentElement.classList.remove('dark')
        return
      }
      if (prevMode !== 'quick' || quickModeThemePreferenceSnapshot.value === undefined) return
      const snapshot = quickModeThemePreferenceSnapshot.value
      quickModeThemePreferenceSnapshot.value = undefined
      if (snapshot == null) {
        localStorage.removeItem(THEME_PREFERENCE_KEY)
        isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
        return
      }
      localStorage.setItem(THEME_PREFERENCE_KEY, snapshot)
      isDark.value = snapshot === 'true'
    },
    {immediate: true}
)

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

watch(
    () => tabStore.tabs.map((t: any) => 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()
      })
    }
)

watch(
    () => locale.value,
    () => {
      syncTabLabelsByLocale()
    },
    {immediate: true}
)
</script>

<template>
  <ToastProvider>
    <TooltipProvider>
      <div class="flex flex-col w-full h-screen bg-background overflow-hidden">
        <!-- Top Title Bar -->
        <div class="flex items-center justify-between h-12 px-4 border-b bg-muted/10 flex-none">
          <span class="text-lg font-bold text-[#1976d2]">工程计算入口</span>

          <!-- Right Button Group Container -->
          <div class="flex shrink-0 self-center items-center gap-2">
            <Button
                v-if="workspaceModeForUi !== 'quick'"
                variant="ghost"
                size="icon"
                class="h-9 w-9 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"
                :disabled="isResetting"
                :title="isDark ? t('tab.toolbar.dark') : t('tab.toolbar.light')"
                :aria-label="isDark ? t('tab.toolbar.dark') : t('tab.toolbar.light')"
                @click="handleToggleTheme"
            >
              <component
                  :is="isDark ? Moon : Sun"
                  class="h-4 w-4 transition-transform duration-200"
                  :class="isDark ? 'rotate-0' : 'rotate-180'"
              />
            </Button>
            <Button
                v-if="workspaceModeForUi !== 'quick'"
                variant="ghost"
                size="icon"
                class="h-9 w-9 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"
                :disabled="isResetting"
                :title="t('tab.toolbar.language')"
                :aria-label="t('tab.toolbar.language')"
                @click="toggleLocale"
            >
              <span class="text-base font-medium">中</span>
            </Button>
            <Button
                v-if="workspaceModeForUi !== 'quick'"
                variant="default"
                size="sm"
                class="h-9 shrink-0 cursor-pointer bg-[#1a1a1a] text-white px-3 text-sm font-medium hover:bg-[#1a1a1a]/90"
                :disabled="isResetting"
                @click="exportData"
            >
              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                   stroke-linecap="round" stroke-linejoin="round">
                <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
                <polyline points="17 21 17 13 7 13 7 21"/>
                <polyline points="7 3 7 8 15 8"/>
              </svg>
              保存数据
            </Button>
            <div class="relative" ref="dataMenuRef">
              <Button
                  variant="outline"
                  size="sm"
                  class="h-9 shrink-0 cursor-pointer rounded-md border-slate-200 bg-white px-3 text-sm font-medium text-[#333] shadow-sm transition hover:bg-slate-50"
                  :disabled="isResetting"
                  @click="dataMenuOpen = !dataMenuOpen"
              >
                <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                     stroke-linecap="round" stroke-linejoin="round">
                  <path
                      d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
                  <polyline points="3.29 7 12 12 20.71 7"/>
                  <line x1="12" y1="22" x2="12" y2="12"/>
                </svg>
                导入/导出
              </Button>
              <!-- 导入/导出下拉菜单 -->
              <Transition name="fade">
                <div v-if="dataMenuOpen"
                     class="absolute right-0 top-12 z-50 w-48 rounded-lg border bg-card p-1 shadow-lg">
                  <button
                      class="flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80 transition-colors"
                      @click="exportReport2"
                  >
                    导出表格
                  </button>
                  <button
                      class="flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80 transition-colors mt-1"
                      @click="exportWord"
                  >
                    导出Word文档
                  </button>
                  <div class="my-1 h-px bg-border"></div>
                  <!--                  <button
                                        class="flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80 transition-colors"
                                        @click="triggerImport"
                                    >
                                      导入数据
                                    </button>
                                    <button
                                        v-if="workspaceModeForUi !== 'quick'"
                                        class="flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-muted/80 transition-colors mt-1"
                                        @click="exportData"
                                    >
                                      导出数据
                                    </button>
                                    <div class="my-1 h-px bg-border"></div>
                                    <button
                                        class="flex w-full cursor-pointer items-center rounded-md px-3 py-2 text-left text-sm text-foreground hover:bg-red-50 hover:text-red-600 transition-colors"
                                        @click="resetConfirmOpen = true; dataMenuOpen = false"
                                    >
                                      重置数据
                                    </button>-->
                </div>
              </Transition>
            </div>
            <Button
                v-if="workspaceModeForUi !== 'quick'"
                variant="outline"
                size="sm"
                class="h-9 shrink-0 cursor-pointer rounded-md border-slate-200 bg-white px-3 text-sm font-medium text-[#333] shadow-sm transition hover:bg-slate-50"
                :disabled="isResetting"
                @click="projectMenuOpen = !projectMenuOpen; if (projectMenuOpen) void refreshProjectList()"
            >
              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                   stroke-linecap="round" stroke-linejoin="round">
                <line x1="8" y1="6" x2="21" y2="6"/>
                <line x1="8" y1="12" x2="21" y2="12"/>
                <line x1="8" y1="18" x2="21" y2="18"/>
                <line x1="3" y1="6" x2="3.01" y2="6"/>
                <line x1="3" y1="12" x2="3.01" y2="12"/>
                <line x1="3" y1="18" x2="3.01" y2="18"/>
              </svg>
              项目管理
            </Button>
            <Button
                variant="outline"
                size="sm"
                class="h-9 shrink-0 cursor-pointer rounded-md border-slate-200 bg-white px-3 text-sm font-medium text-[#333] shadow-sm transition hover:bg-slate-50"
                @click="returnToHome"
            >
              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                   stroke-linecap="round" stroke-linejoin="round">
                <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
                <polyline points="9 22 9 12 15 12 15 22"/>
              </svg>
              返回首页
            </Button>
          </div>
        </div>


        <!-- Tab Strip and Right Button Group Container -->
        <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">
          <!-- Tab Strip Container -->
          <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, index }">
                  <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',
                    index !== 0 ? '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="index !== 0" 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>
        <!--        <div class="flex items-center h-12 px-4 border-b bg-muted/10 flex-none">-->
        <!--          <span class="text-lg font-bold text-[#1976d2]">工程计算系统</span>-->
        <!--        </div>-->
        <!--        <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, index }">-->
        <!--                  <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',-->
        <!--                    index !== 0 ? '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="index !== 0" 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-2">-->
        <!--            <Button-->
        <!--                v-if="workspaceModeForUi !== 'quick'"-->
        <!--                variant="ghost"-->
        <!--                size="icon"-->
        <!--                class="h-9 w-9 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"-->
        <!--                :disabled="isResetting"-->
        <!--                :title="isDark ? t('tab.toolbar.dark') : t('tab.toolbar.light')"-->
        <!--                :aria-label="isDark ? t('tab.toolbar.dark') : t('tab.toolbar.light')"-->
        <!--                @click="handleToggleTheme"-->
        <!--            >-->
        <!--              <component-->
        <!--                  :is="isDark ? Moon : Sun"-->
        <!--                  class="h-4 w-4 transition-transform duration-200"-->
        <!--                  :class="isDark ? 'rotate-0' : 'rotate-180'"-->
        <!--              />-->
        <!--            </Button>-->
        <!--            <Button-->
        <!--                v-if="workspaceModeForUi !== 'quick'"-->
        <!--                variant="ghost"-->
        <!--                size="icon"-->
        <!--                class="h-9 w-9 shrink-0 cursor-pointer text-muted-foreground transition-all duration-200 hover:text-foreground"-->
        <!--                :disabled="isResetting"-->
        <!--                :title="t('tab.toolbar.language')"-->
        <!--                :aria-label="t('tab.toolbar.language')"-->
        <!--                @click="toggleLocale"-->
        <!--            >-->
        <!--              <span class="text-base font-medium">蚁</span>-->
        <!--            </Button>-->
        <!--            <Button-->
        <!--                v-if="workspaceModeForUi !== 'quick'"-->
        <!--                variant="default"-->
        <!--                size="sm"-->
        <!--                class="h-9 shrink-0 cursor-pointer bg-[#1a1a1a] text-white px-3 text-sm font-medium hover:bg-[#1a1a1a]/90"-->
        <!--                :disabled="isResetting"-->
        <!--                @click="exportData"-->
        <!--            >-->
        <!--              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>-->
        <!--              保存内容-->
        <!--            </Button>-->
        <!--            <Button-->
        <!--                variant="outline"-->
        <!--                size="sm"-->
        <!--                class="h-9 shrink-0 cursor-pointer rounded-md border-slate-200 bg-white px-3 text-sm font-medium text-[#333] shadow-sm transition hover:bg-slate-50"-->
        <!--                :disabled="isResetting"-->
        <!--                @click="dataMenuOpen = !dataMenuOpen"-->
        <!--            >-->
        <!--              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.29 7 12 12 20.71 7"/><line x1="12" y1="22" x2="12" y2="12"/></svg>-->
        <!--              导出/导入数据-->
        <!--            </Button>-->
        <!--            <Button-->
        <!--                v-if="workspaceModeForUi !== 'quick'"-->
        <!--                variant="outline"-->
        <!--                size="sm"-->
        <!--                class="h-9 shrink-0 cursor-pointer rounded-md border-slate-200 bg-white px-3 text-sm font-medium text-[#333] shadow-sm transition hover:bg-slate-50"-->
        <!--                :disabled="isResetting"-->
        <!--                @click="projectMenuOpen = !projectMenuOpen; if (projectMenuOpen) void refreshProjectList()"-->
        <!--            >-->
        <!--              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>-->
        <!--              项目列表-->
        <!--            </Button>-->
        <!--            <Button-->
        <!--                variant="outline"-->
        <!--                size="sm"-->
        <!--                class="h-9 shrink-0 cursor-pointer rounded-md border-slate-200 bg-white px-3 text-sm font-medium text-[#333] shadow-sm transition hover:bg-slate-50"-->
        <!--                @click="returnToHome"-->
        <!--            >-->
        <!--              <svg class="mr-1.5 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>-->
        <!--              返回首页-->
        <!--            </Button>-->

        <!-- Project Sidebar -->
        <Transition name="slide-in-left">
          <div v-if="projectMenuOpen" class="fixed inset-y-0 right-0 z-[100] w-[360px] bg-background shadow-2xl">
            <div class="flex flex-col h-full">
              <!-- Header -->
              <div class="flex items-center justify-between border-b px-5 py-4">
                <div>
                  <h2 class="text-lg font-semibold">{{ t('tab.toolbar.projectList') }}</h2>
                  <p class="mt-1 text-xs text-muted-foreground">
                    {{ t('tab.toolbar.projectCount', {count: projectList.length, max: MAX_PROJECT_COUNT}) }}</p>
                </div>
                <Button variant="ghost" size="icon" class="h-8 w-8" @click="projectMenuOpen = false">
                  <X class="h-4 w-4"/>
                </Button>
              </div>

              <!-- Project List -->
              <div class="flex-1 space-y-2 overflow-auto p-5">
                <div
                    v-for="project in projectList"
                    :key="project.id"
                    class="flex items-center justify-between rounded-lg border px-4 py-3 transition-colors cursor-pointer"
                    :class="isProjectOpen(project.id) ? 'border-blue-500 bg-blue-50 hover:bg-blue-100' : 'border-slate-200 bg-white hover:bg-slate-50'"
                    @click="switchToProject(project.id)"
                >
                  <div class="flex-1">
                    <div class="font-medium" :class="isProjectOpen(project.id) ? 'text-blue-600' : 'text-foreground'">
                      {{ project.name }}
                    </div>
                    <div class="mt-1 text-xs text-muted-foreground">
                      {{ t('tab.toolbar.lastEdited', {time: formatProjectEditedTime(project.updatedAt)}) }}
                    </div>
                  </div>
                  <Button
                      variant="ghost"
                      size="sm"
                      class="h-8 px-3 text-sm text-muted-foreground hover:text-red-600"
                      @click.stop="requestRemoveProjectItem(project)"
                  >
                    {{ t('common.delete') }}
                  </Button>
                </div>
              </div>

              <!-- Footer -->
              <div class="border-t p-5">
                <Button
                    variant="default"
                    size="sm"
                    class="w-full h-11 bg-blue-600 text-white px-4 text-base font-medium hover:bg-blue-700 rounded-lg"
                    @click="openCreateProjectDialog"
                >
                  {{ t('tab.toolbar.createProject') }}
                </Button>
              </div>
            </div>
          </div>
        </Transition>

        <!-- Overlay -->
        <Transition name="fade">
          <div v-if="projectMenuOpen" class="fixed inset-0 z-[90] bg-black/45" @click="projectMenuOpen = false"/>
        </Transition>

        <AlertDialogRoot :open="resetConfirmOpen" @update:open="handleResetConfirmOpenChange">
          <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">{{ t('tab.dialog.resetTitle') }}</AlertDialogTitle>
              <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                {{ t('tab.dialog.resetDesc') }}
              </AlertDialogDescription>
              <div class="mt-4 flex items-center justify-end gap-2">
                <Button variant="outline" :disabled="isResetting" @click="resetConfirmOpen = false">
                  {{ t('common.cancel') }}
                </Button>
                <Button variant="destructive" :disabled="isResetting" @click="handleReset">
                  <Loader2 v-if="isResetting" class="mr-1 h-4 w-4 animate-spin"/>
                  {{ isResetting ? t('tab.toolbar.resetting') : t('tab.dialog.confirmReset') }}
                </Button>
              </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">{{
                  t('tab.dialog.importOverrideTitle')
                }}
              </AlertDialogTitle>
              <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                {{ t('tab.dialog.importOverrideDesc', {file: pendingImportFileName || t('home.cards.pickFile')}) }}
              </AlertDialogDescription>
              <div class="mt-4 flex items-center justify-end gap-2">
                <AlertDialogCancel as-child>
                  <Button variant="outline" @click="cancelImportConfirm">{{ t('common.cancel') }}</Button>
                </AlertDialogCancel>
                <AlertDialogAction as-child>
                  <Button variant="destructive" @click="confirmImportOverride">{{
                      t('tab.dialog.confirmOverride')
                    }}
                  </Button>
                </AlertDialogAction>
              </div>
            </AlertDialogContent>
          </AlertDialogPortal>
        </AlertDialogRoot>

        <AlertDialogRoot :open="newProjectDialogOpen" @update:open="handleCreateProjectDialogOpenChange">
          <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-[480px] -translate-x-1/2 -translate-y-1/2 rounded-2xl border bg-background shadow-2xl">
              <!-- Header -->
              <div class="flex items-start justify-between border-b px-6 py-5">
                <div>
                  <AlertDialogTitle class="text-xl font-semibold">{{
                      t('tab.dialog.newProjectTitle')
                    }}
                  </AlertDialogTitle>
                  <AlertDialogDescription class="mt-1.5 text-sm text-muted-foreground">
                    选择工程行业后，进入项目计算页面
                  </AlertDialogDescription>
                </div>
                <Button variant="ghost" size="icon" class="h-8 w-8" @click="closeCreateProjectDialog">
                  <X class="h-5 w-5 text-muted-foreground"/>
                </Button>
              </div>

              <!-- Body -->
              <div class="p-6">
                <h3 class="text-base font-semibold text-foreground">{{ t('home.dialog.industry') }}</h3>
                <div class="mt-4 space-y-3">
                  <button
                      v-for="item in industryTypeList"
                      :key="`industry-${item.id}`"
                      class="w-full rounded-xl border-2 px-4 py-3.5 text-left transition-all"
                      :class="newProjectIndustry === String(item.id)
                            ? 'border-[#1a1a1a] bg-[#fafafa]' 
                            : 'border-slate-200 bg-white hover:border-slate-300'"
                      @click="newProjectIndustry = String(item.id)"
                  >
                    <div class="flex items-center justify-between">
                          <span class="text-base font-medium text-foreground">
                            {{ getIndustryDisplayName(item.id, locale) }}
                          </span>
                      <div class="flex h-6 w-6 items-center justify-center rounded-full border-2 transition-all"
                           :class="newProjectIndustry === String(item.id)
                                 ? 'border-[#1a1a1a] bg-[#1a1a1a]' 
                                 : 'border-slate-300'">
                        <Check v-if="newProjectIndustry === String(item.id)" class="h-4 w-4 text-white"/>
                      </div>
                    </div>
                  </button>
                </div>
              </div>

              <!-- Footer -->
              <div class="flex items-center justify-end gap-3 border-t px-6 py-5">
                <Button
                    variant="default"
                    size="sm"
                    class="h-11 rounded-xl bg-[#1a1a1a] px-6 text-sm font-medium text-white hover:bg-[#1a1a1a]/90"
                    :disabled="newProjectSubmitting || !newProjectIndustry"
                    @click="handleCreateProjectConfirm"
                >
                  <Loader2 v-if="newProjectSubmitting" class="mr-1.5 h-4 w-4 animate-spin"/>
                  {{ newProjectSubmitting ? t('tab.dialog.creating') : '进入项目计算' }}
                </Button>
              </div>
            </AlertDialogContent>
          </AlertDialogPortal>
        </AlertDialogRoot>

        <AlertDialogRoot :open="projectLimitDialogOpen" @update:open="projectLimitDialogOpen = $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">{{
                  t('tab.dialog.projectLimitTitle')
                }}
              </AlertDialogTitle>
              <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                {{ t('tab.dialog.projectLimitDesc', {max: MAX_PROJECT_COUNT}) }}
              </AlertDialogDescription>
              <div class="mt-4 flex items-center justify-end gap-2">
                <AlertDialogAction as-child>
                  <Button @click="projectLimitDialogOpen = false">{{ t('tab.dialog.iKnow') }}</Button>
                </AlertDialogAction>
              </div>
            </AlertDialogContent>
          </AlertDialogPortal>
        </AlertDialogRoot>

        <AlertDialogRoot :open="projectDeleteConfirmOpen" @update:open="handleProjectDeleteDialogOpenChange">
          <AlertDialogPortal>
            <AlertDialogOverlay class="fixed inset-0 z-[110] bg-black/45"/>
            <AlertDialogContent
                class="fixed left-1/2 top-1/2 z-[110] 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">{{
                  t('tab.dialog.deleteProjectTitle')
                }}
              </AlertDialogTitle>
              <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                {{
                  pendingDeleteProject?.id === currentProjectId
                      ? t('tab.dialog.deleteCurrentProjectDesc', {name: pendingDeleteProject?.name || ''})
                      : t('tab.dialog.deleteProjectDesc', {name: pendingDeleteProject?.name || ''})
                }}
              </AlertDialogDescription>
              <div class="mt-4 flex items-center justify-end gap-2">
                <Button variant="outline" @click="cancelRemoveProjectItem">{{ t('common.cancel') }}</Button>
                <Button variant="destructive" @click="confirmRemoveProjectItem">{{ t('common.confirm') }}</Button>
              </div>
            </AlertDialogContent>
          </AlertDialogPortal>
        </AlertDialogRoot>

        <AlertDialogRoot :open="messageDialogOpen" @update:open="messageDialogOpen = $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">{{ messageDialogTitle }}</AlertDialogTitle>
              <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
                {{ messageDialogDesc }}
              </AlertDialogDescription>
              <div class="mt-4 flex items-center justify-end gap-2">
                <AlertDialogAction as-child>
                  <Button @click="messageDialogOpen = false">{{ t('tab.dialog.iKnow') }}</Button>
                </AlertDialogAction>
              </div>
            </AlertDialogContent>
          </AlertDialogPortal>
        </AlertDialogRoot>
        <!--          </div>-->

        <!--        </div>-->
        <div v-if="isResetting" class="fixed inset-0 z-40 cursor-wait bg-transparent"/>

        <div class="flex-1 overflow-auto relative">
          <div
              v-if="activeTab"
              :key="`${activeTab.id}-${locale}`"
              :ref="el => setTabPanelRef(activeTabId, el)"
              class="h-full w-full animate-in fade-in duration-300"
          >
            <component :is="componentMap[activeTab.componentName]" v-bind="activeTab.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')">
            {{ t('tab.menu.closeAll') }}
          </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')">
            {{ t('tab.menu.closeLeft') }}
          </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')">
            {{ t('tab.menu.closeRight') }}
          </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')">
            {{ t('tab.menu.closeOther') }}
          </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">{{ t('tab.guide.title') }} · {{ 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="t('tab.guide.jumpToStep', { index: index + 1 })"
                        @click="jumpToGuideStep(index)"/>
              </div>
              <div class="flex items-center justify-end gap-2">
                <Button variant="ghost" @click="closeUserGuide(false)">{{ t('tab.guide.later') }}</Button>
                <Button variant="outline" :disabled="isFirstGuideStep" @click="prevUserGuideStep">{{
                    t('tab.guide.prev')
                  }}
                </Button>
                <Button @click="nextUserGuideStep">{{
                    isLastGuideStep ? t('tab.guide.finish') : t('tab.guide.next')
                  }}
                </Button>
              </div>
            </div>
          </div>
        </div>
      </div>
      <ToastRoot
          v-model:open="reportExportToastOpen"
          :duration="0"
          class="pointer-events-auto rounded-xl border border-border bg-card px-4 py-3 text-foreground shadow-lg"
          @update:open="(val) => { if (!val) dismissReportToast() }"
      >
        <div class="flex items-start justify-between gap-2">
          <ToastTitle class="text-sm font-semibold text-foreground">
            {{
              reportExportStatus === 'running' ? t('tab.toast.export') : (reportExportStatus === 'success' ? t('tab.toast.success') : t('tab.toast.failed'))
            }}
          </ToastTitle>
          <Button
              variant="ghost"
              size="sm"
              class="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
              @click="dismissReportToast"
          >
            <X class="h-3.5 w-3.5"/>
          </Button>
        </div>
        <ToastDescription class="mt-1 text-xs text-muted-foreground">{{ reportExportText }}</ToastDescription>
        <!-- <div v-if="reportExportStatus === 'success' && reportExportBlobUrl" class="mt-2 flex items-center gap-2">
          <Button size="sm" class="h-7 rounded-md px-3 text-xs" @click="openExportedReport">
            {{ t('tab.messages.openFile') }}
          </Button>
          <Button variant="ghost" size="sm" class="h-7 rounded-md px-2 text-xs text-muted-foreground" @click="dismissReportToast">
            {{ t('common.close') }}
          </Button>
        </div> -->
        <div class="mt-2 flex items-center gap-2">
          <div class="h-1.5 flex-1 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>
          <span class="shrink-0 text-[11px] tabular-nums text-muted-foreground">{{ reportExportProgress }}%</span>
        </div>
      </ToastRoot>
      <ToastViewport class="fixed bottom-5 right-5 z-[85] flex w-[380px] max-w-[92vw] flex-col gap-2 outline-none"/>
    </TooltipProvider>
  </ToastProvider>
</template>

<style scoped src="@/features/tab/tab.css"></style>
