调整存储的逻辑
This commit is contained in:
parent
bbc8777b74
commit
3ad7bae1a9
@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
|||||||
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { syncPricingTotalToZxFw, type ZxFwPricingField, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw, type ZxFwPricingField } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
@ -402,7 +402,7 @@ const saveToIndexedDB = async () => {
|
|||||||
field: props.syncField,
|
field: props.syncField,
|
||||||
value: totalServiceBudget.value
|
value: totalServiceBudget.value
|
||||||
})
|
})
|
||||||
if (synced) pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
|
if (!synced) return
|
||||||
}
|
}
|
||||||
if (props.syncMainStorageKey && props.syncRowId) {
|
if (props.syncMainStorageKey && props.syncRowId) {
|
||||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
||||||
|
|||||||
@ -11,8 +11,7 @@ import { Pencil, Eraser } from 'lucide-vue-next'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
@ -65,15 +64,6 @@ interface MethodQuantityState {
|
|||||||
detailRows?: MethodQuantityRowLike[]
|
detailRows?: MethodQuantityRowLike[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ZxFwRowLike {
|
|
||||||
id?: unknown
|
|
||||||
subtotal?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZxFwStateLike {
|
|
||||||
detailRows?: ZxFwRowLike[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LegacyFeeRow {
|
interface LegacyFeeRow {
|
||||||
id?: string
|
id?: string
|
||||||
feeItem?: string
|
feeItem?: string
|
||||||
@ -91,7 +81,7 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
|
||||||
const createRowId = () => `fee-method-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
const createRowId = () => `fee-method-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||||
const createDefaultRow = (name = ''): FeeMethodRow => ({
|
const createDefaultRow = (name = ''): FeeMethodRow => ({
|
||||||
@ -121,17 +111,9 @@ const loadContractServiceFeeBase = async (): Promise<number | null> => {
|
|||||||
const contractId = String(props.contractId || '').trim()
|
const contractId = String(props.contractId || '').trim()
|
||||||
if (!contractId) return null
|
if (!contractId) return null
|
||||||
try {
|
try {
|
||||||
const data = await localforage.getItem<ZxFwStateLike>(`zxFW-${contractId}`)
|
await zxFwPricingStore.loadContract(contractId)
|
||||||
const rows = Array.isArray(data?.detailRows) ? data.detailRows : []
|
const base = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||||
const fixedRow = rows.find(row => String(row?.id || '') === 'fixed-budget-c')
|
return base == null ? null : round3(base)
|
||||||
const fixedSubtotal = toFiniteUnknown(fixedRow?.subtotal)
|
|
||||||
if (fixedSubtotal != null) return round3(fixedSubtotal)
|
|
||||||
const sum = rows.reduce((acc, row) => {
|
|
||||||
if (String(row?.id || '') === 'fixed-budget-c') return acc
|
|
||||||
const subtotal = toFiniteUnknown(row?.subtotal)
|
|
||||||
return subtotal == null ? acc : acc + subtotal
|
|
||||||
}, 0)
|
|
||||||
return round3(sum)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadContractServiceFeeBase failed:', error)
|
console.error('loadContractServiceFeeBase failed:', error)
|
||||||
return null
|
return null
|
||||||
@ -610,12 +592,13 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => pricingPaneReloadStore.persistedSeq,
|
() => {
|
||||||
|
const contractId = String(props.contractId || '').trim()
|
||||||
|
if (!contractId) return 0
|
||||||
|
return zxFwPricingStore.contractVersions[contractId] || 0
|
||||||
|
},
|
||||||
(nextVersion, prevVersion) => {
|
(nextVersion, prevVersion) => {
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||||
const contractId = String(props.contractId || '').trim()
|
|
||||||
if (!contractId) return
|
|
||||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastPersistedEvent, contractId, ZXFW_RELOAD_SERVICE_KEY)) return
|
|
||||||
void loadFromIndexedDB()
|
void loadFromIndexedDB()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,18 +3,8 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
|
|||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||||
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
interface ZxFwRowLike {
|
|
||||||
id?: unknown
|
|
||||||
subtotal?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZxFwStateLike {
|
|
||||||
detailRows?: ZxFwRowLike[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RateMethodState {
|
interface RateMethodState {
|
||||||
rate: number | null
|
rate: number | null
|
||||||
@ -28,18 +18,18 @@ const props = defineProps<{
|
|||||||
syncMainStorageKey?: string
|
syncMainStorageKey?: string
|
||||||
syncRowId?: string
|
syncRowId?: string
|
||||||
}>()
|
}>()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
|
||||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||||
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
|
||||||
const base = ref<number | null>(null)
|
const base = ref<number | null>(null)
|
||||||
const rate = ref<number | null>(null)
|
const rate = ref<number | null>(null)
|
||||||
const remark = ref('')
|
const remark = ref('')
|
||||||
const rateInput = ref('')
|
const rateInput = ref('')
|
||||||
|
const contractVersion = computed(() => {
|
||||||
const toFinite = (value: unknown): number | null => {
|
const contractId = String(props.contractId || '').trim()
|
||||||
const numeric = Number(value)
|
if (!contractId) return 0
|
||||||
return Number.isFinite(numeric) ? numeric : null
|
return zxFwPricingStore.contractVersions[contractId] || 0
|
||||||
}
|
})
|
||||||
|
|
||||||
const round3 = (value: number) => Number(value.toFixed(3))
|
const round3 = (value: number) => Number(value.toFixed(3))
|
||||||
const budgetFee = computed<number | null>(() => {
|
const budgetFee = computed<number | null>(() => {
|
||||||
@ -51,27 +41,15 @@ const formatAmount = (value: number | null) =>
|
|||||||
value == null ? '' : formatThousandsFlexible(value, 3)
|
value == null ? '' : formatThousandsFlexible(value, 3)
|
||||||
|
|
||||||
const loadBase = async () => {
|
const loadBase = async () => {
|
||||||
|
|
||||||
const contractId = String(props.contractId || '').trim()
|
const contractId = String(props.contractId || '').trim()
|
||||||
if (!contractId) {
|
if (!contractId) {
|
||||||
base.value = null
|
base.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await localforage.getItem<ZxFwStateLike>(`zxFW-${contractId}`)
|
await zxFwPricingStore.loadContract(contractId)
|
||||||
const rows = Array.isArray(data?.detailRows) ? data.detailRows : []
|
const nextBase = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||||
const fixedRow = rows.find(row => String(row?.id || '') === 'fixed-budget-c')
|
base.value = nextBase == null ? null : round3(nextBase)
|
||||||
const fixedSubtotal = toFinite(fixedRow?.subtotal)
|
|
||||||
if (fixedSubtotal != null) {
|
|
||||||
base.value = round3(fixedSubtotal)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sum = rows.reduce((acc, row) => {
|
|
||||||
if (String(row?.id || '') === 'fixed-budget-c') return acc
|
|
||||||
const subtotal = toFinite(row?.subtotal)
|
|
||||||
return subtotal == null ? acc : acc + subtotal
|
|
||||||
}, 0)
|
|
||||||
base.value = round3(sum)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('load rate base failed:', error)
|
console.error('load rate base failed:', error)
|
||||||
base.value = null
|
base.value = null
|
||||||
@ -127,12 +105,9 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => pricingPaneReloadStore.persistedSeq,
|
() => contractVersion.value,
|
||||||
(nextVersion, prevVersion) => {
|
(nextVersion, prevVersion) => {
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||||
const contractId = String(props.contractId || '').trim()
|
|
||||||
if (!contractId) return
|
|
||||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastPersistedEvent, contractId, ZXFW_RELOAD_SERVICE_KEY)) return
|
|
||||||
void loadBase()
|
void loadBase()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorI
|
|||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
@ -1026,9 +1026,7 @@ const saveToIndexedDB = async () => {
|
|||||||
field: 'investScale',
|
field: 'investScale',
|
||||||
value: totalBudgetFee.value
|
value: totalBudgetFee.value
|
||||||
})
|
})
|
||||||
if (synced) {
|
if (!synced) return
|
||||||
pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorI
|
|||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
@ -879,9 +879,7 @@ const saveToIndexedDB = async () => {
|
|||||||
field: 'landScale',
|
field: 'landScale',
|
||||||
value: totalBudgetFee.value
|
value: totalBudgetFee.value
|
||||||
})
|
})
|
||||||
if (synced) {
|
if (!synced) return
|
||||||
pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
|||||||
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||||
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||||
@ -432,9 +432,7 @@ const saveToIndexedDB = async () => {
|
|||||||
field: 'workload',
|
field: 'workload',
|
||||||
value: totalServiceFee.value
|
value: totalServiceFee.value
|
||||||
})
|
})
|
||||||
if (synced) {
|
if (!synced) return
|
||||||
pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,7 @@ import {
|
|||||||
getPricingMethodTotalsForServices,
|
getPricingMethodTotalsForServices,
|
||||||
type PricingMethodTotals
|
type PricingMethodTotals
|
||||||
} from '@/lib/pricingMethodTotals'
|
} from '@/lib/pricingMethodTotals'
|
||||||
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
|
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
|
||||||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -34,6 +33,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
|
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
|
||||||
|
|
||||||
interface ServiceItem {
|
interface ServiceItem {
|
||||||
@ -78,14 +78,16 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||||
const DB_KEY = computed(() => `zxFW-${props.contractId}`)
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||||||
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||||||
const projectIndustry = ref('')
|
const projectIndustry = ref('')
|
||||||
const reloadSignal = ref(0)
|
const syncingFromStore = ref(false)
|
||||||
|
const localSavedVersion = ref(0)
|
||||||
|
const zxFwStoreVersion = computed(() => zxFwPricingStore.contractVersions[props.contractId] || 0)
|
||||||
|
|
||||||
type ServiceListItem = {
|
type ServiceListItem = {
|
||||||
code?: string
|
code?: string
|
||||||
@ -959,15 +961,18 @@ const saveToIndexedDB = async () => {
|
|||||||
selectedIds: [...selectedIds.value],
|
selectedIds: [...selectedIds.value],
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||||
}
|
}
|
||||||
await localforage.setItem(DB_KEY.value, payload)
|
await zxFwPricingStore.setContractState(props.contractId, payload)
|
||||||
pricingPaneReloadStore.emitPersisted(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
|
localSavedVersion.value = zxFwPricingStore.contractVersions[props.contractId] || 0
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
|
if (syncingFromStore.value) return
|
||||||
|
syncingFromStore.value = true
|
||||||
try {
|
try {
|
||||||
const data = await localforage.getItem<ZxFwState>(DB_KEY.value)
|
await zxFwPricingStore.loadContract(props.contractId)
|
||||||
|
const data = zxFwPricingStore.getContractState(props.contractId)
|
||||||
if (!data) {
|
if (!data) {
|
||||||
selectedIds.value = []
|
selectedIds.value = []
|
||||||
detailRows.value = []
|
detailRows.value = []
|
||||||
@ -1002,6 +1007,8 @@ const loadFromIndexedDB = async () => {
|
|||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
selectedIds.value = []
|
selectedIds.value = []
|
||||||
detailRows.value = []
|
detailRows.value = []
|
||||||
|
} finally {
|
||||||
|
syncingFromStore.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1017,22 +1024,11 @@ const loadProjectIndustry = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => pricingPaneReloadStore.seq,
|
() => zxFwStoreVersion.value,
|
||||||
(nextVersion, prevVersion) => {
|
(nextVersion, prevVersion) => {
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, ZXFW_RELOAD_SERVICE_KEY)) return
|
if (nextVersion === localSavedVersion.value) return
|
||||||
reloadSignal.value += 1
|
void loadFromIndexedDB()
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => reloadSignal.value,
|
|
||||||
(nextVersion, prevVersion) => {
|
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
|
||||||
void (async () => {
|
|
||||||
await loadFromIndexedDB()
|
|
||||||
await saveToIndexedDB()
|
|
||||||
})()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,6 @@
|
|||||||
import localforage from 'localforage'
|
import { useZxFwPricingStore, type ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
|
||||||
|
|
||||||
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
export type { ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
interface ZxFwDetailRow {
|
|
||||||
id: string
|
|
||||||
investScale: number | null
|
|
||||||
landScale: number | null
|
|
||||||
workload: number | null
|
|
||||||
hourly: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZxFwState {
|
|
||||||
selectedIds?: string[]
|
|
||||||
selectedCodes?: string[]
|
|
||||||
detailRows: ZxFwDetailRow[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
|
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
|
||||||
|
|
||||||
@ -25,30 +10,6 @@ export const syncPricingTotalToZxFw = async (params: {
|
|||||||
field: ZxFwPricingField
|
field: ZxFwPricingField
|
||||||
value: number | null | undefined
|
value: number | null | undefined
|
||||||
}) => {
|
}) => {
|
||||||
const dbKey = `zxFW-${params.contractId}`
|
const store = useZxFwPricingStore()
|
||||||
const data = await localforage.getItem<ZxFwState>(dbKey)
|
return store.updatePricingField(params)
|
||||||
if (!data?.detailRows?.length) return false
|
|
||||||
|
|
||||||
const targetServiceId = String(params.serviceId)
|
|
||||||
const nextValue = toFiniteNumberOrNull(params.value)
|
|
||||||
let changed = false
|
|
||||||
|
|
||||||
const nextRows = data.detailRows.map(row => {
|
|
||||||
if (String(row.id) !== targetServiceId) return row
|
|
||||||
const currentValue = toFiniteNumberOrNull(row[params.field])
|
|
||||||
if (currentValue === nextValue) return row
|
|
||||||
changed = true
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
[params.field]: nextValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!changed) return false
|
|
||||||
|
|
||||||
await localforage.setItem(dbKey, {
|
|
||||||
...data,
|
|
||||||
detailRows: nextRows
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|||||||
235
src/pinia/zxFwPricing.ts
Normal file
235
src/pinia/zxFwPricing.ts
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import localforage from 'localforage'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { addNumbers } from '@/lib/decimal'
|
||||||
|
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||||
|
|
||||||
|
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||||
|
|
||||||
|
export interface ZxFwDetailRow {
|
||||||
|
id: string
|
||||||
|
code?: string
|
||||||
|
name?: string
|
||||||
|
investScale: number | null
|
||||||
|
landScale: number | null
|
||||||
|
workload: number | null
|
||||||
|
hourly: number | null
|
||||||
|
subtotal?: number | null
|
||||||
|
actions?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZxFwState {
|
||||||
|
selectedIds?: string[]
|
||||||
|
selectedCodes?: string[]
|
||||||
|
detailRows: ZxFwDetailRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const FIXED_ROW_ID = 'fixed-budget-c'
|
||||||
|
|
||||||
|
const toKey = (contractId: string | number) => String(contractId || '').trim()
|
||||||
|
const dbKeyOf = (contractId: string) => `zxFW-${contractId}`
|
||||||
|
const round3 = (value: number) => Number(value.toFixed(3))
|
||||||
|
const toNumberOrZero = (value: unknown) => {
|
||||||
|
const numeric = Number(value)
|
||||||
|
return Number.isFinite(numeric) ? numeric : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
|
||||||
|
(Array.isArray(rows) ? rows : []).map(item => {
|
||||||
|
const row = item as Partial<ZxFwDetailRow>
|
||||||
|
return {
|
||||||
|
id: String(row.id || ''),
|
||||||
|
code: typeof row.code === 'string' ? row.code : '',
|
||||||
|
name: typeof row.name === 'string' ? row.name : '',
|
||||||
|
investScale: toFiniteNumberOrNull(row.investScale),
|
||||||
|
landScale: toFiniteNumberOrNull(row.landScale),
|
||||||
|
workload: toFiniteNumberOrNull(row.workload),
|
||||||
|
hourly: toFiniteNumberOrNull(row.hourly),
|
||||||
|
subtotal: toFiniteNumberOrNull(row.subtotal),
|
||||||
|
actions: row.actions
|
||||||
|
}
|
||||||
|
}).filter(row => row.id)
|
||||||
|
|
||||||
|
const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
|
||||||
|
const normalized = rows.map(row => ({ ...row }))
|
||||||
|
const nonFixedRows = normalized.filter(row => row.id !== FIXED_ROW_ID)
|
||||||
|
const totalInvestScale = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.investScale)), 0)
|
||||||
|
const totalLandScale = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.landScale)), 0)
|
||||||
|
const totalWorkload = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.workload)), 0)
|
||||||
|
const totalHourly = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.hourly)), 0)
|
||||||
|
const fixedSubtotal = addNumbers(totalInvestScale, totalLandScale, totalWorkload, totalHourly)
|
||||||
|
|
||||||
|
return normalized.map(row => {
|
||||||
|
if (row.id === FIXED_ROW_ID) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
investScale: round3(totalInvestScale),
|
||||||
|
landScale: round3(totalLandScale),
|
||||||
|
workload: round3(totalWorkload),
|
||||||
|
hourly: round3(totalHourly),
|
||||||
|
subtotal: round3(fixedSubtotal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const subtotal = addNumbers(
|
||||||
|
toNumberOrZero(row.investScale),
|
||||||
|
toNumberOrZero(row.landScale),
|
||||||
|
toNumberOrZero(row.workload),
|
||||||
|
toNumberOrZero(row.hourly)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
subtotal: round3(subtotal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeState = (state: ZxFwState | null | undefined): ZxFwState => ({
|
||||||
|
selectedIds: Array.isArray(state?.selectedIds)
|
||||||
|
? state.selectedIds.map(id => String(id || '')).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
selectedCodes: Array.isArray(state?.selectedCodes)
|
||||||
|
? state.selectedCodes.map(code => String(code || '')).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
detailRows: applyRowSubtotals(normalizeRows(state?.detailRows))
|
||||||
|
})
|
||||||
|
|
||||||
|
const cloneState = (state: ZxFwState): ZxFwState => ({
|
||||||
|
selectedIds: [...(state.selectedIds || [])],
|
||||||
|
selectedCodes: [...(state.selectedCodes || [])],
|
||||||
|
detailRows: state.detailRows.map(row => ({ ...row }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadTasks = new Map<string, Promise<ZxFwState>>()
|
||||||
|
const persistQueues = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
|
export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||||
|
const contracts = ref<Record<string, ZxFwState>>({})
|
||||||
|
const contractVersions = ref<Record<string, number>>({})
|
||||||
|
|
||||||
|
const touchVersion = (contractId: string) => {
|
||||||
|
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuePersist = async (contractId: string) => {
|
||||||
|
const prev = persistQueues.get(contractId) || Promise.resolve()
|
||||||
|
const next = prev
|
||||||
|
.then(async () => {
|
||||||
|
const current = contracts.value[contractId]
|
||||||
|
if (!current) return
|
||||||
|
await localforage.setItem<ZxFwState>(dbKeyOf(contractId), cloneState(current))
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('zxFwPricing persist failed:', error)
|
||||||
|
})
|
||||||
|
persistQueues.set(contractId, next)
|
||||||
|
await next
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContractState = (contractIdRaw: string | number) => {
|
||||||
|
const contractId = toKey(contractIdRaw)
|
||||||
|
if (!contractId) return null
|
||||||
|
const data = contracts.value[contractId]
|
||||||
|
return data ? cloneState(data) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
||||||
|
const contractId = toKey(contractIdRaw)
|
||||||
|
if (!contractId) return null
|
||||||
|
if (!force && contracts.value[contractId]) return getContractState(contractId)
|
||||||
|
if (!force && loadTasks.has(contractId)) return loadTasks.get(contractId) as Promise<ZxFwState>
|
||||||
|
|
||||||
|
const task = (async () => {
|
||||||
|
const raw = await localforage.getItem<ZxFwState>(dbKeyOf(contractId))
|
||||||
|
const normalized = normalizeState(raw)
|
||||||
|
contracts.value[contractId] = normalized
|
||||||
|
touchVersion(contractId)
|
||||||
|
return cloneState(normalized)
|
||||||
|
})()
|
||||||
|
loadTasks.set(contractId, task)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await task
|
||||||
|
} finally {
|
||||||
|
loadTasks.delete(contractId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
||||||
|
const contractId = toKey(contractIdRaw)
|
||||||
|
if (!contractId) return
|
||||||
|
contracts.value[contractId] = normalizeState(state)
|
||||||
|
touchVersion(contractId)
|
||||||
|
await queuePersist(contractId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePricingField = async (params: {
|
||||||
|
contractId: string
|
||||||
|
serviceId: string | number
|
||||||
|
field: ZxFwPricingField
|
||||||
|
value: number | null | undefined
|
||||||
|
}) => {
|
||||||
|
const contractId = toKey(params.contractId)
|
||||||
|
if (!contractId) return false
|
||||||
|
|
||||||
|
if (!contracts.value[contractId]) {
|
||||||
|
await loadContract(contractId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = contracts.value[contractId]
|
||||||
|
if (!current?.detailRows?.length) return false
|
||||||
|
|
||||||
|
const targetServiceId = String(params.serviceId || '').trim()
|
||||||
|
if (!targetServiceId) return false
|
||||||
|
|
||||||
|
const nextValue = toFiniteNumberOrNull(params.value)
|
||||||
|
let changed = false
|
||||||
|
const nextRows = current.detailRows.map(row => {
|
||||||
|
if (String(row.id || '') !== targetServiceId) return row
|
||||||
|
const oldValue = toFiniteNumberOrNull(row[params.field])
|
||||||
|
if (oldValue === nextValue) return row
|
||||||
|
changed = true
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
[params.field]: nextValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!changed) return false
|
||||||
|
|
||||||
|
contracts.value[contractId] = normalizeState({
|
||||||
|
...current,
|
||||||
|
detailRows: nextRows
|
||||||
|
})
|
||||||
|
touchVersion(contractId)
|
||||||
|
await queuePersist(contractId)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
|
||||||
|
const contractId = toKey(contractIdRaw)
|
||||||
|
if (!contractId) return null
|
||||||
|
const state = contracts.value[contractId]
|
||||||
|
if (!state?.detailRows?.length) return null
|
||||||
|
|
||||||
|
const fixedRow = state.detailRows.find(row => String(row.id || '') === FIXED_ROW_ID)
|
||||||
|
const fixedSubtotal = toFiniteNumberOrNull(fixedRow?.subtotal)
|
||||||
|
if (fixedSubtotal != null) return round3(fixedSubtotal)
|
||||||
|
|
||||||
|
const sum = state.detailRows.reduce((acc, row) => {
|
||||||
|
if (String(row.id || '') === FIXED_ROW_ID) return acc
|
||||||
|
const subtotal = toFiniteNumberOrNull(row.subtotal)
|
||||||
|
return subtotal == null ? acc : acc + subtotal
|
||||||
|
}, 0)
|
||||||
|
return round3(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contracts,
|
||||||
|
contractVersions,
|
||||||
|
getContractState,
|
||||||
|
loadContract,
|
||||||
|
setContractState,
|
||||||
|
updatePricingField,
|
||||||
|
getBaseSubtotal
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user