diff --git a/src/components/ht/Ht.vue b/src/components/ht/Ht.vue index 3cef0f2..9dcb6b8 100644 --- a/src/components/ht/Ht.vue +++ b/src/components/ht/Ht.vue @@ -7,6 +7,8 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip' import { useTabStore } from '@/pinia/tab' import { useZxFwPricingStore } from '@/pinia/zxFwPricing' +import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys' +import { useZxFwPricingHtFeeStore } from '@/pinia/zxFwPricingHtFee' import { useKvStore } from '@/pinia/kv' import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next' import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive' @@ -51,10 +53,12 @@ interface ContractSegmentPackage { } storage?: { localforageEntries: DataEntry[] + keyedEntries?: DataEntry[] } contracts: ContractItem[] projectIndustry?: string localforageEntries?: DataEntry[] + keyedEntries?: DataEntry[] pinia?: { zxFwPricing?: { contracts?: Record @@ -127,6 +131,8 @@ const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly const tabStore = useTabStore() const zxFwPricingStore = useZxFwPricingStore() +const zxFwPricingKeysStore = useZxFwPricingKeysStore() +const zxFwPricingHtFeeStore = useZxFwPricingHtFeeStore() const kvStore = useKvStore() @@ -633,6 +639,7 @@ const normalizeContractSegmentPackage = (payload: ContractSegmentPackage) => ({ ? payload.project.industry.trim() : (typeof payload.projectIndustry === 'string' ? payload.projectIndustry.trim() : ''), localforageEntries: normalizeDataEntries(payload.storage?.localforageEntries ?? payload.localforageEntries), + keyedEntries: normalizeDataEntries(payload.storage?.keyedEntries ?? payload.keyedEntries), piniaState: payload.pinia ?? payload.piniaState }) @@ -756,6 +763,13 @@ const isContractRelatedForageKey = (key: string, contractId: string) => { return false } +const isContractRelatedKeyedStateKey = (key: string, contractId: string) => { + if (key === `ht-base-info-${contractId}`) return true + if (key.startsWith(`work-content-${contractId}-`)) return true + if (key.startsWith(`work-content-htExtraFee-${contractId}-`)) return true + return false +} + const readContractRelatedForageEntries = async (contractIds: string[]) => { const keys = await kvStore.keys() const idSet = new Set(contractIds) @@ -773,6 +787,21 @@ const readContractRelatedForageEntries = async (contractIds: string[]) => { ) } +const readContractRelatedKeyedEntries = (contractIds: string[]) => { + const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean)) + return Object.entries(zxFwPricingStore.keyedStates) + .filter(([key]) => { + for (const id of idSet) { + if (isContractRelatedKeyedStateKey(key, id)) return true + } + return false + }) + .map(([key, value]) => ({ + key, + value: cloneJson(value) + })) +} + const rewriteKeyWithContractId = (key: string, fromId: string, toId: string) => { if (key === `${CONTRACT_KEY_PREFIX}${fromId}`) return `${CONTRACT_KEY_PREFIX}${toId}` if (key === `${SERVICE_KEY_PREFIX}${fromId}`) return `${SERVICE_KEY_PREFIX}${toId}` @@ -782,6 +811,13 @@ const rewriteKeyWithContractId = (key: string, fromId: string, toId: string) => if (key === `${CONTRACT_MAJOR_FACTOR_KEY_PREFIX}${fromId}`) { return `${CONTRACT_MAJOR_FACTOR_KEY_PREFIX}${toId}` } + if (key === `ht-base-info-${fromId}`) return `ht-base-info-${toId}` + if (key.startsWith(`work-content-${fromId}-`)) { + return key.replace(`work-content-${fromId}-`, `work-content-${toId}-`) + } + if (key.startsWith(`work-content-htExtraFee-${fromId}-`)) { + return key.replace(`work-content-htExtraFee-${fromId}-`, `work-content-htExtraFee-${toId}-`) + } for (const prefix of PRICING_KEY_PREFIXES) { if (key.startsWith(`${prefix}${fromId}-`)) { return key.replace(`${prefix}${fromId}-`, `${prefix}${toId}-`) @@ -817,6 +853,9 @@ const exportSelectedContracts = async () => { const localforageEntries = await readContractRelatedForageEntries( selectedContracts.map(item => item.id) ) + const keyedEntries = readContractRelatedKeyedEntries( + selectedContracts.map(item => item.id) + ) const piniaPayload = await buildContractPiniaPayload(selectedContracts.map(item => item.id)) const projectIndustry = await getCurrentProjectIndustry() @@ -835,7 +874,8 @@ const exportSelectedContracts = async () => { }, contracts: selectedContracts, storage: { - localforageEntries + localforageEntries, + keyedEntries }, pinia: { zxFwPricing: piniaPayload @@ -898,6 +938,7 @@ const importContractSegments = async (event: Event) => { } const importedEntries = normalizedPackage.localforageEntries + const importedKeyedEntries = normalizedPackage.keyedEntries const usedIds = new Set(contracts.value.map(item => item.id)) const oldToNewIdMap = new Map() const nextContracts: ContractItem[] = importedContracts.map((item, index) => { @@ -923,11 +964,31 @@ const importContractSegments = async (event: Event) => { } }) + const rewrittenKeyedEntries = importedKeyedEntries.map(entry => { + let nextKey = entry.key + for (const [oldId, newId] of oldToNewIdMap.entries()) { + if (!nextKey.includes(oldId)) continue + nextKey = rewriteKeyWithContractId(nextKey, oldId, newId) + } + return { + key: nextKey, + value: entry.value + } + }) + await Promise.all(rewrittenEntries.map(entry => kvStore.setItem(entry.key, entry.value))) + for (const entry of rewrittenKeyedEntries) { + zxFwPricingStore.setKeyState(entry.key, cloneJson(entry.value), { force: true }) + } await applyImportedContractPiniaPayload(normalizedPackage.piniaState, oldToNewIdMap) contracts.value = [...contracts.value, ...nextContracts] await saveContracts() + await Promise.all([ + zxFwPricingStore.$persistNow?.(), + zxFwPricingKeysStore.$persistNow?.(), + zxFwPricingHtFeeStore.$persistNow?.() + ]) await refreshContractBudgets() notify(`导入成功(${nextContracts.length} 个合同段)`) await nextTick() @@ -984,7 +1045,11 @@ const cleanupContractRelatedData = async (contractId: string) => { await Promise.all([ removeForageKeysByContractId(kvStore, contractId), ]) - await zxFwPricingStore.$persistNow?.() + await Promise.all([ + zxFwPricingStore.$persistNow?.(), + zxFwPricingKeysStore.$persistNow?.(), + zxFwPricingHtFeeStore.$persistNow?.() + ]) } const loadContracts = async () => { diff --git a/src/components/ht/zxFw.vue b/src/components/ht/zxFw.vue index a36246f..b11c087 100644 --- a/src/components/ht/zxFw.vue +++ b/src/components/ht/zxFw.vue @@ -896,6 +896,7 @@ const applySelection = async (codes: string[]) => { const baseRows: DetailRow[] = uniqueIds .map(id => { + const dictItem = serviceById.value.get(id) if (!dictItem) return null @@ -957,6 +958,7 @@ const applySelection = async (codes: string[]) => { * 服务勾选变化入口:先更新行,再刷新新增服务的计价汇总。 */ const handleServiceSelectionChange = async (ids: string[]) => { + const prevIds = [...selectedIds.value] await applySelection(ids) const nextSelectedIds = getCurrentContractState().selectedIds || [] diff --git a/src/components/shared/xmCommonAgGrid.vue b/src/components/shared/xmCommonAgGrid.vue index dca2b27..d8621a3 100644 --- a/src/components/shared/xmCommonAgGrid.vue +++ b/src/components/shared/xmCommonAgGrid.vue @@ -7,6 +7,7 @@ import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgG import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { formatThousandsFlexible } from '@/lib/numberFormat' import { industryTypeList, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql' +import { syncContractScaleToPricing } from '@/lib/zxFwPricingSync' import { SwitchRoot, SwitchThumb } from 'reka-ui' import { useKvStore } from '@/pinia/kv' @@ -41,6 +42,7 @@ interface XmBaseInfoState { } const XM_SCALE_FLUSH_EVENT = 'jgjs:xm-scale-flush-request' +const CONTRACT_SCALE_KEY_PREFIX = 'ht-info-v3-' type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean } const kvStore = useKvStore() @@ -471,6 +473,12 @@ const saveToIndexedDB = async () => { payload.roughCalcEnabled = roughCalcEnabled.value payload.totalAmount = normalizedTotalAmount await kvStore.setItem(props.dbKey, payload) + if (props.dbKey.startsWith(CONTRACT_SCALE_KEY_PREFIX)) { + const contractId = props.dbKey.slice(CONTRACT_SCALE_KEY_PREFIX.length).trim() + if (contractId) { + await syncContractScaleToPricing(contractId) + } + } } catch (error) { console.error('saveToIndexedDB failed:', error) } diff --git a/src/components/views/HomeEntryView.vue b/src/components/views/HomeEntryView.vue index d249e0d..9a57770 100644 --- a/src/components/views/HomeEntryView.vue +++ b/src/components/views/HomeEntryView.vue @@ -35,6 +35,7 @@ import { QUICK_CONTRACT_META_KEY, QUICK_MAJOR_FACTOR_KEY, QUICK_PROJECT_INFO_KEY, + setPendingHomeImportFile, writeWorkspaceMode } from '@/lib/workspace' @@ -73,7 +74,6 @@ const kvStore = useKvStore() const projectDialogOpen = ref(false) const projectIndustry = ref(String(industryTypeList[0]?.id || '')) const projectSubmitting = ref(false) -const quickDialogOpen = ref(false) const quickIndustry = ref(String(industryTypeList[0]?.id || '')) const quickContractName = ref(QUICK_CONTRACT_FALLBACK_NAME) const quickSubmitting = ref(false) @@ -161,19 +161,28 @@ const loadQuickDefaults = async () => { : QUICK_CONTRACT_FALLBACK_NAME } -const openQuickCalcDialog = async () => { +const enterQuickCalc = (contractName: string) => { + writeWorkspaceMode('quick') + tabStore.enterWorkspace({ + id: `contract-${QUICK_CONTRACT_ID}`, + title: contractName, + componentName: 'QuickCalcWorkbenchView', + props: { + contractId: QUICK_CONTRACT_ID, + contractName, + projectInfoKey: QUICK_PROJECT_INFO_KEY, + projectScaleKey: null, + projectConsultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY, + projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY + } + }) + tabStore.hasCompletedSetup = true +} + +const openQuickCalc = async () => { await loadQuickDefaults() - quickDialogOpen.value = true -} - -const closeQuickCalcDialog = () => { - quickDialogOpen.value = false -} - -const confirmQuickCalc = async () => { - const contractName = quickContractName.value.trim() + const contractName = quickContractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME const industry = quickIndustry.value.trim() - if (!contractName || !industry) return quickSubmitting.value = true try { @@ -188,31 +197,17 @@ const confirmQuickCalc = async () => { name: contractName, updatedAt: new Date().toISOString() }) - await initializeProjectFactorStates( - kvStore, - industry, - QUICK_CONSULT_CATEGORY_FACTOR_KEY, - QUICK_MAJOR_FACTOR_KEY - ) - - writeWorkspaceMode('quick') - tabStore.enterWorkspace({ - id: `contract-${QUICK_CONTRACT_ID}`, - title: contractName, - componentName: 'QuickCalcWorkbenchView', - props: { - contractId: QUICK_CONTRACT_ID, - contractName, - projectInfoKey: QUICK_PROJECT_INFO_KEY, - projectScaleKey: null, - projectConsultCategoryFactorKey: QUICK_CONSULT_CATEGORY_FACTOR_KEY, - projectMajorFactorKey: QUICK_MAJOR_FACTOR_KEY - } - }) - tabStore.hasCompletedSetup = true + if (industry) { + await initializeProjectFactorStates( + kvStore, + industry, + QUICK_CONSULT_CATEGORY_FACTOR_KEY, + QUICK_MAJOR_FACTOR_KEY + ) + } + enterQuickCalc(contractName) } finally { quickSubmitting.value = false - quickDialogOpen.value = false } } @@ -220,6 +215,7 @@ const handleHomeImportChange = (event: Event) => { const input = event.target as HTMLInputElement const file = input.files?.[0] if (!file) return + setPendingHomeImportFile(file) window.dispatchEvent(new CustomEvent('home-import-selected', { detail: { file @@ -303,9 +299,9 @@ onMounted(() => { role="button" tabindex="0" class="home-card group flex cursor-pointer flex-col justify-between rounded-xl border border-slate-200/80 bg-white/95 px-5 py-5 shadow-[0_4px_20px_rgba(15,23,42,0.06)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200" - @click="openQuickCalcDialog" - @keydown.enter.prevent="openQuickCalcDialog" - @keydown.space.prevent="openQuickCalcDialog" + @click="openQuickCalc" + @keydown.enter.prevent="openQuickCalc" + @keydown.space.prevent="openQuickCalc" >
{
单项速算 - 单合同段预算、单项试算,选择行业与咨询类型,输入基数秒出结果 + 单项速算,选择行业与咨询类型,输入基数秒出结果
@@ -430,77 +426,6 @@ onMounted(() => {
-
-
-
-
-

单项速算

-

输入合同名称后,进入单项速算页面。

-
- -
- -
- - - -
- -
- - -
-
-