771 lines
27 KiB
TypeScript
771 lines
27 KiB
TypeScript
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
|
||
})
|