diff --git a/.codex-import-smoke.zw b/.codex-import-smoke.zw new file mode 100644 index 0000000..8e1be18 --- /dev/null +++ b/.codex-import-smoke.zw @@ -0,0 +1,2 @@ +JGJSZW #WgtK_G;wf)Au:퇼!PY2Z[B[ߺ mFSs;eQqd;gf}DF`4ѼI{K1Nr|[jȄ(YX9?2Hv^0dL6gNvZ~?mq^#ʎIm ,Hu?[. P5BM8PG.dgP>fl: +ql +:ՠ#TZ8]X((WVgI\?ۣsQ k X6鴦?אi+\pcLX;ҹw,q2$ą< ^A Sc\>rp-Ap7F@8jco Hڱa9 5u ^da-̡.% N~δIݳtIVnpIikcJ>vMp main, origin/main, origin/HEAD) HEAD@{0}: reset: moving to HEAD^ +9c11604 HEAD@{1}: checkout: moving from 9c11604ba744feb874018575a6a679700971e548 to main +9c11604 HEAD@{2}: checkout: moving from main to 9c11604ba744feb874018575a6a679700971e548 +9c11604 HEAD@{3}: reset: moving to 9c11604ba744feb874018575a6a679700971e548 +9c11604 HEAD@{4}: commit: 首页 +d3695c8 (HEAD -> main, origin/main, origin/HEAD) HEAD@{5}: pull -f: Fast-forward +1c600e6 HEAD@{6}: commit: fix +f4f6e5c HEAD@{7}: commit: final +398fca9 HEAD@{8}: pull: Fast-forward +f4c768d HEAD@{9}: commit: fix +cd10760 HEAD@{10}: commit: 1 +0f71fff HEAD@{11}: commit: fix +3d26b0b HEAD@{12}: commit: fix,去掉大部分indexdb的逻辑 +9a045cf HEAD@{13}: commit: 大改,使用pinia传值,indexdb做持久化 +3ad7bae HEAD@{14}: commit: 调整存储的逻辑 +bbc8777 HEAD@{15}: commit: fix +5614e31 HEAD@{16}: commit: 修复bug +5bb6609 HEAD@{17}: commit: fix bug +1910f15 HEAD@{18}: pull: Fast-forward +2a2c0fe HEAD@{19}: commit: 1 +f79e8e0 HEAD@{20}: commit: merge +ab310b4 HEAD@{21}: commit: 1 +d1dda7f HEAD@{22}: pull: Fast-forward +8a15587 HEAD@{23}: reset: moving to HEAD +8a15587 HEAD@{24}: commit: 备份 +fc26a87 HEAD@{25}: commit: 系数字段修改 +21d3f03 HEAD@{26}: pull: Fast-forward +303f54b HEAD@{27}: commit: if +043e1fc HEAD@{28}: commit: fix +ad4e9cd HEAD@{29}: commit: fix someone +c482faa HEAD@{30}: commit: fix +626513b HEAD@{31}: commit: fix +d8f8b62 HEAD@{32}: commit: fix +75f293f HEAD@{33}: commit: '20260305修复bug' +53c1b2c HEAD@{34}: commit: 1 +75d5066 HEAD@{35}: commit: 1 +e4a2b53 HEAD@{36}: commit: 1 +42fd6e4 HEAD@{37}: commit: 重构 +33913c2 HEAD@{38}: commit: 1 +62546bc HEAD@{39}: commit: 1 +a10359f HEAD@{40}: commit: 优化 +3950057 HEAD@{41}: commit: fix +757de9a HEAD@{42}: commit: 1 +ea6a244 HEAD@{43}: commit: fix +13b03e0 HEAD@{44}: commit: 完成大部分 +e97707a HEAD@{45}: commit: fix +9849801 HEAD@{46}: commit: fix all +badf131 HEAD@{47}: commit: fix +57a2029 HEAD@{48}: commit: fix 拖动流畅度 +37f4a99 HEAD@{49}: commit: fix bug +1609f19 HEAD@{50}: commit: fix more +5734cfa HEAD@{51}: commit: fix more +f121aa2 HEAD@{52}: commit: fix all +6ba08da HEAD@{53}: clone: from https://git.zwgczx.com/zwgczx/JGJS2026.git diff --git a/home-entry-after.png b/home-entry-after.png new file mode 100644 index 0000000..0a1f1f5 Binary files /dev/null and b/home-entry-after.png differ diff --git a/home-entry-before.png b/home-entry-before.png new file mode 100644 index 0000000..8dde8e0 Binary files /dev/null and b/home-entry-before.png differ diff --git a/src/components/common/HtFeeMethodGrid.vue b/src/components/common/HtFeeMethodGrid.vue index 187d026..3a54bd3 100644 --- a/src/components/common/HtFeeMethodGrid.vue +++ b/src/components/common/HtFeeMethodGrid.vue @@ -113,6 +113,7 @@ const getRowSubtotal = (row: FeeMethodRow | null | undefined) => { return round3(toFinite(row.rateFee) + toFinite(row.hourlyFee) + toFinite(row.quantityUnitPriceFee)) } const toFiniteUnknown = (value: unknown): number | null => { + if (value == null || value === '') return null const numeric = Number(value) return Number.isFinite(numeric) ? numeric : null } @@ -332,12 +333,14 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => { ) const rows = sourceRows.map(item => { const row = item as Partial & LegacyFeeRow + return { id: typeof row.id === 'string' && row.id ? row.id : createRowId(), name: typeof row.name === 'string' ? row.name : (typeof row.feeItem === 'string' ? row.feeItem : ''), + rateFee: typeof row.rateFee === 'number' ? row.rateFee : null, hourlyFee: typeof row.hourlyFee === 'number' ? row.hourlyFee : null, quantityUnitPriceFee: diff --git a/src/components/common/xmCommonAgGrid.vue b/src/components/common/xmCommonAgGrid.vue index 43e6606..c7f84fa 100644 --- a/src/components/common/xmCommonAgGrid.vue +++ b/src/components/common/xmCommonAgGrid.vue @@ -40,7 +40,7 @@ interface XmBaseInfoState { projectIndustry?: string } -const BASE_INFO_KEY = 'xm-base-info-v1' +const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request' type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean } const kvStore = useKvStore() @@ -198,7 +198,7 @@ const applyPinnedTotalAmount = ( const loadFromIndexedDB = async (api: GridApi) => { try { const [baseInfo, contractData] = await Promise.all([ - kvStore.getItem(BASE_INFO_KEY), + kvStore.getItem(props.baseInfoKey || 'xm-base-info-v1'), kvStore.getItem(props.dbKey) ]) @@ -296,6 +296,7 @@ const props = defineProps<{ title: string dbKey: string xmInfoKey?: string | null + baseInfoKey?: string }>() let persistTimer: ReturnType | null = null @@ -444,11 +445,29 @@ const saveToIndexedDB = async () => { hide: Boolean(row.hide), isGroupRow: false })) + const totalAmountFromRows = (() => { + let hasValue = false + let total = 0 + for (const row of leafRows) { + const amount = row?.amount + if (typeof amount !== 'number' || !Number.isFinite(amount)) continue + total += amount + hasValue = true + } + return hasValue ? roundTo(total, 2) : null + })() + const pinnedAmount = pinnedTopRowData.value[0].amount + const normalizedPinnedAmount = + typeof pinnedAmount === 'number' && Number.isFinite(pinnedAmount) + ? roundTo(pinnedAmount, 2) + : null + const normalizedTotalAmount = roughCalcEnabled.value ? normalizedPinnedAmount : totalAmountFromRows + pinnedTopRowData.value[0].amount = normalizedTotalAmount const payload: GridPersistState = { detailRows: [...leafRows, ...buildGroupRows(leafRows)] } payload.roughCalcEnabled = roughCalcEnabled.value - payload.totalAmount = pinnedTopRowData.value[0].amount + payload.totalAmount = normalizedTotalAmount await kvStore.setItem(props.dbKey, payload) } catch (error) { console.error('saveToIndexedDB failed:', error) @@ -462,6 +481,18 @@ const schedulePersist = () => { }, 600) } +const handleFlushPersistRequest = (event: Event) => { + const customEvent = event as CustomEvent<{ done?: () => void }> + const done = customEvent?.detail?.done + if (persistTimer) { + clearTimeout(persistTimer) + persistTimer = null + } + void saveToIndexedDB().finally(() => { + done?.() + }) +} + const setDetailRowsHidden = (hidden: boolean) => { for (const row of detailRows.value) { row.hide = hidden @@ -566,9 +597,14 @@ const syncPinnedTotalForNormalMode = () => { onBeforeUnmount(() => { if (persistTimer) clearTimeout(persistTimer) + window.removeEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener) gridApi.value = null void saveToIndexedDB() }) + +onMounted(() => { + window.addEventListener(XM_SCALE_FLUSH_EVENT, handleFlushPersistRequest as EventListener) +}) diff --git a/src/components/views/htInfo.vue b/src/components/views/htInfo.vue index 3a06cbd..7deeae0 100644 --- a/src/components/views/htInfo.vue +++ b/src/components/views/htInfo.vue @@ -5,13 +5,19 @@ import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue' const props = defineProps<{ contractId: string + projectScaleKey?: string | null + projectInfoKey?: string }>() const DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) -const XM_DB_KEY = 'xm-info-v3' +const XM_DB_KEY = computed(() => { + if (props.projectScaleKey === null) return undefined + return props.projectScaleKey || 'xm-info-v3' +}) +const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1') diff --git a/src/components/views/info.vue b/src/components/views/info.vue index 5a22445..55664ea 100644 --- a/src/components/views/info.vue +++ b/src/components/views/info.vue @@ -2,12 +2,7 @@ import { parseDate } from '@internationalized/date' import { onMounted, ref, watch } from 'vue' import { - getMajorDictEntries, - getServiceDictEntries, - getIndustryTypeValue, industryTypeList, - isIndustryEnabledByType, - isMajorIdInIndustryScope } from '@/sql' import { useKvStore } from '@/pinia/kv' import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next' @@ -45,31 +40,10 @@ interface XmInfoState { } type MajorParentNode = { id: string; name: string } -type DictItemLite = { - code?: string - name?: string - defCoe?: number | null - notshowByzxflxs?: boolean -} -type FactorPersistRow = { - id: string - code: string - name: string - standardFactor: number | null - budgetValue: number | null - remark: string - path: string[] -} -type FactorPersistState = { - detailRows: FactorPersistRow[] -} const DB_KEY = 'xm-base-info-v1' -const XM_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1' -const XM_MAJOR_FACTOR_KEY = 'xm-major-factor-v1' const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务' const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择' -const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed' const getTodayDateString = () => { const now = new Date() const year = String(now.getFullYear()) @@ -79,8 +53,6 @@ const getTodayDateString = () => { } const isProjectInitialized = ref(false) -const showCreateDialog = ref(false) -const pendingIndustry = ref('') const projectName = ref('') const projectIndustry = ref('') @@ -128,76 +100,6 @@ const majorParentCodeSet = new Set(majorParentNodes.map(item => item.id)) const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || '' const kvStore = useKvStore() -const buildCodePath = (code: string, selfId: string, codeIdMap: Map) => { - const parts = code.split('-').filter(Boolean) - if (!parts.length) return [selfId] - const path: string[] = [] - let currentCode = parts[0] - const firstId = codeIdMap.get(currentCode) - if (firstId) path.push(firstId) - for (let i = 1; i < parts.length; i += 1) { - currentCode = `${currentCode}-${parts[i]}` - const id = codeIdMap.get(currentCode) - if (id) path.push(id) - } - if (!path.length || path[path.length - 1] !== selfId) path.push(selfId) - return path -} - -const buildFactorRowsFromEntries = (entries: Array<{ id: string; item: DictItemLite }>): FactorPersistRow[] => { - const codeIdMap = new Map() - for (const entry of entries) { - const code = String(entry.item?.code || '').trim() - if (!code) continue - codeIdMap.set(code, entry.id) - } - return entries - .map(entry => { - const code = String(entry.item?.code || '').trim() - const name = String(entry.item?.name || '').trim() - if (!code || !name) return null - const standardFactor = - typeof entry.item?.defCoe === 'number' && Number.isFinite(entry.item.defCoe) - ? entry.item.defCoe - : null - return { - id: entry.id, - code, - name, - standardFactor, - budgetValue: standardFactor, - remark: '', - path: buildCodePath(code, entry.id, codeIdMap) - } - }) - .filter((item): item is FactorPersistRow => Boolean(item)) -} - -const initializeProjectFactorStates = async (industry: string) => { - const industryType = getIndustryTypeValue(industry) - const consultEntries = getServiceDictEntries() - .map(({ id, item }) => ({ id, item: item as DictItemLite })) - .filter(({ item }) => { - if (item.notshowByzxflxs === true) return false - return isIndustryEnabledByType(item as Record, industryType) - }) - const majorEntries = getMajorDictEntries() - .map(({ id, item }) => ({ id, item: item as DictItemLite })) - .filter(({ id, item }) => item.notshowByzxflxs !== true && isMajorIdInIndustryScope(id, industry)) - - const consultPayload: FactorPersistState = { - detailRows: buildFactorRowsFromEntries(consultEntries) - } - const majorPayload: FactorPersistState = { - detailRows: buildFactorRowsFromEntries(majorEntries) - } - - await Promise.all([ - kvStore.setItem(XM_CONSULT_CATEGORY_FACTOR_KEY, consultPayload), - kvStore.setItem(XM_MAJOR_FACTOR_KEY, majorPayload) - ]) -} - const saveToIndexedDB = async () => { try { const payload: XmInfoState = { @@ -268,38 +170,6 @@ const handleProjectNameBlur = () => { } } -const openCreateDialog = () => { - pendingIndustry.value = DEFAULT_PROJECT_INDUSTRY - showCreateDialog.value = true -} - -const closeCreateDialog = () => { - showCreateDialog.value = false -} - -const createProject = async () => { - const selectedIndustry = majorParentCodeSet.has(pendingIndustry.value) - ? pendingIndustry.value - : DEFAULT_PROJECT_INDUSTRY - - projectIndustry.value = selectedIndustry - projectName.value = DEFAULT_PROJECT_NAME - preparedBy.value = '' - reviewedBy.value = '' - preparedCompany.value = '' - preparedDate.value = getTodayDateString() - syncPreparedDatePickerFromString() - isProjectInitialized.value = true - showCreateDialog.value = false - await saveToIndexedDB() - try { - await initializeProjectFactorStates(selectedIndustry) - } catch (error) { - console.error('initializeProjectFactorStates failed:', error) - } - window.dispatchEvent(new CustomEvent(PROJECT_INIT_CHANGED_EVENT, { detail: true })) -} - watch( [projectIndustry, projectName, preparedBy, reviewedBy, preparedCompany, preparedDate], schedulePersist @@ -315,15 +185,9 @@ onMounted(async () => {
- + 请从首页先新建项目后再进入此页面。
@@ -516,44 +380,6 @@ onMounted(async () => {
- -
-
-

新建项目

-

请选择工程行业

-
- -
-
- - -
-
-
diff --git a/src/components/views/pricingView/InvestmentScalePricingPane.vue b/src/components/views/pricingView/InvestmentScalePricingPane.vue index baea255..953c67b 100644 --- a/src/components/views/pricingView/InvestmentScalePricingPane.vue +++ b/src/components/views/pricingView/InvestmentScalePricingPane.vue @@ -92,6 +92,7 @@ interface ServiceLite { const props = defineProps<{ contractId: string, serviceId: string | number + projectInfoKey?: string }>() const zxFwPricingStore = useZxFwPricingStore() const kvStore = useKvStore() @@ -99,7 +100,7 @@ const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`) const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`) const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`) -const BASE_INFO_KEY = 'xm-base-info-v1' +const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1') const activeIndustryCode = ref('') const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:' const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:' @@ -1149,7 +1150,7 @@ const applyProjectCountChange = async (nextValue: unknown) => { const loadFromIndexedDB = async () => { try { - const baseInfo = await kvStore.getItem(BASE_INFO_KEY) + const baseInfo = await kvStore.getItem(BASE_INFO_KEY.value) activeIndustryCode.value = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '' projectCount.value = 1 @@ -1214,7 +1215,7 @@ const loadFromIndexedDB = async () => { const importContractData = async () => { try { - const baseInfo = await kvStore.getItem(BASE_INFO_KEY) + const baseInfo = await kvStore.getItem(BASE_INFO_KEY.value) activeIndustryCode.value = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '' diff --git a/src/components/views/pricingView/LandScalePricingPane.vue b/src/components/views/pricingView/LandScalePricingPane.vue index 0761337..217b53a 100644 --- a/src/components/views/pricingView/LandScalePricingPane.vue +++ b/src/components/views/pricingView/LandScalePricingPane.vue @@ -92,6 +92,7 @@ interface ServiceLite { const props = defineProps<{ contractId: string, serviceId: string | number + projectInfoKey?: string }>() const zxFwPricingStore = useZxFwPricingStore() const kvStore = useKvStore() @@ -99,7 +100,7 @@ const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`) const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`) const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`) const HT_MAJOR_FACTOR_KEY = computed(() => `ht-major-factor-v1-${props.contractId}`) -const BASE_INFO_KEY = 'xm-base-info-v1' +const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1') const activeIndustryCode = ref('') const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:' const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:' @@ -997,7 +998,7 @@ const applyProjectCountChange = async (nextValue: unknown) => { const loadFromIndexedDB = async () => { try { - const baseInfo = await kvStore.getItem(BASE_INFO_KEY) + const baseInfo = await kvStore.getItem(BASE_INFO_KEY.value) activeIndustryCode.value = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '' projectCount.value = 1 @@ -1049,7 +1050,7 @@ const loadFromIndexedDB = async () => { const importContractData = async () => { try { - const baseInfo = await kvStore.getItem(BASE_INFO_KEY) + const baseInfo = await kvStore.getItem(BASE_INFO_KEY.value) activeIndustryCode.value = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '' diff --git a/src/components/views/xmCard.vue b/src/components/views/xmCard.vue index 3885488..c472fbb 100644 --- a/src/components/views/xmCard.vue +++ b/src/components/views/xmCard.vue @@ -3,17 +3,14 @@ scene="xm-tab" title="" storage-key="project-active-cat" - default-category="info" + default-category="scale-info" :categories="xmCategories" /> diff --git a/src/components/views/zxFw.vue b/src/components/views/zxFw.vue index cd328b4..f603eb0 100644 --- a/src/components/views/zxFw.vue +++ b/src/components/views/zxFw.vue @@ -74,11 +74,12 @@ interface ServiceMethodType { const props = defineProps<{ contractId: string contractName?: string + projectInfoKey?: string }>() const tabStore = useTabStore() const zxFwPricingStore = useZxFwPricingStore() const kvStore = useKvStore() -const PROJECT_INFO_KEY = 'xm-base-info-v1' +const PROJECT_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1') const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:' const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:' const PRICING_CLEAR_SKIP_TTL_MS = 5000 @@ -500,7 +501,8 @@ const openEditTab = (row: DetailRow) => { contractName: props.contractName || '', serviceId: row.id, fwName: row.code + row.name, - type: serviceType ? { ...serviceType } : undefined + type: serviceType ? { ...serviceType } : undefined, + projectInfoKey: props.projectInfoKey } }) } @@ -1098,7 +1100,7 @@ const initializeContractState = async () => { const loadProjectIndustry = async () => { try { - const data = await kvStore.getItem(PROJECT_INFO_KEY) + const data = await kvStore.getItem(PROJECT_INFO_KEY.value) projectIndustry.value = typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : '' } catch (error) { diff --git a/src/layout/tab.vue b/src/layout/tab.vue index b407d20..33c673d 100644 --- a/src/layout/tab.vue +++ b/src/layout/tab.vue @@ -27,8 +27,28 @@ import { ToastViewport, } from 'reka-ui' import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive' -import { addNumbers, roundTo } from '@/lib/decimal' +import { addNumbers, roundTo, toDecimal } from '@/lib/decimal' +import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee' import { exportFile, serviceList, additionalWorkList, reserveList } from '@/sql' +import { + HOME_TAB_ID, + LEGACY_PROJECT_TAB_ID, + PROJECT_TAB_ID, + QUICK_CONSULT_CATEGORY_FACTOR_KEY, + QUICK_CONTRACT_FALLBACK_NAME, + QUICK_CONTRACT_ID, + QUICK_CONTRACT_META_KEY, + QUICK_CONTRACT_TAB_ID, + QUICK_MAJOR_FACTOR_KEY, + QUICK_PROJECT_INFO_KEY, + QUICK_PROJECT_SCALE_KEY, + QUICK_TAB_ID, + type QuickContractMeta, + normalizeWorkspaceMode, + readWorkspaceMode, + writeWorkspaceMode, + type WorkspaceMode +} from '@/lib/workspace' interface DataEntry { key: string @@ -40,15 +60,29 @@ interface ForageStoreSnapshot { entries: DataEntry[] } -interface DataPackage { - version: number - exportedAt: string +interface DataPackageStorage { localStorage: DataEntry[] sessionStorage: DataEntry[] localforageDefault: DataEntry[] localforageStores?: ForageStoreSnapshot[] } +interface DataPackageWorkspace { + mode: WorkspaceMode +} + +interface DataPackage { + version: number + exportedAt: string + packageType?: 'workspace-snapshot' + workspace?: DataPackageWorkspace + storage?: DataPackageStorage + localStorage?: DataEntry[] + sessionStorage?: DataEntry[] + localforageDefault?: DataEntry[] + localforageStores?: ForageStoreSnapshot[] +} + interface UserGuideStep { title: string description: string @@ -85,6 +119,16 @@ interface ContractCardItem { order?: number } +interface ReportWorkspaceConfig { + mode: WorkspaceMode + projectInfoKey: string + projectScaleKey: string + consultCategoryFactorKey: string + majorFactorKey: string + contractCardsKey?: string + quickContractKey?: string +} + interface ZxFwRowLike { id: string process?: unknown @@ -107,6 +151,8 @@ interface ScaleMethodRowLike extends ScaleRowLike { benchmarkBudget?: unknown benchmarkBudgetBasic?: unknown benchmarkBudgetOptional?: unknown + benchmarkBudgetBasicChecked?: unknown + benchmarkBudgetOptionalChecked?: unknown budgetFee?: unknown budgetFeeBasic?: unknown budgetFeeOptional?: unknown @@ -139,6 +185,7 @@ interface QuantityMethodRowLike { interface WorkloadMethodRowLike { id: string + conversion?: unknown budgetAdoptedUnitPrice?: unknown workload?: unknown basicFee?: unknown @@ -343,17 +390,32 @@ interface ExportReserve { } interface ExportReportPayload { + version: number + reportType: 'budget-report' + mode: WorkspaceMode name: string writer: string reviewer: string date: string industry: number fee: number - scaleCost: number + scaleCost: number | null scale: ExportScaleRow[] serviceCoes: ExportServiceCoe[] majorCoes: ExportMajorCoe[] contracts: ExportContract[] + project: { + name: string + writer: string + reviewer: string + date: string + industry: number + fee: number + scaleCost: number | null + scale: ExportScaleRow[] + serviceCoes: ExportServiceCoe[] + majorCoes: ExportMajorCoe[] + } } const USER_GUIDE_COMPLETED_KEY = 'jgjs-user-guide-completed-v1' @@ -364,6 +426,7 @@ const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1' const PINIA_PERSIST_DB_NAME = 'DB' const PINIA_PERSIST_BASE_STORE_NAME = 'pinia' const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const +const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request' const userGuideSteps: UserGuideStep[] = [ { title: '欢迎使用', @@ -440,7 +503,9 @@ const userGuideSteps: UserGuideStep[] = [ ] const componentMap: Record = { - XmView: markRaw(defineAsyncComponent(() => import('@/components/views/xmCard.vue'))), + [HOME_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/HomeEntryView.vue'))), + [PROJECT_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/ProjectWorkspaceView.vue'))), + [LEGACY_PROJECT_TAB_ID]: markRaw(defineAsyncComponent(() => import('@/components/views/ProjectWorkspaceView.vue'))), ContractDetailView: markRaw(defineAsyncComponent(() => import('@/components/views/htCard.vue'))), ZxFwView: markRaw(defineAsyncComponent(() => import('@/components/views/ZxFwView.vue'))), HtFeeMethodTypeLineView: markRaw(defineAsyncComponent(() => import('@/components/views/HtFeeMethodTypeLineView.vue'))), @@ -449,13 +514,15 @@ const componentMap: Record = { const tabStore = useTabStore() const zxFwPricingStore = useZxFwPricingStore() const kvStore = useKvStore() +const protectedTabIdSet = new Set([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID]) +const isTabClosable = (tabId: string) => !protectedTabIdSet.has(tabId) const tabContextOpen = ref(false) const tabContextX = ref(0) const tabContextY = ref(0) -const contextTabId = ref('XmView') +const contextTabId = ref(HOME_TAB_ID) const tabContextRef = ref(null) const dataMenuOpen = ref(false) @@ -491,8 +558,13 @@ const tabsModel = computed({ }) const contextTabIndex = computed(() => tabStore.tabs.findIndex(t => t.id === contextTabId.value)) +const isHomeOnlyView = computed( + () => tabStore.tabs.length === 1 && tabStore.tabs[0]?.id === HOME_TAB_ID && tabStore.activeTabId === HOME_TAB_ID +) +const showTabStrip = computed(() => !isHomeOnlyView.value) +const showWorkspaceActions = computed(() => !isHomeOnlyView.value) -const hasClosableTabs = computed(() => tabStore.tabs.some(t => t.id !== 'XmView')) +const hasClosableTabs = computed(() => tabStore.tabs.some(t => isTabClosable(t.id))) const activeGuideStep = computed( () => userGuideSteps[userGuideStepIndex.value] || userGuideSteps[0] ) @@ -501,14 +573,14 @@ const isLastGuideStep = computed(() => userGuideStepIndex.value >= userGuideStep const guideProgressText = computed(() => `${userGuideStepIndex.value + 1} / ${userGuideSteps.length}`) const canCloseLeft = computed(() => { if (contextTabIndex.value <= 0) return false - return tabStore.tabs.slice(0, contextTabIndex.value).some(t => t.id !== 'XmView') + return tabStore.tabs.slice(0, contextTabIndex.value).some(t => isTabClosable(t.id)) }) const canCloseRight = computed(() => { if (contextTabIndex.value < 0) return false - return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => t.id !== 'XmView') + return tabStore.tabs.slice(contextTabIndex.value + 1).some(t => isTabClosable(t.id)) }) const canCloseOther = computed(() => - tabStore.tabs.some(t => t.id !== 'XmView' && t.id !== contextTabId.value) + tabStore.tabs.some(t => isTabClosable(t.id) && t.id !== contextTabId.value) ) const closeMenus = () => { @@ -516,6 +588,79 @@ const closeMenus = () => { dataMenuOpen.value = false } +const normalizeTabItem = (tab: Record) => { + const rawId = typeof tab.id === 'string' ? tab.id : '' + const rawComponentName = typeof tab.componentName === 'string' ? tab.componentName : rawId + if (!rawId && !rawComponentName) return null + + if (rawId === LEGACY_PROJECT_TAB_ID || rawComponentName === LEGACY_PROJECT_TAB_ID) { + return { + ...tab, + id: PROJECT_TAB_ID, + title: '项目计算', + componentName: PROJECT_TAB_ID + } + } + + if (rawId === HOME_TAB_ID || rawComponentName === HOME_TAB_ID) { + return { + ...tab, + id: HOME_TAB_ID, + title: typeof tab.title === 'string' && tab.title.trim() ? tab.title : '首页', + componentName: HOME_TAB_ID + } + } + + if (rawId === PROJECT_TAB_ID || rawComponentName === PROJECT_TAB_ID) { + return { + ...tab, + id: PROJECT_TAB_ID, + title: typeof tab.title === 'string' && tab.title.trim() ? tab.title : '项目计算', + componentName: PROJECT_TAB_ID + } + } + + if (rawId === QUICK_TAB_ID || rawComponentName === QUICK_TAB_ID) { + return { + ...tab, + id: HOME_TAB_ID, + title: '首页', + componentName: HOME_TAB_ID + } + } + + return { + ...tab, + id: rawId || rawComponentName, + componentName: rawComponentName || rawId + } +} + +const normalizeTabStoreState = () => { + const normalizedTabs = (Array.isArray(tabStore.tabs) ? tabStore.tabs : []) + .map(tab => normalizeTabItem(tab as unknown as Record)) + .filter((tab): tab is NonNullable> => Boolean(tab)) + + const uniqueTabs = normalizedTabs.filter((tab, index, source) => + source.findIndex(item => item.id === tab.id) === index + ) + + const nextTabs = uniqueTabs.filter(tab => !(tab.id === HOME_TAB_ID && uniqueTabs.length > 1)) + tabStore.tabs = (nextTabs.length > 0 + ? nextTabs + : [{ + id: HOME_TAB_ID, + title: '首页', + componentName: HOME_TAB_ID + }]) as any + if (tabStore.activeTabId === LEGACY_PROJECT_TAB_ID) { + tabStore.activeTabId = PROJECT_TAB_ID + } + if (!tabStore.tabs.some(tab => tab.id === tabStore.activeTabId)) { + tabStore.activeTabId = tabStore.tabs[0]?.id || HOME_TAB_ID + } +} + const clearReportExportToastTimer = () => { if (!reportExportToastTimer) return clearTimeout(reportExportToastTimer) @@ -564,9 +709,9 @@ const hasNonDefaultTabState = () => { if (!raw) return false const parsed = JSON.parse(raw) as { tabs?: Array<{ id?: string }>; activeTabId?: string } const tabs = Array.isArray(parsed?.tabs) ? parsed.tabs : [] - const hasCustomTabs = tabs.some(item => item?.id && item.id !== 'XmView') + const hasCustomTabs = tabs.some(item => item?.id && isTabClosable(item.id)) const activeTabId = typeof parsed?.activeTabId === 'string' ? parsed.activeTabId : '' - return hasCustomTabs || (activeTabId !== '' && activeTabId !== 'XmView') + return hasCustomTabs || (activeTabId !== '' && isTabClosable(activeTabId)) } catch (error) { console.error('parse tabs cache failed:', error) return false @@ -574,6 +719,7 @@ const hasNonDefaultTabState = () => { } const shouldAutoOpenGuide = async () => { + if (readWorkspaceMode() === 'home') return false if (hasGuideCompleted()) return false if (hasNonDefaultTabState()) return false try { @@ -672,7 +818,7 @@ const runTabMenuAction = (action: 'all' | 'left' | 'right' | 'other') => { const canMoveTab = (event: any) => { const draggedId = event?.draggedContext?.element?.id const targetIndex = event?.relatedContext?.index - if (draggedId === 'XmView') return false + if (protectedTabIdSet.has(draggedId)) return false if (typeof targetIndex === 'number' && targetIndex === 0) return false return true } @@ -889,6 +1035,27 @@ const flushPiniaPersistNow = async () => { ]) } +const requestXmScalePersistFlush = async () => { + await new Promise(resolve => { + let settled = false + const timer = setTimeout(() => { + if (settled) return + settled = true + resolve() + }, 350) + window.dispatchEvent(new CustomEvent(XM_SCALE_FLUSH_EVENT, { + detail: { + done: () => { + if (settled) return + settled = true + clearTimeout(timer) + resolve() + } + } + })) + }) +} + const readForage = async (store: ForageStore): Promise => { const keys = await store.keys() const entries: DataEntry[] = [] @@ -954,12 +1121,80 @@ const formatExportTimestamp = (date: Date): string => { return `${yyyy}${mm}${dd}-${hh}${mi}` } -const getExportProjectName = (entries: DataEntry[]): string => { +const getExportProjectName = (entries: DataEntry[], forageStores: ForageStoreSnapshot[] = []): string => { const target = entries.find(item => item.key === PROJECT_INFO_DB_KEY) || entries.find(item => item.key === LEGACY_PROJECT_DB_KEY) const data = (target?.value || {}) as XmInfoLike - return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目' + if (typeof data.projectName === 'string' && data.projectName.trim()) { + return sanitizeFileNamePart(data.projectName) + } + + const quickContract = + entries.find(item => item.key === QUICK_CONTRACT_META_KEY) || + forageStores.flatMap(store => store.entries).find(item => item.key === QUICK_CONTRACT_META_KEY) + const quickName = (quickContract?.value as QuickContractMeta | null)?.name + return typeof quickName === 'string' && quickName.trim() + ? sanitizeFileNamePart(quickName) + : '造价项目' +} + +const normalizeDataPackageStorage = (payload: DataPackage): DataPackageStorage => { + const storage = payload.storage + return { + localStorage: normalizeEntries(storage?.localStorage ?? payload.localStorage), + sessionStorage: normalizeEntries(storage?.sessionStorage ?? payload.sessionStorage), + localforageDefault: normalizeEntries(storage?.localforageDefault ?? payload.localforageDefault), + localforageStores: normalizeForageStoreSnapshots(storage?.localforageStores ?? payload.localforageStores) + } +} + +const normalizeDataPackageWorkspace = (payload: DataPackage): DataPackageWorkspace => ({ + mode: normalizeWorkspaceMode(payload.workspace?.mode ?? readWorkspaceMode()) +}) + +const resolveWorkspaceModeForExport = () => { + const activeTab = tabStore.tabs.find(tab => tab.id === tabStore.activeTabId) + const activeContractId = typeof activeTab?.props?.contractId === 'string' ? activeTab.props.contractId : '' + if (tabStore.activeTabId === QUICK_TAB_ID || activeContractId === QUICK_CONTRACT_ID) return 'quick' + if (activeContractId) return 'project' + if (tabStore.activeTabId === PROJECT_TAB_ID || tabStore.activeTabId === LEGACY_PROJECT_TAB_ID) return 'project' + return normalizeWorkspaceMode(readWorkspaceMode()) +} + +const getReportWorkspaceConfig = (): ReportWorkspaceConfig => { + const mode = resolveWorkspaceModeForExport() + if (mode === 'quick') { + return { + mode, + projectInfoKey: QUICK_PROJECT_INFO_KEY, + projectScaleKey: QUICK_PROJECT_SCALE_KEY, + consultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY, + majorFactorKey: QUICK_MAJOR_FACTOR_KEY, + quickContractKey: QUICK_CONTRACT_META_KEY + } + } + + return { + mode: 'project', + projectInfoKey: PROJECT_INFO_DB_KEY, + projectScaleKey: LEGACY_PROJECT_DB_KEY, + consultCategoryFactorKey: CONSULT_CATEGORY_FACTOR_DB_KEY, + majorFactorKey: MAJOR_FACTOR_DB_KEY, + contractCardsKey: 'ht-card-v1' + } +} + +const normalizeQuickContractMeta = (value: unknown): QuickContractMeta => { + const raw = (value || {}) as Partial + return { + id: typeof raw.id === 'string' && raw.id.trim() ? raw.id.trim() : QUICK_CONTRACT_ID, + name: typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : QUICK_CONTRACT_FALLBACK_NAME, + updatedAt: + typeof raw.updatedAt === 'string' && raw.updatedAt.trim() + ? raw.updatedAt + : new Date().toISOString() + } } const toFiniteNumber = (value: unknown): number | null => { @@ -985,6 +1220,74 @@ const sumNumbers = (values: Array): number => const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0 +const resolveScaleMethodComputedValues = ( + row: ScaleMethodRowLike, + mode: 'cost' | 'area' +) => { + const scaleValue = mode === 'cost' ? row.amount : row.landArea + const rawSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode) + const basicChecked = row.benchmarkBudgetBasicChecked !== false + const optionalChecked = row.benchmarkBudgetOptionalChecked !== false + const fallbackBasic = rawSplit ? (basicChecked ? rawSplit.basic : 0) : null + const fallbackOptional = rawSplit ? (optionalChecked ? rawSplit.optional : 0) : null + const benchmarkBudgetBasic = toFiniteNumber(row.benchmarkBudgetBasic) ?? fallbackBasic + const benchmarkBudgetOptional = toFiniteNumber(row.benchmarkBudgetOptional) ?? fallbackOptional + const benchmarkBudget = + toFiniteNumber(row.benchmarkBudget) ?? + ( + benchmarkBudgetBasic != null || benchmarkBudgetOptional != null + ? roundTo(addNumbers(benchmarkBudgetBasic ?? 0, benchmarkBudgetOptional ?? 0), 2) + : null + ) + const budgetFeeSplit = getScaleBudgetFeeSplit({ + benchmarkBudgetBasic, + benchmarkBudgetOptional, + majorFactor: row.majorFactor, + consultCategoryFactor: row.consultCategoryFactor, + workStageFactor: row.workStageFactor, + workRatio: row.workRatio + }) + + return { + benchmarkBudget, + benchmarkBudgetBasic, + benchmarkBudgetOptional, + basicFormula: + typeof row.basicFormula === 'string' + ? row.basicFormula + : (basicChecked ? (rawSplit?.basicFormula ?? '') : ''), + optionalFormula: + typeof row.optionalFormula === 'string' + ? row.optionalFormula + : (optionalChecked ? (rawSplit?.optionalFormula ?? '') : ''), + budgetFee: toFiniteNumber(row.budgetFee) ?? budgetFeeSplit?.total ?? null, + budgetFeeBasic: toFiniteNumber(row.budgetFeeBasic) ?? budgetFeeSplit?.basic ?? null, + budgetFeeOptional: toFiniteNumber(row.budgetFeeOptional) ?? budgetFeeSplit?.optional ?? null + } +} + +const calcWorkloadBasicFeeFromRow = (row: WorkloadMethodRowLike) => { + const price = toFiniteNumber(row.budgetAdoptedUnitPrice) + const conversion = toFiniteNumber(row.conversion) + const workload = toFiniteNumber(row.workload) + if (price == null || conversion == null || workload == null) return null + return roundTo(toDecimal(price).mul(conversion).mul(workload), 2) +} + +const calcWorkloadServiceFeeFromRow = (row: WorkloadMethodRowLike, basicFee: number | null) => { + const serviceCoe = toFiniteNumber(row.consultCategoryFactor) + if (basicFee == null || serviceCoe == null) return null + return roundTo(toDecimal(basicFee).mul(serviceCoe), 2) +} + +const calcHourlyServiceFeeFromRow = (row: HourlyMethodRowLike) => { + const price = toFiniteNumber(row.adoptedBudgetUnitPrice) + const personNum = toFiniteNumber(row.personnelCount) + const workDay = toFiniteNumber(row.workdayCount) + if (price == null || personNum == null || workDay == null) return null + return roundTo(toDecimal(price).mul(personNum).mul(workDay), 2) +} + const getTaskIdFromRowId = (value: string): number | null => { const match = /^task-(\d+)-\d+$/.exec(value) return match ? toSafeInteger(match[1]) : null @@ -1063,18 +1366,6 @@ const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] = .filter((item): item is ExportScaleRow => Boolean(item)) } -const sumLeafScaleCost = (rows: ScaleRowLike[] | undefined) => { - if (!Array.isArray(rows)) return 0 - return sumNumbers( - rows.map(row => { - if (row?.isGroupRow === true) return null - return toFiniteNumber(row?.amount) - }) - ) -} - - - const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => { if (!Array.isArray(rows)) return null let hasTotalValue = false @@ -1083,10 +1374,11 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n const major = toSafeInteger(row.id) if (major == null) return null const cost = toFiniteNumber(row.amount) - const basicFee = toFiniteNumber(row.benchmarkBudget) - const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic) - const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional) - const fee = toFiniteNumber(row.budgetFee) + const computed = resolveScaleMethodComputedValues(row, 'cost') + const basicFee = computed.benchmarkBudget + const basicFeeBasic = computed.benchmarkBudgetBasic + const basicFeeOptional = computed.benchmarkBudgetOptional + const fee = computed.budgetFee if (basicFee != null || fee != null) hasTotalValue = true const remark = typeof row.remark === 'string' ? row.remark : '' const hasValue = @@ -1101,9 +1393,9 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n major, cost: cost ?? 0, basicFee: basicFee ?? 0, - basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '', + basicFormula: computed.basicFormula, basicFee_basic: basicFeeBasic ?? 0, - optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '', + optionalFormula: computed.optionalFormula, basicFee_optional: basicFeeOptional ?? 0, serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor), @@ -1134,10 +1426,11 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n const major = toSafeInteger(row.id) if (major == null) return null const area = toFiniteNumber(row.landArea) - const basicFee = toFiniteNumber(row.benchmarkBudget) - const basicFeeBasic = toFiniteNumber(row.benchmarkBudgetBasic) - const basicFeeOptional = toFiniteNumber(row.benchmarkBudgetOptional) - const fee = toFiniteNumber(row.budgetFee) + const computed = resolveScaleMethodComputedValues(row, 'area') + const basicFee = computed.benchmarkBudget + const basicFeeBasic = computed.benchmarkBudgetBasic + const basicFeeOptional = computed.benchmarkBudgetOptional + const fee = computed.budgetFee if (basicFee != null || fee != null) hasTotalValue = true const remark = typeof row.remark === 'string' ? row.remark : '' const hasValue = @@ -1152,9 +1445,9 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n major, area: area ?? 0, basicFee: basicFee ?? 0, - basicFormula: typeof row.basicFormula === 'string' ? row.basicFormula : '', + basicFormula: computed.basicFormula, basicFee_basic: basicFeeBasic ?? 0, - optionalFormula: typeof row.optionalFormula === 'string' ? row.optionalFormula : '', + optionalFormula: computed.optionalFormula, basicFee_optional: basicFeeOptional ?? 0, serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor), @@ -1183,10 +1476,10 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3 const det = rows .map(row => { const task = getTaskIdFromRowId(row.id) - if (task == null || row.basicFee == null) return null + if (task == null) return null const amount = toFiniteNumber(row.workload) - const basicFee = toFiniteNumber(row.basicFee) - const fee = toFiniteNumber(row.serviceFee) + const basicFee = toFiniteNumber(row.basicFee) ?? calcWorkloadBasicFeeFromRow(row) + const fee = toFiniteNumber(row.serviceFee) ?? calcWorkloadServiceFeeFromRow(row, basicFee) if (fee != null) hasTotalValue = true const remark = typeof row.remark === 'string' ? row.remark : '' const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark) @@ -1217,10 +1510,10 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 | const det = rows .map(row => { const expert = getExpertIdFromRowId(row.id) - if (expert == null || row.serviceBudget == null) return null + if (expert == null) return null const personNum = toFiniteNumber(row.personnelCount) const workDay = toFiniteNumber(row.workdayCount) - const fee = toFiniteNumber(row.serviceBudget) + const fee = toFiniteNumber(row.serviceBudget) ?? calcHourlyServiceFeeFromRow(row) if (fee != null) hasTotalValue = true const remark = typeof row.remark === 'string' ? row.remark : '' const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark) @@ -1416,18 +1709,20 @@ const buildReserveExport = async (contractId: string): Promise => { - const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([ - kvStore.getItem(PROJECT_INFO_DB_KEY), - kvStore.getItem(LEGACY_PROJECT_DB_KEY), - kvStore.getItem>(CONSULT_CATEGORY_FACTOR_DB_KEY), - kvStore.getItem>(MAJOR_FACTOR_DB_KEY), - kvStore.getItem('ht-card-v1') + const config = getReportWorkspaceConfig() + const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw, quickContractRaw] = await Promise.all([ + kvStore.getItem(config.projectInfoKey), + kvStore.getItem(config.projectScaleKey), + kvStore.getItem>(config.consultCategoryFactorKey), + kvStore.getItem>(config.majorFactorKey), + config.contractCardsKey ? kvStore.getItem(config.contractCardsKey) : Promise.resolve(null), + config.quickContractKey ? kvStore.getItem(config.quickContractKey) : Promise.resolve(null) ]) const projectInfo = projectInfoRaw || {} const projectScaleSource = projectScaleRaw || {} const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows) - const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumLeafScaleCost(projectScaleSource.detailRows) + const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) projectScale.push({ major: -1, cost: projectScaleCost, area: null @@ -1435,13 +1730,21 @@ const buildExportReportPayload = async (): Promise => { const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows) const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows) - const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目' + const quickContract = normalizeQuickContractMeta(quickContractRaw) + const projectName = + config.mode === 'quick' + ? `${quickContract.name}-快速计算` + : (isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目') const writer = isNonEmptyString(projectInfo.preparedBy) ? projectInfo.preparedBy.trim() : '' const reviewer = isNonEmptyString(projectInfo.reviewedBy) ? projectInfo.reviewedBy.trim() : '' const date = isNonEmptyString(projectInfo.preparedDate) ? projectInfo.preparedDate.trim() : '' const industry = mapIndustryCodeToExportIndustry(projectInfo.projectIndustry) - const contractCards = (Array.isArray(contractCardsRaw) ? contractCardsRaw : []) + const contractCards = ( + config.mode === 'quick' + ? [{ id: quickContract.id, name: quickContract.name, order: 0 }] + : (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)) @@ -1576,6 +1879,9 @@ const buildExportReportPayload = async (): Promise => { } return { + version: 2, + reportType: 'budget-report', + mode: config.mode, name: projectName, writer, reviewer, @@ -1586,7 +1892,19 @@ const buildExportReportPayload = async (): Promise => { scale: projectScale, serviceCoes: projectServiceCoes, majorCoes: projectMajorCoes, - contracts + contracts, + project: { + name: projectName, + writer, + reviewer, + date, + industry, + fee: sumNumbers(contracts.map(item => item.fee)), + scaleCost: projectScaleCost, + scale: projectScale, + serviceCoes: projectServiceCoes, + majorCoes: projectMajorCoes + } } } @@ -1602,12 +1920,18 @@ const exportData = async () => { })) ) const payload: DataPackage = { - version: 2, + version: 3, + packageType: 'workspace-snapshot', exportedAt: now.toISOString(), - localStorage: readWebStorage(localStorage), - sessionStorage: readWebStorage(sessionStorage), - localforageDefault: await readForage(localforage), - localforageStores: piniaForageStores + workspace: { + mode: resolveWorkspaceModeForExport() + }, + storage: { + localStorage: readWebStorage(localStorage), + sessionStorage: readWebStorage(sessionStorage), + localforageDefault: await readForage(localforage), + localforageStores: piniaForageStores + } } const content = await encodeZwArchive(payload) @@ -1617,7 +1941,7 @@ const exportData = async () => { const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url - const projectName = getExportProjectName(payload.localforageDefault) + const projectName = getExportProjectName(payload.storage?.localforageDefault || [], piniaForageStores) const timestamp = formatExportTimestamp(now) link.download = `${projectName}-${timestamp}${ZW_FILE_EXTENSION}` document.body.appendChild(link) @@ -1635,12 +1959,12 @@ const exportData = async () => { const exportReport = async () => { try { showReportExportProgress(10, '正在准备报表导出...') + await requestXmScalePersistFlush() const now = new Date() showReportExportProgress(40, '正在汇总报表数据...') const payload = await buildExportReportPayload() showReportExportProgress(80, '正在生成并写出报表文件...') const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}` - console.log(payload) await exportFile(fileName, payload) finishReportExportProgress(true, '报表导出完成') } catch (error) { @@ -1654,9 +1978,7 @@ const triggerImport = () => { importFileRef.value?.click() } -const importData = async (event: Event) => { - const input = event.target as HTMLInputElement - const file = input.files?.[0] +const handleSelectedImportFile = async (file: File) => { if (!file) return try { @@ -1665,12 +1987,34 @@ const importData = async (event: Event) => { } const buffer = await file.arrayBuffer() const payload = await decodeZwArchive(buffer) - pendingImportPayload.value = payload + const normalizedStorage = normalizeDataPackageStorage(payload) + pendingImportPayload.value = { + ...payload, + workspace: normalizeDataPackageWorkspace(payload), + storage: normalizedStorage + } pendingImportFileName.value = file.name importConfirmOpen.value = true } catch (error) { console.error('import failed:', error) window.alert('导入失败:文件无效、已损坏或被修改。') + } +} + +const handleHomeImportSelected = (event: Event) => { + const customEvent = event as CustomEvent<{ file?: File | null }> + const file = customEvent.detail?.file + if (!file) return + void handleSelectedImportFile(file) +} + +const importData = async (event: Event) => { + const input = event.target as HTMLInputElement + const file = input.files?.[0] + if (!file) return + + try { + await handleSelectedImportFile(file) } finally { input.value = '' } @@ -1686,10 +2030,12 @@ const confirmImportOverride = async () => { const payload = pendingImportPayload.value if (!payload) return try { - writeWebStorage(localStorage, normalizeEntries(payload.localStorage)) - writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage)) - await writeForage(localforage, normalizeEntries(payload.localforageDefault)) - const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores) + const normalizedStorage = normalizeDataPackageStorage(payload) + const normalizedWorkspace = normalizeDataPackageWorkspace(payload) + writeWebStorage(localStorage, normalizedStorage.localStorage) + writeWebStorage(sessionStorage, normalizedStorage.sessionStorage) + await writeForage(localforage, normalizedStorage.localforageDefault) + const piniaSnapshots = normalizeForageStoreSnapshots(normalizedStorage.localforageStores) const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries])) await Promise.all( getPiniaPersistStores().map(async ({ storeName, store }) => { @@ -1714,6 +2060,7 @@ const confirmImportOverride = async () => { } else { tabStore.resetTabs() } + normalizeTabStoreState() const zxFwPricingState = readPersistedState('zxFwPricing') if (zxFwPricingState) { @@ -1730,6 +2077,7 @@ const confirmImportOverride = async () => { zxFwPricingStore.$persistNow?.(), kvStore.$persistNow?.() ]) + writeWorkspaceMode(normalizedWorkspace.mode) dataMenuOpen.value = false window.location.reload() } catch (error) { @@ -1761,8 +2109,10 @@ const handleReset = async () => { } onMounted(() => { + normalizeTabStoreState() window.addEventListener('mousedown', handleGlobalMouseDown) window.addEventListener('keydown', handleGlobalKeyDown) + window.addEventListener('home-import-selected', handleHomeImportSelected as EventListener) window.addEventListener('resize', scheduleUpdateTabTitleOverflow) void nextTick(() => { bindTabStripScroll() @@ -1782,6 +2132,7 @@ onBeforeUnmount(() => { clearReportExportToastTimer() window.removeEventListener('mousedown', handleGlobalMouseDown) window.removeEventListener('keydown', handleGlobalKeyDown) + window.removeEventListener('home-import-selected', handleHomeImportSelected as EventListener) window.removeEventListener('resize', scheduleUpdateTabTitleOverflow) if (tabStripViewportEl) { tabStripViewportEl.removeEventListener('scroll', handleTabStripScroll) @@ -1834,8 +2185,15 @@ watch(
-
-
+
-
- -
-
- - +
+
+ {{ props.metaText }} +
diff --git a/src/lib/projectWorkspace.ts b/src/lib/projectWorkspace.ts new file mode 100644 index 0000000..dd2d2ab --- /dev/null +++ b/src/lib/projectWorkspace.ts @@ -0,0 +1,112 @@ +import { + getIndustryTypeValue, + getMajorDictEntries, + getServiceDictEntries, + isIndustryEnabledByType, + isMajorIdInIndustryScope +} from '@/sql' + +interface KvStoreLike { + setItem: (keyRaw: string | number, value: T) => Promise +} + +type DictItemLite = { + code?: string + name?: string + defCoe?: number | null + notshowByzxflxs?: boolean +} + +type FactorPersistRow = { + id: string + code: string + name: string + standardFactor: number | null + budgetValue: number | null + remark: string + path: string[] +} + +type FactorPersistState = { + detailRows: FactorPersistRow[] +} + +const buildCodePath = (code: string, selfId: string, codeIdMap: Map) => { + const parts = code.split('-').filter(Boolean) + if (!parts.length) return [selfId] + + const path: string[] = [] + let currentCode = parts[0] + const firstId = codeIdMap.get(currentCode) + if (firstId) path.push(firstId) + + for (let index = 1; index < parts.length; index += 1) { + currentCode = `${currentCode}-${parts[index]}` + const id = codeIdMap.get(currentCode) + if (id) path.push(id) + } + + if (!path.length || path[path.length - 1] !== selfId) path.push(selfId) + return path +} + +const buildFactorRowsFromEntries = (entries: Array<{ id: string; item: DictItemLite }>): FactorPersistRow[] => { + const codeIdMap = new Map() + for (const entry of entries) { + const code = String(entry.item?.code || '').trim() + if (!code) continue + codeIdMap.set(code, entry.id) + } + + return entries + .map(entry => { + const code = String(entry.item?.code || '').trim() + const name = String(entry.item?.name || '').trim() + if (!code || !name) return null + const standardFactor = + typeof entry.item?.defCoe === 'number' && Number.isFinite(entry.item.defCoe) + ? entry.item.defCoe + : null + return { + id: entry.id, + code, + name, + standardFactor, + budgetValue: standardFactor, + remark: '', + path: buildCodePath(code, entry.id, codeIdMap) + } + }) + .filter((item): item is FactorPersistRow => Boolean(item)) +} + +export const initializeProjectFactorStates = async ( + kvStore: KvStoreLike, + industry: string, + consultCategoryFactorKey: string, + majorFactorKey: string +) => { + const industryType = getIndustryTypeValue(industry) + const consultEntries = getServiceDictEntries() + .map(({ id, item }) => ({ id, item: item as DictItemLite })) + .filter(({ item }) => { + if (item.notshowByzxflxs === true) return false + return isIndustryEnabledByType(item as Record, industryType) + }) + + const majorEntries = getMajorDictEntries() + .map(({ id, item }) => ({ id, item: item as DictItemLite })) + .filter(({ id, item }) => item.notshowByzxflxs !== true && isMajorIdInIndustryScope(id, industry)) + + const consultPayload: FactorPersistState = { + detailRows: buildFactorRowsFromEntries(consultEntries) + } + const majorPayload: FactorPersistState = { + detailRows: buildFactorRowsFromEntries(majorEntries) + } + + await Promise.all([ + kvStore.setItem(consultCategoryFactorKey, consultPayload), + kvStore.setItem(majorFactorKey, majorPayload) + ]) +} diff --git a/src/lib/workspace.ts b/src/lib/workspace.ts new file mode 100644 index 0000000..e83419d --- /dev/null +++ b/src/lib/workspace.ts @@ -0,0 +1,52 @@ +export type WorkspaceMode = 'home' | 'project' | 'quick' + +export const HOME_TAB_ID = 'HomeView' +export const PROJECT_TAB_ID = 'ProjectCalcView' +export const QUICK_TAB_ID = 'QuickCalcView' +export const LEGACY_PROJECT_TAB_ID = 'XmView' +export const FIXED_WORKSPACE_TAB_IDS = [PROJECT_TAB_ID, QUICK_TAB_ID] as const + +export const WORKSPACE_MODE_STORAGE_KEY = 'jgjs-workspace-mode-v1' + +export const QUICK_CONTRACT_ID = 'quick-contract-default' +export const QUICK_CONTRACT_META_KEY = 'quick-contract-meta-v1' +export const QUICK_CONTRACT_FALLBACK_NAME = '快速计算合同' +export const QUICK_CONTRACT_TAB_ID = `contract-${QUICK_CONTRACT_ID}` + +export const QUICK_PROJECT_INFO_KEY = 'quick-xm-base-info-v1' +export const QUICK_PROJECT_SCALE_KEY = 'quick-xm-info-v3' +export const QUICK_CONSULT_CATEGORY_FACTOR_KEY = 'quick-xm-consult-category-factor-v1' +export const QUICK_MAJOR_FACTOR_KEY = 'quick-xm-major-factor-v1' + +export interface QuickContractMeta { + id: string + name: string + updatedAt: string +} + +export const normalizeWorkspaceMode = (value: unknown): WorkspaceMode => { + if (value === 'project' || value === 'quick' || value === 'home') return value + return 'home' +} + +export const readWorkspaceMode = (): WorkspaceMode => { + try { + return normalizeWorkspaceMode(window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY)) + } catch { + return 'home' + } +} + +export const writeWorkspaceMode = (mode: WorkspaceMode) => { + try { + window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, normalizeWorkspaceMode(mode)) + } catch { + // 忽略只读或隐私模式下的写入失败。 + } +} + +export const createDefaultQuickContractMeta = (): QuickContractMeta => ({ + id: QUICK_CONTRACT_ID, + name: QUICK_CONTRACT_FALLBACK_NAME, + updatedAt: new Date().toISOString() +}) diff --git a/src/pinia/Plugin/indexdb.ts b/src/pinia/Plugin/indexdb.ts index 6cfa828..9ac40dd 100644 --- a/src/pinia/Plugin/indexdb.ts +++ b/src/pinia/Plugin/indexdb.ts @@ -123,18 +123,19 @@ export default (config?: PiniaStorageConfig) => { { detached: true } ) + hydrating = true void lf.getItem>(key) .then(state => { if (!state || typeof state !== 'object') return // 若在异步hydrate返回前,store已被用户修改(如removeTab),不再回填旧缓存覆盖当前状态。 if (userMutatedBeforeHydrate) return - hydrating = true context.store.$patch(state as any) - hydrating = false }) .catch(error => { - hydrating = false console.error('pinia hydrate failed:', error) }) + .finally(() => { + hydrating = false + }) } } diff --git a/src/pinia/tab.ts b/src/pinia/tab.ts index 71e350f..a66f9b3 100644 --- a/src/pinia/tab.ts +++ b/src/pinia/tab.ts @@ -1,5 +1,6 @@ import { defineStore } from 'pinia' import { ref } from 'vue' +import { HOME_TAB_ID, PROJECT_TAB_ID, QUICK_CONTRACT_TAB_ID } from '@/lib/workspace' export interface TabItem> { id: string @@ -8,14 +9,14 @@ export interface TabItem> { props?: TProps } -const HOME_TAB_ID = 'XmView' const DEFAULT_TAB: TabItem = { id: HOME_TAB_ID, - title: '项目卡片', + title: '首页', componentName: HOME_TAB_ID } const createDefaultTabs = (): TabItem[] => [{ ...DEFAULT_TAB }] +const PROTECTED_TAB_ID_SET = new Set([HOME_TAB_ID, QUICK_CONTRACT_TAB_ID]) export const useTabStore = defineStore( 'tabs', @@ -36,6 +37,11 @@ export const useTabStore = defineStore( } } + const enterWorkspace = (config: TabItem) => { + tabs.value = [{ ...config }] + activeTabId.value = config.id + } + const openTab = (config: TabItem) => { if (!tabs.value.some(tab => tab.id === config.id)) { tabs.value = [...tabs.value, config] @@ -44,8 +50,7 @@ export const useTabStore = defineStore( } const removeTab = (id: string) => { - // 首页标签固定保留,不允许关闭。 - if (id === HOME_TAB_ID) return + if (PROTECTED_TAB_ID_SET.has(id)) return const index = tabs.value.findIndex(tab => tab.id === id) if (index < 0) return @@ -64,26 +69,27 @@ export const useTabStore = defineStore( } const closeAllTabs = () => { - tabs.value = createDefaultTabs() - activeTabId.value = HOME_TAB_ID + const protectedTabs = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id)) + tabs.value = protectedTabs.length > 0 ? protectedTabs : createDefaultTabs() + activeTabId.value = tabs.value[0]?.id ?? HOME_TAB_ID } const closeLeftTabs = (targetId: string) => { const targetIndex = tabs.value.findIndex(tab => tab.id === targetId) if (targetIndex < 0) return - tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index >= targetIndex) + tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index >= targetIndex) ensureActiveValid() } const closeRightTabs = (targetId: string) => { const targetIndex = tabs.value.findIndex(tab => tab.id === targetId) if (targetIndex < 0) return - tabs.value = tabs.value.filter((tab, index) => tab.id === HOME_TAB_ID || index <= targetIndex) + tabs.value = tabs.value.filter((tab, index) => PROTECTED_TAB_ID_SET.has(tab.id) || index <= targetIndex) ensureActiveValid() } const closeOtherTabs = (targetId: string) => { - tabs.value = tabs.value.filter(tab => tab.id === HOME_TAB_ID || tab.id === targetId) + tabs.value = tabs.value.filter(tab => PROTECTED_TAB_ID_SET.has(tab.id) || tab.id === targetId) ensureHomeTab() activeTabId.value = tabs.value.some(tab => tab.id === targetId) ? targetId : HOME_TAB_ID } @@ -96,6 +102,7 @@ export const useTabStore = defineStore( return { tabs, activeTabId, + enterWorkspace, openTab, removeTab, closeAllTabs, diff --git a/src/pinia/zxFwPricing.ts b/src/pinia/zxFwPricing.ts index 1b1f97d..bc255b9 100644 --- a/src/pinia/zxFwPricing.ts +++ b/src/pinia/zxFwPricing.ts @@ -328,6 +328,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return state[method] || null } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取咨询服务计价方法状态 + * @returns {*} + */ const getServicePricingMethodState = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -339,6 +346,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState | undefined) || null } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 设置咨询服务计价方法状态并同步版本 + * @returns {*} + */ const setServicePricingMethodState = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -379,6 +393,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 从缓存或持久化存储加载咨询服务计价方法状态 + * @returns {*} + */ const loadServicePricingMethodState = async ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -404,6 +425,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return getServicePricingMethodState(contractId, serviceId, method) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 删除单个咨询服务计价方法状态 + * @returns {*} + */ const removeServicePricingMethodState = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -422,6 +450,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return had } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取单个咨询服务计价方法存储键 + * @returns {*} + */ const getServicePricingStorageKey = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -433,6 +468,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return serviceMethodDbKeyOf(contractId, serviceId, method) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取咨询服务全部计价方法存储键 + * @returns {*} + */ const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => { const contractId = toKey(contractIdRaw) const serviceId = toServiceKey(serviceIdRaw) @@ -442,6 +484,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { ) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 删除咨询服务全部计价方法状态 + * @returns {*} + */ const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => { let changed = false for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) { @@ -450,12 +499,26 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return changed } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取合同附加费用主表状态 + * @returns {*} + */ const getHtFeeMainState = (mainStorageKeyRaw: string | number): HtFeeMainState | null => { const mainStorageKey = toKey(mainStorageKeyRaw) if (!mainStorageKey) return null return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState | undefined) || null } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 设置合同附加费用主表状态并同步版本 + * @returns {*} + */ const setHtFeeMainState = ( mainStorageKeyRaw: string | number, payload: Partial> | null | undefined, @@ -494,10 +557,18 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 从缓存或持久化存储加载合同附加费用主表状态 + * @returns {*} + */ const loadHtFeeMainState = async ( mainStorageKeyRaw: string | number, force = false ): Promise | null> => { + const mainStorageKey = toKey(mainStorageKeyRaw) if (!mainStorageKey) return null if (!force) { @@ -513,6 +584,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return getHtFeeMainState(mainStorageKey) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 删除合同附加费用主表状态 + * @returns {*} + */ const removeHtFeeMainState = (mainStorageKeyRaw: string | number) => setHtFeeMainState(mainStorageKeyRaw, null) @@ -529,6 +607,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return htFeeMethodStates.value[mainStorageKey][rowId] } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取合同附加费用子方法存储键 + * @returns {*} + */ const getHtFeeMethodStorageKey = ( mainStorageKeyRaw: string | number, rowIdRaw: string | number, @@ -540,6 +625,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return `${mainStorageKey}-${rowId}-${method}` } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取合同附加费用子方法状态 + * @returns {*} + */ const getHtFeeMethodState = ( mainStorageKeyRaw: string | number, rowIdRaw: string | number, @@ -552,6 +644,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return value == null ? null : (cloneAny(value) as TPayload) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 设置合同附加费用子方法状态并同步版本 + * @returns {*} + */ const setHtFeeMethodState = ( mainStorageKeyRaw: string | number, rowIdRaw: string | number, @@ -564,6 +663,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { ) => { const mainStorageKey = toKey(mainStorageKeyRaw) const rowId = toKey(rowIdRaw) + if (!mainStorageKey || !rowId) return false const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method) if (!storageKey) return false @@ -605,6 +705,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 从缓存或持久化存储加载合同附加费用子方法状态 + * @returns {*} + */ const loadHtFeeMethodState = async ( mainStorageKeyRaw: string | number, rowIdRaw: string | number, @@ -628,12 +735,26 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return getHtFeeMethodState(mainStorageKey, rowId, method) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 删除合同附加费用子方法状态 + * @returns {*} + */ const removeHtFeeMethodState = ( mainStorageKeyRaw: string | number, rowIdRaw: string | number, method: HtFeeMethodType ) => setHtFeeMethodState(mainStorageKeyRaw, rowIdRaw, method, null) + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 按通用键获取状态 + * @returns {*} + */ const getKeyState = (keyRaw: string | number): T | null => { const key = toKey(keyRaw) if (!key) return null @@ -664,6 +785,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return cloneAny(keyedStates.value[key] as T) } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 按通用键从缓存或持久化存储加载状态 + * @returns {*} + */ const loadKeyState = async (keyRaw: string | number, force = false): Promise => { const key = toKey(keyRaw) if (!key) return null @@ -725,6 +853,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { } } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 按通用键设置状态并同步版本 + * @returns {*} + */ const setKeyState = ( keyRaw: string | number, value: T, @@ -749,6 +884,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { { force: true, syncKeyState: false } ) } + const htMethodMeta = parseHtFeeMethodStorageKey(key) if (htMethodMeta) { setHtFeeMethodState( @@ -769,6 +905,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 按通用键删除状态 + * @returns {*} + */ const removeKeyState = (keyRaw: string | number) => { const key = toKey(keyRaw) if (!key) return false @@ -795,12 +938,26 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return hadValue } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取指定键的版本号 + * @returns {*} + */ const getKeyVersion = (keyRaw: string | number) => { const key = toKey(keyRaw) if (!key) return 0 return keyVersions.value[key] || 0 } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取合同咨询服务主表状态 + * @returns {*} + */ const getContractState = (contractIdRaw: string | number) => { const contractId = toKey(contractIdRaw) if (!contractId) return null @@ -808,6 +965,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return data ? cloneState(data) : null } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 从缓存或持久化存储加载合同咨询服务主表状态 + * @returns {*} + */ const loadContract = async (contractIdRaw: string | number, force = false) => { const contractId = toKey(contractIdRaw) if (!contractId) return null @@ -843,6 +1007,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { } } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 设置合同咨询服务主表状态 + * @returns {*} + */ const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => { const contractId = toKey(contractIdRaw) if (!contractId) return false @@ -855,6 +1026,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 更新合同咨询服务行的计价汇总字段 + * @returns {*} + */ const updatePricingField = async (params: { contractId: string serviceId: string | number @@ -900,6 +1078,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 获取合同咨询服务基础小计 + * @returns {*} + */ const getBaseSubtotal = (contractIdRaw: string | number): number | null => { const contractId = toKey(contractIdRaw) if (!contractId) return null @@ -920,6 +1105,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return hasValid ? round3(sum) : null } + /** + * @Author: wintsa + * @Date: 2026-03-13 + * @LastEditors: wintsa + * @Description: 删除合同关联的咨询服务、附加费用和键状态数据 + * @returns {*} + */ const removeContractData = (contractIdRaw: string | number) => { const contractId = toKey(contractIdRaw) if (!contractId) return false diff --git a/src/sql.ts b/src/sql.ts index e6d0ed4..2862a66 100644 --- a/src/sql.ts +++ b/src/sql.ts @@ -1114,16 +1114,18 @@ async function generateTemplate(data) { } }); let endRows = 1; - cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => { - targetRow.getCell(1).value = ci.services.length + endRows; - targetRow.getCell(2).value = ''; - targetRow.getCell(3).value = '基本、可选工作小计'; - targetRow.getCell(4).value = numberFormatter(m1Sum, 2); - targetRow.getCell(5).value = numberFormatter(m2Sum, 2); - targetRow.getCell(6).value = numberFormatter(m3Sum, 2); - targetRow.getCell(7).value = numberFormatter(m4Sum, 2); - targetRow.getCell(8).value = numberFormatter(serviceSum, 2); - }); + if (ci.services.length) { + cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => { + targetRow.getCell(1).value = ci.services.length + endRows; + targetRow.getCell(2).value = ''; + targetRow.getCell(3).value = '基本、可选工作小计'; + targetRow.getCell(4).value = numberFormatter(m1Sum, 2); + targetRow.getCell(5).value = numberFormatter(m2Sum, 2); + targetRow.getCell(6).value = numberFormatter(m3Sum, 2); + targetRow.getCell(7).value = numberFormatter(m4Sum, 2); + targetRow.getCell(8).value = numberFormatter(serviceSum, 2); + }); + } if (ci.addtional) { endRows++; cusInsertRowFunc(ci.services.length + 3 + endRows, [sheet_1.getRow(3)], sheet_1, (targetRow) => {