From 35f06746fef18047ed89ff3bda1d4420848e3366 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Thu, 26 Mar 2026 10:09:16 +0800 Subject: [PATCH] 1 --- src/App.vue | 51 +++++ src/features/ht/components/zxFw.vue | 100 +++++++++- .../workbench/components/HomeEntryView.vue | 135 ++++++++++++- src/i18n/locales/en-US.ts | 9 +- src/i18n/locales/zh-CN.ts | 9 +- src/layout/tab.vue | 23 ++- src/lib/projectEvents.ts | 180 ++++++++++++++++++ tsconfig.tsbuildinfo | 2 +- 8 files changed, 489 insertions(+), 20 deletions(-) create mode 100644 src/lib/projectEvents.ts diff --git a/src/App.vue b/src/App.vue index 0613f88..2395a65 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,7 @@ import { waitForHydration } from '@/pinia/Plugin/indexdb' import localforage from 'localforage' import { buildProjectUrl, + DEFAULT_PROJECT_ID, ensureProjectIdInUrl, FORCE_HOME_QUERY_KEY, getProjectDbName, @@ -16,6 +17,7 @@ import { QUICK_PROJECT_ID } from '@/lib/workspace' import { collectActiveProjectSessionLocks, initProjectSessionLock } from '@/lib/projectSessionLock' +import { listenProjectDeleted, listenResetAll } from '@/lib/projectEvents' import { createProject, listProjects, type ProjectMeta } from '@/lib/projectRegistry' const tabStore = useTabStore() @@ -29,6 +31,10 @@ const openedProjectIds = ref([]) const closeCountdown = ref(10) let closeCountdownTimer: ReturnType | null = null let releaseLock: (() => void) | null = null +let stopProjectDeletedListener: (() => void) | null = null +let stopResetAllListener: (() => void) | null = null +let isHandlingDeletedProject = false +let isHandlingGlobalReset = false const showHomeEntry = computed(() => !tabStore.hasCompletedSetup) @@ -119,6 +125,41 @@ const handleReleaseProjectLock = () => { lockConflict.value = false } +const handleProjectDeleted = (deletedProjectId: string) => { + if (String(deletedProjectId || '').trim() !== currentProjectId.value) return + if (isHandlingDeletedProject) return + isHandlingDeletedProject = true + handleReleaseProjectLock() + tabStore.resetTabs() + tabStore.hasCompletedSetup = false + const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true }) + try { + window.close() + } catch { + // ignore and fallback to redirect + } + window.setTimeout(() => { + window.location.href = href + }, 120) +} + +const handleResetAll = () => { + if (isHandlingGlobalReset) return + isHandlingGlobalReset = true + handleReleaseProjectLock() + tabStore.resetTabs() + tabStore.hasCompletedSetup = false + const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true }) + try { + window.close() + } catch { + // ignore and fallback to redirect + } + window.setTimeout(() => { + window.location.href = href + }, 120) +} + onMounted(() => { currentProjectId.value = ensureProjectIdInUrl() refreshConflictProjectList() @@ -152,6 +193,8 @@ onMounted(() => { window.addEventListener('home-import-selected', handleImportComplete) window.addEventListener('jgjs-release-project-lock', handleReleaseProjectLock) + stopProjectDeletedListener = listenProjectDeleted(handleProjectDeleted) + stopResetAllListener = listenResetAll(handleResetAll) waitForHydration('tabs').then(() => { if (forceHomeRequest) { tabStore.resetTabs() @@ -187,6 +230,14 @@ onBeforeUnmount(() => { clearCloseCountdown() window.removeEventListener('home-import-selected', handleImportComplete) window.removeEventListener('jgjs-release-project-lock', handleReleaseProjectLock) + if (stopProjectDeletedListener) { + stopProjectDeletedListener() + stopProjectDeletedListener = null + } + if (stopResetAllListener) { + stopResetAllListener() + stopResetAllListener = null + } if (releaseLock) { releaseLock() releaseLock = null diff --git a/src/features/ht/components/zxFw.vue b/src/features/ht/components/zxFw.vue index 8971d67..30a99f8 100644 --- a/src/features/ht/components/zxFw.vue +++ b/src/features/ht/components/zxFw.vue @@ -30,7 +30,8 @@ import { } from 'reka-ui' import { Button } from '@/components/ui/button' import { TooltipProvider } from '@/components/ui/tooltip' -import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql' +import { getServiceDictEntries, getServiceDictItemById, getWorkListEntries, isIndustryEnabledByType, getIndustryTypeValue, wholeProcessTasks } from '@/sql' +import type { WorkType } from '@/sql' import { useTabStore } from '@/pinia/tab' import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useKvStore } from '@/pinia/kv' @@ -64,6 +65,19 @@ interface ZxFwViewState { detailRows: DetailRow[] } +interface WorkContentRowState { + id: string + content: string + type: WorkType + dictOrder?: number + serviceGroup?: string + serviceid?: number | null + remark: string + checked: boolean + custom: boolean + path: string[] +} + interface XmBaseInfoState { projectIndustry?: string } @@ -962,6 +976,7 @@ const handleServiceSelectionChange = async (ids: string[]) => { const nextSelectedIds = getCurrentContractState().selectedIds || [] const nextSelectedSet = new Set(nextSelectedIds) const addedIds = nextSelectedIds.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id)) + await ensureWorkContentStateForServices(addedIds) await fillPricingTotalsForServiceIds(addedIds) await ensurePricingDetailRowsForCurrentSelection() } @@ -980,6 +995,7 @@ const initializeContractState = async () => { // 重新获取所有已选服务的计价总额,确保从编辑页返回后 finalFee 和小计行都更新 const allServiceIds = getSelectedServiceIdsWithoutFixed() if (allServiceIds.length > 0) { + await ensureWorkContentStateForServices(allServiceIds) await fillPricingTotalsForServiceIds(allServiceIds) } } catch (error) { @@ -1015,6 +1031,88 @@ const loadProjectIndustry = async () => { } } +const resolveProjectIndustryId = () => { + const raw = String(projectIndustry.value || '').trim() + if (!raw) return null + if (raw.toUpperCase() === 'E2') return 0 + if (raw.toUpperCase() === 'E3') return 1 + if (raw.toUpperCase() === 'E4') return 2 + const parsed = Number(raw) + return Number.isFinite(parsed) ? parsed : null +} + +const buildDefaultWorkContentRowsForService = (serviceIdRaw: string) => { + const serviceId = Number(serviceIdRaw) + if (!Number.isFinite(serviceId)) return [] as WorkContentRowState[] + const entries = getWorkListEntries(locale.value) as Array<{ text: string; serviceid: number; order: number; type: number }> + const industryId = resolveProjectIndustryId() + const wholeProcessGroup = wholeProcessTasks.find( + item => Number(item.fid) === serviceId && Number(item.industry) === industryId + ) + const filtered = wholeProcessGroup + ? (() => { + const groupedServiceIds = Array.isArray(wholeProcessGroup.sid) + ? wholeProcessGroup.sid.map(id => Number(id)).filter(Number.isFinite) + : [] + const groupedSet = new Set(groupedServiceIds) + return entries + .filter(item => groupedSet.has(Number(item.serviceid))) + .sort((a, b) => { + const indexA = groupedServiceIds.indexOf(Number(a.serviceid)) + const indexB = groupedServiceIds.indexOf(Number(b.serviceid)) + if (indexA !== indexB) return indexA - indexB + return a.order - b.order + }) + })() + : entries.filter(item => Number(item.serviceid) === serviceId).sort((a, b) => a.order - b.order) + + const toTypeLabel = (type: number): WorkType => { + if (type === 1) return t('workContent.type.optional') as WorkType + if (type === 2) return t('workContent.type.daily') as WorkType + if (type === 3) return t('workContent.type.special') as WorkType + if (type === 4) return t('workContent.type.additional') as WorkType + return t('workContent.type.basic') as WorkType + } + + return filtered + .map((entry) => { + const content = String(entry.text || '').trim() + if (!content) return null + const serviceItem = getServiceDictItemById(entry.serviceid) as { code?: string; name?: string } | undefined + const serviceGroup = serviceItem + ? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim() + : '' + return { + id: `dict-${entry.serviceid}-${entry.order}`, + content, + type: toTypeLabel(entry.type), + dictOrder: entry.order, + serviceGroup, + serviceid: Number.isFinite(Number(entry.serviceid)) ? Number(entry.serviceid) : null, + remark: '', + checked: true, + custom: false, + path: serviceGroup ? [serviceGroup, content] : [toTypeLabel(entry.type), content] + } + }) + .filter((item): item is WorkContentRowState => Boolean(item)) +} + +const ensureWorkContentStateForService = async (serviceId: string) => { + const storageKey = `work-content-${props.contractId}-${serviceId}` + const saved = await zxFwPricingStore.loadKeyState<{ detailRows?: unknown[] }>(storageKey) + if (Array.isArray(saved?.detailRows) && saved.detailRows.length > 0) return + const defaultRows = buildDefaultWorkContentRowsForService(serviceId) + if (defaultRows.length === 0) return + zxFwPricingStore.setKeyState(storageKey, { detailRows: defaultRows }) +} + +const ensureWorkContentStateForServices = async (serviceIds: string[]) => { + const uniqueIds = Array.from(new Set(serviceIds.map(id => String(id || '').trim()).filter(Boolean))) + if (uniqueIds.length === 0) return + await Promise.all(uniqueIds.map(id => ensureWorkContentStateForService(id))) +} + watch(serviceIdSignature, () => { const availableIds = new Set(serviceDict.value.map(item => item.id)) const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id)) diff --git a/src/features/workbench/components/HomeEntryView.vue b/src/features/workbench/components/HomeEntryView.vue index 41a1473..f4f063f 100644 --- a/src/features/workbench/components/HomeEntryView.vue +++ b/src/features/workbench/components/HomeEntryView.vue @@ -44,7 +44,7 @@ import { setPendingHomeImportFile, writeWorkspaceMode } from '@/lib/workspace' -import { createProject, upsertProject } from '@/lib/projectRegistry' +import { createProject, listProjects, upsertProject } from '@/lib/projectRegistry' interface QuickProjectInfoState { projectIndustry?: string @@ -90,6 +90,10 @@ const homeImportInputRef = ref(null) const homeImportConfirmOpen = ref(false) const pendingHomeImportFile = ref(null) const pendingHomeImportFileName = ref('') +const existingProjectDialogOpen = ref(false) +const existingProjects = ref>([]) +const existingProjectLoading = ref(false) +const hasExistingProjects = ref(false) const projectIndustryLabel = computed(() => { const target = String(projectIndustry.value || '').trim() if (!target) return '' @@ -109,6 +113,13 @@ const getTodayDateString = () => { return `${year}-${month}-${day}` } +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 enterProjectCalc = () => { const projectId = getActiveProjectId() upsertProject(projectId, projectId === DEFAULT_PROJECT_ID ? t('tab.messages.defaultProjectLabel') : undefined) @@ -135,6 +146,51 @@ const openProjectCalc = async () => { projectDialogOpen.value = true } +const refreshExistingProjects = async () => { + existingProjectLoading.value = true + try { + const projects = listProjects() + .filter(item => item.id !== QUICK_PROJECT_ID) + .sort((a, b) => { + const left = new Date(a.updatedAt).getTime() + const right = new Date(b.updatedAt).getTime() + return (Number.isFinite(right) ? right : 0) - (Number.isFinite(left) ? left : 0) + }) + existingProjects.value = projects.map(project => ({ + id: project.id, + name: project.name, + updatedAt: project.updatedAt + })) + hasExistingProjects.value = projects.length > 0 + } finally { + existingProjectLoading.value = false + } +} + +const openExistingProjectDialog = async () => { + existingProjectDialogOpen.value = true + await refreshExistingProjects() +} + +const closeExistingProjectDialog = () => { + existingProjectDialogOpen.value = false +} + +const enterExistingProject = (projectIdRaw: string) => { + const projectId = String(projectIdRaw || '').trim() + if (!projectId) return + upsertProject(projectId, projectId === DEFAULT_PROJECT_ID ? t('tab.messages.defaultProjectLabel') : undefined) + writeProjectIdToUrl(projectId) + writeWorkspaceMode('project') + tabStore.enterWorkspace({ + id: PROJECT_TAB_ID, + title: t('home.projectCalcTab'), + componentName: 'ProjectCalcView' + }) + tabStore.hasCompletedSetup = true + closeExistingProjectDialog() +} + const closeProjectCalcDialog = () => { projectDialogOpen.value = false } @@ -258,11 +314,15 @@ const confirmHomeImport = () => { const file = pendingHomeImportFile.value if (!file) return setPendingHomeImportFile(file, { skipWorkspaceConfirm: true }) - window.dispatchEvent(new CustomEvent('home-import-selected', { - detail: { file } - })) - const project = createProject() - writeProjectIdToUrl(project.id) + const currentProjectId = getActiveProjectId() + const projects = listProjects() + const currentProjectMeta = projects.find(item => item.id === currentProjectId) + const isDeleteFallbackProject = + Boolean(currentProjectMeta) + && projects.length === 1 + && String(currentProjectMeta?.name || '').trim() === t('xmInfo.defaultProjectName') + const targetProjectId = isDeleteFallbackProject ? currentProjectId : createProject().id + writeProjectIdToUrl(targetProjectId) writeWorkspaceMode('project') tabStore.enterWorkspace({ id: PROJECT_TAB_ID, @@ -274,6 +334,7 @@ const confirmHomeImport = () => { } onMounted(() => { + void refreshExistingProjects() void loadProjectDefaults() void loadQuickDefaults() try { @@ -363,9 +424,19 @@ onMounted(() => { {{ t('home.cards.projectBudgetDesc') }} -
- {{ t('home.cards.enter') }} - +
+ +
+ {{ t('home.cards.enter') }} + +
@@ -443,6 +514,52 @@ onMounted(() => {
+
+
+
+
+

{{ t('home.dialog.chooseExistingProject') }}

+

{{ t('home.dialog.chooseExistingProjectDesc') }}

+
+ +
+ +
+
+ {{ t('home.dialog.noProjectYet') }} +
+ +
+ +
+ +
+
+
+
{ 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 '-' @@ -629,6 +638,7 @@ const removeProjectItem = async (project: ProjectMeta) => { await deleteIndexedDBByName(getProjectDbName(project.id)) const removed = deleteProject(project.id) if (!removed) return + emitProjectDeleted(project.id) const nextProject = createProject(DEFAULT_PROJECT_NAME) const defaultIndustry = String(industryTypeList[0]?.id || '').trim() if (defaultIndustry) { @@ -661,6 +671,7 @@ const removeProjectItem = async (project: ProjectMeta) => { const removed = deleteProject(project.id) if (!removed) return + emitProjectDeleted(project.id) try { await deleteIndexedDBByName(getProjectDbName(project.id)) } catch (error) { @@ -1564,6 +1575,7 @@ const handleReset = async () => { 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 @@ -1882,14 +1894,12 @@ watch(
@@ -1946,6 +1956,9 @@ watch( + diff --git a/src/lib/projectEvents.ts b/src/lib/projectEvents.ts new file mode 100644 index 0000000..b240188 --- /dev/null +++ b/src/lib/projectEvents.ts @@ -0,0 +1,180 @@ +const PROJECT_EVENTS_CHANNEL = 'jgjs-project-events' +const PROJECT_DELETED_STORAGE_KEY = 'jgjs-project-event:project-deleted' +const RESET_ALL_STORAGE_KEY = 'jgjs-project-event:reset-all' +const PROJECT_EVENT_SESSION_ID_KEY = 'jgjs-project-event-session-id' + +type ProjectDeletedPayload = { + type: 'project-deleted' + projectId: string + sourceSessionId: string + at: number +} + +type ResetAllPayload = { + type: 'reset-all' + sourceSessionId: string + at: number +} + +const randomSessionId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}` + +const getOrCreateSessionId = () => { + try { + const existing = String(window.sessionStorage.getItem(PROJECT_EVENT_SESSION_ID_KEY) || '').trim() + if (existing) return existing + const next = randomSessionId() + window.sessionStorage.setItem(PROJECT_EVENT_SESSION_ID_KEY, next) + return next + } catch { + return randomSessionId() + } +} + +const parseProjectDeletedPayload = (raw: string | null): ProjectDeletedPayload | null => { + if (!raw) return null + try { + const parsed = JSON.parse(raw) as Partial + if (!parsed || typeof parsed !== 'object') return null + if (parsed.type !== 'project-deleted') return null + if (typeof parsed.projectId !== 'string' || !parsed.projectId.trim()) return null + if (typeof parsed.sourceSessionId !== 'string' || !parsed.sourceSessionId.trim()) return null + if (typeof parsed.at !== 'number' || !Number.isFinite(parsed.at)) return null + return { + type: 'project-deleted', + projectId: parsed.projectId.trim(), + sourceSessionId: parsed.sourceSessionId.trim(), + at: parsed.at + } + } catch { + return null + } +} + +const parseResetAllPayload = (raw: string | null): ResetAllPayload | null => { + if (!raw) return null + try { + const parsed = JSON.parse(raw) as Partial + if (!parsed || typeof parsed !== 'object') return null + if (parsed.type !== 'reset-all') return null + if (typeof parsed.sourceSessionId !== 'string' || !parsed.sourceSessionId.trim()) return null + if (typeof parsed.at !== 'number' || !Number.isFinite(parsed.at)) return null + return { + type: 'reset-all', + sourceSessionId: parsed.sourceSessionId.trim(), + at: parsed.at + } + } catch { + return null + } +} + +export const emitProjectDeleted = (projectIdRaw: string) => { + const projectId = String(projectIdRaw || '').trim() + if (!projectId) return + const payload: ProjectDeletedPayload = { + type: 'project-deleted', + projectId, + sourceSessionId: getOrCreateSessionId(), + at: Date.now() + } + try { + if (typeof BroadcastChannel !== 'undefined') { + const bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL) + bc.postMessage(payload) + bc.close() + } + } catch { + // ignore + } + try { + localStorage.setItem(PROJECT_DELETED_STORAGE_KEY, JSON.stringify(payload)) + } catch { + // ignore + } +} + +export const listenProjectDeleted = (onDeleted: (projectId: string) => void) => { + const localSessionId = getOrCreateSessionId() + let bc: BroadcastChannel | null = null + + const handlePayload = (payload: ProjectDeletedPayload | null) => { + if (!payload) return + if (payload.sourceSessionId === localSessionId) return + onDeleted(payload.projectId) + } + + const onStorage = (event: StorageEvent) => { + if (event.key !== PROJECT_DELETED_STORAGE_KEY) return + handlePayload(parseProjectDeletedPayload(event.newValue)) + } + + if (typeof BroadcastChannel !== 'undefined') { + bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL) + bc.onmessage = (event: MessageEvent) => { + handlePayload(parseProjectDeletedPayload(JSON.stringify(event.data))) + } + } + window.addEventListener('storage', onStorage) + + return () => { + window.removeEventListener('storage', onStorage) + if (bc) { + bc.close() + bc = null + } + } +} + +export const emitResetAll = () => { + const payload: ResetAllPayload = { + type: 'reset-all', + sourceSessionId: getOrCreateSessionId(), + at: Date.now() + } + try { + if (typeof BroadcastChannel !== 'undefined') { + const bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL) + bc.postMessage(payload) + bc.close() + } + } catch { + // ignore + } + try { + localStorage.setItem(RESET_ALL_STORAGE_KEY, JSON.stringify(payload)) + } catch { + // ignore + } +} + +export const listenResetAll = (onResetAll: () => void) => { + const localSessionId = getOrCreateSessionId() + let bc: BroadcastChannel | null = null + + const handlePayload = (payload: ResetAllPayload | null) => { + if (!payload) return + if (payload.sourceSessionId === localSessionId) return + onResetAll() + } + + const onStorage = (event: StorageEvent) => { + if (event.key !== RESET_ALL_STORAGE_KEY) return + handlePayload(parseResetAllPayload(event.newValue)) + } + + if (typeof BroadcastChannel !== 'undefined') { + bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL) + bc.onmessage = (event: MessageEvent) => { + handlePayload(parseResetAllPayload(JSON.stringify(event.data))) + } + } + window.addEventListener('storage', onStorage) + + return () => { + window.removeEventListener('storage', onStorage) + if (bc) { + bc.close() + bc = null + } + } +} diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index ca6c5c9..896c85b 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/features/ht/contracts.ts","./src/features/ht/importexport.ts","./src/features/ht/types.ts","./src/features/tab/importexport.ts","./src/features/tab/types.ts","./src/i18n/dictionary-en.ts","./src/i18n/index.ts","./src/i18n/locales/en-us.ts","./src/i18n/locales/zh-cn.ts","./src/lib/aggridreadonlyautoheight.ts","./src/lib/aggridresetheader.ts","./src/lib/contractsegment.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricinghourlycalc.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingpersistcontrol.ts","./src/lib/pricingpinnedrows.ts","./src/lib/pricingscalecalc.ts","./src/lib/pricingscalecolumns.ts","./src/lib/pricingscaledetail.ts","./src/lib/pricingscaledict.ts","./src/lib/pricingscalefee.ts","./src/lib/pricingscalegrid.ts","./src/lib/pricingscalelink.ts","./src/lib/pricingscalepanedata.ts","./src/lib/pricingscalepanelifecycle.ts","./src/lib/pricingscaleproject.ts","./src/lib/pricingscalerowmap.ts","./src/lib/pricingworkloadcalc.ts","./src/lib/projectregistry.ts","./src/lib/projectsessionlock.ts","./src/lib/projectworkspace.ts","./src/lib/reportexportbuilders.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/uiprefs.ts","./src/pinia/zxfwpricing.ts","./src/pinia/zxfwpricinghtfee.ts","./src/pinia/zxfwpricingkeys.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/types/pricing.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/features/ht/components/ht.vue","./src/features/ht/components/htadditionalworkfee.vue","./src/features/ht/components/htbaseinfo.vue","./src/features/ht/components/htconsultcategoryfactor.vue","./src/features/ht/components/htcontractsummary.vue","./src/features/ht/components/htfeeratemethodform.vue","./src/features/ht/components/htmajorfactor.vue","./src/features/ht/components/htreservefee.vue","./src/features/ht/components/htcard.vue","./src/features/ht/components/htinfo.vue","./src/features/ht/components/zxfw.vue","./src/features/pricing/components/hourlypricingpane.vue","./src/features/pricing/components/investmentscalepricingpane.vue","./src/features/pricing/components/landscalepricingpane.vue","./src/features/pricing/components/workloadpricingpane.vue","./src/features/shared/components/hourlyfeegrid.vue","./src/features/shared/components/htfeegrid.vue","./src/features/shared/components/htfeemethodgrid.vue","./src/features/shared/components/methodunavailablenotice.vue","./src/features/shared/components/servicecheckboxselector.vue","./src/features/shared/components/workcontentgrid.vue","./src/features/shared/components/xmfactorgrid.vue","./src/features/shared/components/xmcommonaggrid.vue","./src/features/workbench/components/homeentryview.vue","./src/features/workbench/components/htfeemethodtypelineview.vue","./src/features/workbench/components/quickcalcworkbenchview.vue","./src/features/workbench/components/zxfwview.vue","./src/features/xm/components/xmconsultcategoryfactor.vue","./src/features/xm/components/xmmajorfactor.vue","./src/features/xm/components/info.vue","./src/features/xm/components/xmcard.vue","./src/features/xm/components/xminfo.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/features/ht/contracts.ts","./src/features/ht/importexport.ts","./src/features/ht/types.ts","./src/features/tab/importexport.ts","./src/features/tab/types.ts","./src/i18n/dictionary-en.ts","./src/i18n/index.ts","./src/i18n/locales/en-us.ts","./src/i18n/locales/zh-cn.ts","./src/lib/aggridreadonlyautoheight.ts","./src/lib/aggridresetheader.ts","./src/lib/contractsegment.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricinghourlycalc.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingpersistcontrol.ts","./src/lib/pricingpinnedrows.ts","./src/lib/pricingscalecalc.ts","./src/lib/pricingscalecolumns.ts","./src/lib/pricingscaledetail.ts","./src/lib/pricingscaledict.ts","./src/lib/pricingscalefee.ts","./src/lib/pricingscalegrid.ts","./src/lib/pricingscalelink.ts","./src/lib/pricingscalepanedata.ts","./src/lib/pricingscalepanelifecycle.ts","./src/lib/pricingscaleproject.ts","./src/lib/pricingscalerowmap.ts","./src/lib/pricingworkloadcalc.ts","./src/lib/projectevents.ts","./src/lib/projectregistry.ts","./src/lib/projectsessionlock.ts","./src/lib/projectworkspace.ts","./src/lib/reportexportbuilders.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/uiprefs.ts","./src/pinia/zxfwpricing.ts","./src/pinia/zxfwpricinghtfee.ts","./src/pinia/zxfwpricingkeys.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/types/pricing.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/features/ht/components/ht.vue","./src/features/ht/components/htadditionalworkfee.vue","./src/features/ht/components/htbaseinfo.vue","./src/features/ht/components/htconsultcategoryfactor.vue","./src/features/ht/components/htcontractsummary.vue","./src/features/ht/components/htfeeratemethodform.vue","./src/features/ht/components/htmajorfactor.vue","./src/features/ht/components/htreservefee.vue","./src/features/ht/components/htcard.vue","./src/features/ht/components/htinfo.vue","./src/features/ht/components/zxfw.vue","./src/features/pricing/components/hourlypricingpane.vue","./src/features/pricing/components/investmentscalepricingpane.vue","./src/features/pricing/components/landscalepricingpane.vue","./src/features/pricing/components/workloadpricingpane.vue","./src/features/shared/components/hourlyfeegrid.vue","./src/features/shared/components/htfeegrid.vue","./src/features/shared/components/htfeemethodgrid.vue","./src/features/shared/components/methodunavailablenotice.vue","./src/features/shared/components/servicecheckboxselector.vue","./src/features/shared/components/workcontentgrid.vue","./src/features/shared/components/xmfactorgrid.vue","./src/features/shared/components/xmcommonaggrid.vue","./src/features/workbench/components/homeentryview.vue","./src/features/workbench/components/htfeemethodtypelineview.vue","./src/features/workbench/components/quickcalcworkbenchview.vue","./src/features/workbench/components/zxfwview.vue","./src/features/xm/components/xmconsultcategoryfactor.vue","./src/features/xm/components/xmmajorfactor.vue","./src/features/xm/components/info.vue","./src/features/xm/components/xmcard.vue","./src/features/xm/components/xminfo.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"} \ No newline at end of file