JGJS2026/src/pinia/zxFwPricing.ts
2026-03-20 17:21:33 +08:00

771 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { addNumbers } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { waitForHydration } from '@/pinia/Plugin/indexdb'
import {
parseHtFeeMainStorageKey,
parseHtFeeMethodStorageKey,
useZxFwPricingHtFeeStore
} from '@/pinia/zxFwPricingHtFee'
import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
export type ServicePricingMethod = ZxFwPricingField
export interface ZxFwDetailRow {
id: string
code?: string
name?: string
process?: number | null
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
subtotal?: number | null
finalFee?: number | null
actions?: unknown
}
export interface ZxFwState {
selectedIds?: string[]
selectedCodes?: string[]
detailRows: ZxFwDetailRow[]
}
export interface ServicePricingMethodState<TRow = unknown> {
detailRows: TRow[]
projectCount?: number | null
}
export interface ServicePricingState {
investScale?: ServicePricingMethodState
landScale?: ServicePricingMethodState
workload?: ServicePricingMethodState
hourly?: ServicePricingMethodState
}
export type HtFeeMethodType = 'rate-fee' | 'hourly-fee' | 'quantity-unit-price-fee'
export interface HtFeeMainState<TRow = unknown> {
detailRows: TRow[]
}
export type HtFeeMethodPayload = unknown
const FIXED_ROW_ID = 'fixed-budget-c'
const METHOD_STORAGE_PREFIX_MAP: Record<ServicePricingMethod, string> = {
investScale: 'tzGMF',
landScale: 'ydGMF',
workload: 'gzlF',
hourly: 'hourlyPricing'
}
const STORAGE_PREFIX_METHOD_MAP = new Map<string, ServicePricingMethod>(
Object.entries(METHOD_STORAGE_PREFIX_MAP).map(([method, prefix]) => [prefix, method as ServicePricingMethod])
)
const toKey = (contractId: string | number) => String(contractId || '').trim()
const toServiceKey = (serviceId: string | number) => String(serviceId || '').trim()
const serviceMethodDbKeyOf = (contractId: string, serviceId: string, method: ServicePricingMethod) =>
`${METHOD_STORAGE_PREFIX_MAP[method]}-${contractId}-${serviceId}`
const round3 = (value: number) => Number(value.toFixed(3))
const isFiniteNumberValue = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value)
const sumNullableNumbers = (values: Array<number | null | undefined>): number | null => {
const validValues = values.filter(isFiniteNumberValue)
if (validValues.length === 0) return null
return addNumbers(...validValues)
}
const round3Nullable = (value: number | null | undefined) => {
const numeric = toFiniteNumberOrNull(value)
return numeric == null ? null : round3(numeric)
}
const normalizeProcessValue = (value: unknown, rowId: string) => {
if (rowId === FIXED_ROW_ID) return null
return Number(value) === 1 ? 1 : 0
}
const toKeySnapshot = (value: unknown) => JSON.stringify(value ?? null)
const cloneAny = <T>(value: T): T => {
if (value == null) return value
return JSON.parse(JSON.stringify(value)) as T
}
const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
(Array.isArray(rows) ? rows : []).map(item => {
const row = item as Partial<ZxFwDetailRow>
const rowId = String(row.id || '')
return {
id: rowId,
code: typeof row.code === 'string' ? row.code : '',
name: typeof row.name === 'string' ? row.name : '',
process: normalizeProcessValue(row.process, rowId),
investScale: toFiniteNumberOrNull(row.investScale),
landScale: toFiniteNumberOrNull(row.landScale),
workload: toFiniteNumberOrNull(row.workload),
hourly: toFiniteNumberOrNull(row.hourly),
subtotal: toFiniteNumberOrNull(row.subtotal),
finalFee: toFiniteNumberOrNull(row.finalFee),
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 = sumNullableNumbers(nonFixedRows.map(row => row.investScale))
const totalLandScale = sumNullableNumbers(nonFixedRows.map(row => row.landScale))
const totalWorkload = sumNullableNumbers(nonFixedRows.map(row => row.workload))
const totalHourly = sumNullableNumbers(nonFixedRows.map(row => row.hourly))
const fixedSubtotal = sumNullableNumbers([totalInvestScale, totalLandScale, totalWorkload, totalHourly])
return normalized.map(row => {
if (row.id === FIXED_ROW_ID) {
return {
...row,
investScale: round3Nullable(totalInvestScale),
landScale: round3Nullable(totalLandScale),
workload: round3Nullable(totalWorkload),
hourly: round3Nullable(totalHourly),
subtotal: round3Nullable(fixedSubtotal),
finalFee: row.finalFee
}
}
const subtotal = sumNullableNumbers([
row.investScale,
row.landScale,
row.workload,
row.hourly
])
return {
...row,
subtotal: round3Nullable(subtotal),
finalFee: row.finalFee != null ? round3Nullable(row.finalFee) : round3Nullable(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 isSameStringArray = (a: string[] | undefined, b: string[] | undefined) => {
const left = Array.isArray(a) ? a : []
const right = Array.isArray(b) ? b : []
if (left.length !== right.length) return false
for (let i = 0; i < left.length; i += 1) {
if (left[i] !== right[i]) return false
}
return true
}
const isSameNullableNumber = (a: number | null | undefined, b: number | null | undefined) => {
const left = toFiniteNumberOrNull(a)
const right = toFiniteNumberOrNull(b)
return left === right
}
const isSameRows = (a: ZxFwDetailRow[] | undefined, b: ZxFwDetailRow[] | undefined) => {
const left = Array.isArray(a) ? a : []
const right = Array.isArray(b) ? b : []
if (left.length !== right.length) return false
for (let i = 0; i < left.length; i += 1) {
const l = left[i]
const r = right[i]
if (!l || !r) return false
if (l.id !== r.id) return false
if ((l.code || '') !== (r.code || '')) return false
if ((l.name || '') !== (r.name || '')) return false
if (normalizeProcessValue(l.process, l.id) !== normalizeProcessValue(r.process, r.id)) return false
if (!isSameNullableNumber(l.investScale, r.investScale)) return false
if (!isSameNullableNumber(l.landScale, r.landScale)) return false
if (!isSameNullableNumber(l.workload, r.workload)) return false
if (!isSameNullableNumber(l.hourly, r.hourly)) return false
if (!isSameNullableNumber(l.subtotal, r.subtotal)) return false
if (!isSameNullableNumber(l.finalFee, r.finalFee)) return false
}
return true
}
const isSameState = (a: ZxFwState | null | undefined, b: ZxFwState | null | undefined) => {
if (!a || !b) return false
if (!isSameStringArray(a.selectedIds, b.selectedIds)) return false
if (!isSameStringArray(a.selectedCodes, b.selectedCodes)) return false
return isSameRows(a.detailRows, b.detailRows)
}
const normalizeProjectCount = (value: unknown) => {
const numeric = Number(value)
if (!Number.isFinite(numeric)) return null
return Math.max(1, Math.floor(numeric))
}
const normalizeServiceMethodState = (
payload: Partial<ServicePricingMethodState> | null | undefined
): ServicePricingMethodState => ({
detailRows: Array.isArray(payload?.detailRows) ? cloneAny(payload.detailRows) : [],
projectCount: normalizeProjectCount(payload?.projectCount)
})
const parseServiceMethodStorageKey = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return null
const firstDash = key.indexOf('-')
if (firstDash <= 0 || firstDash >= key.length - 1) return null
const prefix = key.slice(0, firstDash)
const method = STORAGE_PREFIX_METHOD_MAP.get(prefix)
if (!method) return null
const rest = key.slice(firstDash + 1)
const splitIndex = rest.lastIndexOf('-')
if (splitIndex <= 0 || splitIndex >= rest.length - 1) return null
const contractId = rest.slice(0, splitIndex).trim()
const serviceId = rest.slice(splitIndex + 1).trim()
if (!contractId || !serviceId) return null
return { key, method, contractId, serviceId }
}
const loadTasks = new Map<string, Promise<ZxFwState | null>>()
export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
let hydrationReady = false
let hydrationTask: Promise<void> | null = null
const contracts = ref<Record<string, ZxFwState>>({})
const contractLoaded = ref<Record<string, boolean>>({})
const servicePricingStates = ref<Record<string, Record<string, ServicePricingState>>>({})
const keysStore = useZxFwPricingKeysStore()
const htFeeStore = useZxFwPricingHtFeeStore()
const htFeeMainStates = htFeeStore.htFeeMainStates
const htFeeMethodStates = htFeeStore.htFeeMethodStates
const keyedStates = keysStore.keyedStates
const ensureHydrated = async () => {
if (hydrationReady) return
if (!hydrationTask) {
hydrationTask = waitForHydration('zxFwPricing')
.catch(() => undefined)
.finally(() => {
hydrationReady = true
hydrationTask = null
})
}
await hydrationTask
}
const ensureServicePricingState = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return null
if (!servicePricingStates.value[contractId]) {
servicePricingStates.value[contractId] = {}
}
if (!servicePricingStates.value[contractId][serviceId]) {
servicePricingStates.value[contractId][serviceId] = {}
}
return servicePricingStates.value[contractId][serviceId]
}
const setServiceMethodStateInMemory = (
contractIdRaw: string | number,
serviceIdRaw: string | number,
method: ServicePricingMethod,
payload: Partial<ServicePricingMethodState> | null | undefined
) => {
const state = ensureServicePricingState(contractIdRaw, serviceIdRaw)
if (!state) return null
if (!payload) {
delete state[method]
return null
}
state[method] = normalizeServiceMethodState(payload)
return state[method] || null
}
const getServicePricingMethodState = <TRow = unknown>(
contractIdRaw: string | number,
serviceIdRaw: string | number,
method: ServicePricingMethod
) => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return null
return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState<TRow> | undefined) || null
}
// 写入某合同某服务某种计费方式的明细状态。
// 默认会同步更新 keysStore从而触发持久化syncKeyState=false 时只回填内存态。
const setServicePricingMethodState = <TRow = unknown>(
contractIdRaw: string | number,
serviceIdRaw: string | number,
method: ServicePricingMethod,
payload: Partial<ServicePricingMethodState<TRow>> | null | undefined,
options?: {
force?: boolean
syncKeyState?: boolean
}
) => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return false
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
const force = options?.force === true
const syncKeyState = options?.syncKeyState !== false
const normalizedPayload = payload == null ? null : normalizeServiceMethodState(payload)
const prevSnapshot = toKeySnapshot(getServicePricingMethodState(contractId, serviceId, method))
const nextSnapshot = toKeySnapshot(normalizedPayload)
if (!force && prevSnapshot === nextSnapshot) return false
setServiceMethodStateInMemory(contractId, serviceId, method, normalizedPayload)
if (syncKeyState) {
if (normalizedPayload == null) {
keysStore.removeKeyState(storageKey)
} else {
keysStore.setKeyState(storageKey, cloneAny(normalizedPayload), { force: true })
}
}
return true
}
// 按需加载某个服务的计费方式明细。
// 若内存中已有且未强制刷新,直接返回;否则从 keysStore/KV 读取后回填到内存。
const loadServicePricingMethodState = async <TRow = unknown>(
contractIdRaw: string | number,
serviceIdRaw: string | number,
method: ServicePricingMethod,
force = false
): Promise<ServicePricingMethodState<TRow> | null> => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return null
if (!force) {
const existing = getServicePricingMethodState<TRow>(contractId, serviceId, method)
if (existing) return existing
}
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
const payload = await loadKeyState<ServicePricingMethodState<TRow>>(storageKey, force)
if (!payload) {
setServiceMethodStateInMemory(contractId, serviceId, method, null)
return null
}
setServicePricingMethodState(contractId, serviceId, method, payload, { force: true, syncKeyState: false })
return getServicePricingMethodState<TRow>(contractId, serviceId, method)
}
// 删除单个服务某种计费方式的状态,并同步清理对应持久化 key。
const removeServicePricingMethodState = (
contractIdRaw: string | number,
serviceIdRaw: string | number,
method: ServicePricingMethod
) => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return false
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
const had = getServicePricingMethodState(contractId, serviceId, method) != null
setServiceMethodStateInMemory(contractId, serviceId, method, null)
keysStore.removeKeyState(storageKey)
return had
}
// 暴露给外部使用的单个计费方式存储键,便于兼容旧逻辑直接读写底层数据。
const getServicePricingStorageKey = (
contractIdRaw: string | number,
serviceIdRaw: string | number,
method: ServicePricingMethod
) => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return ''
return serviceMethodDbKeyOf(contractId, serviceId, method)
}
// 返回某个服务全部计费方式对应的存储键集合,常用于批量清理或导出。
const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw)
if (!contractId || !serviceId) return [] as string[]
return (Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]).map(method =>
serviceMethodDbKeyOf(contractId, serviceId, method)
)
}
const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
let changed = false
for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
changed = removeServicePricingMethodState(contractIdRaw, serviceIdRaw, method) || changed
}
return changed
}
const getHtFeeMainState = htFeeStore.getHtFeeMainState
const setHtFeeMainState = htFeeStore.setHtFeeMainState
const loadHtFeeMainState = htFeeStore.loadHtFeeMainState
const removeHtFeeMainState = htFeeStore.removeHtFeeMainState
const getHtFeeMethodStorageKey = htFeeStore.getHtFeeMethodStorageKey
const getHtFeeMethodState = htFeeStore.getHtFeeMethodState
const setHtFeeMethodState = htFeeStore.setHtFeeMethodState
const loadHtFeeMethodState = htFeeStore.loadHtFeeMethodState
const removeHtFeeMethodState = htFeeStore.removeHtFeeMethodState
const getKeyState = <T = unknown>(keyRaw: string | number): T | null => {
const key = toKey(keyRaw)
if (!key) return null
const serviceMeta = parseServiceMethodStorageKey(key)
if (serviceMeta) {
const methodState = getServicePricingMethodState(
serviceMeta.contractId,
serviceMeta.serviceId,
serviceMeta.method
)
if (methodState != null) return cloneAny(methodState as T)
}
const htMethodMeta = parseHtFeeMethodStorageKey(key)
if (htMethodMeta) {
const methodState = getHtFeeMethodState(
htMethodMeta.mainStorageKey,
htMethodMeta.rowId,
htMethodMeta.method
)
if (methodState != null) return cloneAny(methodState as T)
}
const htMainMeta = parseHtFeeMainStorageKey(key)
if (htMainMeta) {
const mainState = getHtFeeMainState(htMainMeta.mainStorageKey)
if (mainState != null) return cloneAny(mainState as T)
}
return keysStore.getKeyState<T>(key)
}
const loadKeyState = async <T = unknown>(keyRaw: string | number, force = false): Promise<T | null> => {
const key = toKey(keyRaw)
if (!key) return null
const raw = await keysStore.loadKeyState<T>(key, force)
const serviceMeta = parseServiceMethodStorageKey(key)
if (serviceMeta) {
setServicePricingMethodState(
serviceMeta.contractId,
serviceMeta.serviceId,
serviceMeta.method,
raw as Partial<ServicePricingMethodState>,
{ force: true, syncKeyState: false }
)
}
const htMethodMeta = parseHtFeeMethodStorageKey(key)
if (htMethodMeta) {
setHtFeeMethodState(
htMethodMeta.mainStorageKey,
htMethodMeta.rowId,
htMethodMeta.method,
raw,
{ force: true, syncKeyState: false }
)
}
const htMainMeta = parseHtFeeMainStorageKey(key)
if (htMainMeta) {
setHtFeeMainState(htMainMeta.mainStorageKey, raw as Partial<HtFeeMainState>, { force: true, syncKeyState: false })
}
return getKeyState<T>(key)
}
const setKeyState = <T = unknown>(
keyRaw: string | number,
value: T,
options?: {
force?: boolean
}
) => {
const key = toKey(keyRaw)
if (!key) return false
const serviceMeta = parseServiceMethodStorageKey(key)
if (serviceMeta) {
setServicePricingMethodState(
serviceMeta.contractId,
serviceMeta.serviceId,
serviceMeta.method,
value as Partial<ServicePricingMethodState>,
{ force: true, syncKeyState: false }
)
}
const htMethodMeta = parseHtFeeMethodStorageKey(key)
if (htMethodMeta) {
setHtFeeMethodState(
htMethodMeta.mainStorageKey,
htMethodMeta.rowId,
htMethodMeta.method,
value,
{ force: true, syncKeyState: false }
)
}
const htMainMeta = parseHtFeeMainStorageKey(key)
if (htMainMeta) {
setHtFeeMainState(htMainMeta.mainStorageKey, value as Partial<HtFeeMainState>, { force: true, syncKeyState: false })
}
return keysStore.setKeyState(key, value, options)
}
const removeKeyState = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return false
const serviceMeta = parseServiceMethodStorageKey(key)
if (serviceMeta) {
setServiceMethodStateInMemory(serviceMeta.contractId, serviceMeta.serviceId, serviceMeta.method, null)
}
const htMethodMeta = parseHtFeeMethodStorageKey(key)
if (htMethodMeta) {
setHtFeeMethodState(htMethodMeta.mainStorageKey, htMethodMeta.rowId, htMethodMeta.method, null, {
force: true,
syncKeyState: false
})
}
const htMainMeta = parseHtFeeMainStorageKey(key)
if (htMainMeta) {
setHtFeeMainState(htMainMeta.mainStorageKey, null, { force: true, syncKeyState: false })
}
return keysStore.removeKeyState(key)
}
// 对外返回合同咨询服务状态的深拷贝,避免组件直接改写 store 内部引用。
const getContractState = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return null
const data = contracts.value[contractId]
return data ? cloneState(data) : null
}
// 加载合同维度的咨询服务汇总状态。
// 同一合同在并发场景下会复用同一个加载任务,避免重复读取 KV。
const loadContract = async (contractIdRaw: string | number, force = false) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return null
await ensureHydrated()
if (!force && contractLoaded.value[contractId]) return getContractState(contractId)
if (!force && contracts.value[contractId]) return getContractState(contractId)
if (!force && loadTasks.has(contractId)) return loadTasks.get(contractId) as Promise<ZxFwState | null>
const task = (async () => {
const current = contracts.value[contractId]
if (!current) {
contracts.value[contractId] = normalizeState(null)
}
contractLoaded.value[contractId] = true
return getContractState(contractId)
})()
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 false
const normalized = normalizeState(state)
const current = contracts.value[contractId]
if (current && isSameState(current, normalized)) return false
contracts.value[contractId] = normalized
contractLoaded.value[contractId] = true
return true
}
// 只更新某个服务行上的单个汇总字段,适合计费页回写 investScale/landScale/workload/hourly。
// 为保证“计价法金额变化 -> 确认金额跟随小计”,这里会同步重算 finalFee
// - 普通行finalFee = 当前四种计价法小计
// - 固定小计行finalFee = 普通行 finalFee 合计
const updatePricingField = async (params: {
contractId: string
serviceId: string | number
field: ZxFwPricingField
value: number | null | undefined
}) => {
const contractId = toKey(params.contractId)
if (!contractId) return false
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 updatedRows = 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
const rowsWithSyncedFinalFee = updatedRows.map(row => {
const rowId = String(row.id || '')
if (rowId === FIXED_ROW_ID) return row
if (rowId !== targetServiceId) return row
const rowSubtotal = sumNullableNumbers([
toFiniteNumberOrNull(row.investScale),
toFiniteNumberOrNull(row.landScale),
toFiniteNumberOrNull(row.workload),
toFiniteNumberOrNull(row.hourly)
])
return {
...row,
finalFee: round3Nullable(rowSubtotal)
}
})
const fixedFinalFee = round3Nullable(
sumNullableNumbers(
rowsWithSyncedFinalFee
.filter(row => String(row.id || '') !== FIXED_ROW_ID)
.map(row => toFiniteNumberOrNull(row.finalFee))
)
)
const nextRows = rowsWithSyncedFinalFee.map(row =>
String(row.id || '') === FIXED_ROW_ID
? {
...row,
finalFee: fixedFinalFee
}
: row
)
const nextState = normalizeState({
...current,
detailRows: nextRows
})
if (isSameState(current, nextState)) return false
contracts.value[contractId] = nextState
contractLoaded.value[contractId] = true
const targetRow = nextState.detailRows.find(row => String(row.id || '') === targetServiceId)
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 fixedFinalFee = toFiniteNumberOrNull(fixedRow?.finalFee)
if (fixedFinalFee != null) return round3(fixedFinalFee)
let hasValid = false
const sum = state.detailRows.reduce((acc, row) => {
if (String(row.id || '') === FIXED_ROW_ID) return acc
const fee = toFiniteNumberOrNull(row.finalFee) ?? toFiniteNumberOrNull(row.subtotal)
if (fee != null) hasValid = true
return fee == null ? acc : acc + fee
}, 0)
return hasValid ? round3(sum) : null
}
// 清理合同维度的内存态、版本号以及该合同下所有相关持久化键。
const removeContractData = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return false
let changed = false
if (Object.prototype.hasOwnProperty.call(contracts.value, contractId)) {
delete contracts.value[contractId]
changed = true
}
if (Object.prototype.hasOwnProperty.call(servicePricingStates.value, contractId)) {
delete servicePricingStates.value[contractId]
changed = true
}
if (Object.prototype.hasOwnProperty.call(contractLoaded.value, contractId)) {
delete contractLoaded.value[contractId]
changed = true
}
loadTasks.delete(contractId)
changed = htFeeStore.removeContractHtFeeData(contractId) || changed
const htMainPrefix = `htExtraFee-${contractId}-`
changed = keysStore.removeKeysByPrefix(htMainPrefix) || changed
for (const prefix of Object.values(METHOD_STORAGE_PREFIX_MAP)) {
changed = keysStore.removeKeysByPrefix(`${prefix}-${contractId}-`) || changed
}
return changed
}
return {
contracts,
contractLoaded,
servicePricingStates,
htFeeMainStates,
htFeeMethodStates,
keyedStates,
getContractState,
loadContract,
setContractState,
updatePricingField,
getBaseSubtotal,
removeContractData,
getKeyState,
loadKeyState,
setKeyState,
removeKeyState,
getServicePricingMethodState,
setServicePricingMethodState,
loadServicePricingMethodState,
removeServicePricingMethodState,
getServicePricingStorageKey,
getServicePricingStorageKeys,
removeAllServicePricingMethodStates,
getHtFeeMainState,
setHtFeeMainState,
loadHtFeeMainState,
removeHtFeeMainState,
getHtFeeMethodStorageKey,
getHtFeeMethodState,
setHtFeeMethodState,
loadHtFeeMethodState,
removeHtFeeMethodState
}
}, {
persist: true
})