This commit is contained in:
wintsa 2026-03-18 18:46:58 +08:00
parent 66069ef0f1
commit 4f46b23769
9 changed files with 1271 additions and 579 deletions

View File

@ -474,6 +474,7 @@ const clearRowValues = async (row: DetailRow) => {
landScale: sanitizedTotals.landScale, landScale: sanitizedTotals.landScale,
workload: sanitizedTotals.workload, workload: sanitizedTotals.workload,
hourly: sanitizedTotals.hourly, hourly: sanitizedTotals.hourly,
subtotal: newSubtotal != null ? roundTo(newSubtotal, 2) : null,
finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null
} }
}) })
@ -727,16 +728,16 @@ const columnDefs: ColDef<DetailRow>[] = [
editable: params => !isFixedRow(params.data), editable: params => !isFixedRow(params.data),
valueGetter: params => { valueGetter: params => {
if (!params.data) return null if (!params.data) return null
console.log(detailRows.value)
return params.data.finalFee return params.data.finalFee
}, },
valueSetter: params => { // valueSetter: params => {
const parsed = parseNumberOrNull(params.newValue, { precision: 2 }) // const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
const val = parsed != null ? roundTo(parsed, 2) : null // const val = parsed != null ? roundTo(parsed, 2) : null
if (params.data.finalFee === val) return false // if (params.data.finalFee === val) return false
params.data.finalFee = val // params.data.finalFee = val
return true // return true
}, // },
valueParser: params => { valueParser: params => {
const parsed = parseNumberOrNull(params.newValue, { precision: 2 }) const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
return parsed != null ? roundTo(parsed, 2) : null return parsed != null ? roundTo(parsed, 2) : null

View File

@ -0,0 +1,69 @@
/**
*
*
*
* HourlyPricingPane/HourlyFeeGrid pricingMethodTotals.ts
*/
import { expertList } from '@/sql'
import { roundTo, toDecimal } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import type { HourlyDetailRow, ExpertLite } from '@/types/pricing'
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 获取专家条目列表(按 ID 排序) */
export const getExpertEntries = (): [string, ExpertLite][] =>
Object.entries(expertList as Record<string, ExpertLite>)
.sort((a, b) => Number(a[0]) - Number(b[0]))
/** 计算专家默认采用单价 = 基准单价 × 管理系数 */
const getDefaultHourlyAdoptedPrice = (expert: ExpertLite): number | null => {
if (expert.defPrice == null || expert.manageCoe == null) return null
return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 构建工时法默认行 */
export const buildDefaultHourlyRows = (): HourlyDetailRow[] =>
getExpertEntries().map(([expertId, expert]) => ({
id: `expert-${expertId}`,
adoptedBudgetUnitPrice: getDefaultHourlyAdoptedPrice(expert),
personnelCount: null,
workdayCount: null
}))
/** 合并持久化行与默认行 */
export const mergeHourlyRows = (
rowsFromDb: Array<Partial<HourlyDetailRow> & Pick<HourlyDetailRow, 'id'>> | undefined
): HourlyDetailRow[] => {
const dbMap = new Map<string, Partial<HourlyDetailRow> & Pick<HourlyDetailRow, 'id'>>()
for (const row of rowsFromDb || []) dbMap.set(row.id, row)
return buildDefaultHourlyRows().map(row => {
const fromDb = dbMap.get(row.id)
if (!fromDb) return row
return {
...row,
adoptedBudgetUnitPrice: toFiniteNumberOrNull(fromDb.adoptedBudgetUnitPrice),
personnelCount: toFiniteNumberOrNull(fromDb.personnelCount),
workdayCount: toFiniteNumberOrNull(fromDb.workdayCount)
}
})
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 计算工时法单行费用 = 采用单价 × 人数 × 工日数 */
export const calcHourlyServiceBudget = (row: HourlyDetailRow): number | null => {
const { adoptedBudgetUnitPrice, personnelCount, workdayCount } = row
if (adoptedBudgetUnitPrice == null || personnelCount == null || workdayCount == null) return null
return roundTo(toDecimal(adoptedBudgetUnitPrice).mul(personnelCount).mul(workdayCount), 2)
}

View File

@ -0,0 +1,70 @@
/**
*
*
* sessionStorage skip/force
* /
*/
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
/**
*
*
*/
export const shouldSkipPersist = (dbKey: string, paneCreatedAt: number): boolean => {
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${dbKey}`
const raw = sessionStorage.getItem(storageKey)
if (!raw) return false
const now = Date.now()
if (raw.includes(':')) {
const [issuedRaw, untilRaw] = raw.split(':')
const issuedAt = Number(issuedRaw)
const skipUntil = Number(untilRaw)
if (Number.isFinite(issuedAt) && Number.isFinite(skipUntil) && now <= skipUntil) {
return paneCreatedAt <= issuedAt
}
sessionStorage.removeItem(storageKey)
return false
}
const skipUntil = Number(raw)
if (Number.isFinite(skipUntil) && now <= skipUntil) return true
sessionStorage.removeItem(storageKey)
return false
}
/**
*
*
*/
export const shouldForceDefaultLoad = (dbKey: string): boolean => {
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${dbKey}`
const raw = sessionStorage.getItem(storageKey)
if (!raw) return false
const forceUntil = Number(raw)
sessionStorage.removeItem(storageKey)
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
}
/**
*
* @param dbKey
* @param durationMs 3000ms
*/
export const markSkipPersist = (dbKey: string, durationMs = 3000): void => {
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${dbKey}`
const now = Date.now()
sessionStorage.setItem(storageKey, `${now}:${now + durationMs}`)
}
/**
*
* @param dbKey
* @param durationMs 3000ms
*/
export const markForceDefaultLoad = (dbKey: string, durationMs = 3000): void => {
const storageKey = `${PRICING_FORCE_DEFAULT_PREFIX}${dbKey}`
sessionStorage.setItem(storageKey, String(Date.now() + durationMs))
}

218
src/lib/pricingScaleCalc.ts Normal file
View File

@ -0,0 +1,218 @@
/**
* +
*
*
* ScalePricingPane.vue pricingMethodTotals.ts
*/
import { getMajorDictEntries, getMajorIdAliasMap, getServiceDictById } from '@/sql'
import { toFiniteNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import type {
ScaleCalcRow,
ScaleType,
MajorLite,
ServiceLite
} from '@/types/pricing'
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
const majorById = new Map(
getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite])
)
const majorIdAliasMap = getMajorIdAliasMap()
/** 获取专业叶子节点 ID 列表code 含 '-' 的为叶子) */
export const getMajorLeafIds = (): string[] =>
getMajorDictEntries()
.filter(({ item }) => Boolean(item?.code && String(item.code).includes('-')))
.map(({ id }) => id)
/** 解析专业 ID 别名 */
export const resolveMajorId = (id: string): string =>
majorById.has(id) ? id : majorIdAliasMap.get(id) || id
/** 获取专业默认系数 */
export const getDefaultMajorFactor = (id: string): number | null => {
const resolvedId = resolveMajorId(id)
return toFiniteNumberOrNull(majorById.get(resolvedId)?.defCoe)
}
/** 判断专业是否支持投资规模hasCost */
export const isCostMajor = (id: string): boolean => {
const resolvedId = resolveMajorId(id)
return majorById.get(resolvedId)?.hasCost !== false
}
/** 判断专业是否支持用地规模hasArea */
export const isAreaMajor = (id: string): boolean => {
const resolvedId = resolveMajorId(id)
return majorById.get(resolvedId)?.hasArea !== false
}
/** 判断专业是否同时支持投资和用地 */
export const isDualScaleMajor = (id: string): boolean =>
isCostMajor(id) && isAreaMajor(id)
/** 根据行业 ID 查找对应的专业条目 */
export const getIndustryMajorEntry = (industryId: string | null | undefined) => {
const key = String(industryId || '').trim()
if (!key) return null
for (const [id, item] of majorById.entries()) {
const majorIndustryId = String(item?.industryId ?? '').trim()
if (majorIndustryId === key && !String(item?.code || '').includes('-')) {
return { id, item }
}
}
return null
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 获取咨询服务默认分类系数 */
export const getDefaultConsultCategoryFactor = (serviceId: string | number): number | null => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe)
}
/** 判断是否为仅投资规模服务 */
export const isOnlyCostScaleService = (serviceId: string | number): boolean => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return service?.onlyCostScale === true
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 判断是否为分组汇总行AG Grid tree 用) */
export const isGroupScaleRow = (row: unknown): boolean =>
Boolean(row && typeof row === 'object' && (row as Record<string, unknown>).isGroupRow === true)
/** 过滤掉分组汇总行 */
export const stripGroupScaleRows = <TRow>(rows: TRow[] | undefined): TRow[] =>
(rows || []).filter(row => !isGroupScaleRow(row))
/** 构建规模法默认行(全部专业叶子) */
export const buildDefaultScaleRows = (
serviceId: string | number,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleCalcRow[] => {
const defaultFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getMajorLeafIds().map(id => ({
id,
amount: null,
landArea: null,
consultCategoryFactor: defaultFactor,
majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactor(id),
workStageFactor: 1,
workRatio: 100
}))
}
const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key)
const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => {
const map = new Map<string, TRow>()
for (const row of rows || []) map.set(String(row.id), row)
return map
}
/** 合并持久化行与默认行(保留用户编辑值,补全缺失字段) */
export const mergeScaleRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<ScaleCalcRow> & Pick<ScaleCalcRow, 'id'>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleCalcRow[] => {
const sourceRows = stripGroupScaleRows(rowsFromDb)
const dbValueMap = toRowMap(sourceRows)
// 处理 ID 别名映射
for (const row of sourceRows) {
const nextId = majorIdAliasMap.get(String(row.id))
if (nextId && !dbValueMap.has(nextId)) {
dbValueMap.set(nextId, row as ScaleCalcRow)
}
}
const defaultFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
return {
...row,
amount: toFiniteNumberOrNull(fromDb.amount),
landArea: toFiniteNumberOrNull(fromDb.landArea),
consultCategoryFactor:
toFiniteNumberOrNull(fromDb.consultCategoryFactor) ??
(hasOwn(fromDb, 'consultCategoryFactor') ? null : defaultFactor),
majorFactor:
toFiniteNumberOrNull(fromDb.majorFactor) ??
(hasOwn(fromDb, 'majorFactor') ? null : (majorFactorMap?.get(row.id) ?? getDefaultMajorFactor(row.id))),
workStageFactor:
toFiniteNumberOrNull(fromDb.workStageFactor) ??
(hasOwn(fromDb, 'workStageFactor') ? null : row.workStageFactor),
workRatio:
toFiniteNumberOrNull(fromDb.workRatio) ??
(hasOwn(fromDb, 'workRatio') ? null : row.workRatio)
}
})
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 计算投资规模法单行费用 */
export const calcInvestBudgetFee = (row: ScaleCalcRow): number | null =>
getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByScale(row.amount, 'cost'),
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
/** 计算用地规模法单行费用 */
export const calcLandBudgetFee = (row: ScaleCalcRow): number | null =>
getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByScale(row.landArea, 'area'),
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
/** 根据规模类型计算单行费用 */
export const calcScaleBudgetFee = (row: ScaleCalcRow, scaleType: ScaleType): number | null =>
scaleType === 'invest' ? calcInvestBudgetFee(row) : calcLandBudgetFee(row)
/** 判断行是否属于指定规模类型 */
export const isRowForScaleType = (rowId: string, scaleType: ScaleType): boolean =>
scaleType === 'invest' ? isCostMajor(rowId) : isAreaMajor(rowId)
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 对数组求和,全部为 null 时返回 null */
export const sumNullableBy = <T>(list: T[], pick: (item: T) => number | null | undefined): number | null => {
let hasValid = false
let total = 0
for (const item of list) {
const value = toFiniteNumberOrNull(pick(item))
if (value == null) continue
hasValid = true
total += value
}
return hasValid ? total : null
}

View File

@ -0,0 +1,99 @@
/**
*
*
*
* WorkloadPricingPane.vue pricingMethodTotals.ts
*/
import { taskList } from '@/sql'
import { roundTo, toDecimal } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { getDefaultConsultCategoryFactor } from '@/lib/pricingScaleCalc'
import type { WorkloadCalcRow, TaskLite } from '@/types/pricing'
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 获取指定咨询服务下的任务条目(按 ID 排序) */
export const getTaskEntriesByServiceId = (serviceId: string | number): [string, TaskLite][] =>
Object.entries(taskList as Record<string, TaskLite>)
.filter(([, task]) => Number(task.serviceID) === Number(serviceId))
.sort((a, b) => Number(a[0]) - Number(b[0]))
/** 判断指定服务是否有工作量法任务 */
export const hasWorkloadTasks = (serviceId: string | number): boolean =>
getTaskEntriesByServiceId(serviceId).length > 0
/** 格式化任务参考单价范围文本 */
export const formatTaskReferenceUnitPrice = (task: TaskLite): string => {
const unit = task.unit || ''
const hasMin = typeof task.minPrice === 'number' && Number.isFinite(task.minPrice)
const hasMax = typeof task.maxPrice === 'number' && Number.isFinite(task.maxPrice)
if (hasMin && hasMax) return `${task.minPrice}${unit}-${task.maxPrice}${unit}`
if (hasMin) return `${task.minPrice}${unit}`
if (hasMax) return `${task.maxPrice}${unit}`
return ''
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 构建工作量法默认行 */
export const buildDefaultWorkloadRows = (
serviceId: string | number,
consultCategoryFactorMap?: Map<string, number | null>
): WorkloadCalcRow[] => {
const defaultFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getTaskEntriesByServiceId(serviceId).map(([taskId, task], order) => ({
id: `task-${taskId}-${order}`,
conversion: toFiniteNumberOrNull(task.conversion),
workload: null,
basicFee: null,
budgetAdoptedUnitPrice: toFiniteNumberOrNull(task.defPrice),
consultCategoryFactor: defaultFactor
}))
}
/** 合并持久化行与默认行 */
export const mergeWorkloadRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<WorkloadCalcRow> & Pick<WorkloadCalcRow, 'id'>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>
): WorkloadCalcRow[] => {
const dbMap = new Map<string, Partial<WorkloadCalcRow> & Pick<WorkloadCalcRow, 'id'>>()
for (const row of rowsFromDb || []) dbMap.set(row.id, row)
return buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap).map(row => {
const fromDb = dbMap.get(row.id)
if (!fromDb) return row
return {
...row,
workload: toFiniteNumberOrNull(fromDb.workload),
basicFee: toFiniteNumberOrNull(fromDb.basicFee),
budgetAdoptedUnitPrice: toFiniteNumberOrNull(fromDb.budgetAdoptedUnitPrice),
consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor)
}
})
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 计算工作量法基本费用 = 采用单价 × 换算系数 × 工作量 */
export const calcWorkloadBasicFee = (row: WorkloadCalcRow): number | null => {
const { budgetAdoptedUnitPrice, conversion, workload } = row
if (budgetAdoptedUnitPrice == null || conversion == null || workload == null) return null
return roundTo(toDecimal(budgetAdoptedUnitPrice).mul(conversion).mul(workload), 2)
}
/** 计算工作量法服务费用 = 基本费用 × 咨询分类系数 */
export const calcWorkloadServiceFee = (row: WorkloadCalcRow): number | null => {
if (row.consultCategoryFactor == null) return null
const basicFee = row.basicFee ?? calcWorkloadBasicFee(row)
if (basicFee == null) return null
return roundTo(toDecimal(basicFee).mul(row.consultCategoryFactor), 2)
}

View File

@ -3,6 +3,12 @@ import { ref } from 'vue'
import { addNumbers } from '@/lib/decimal' import { addNumbers } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number' import { toFiniteNumberOrNull } from '@/lib/number'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
import {
parseHtFeeMainStorageKey,
parseHtFeeMethodStorageKey,
useZxFwPricingHtFeeStore
} from '@/pinia/zxFwPricingHtFee'
import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly' export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
export type ServicePricingMethod = ZxFwPricingField export type ServicePricingMethod = ZxFwPricingField
@ -57,8 +63,6 @@ const METHOD_STORAGE_PREFIX_MAP: Record<ServicePricingMethod, string> = {
const STORAGE_PREFIX_METHOD_MAP = new Map<string, ServicePricingMethod>( const STORAGE_PREFIX_METHOD_MAP = new Map<string, ServicePricingMethod>(
Object.entries(METHOD_STORAGE_PREFIX_MAP).map(([method, prefix]) => [prefix, method as ServicePricingMethod]) Object.entries(METHOD_STORAGE_PREFIX_MAP).map(([method, prefix]) => [prefix, method as ServicePricingMethod])
) )
const HT_FEE_MAIN_KEY_PATTERN = /^htExtraFee-(.+)-(additional-work|reserve)$/
const HT_FEE_METHOD_TYPES: HtFeeMethodType[] = ['rate-fee', 'hourly-fee', 'quantity-unit-price-fee']
const toKey = (contractId: string | number) => String(contractId || '').trim() const toKey = (contractId: string | number) => String(contractId || '').trim()
const toServiceKey = (serviceId: string | number) => String(serviceId || '').trim() const toServiceKey = (serviceId: string | number) => String(serviceId || '').trim()
@ -81,6 +85,11 @@ const normalizeProcessValue = (value: unknown, rowId: string) => {
if (rowId === FIXED_ROW_ID) return null if (rowId === FIXED_ROW_ID) return null
return Number(value) === 1 ? 1 : 0 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[] => const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
(Array.isArray(rows) ? rows : []).map(item => { (Array.isArray(rows) ? rows : []).map(item => {
@ -118,8 +127,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
workload: round3Nullable(totalWorkload), workload: round3Nullable(totalWorkload),
hourly: round3Nullable(totalHourly), hourly: round3Nullable(totalHourly),
subtotal: round3Nullable(fixedSubtotal), subtotal: round3Nullable(fixedSubtotal),
finalFee: row.finalFee, finalFee: row.finalFee
} }
} }
const subtotal = sumNullableNumbers([ const subtotal = sumNullableNumbers([
@ -131,8 +139,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
return { return {
...row, ...row,
subtotal: round3Nullable(subtotal), subtotal: round3Nullable(subtotal),
finalFee: round3Nullable(subtotal), finalFee: round3Nullable(subtotal)
} }
}) })
} }
@ -198,15 +205,6 @@ const isSameState = (a: ZxFwState | null | undefined, b: ZxFwState | null | unde
return isSameRows(a.detailRows, b.detailRows) return isSameRows(a.detailRows, b.detailRows)
} }
const loadTasks = new Map<string, Promise<ZxFwState | null>>()
const keyLoadTasks = new Map<string, Promise<unknown>>()
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 normalizeProjectCount = (value: unknown) => { const normalizeProjectCount = (value: unknown) => {
const numeric = Number(value) const numeric = Number(value)
if (!Number.isFinite(numeric)) return null if (!Number.isFinite(numeric)) return null
@ -237,66 +235,26 @@ const parseServiceMethodStorageKey = (keyRaw: string | number) => {
return { key, method, contractId, serviceId } return { key, method, contractId, serviceId }
} }
const normalizeHtFeeMainState = (payload: Partial<HtFeeMainState> | null | undefined): HtFeeMainState => ({ const loadTasks = new Map<string, Promise<ZxFwState | null>>()
detailRows: Array.isArray(payload?.detailRows) ? cloneAny(payload.detailRows) : []
})
const parseHtFeeMainStorageKey = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return null
const match = HT_FEE_MAIN_KEY_PATTERN.exec(key)
if (!match) return null
const contractId = String(match[1] || '').trim()
const feeType = String(match[2] || '').trim()
if (!contractId || !feeType) return null
return {
key,
contractId,
feeType,
mainStorageKey: key
}
}
const parseHtFeeMethodStorageKey = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return null
for (const method of HT_FEE_METHOD_TYPES) {
const suffix = `-${method}`
if (!key.endsWith(suffix)) continue
const withoutMethod = key.slice(0, key.length - suffix.length)
const mainMatch = /^(htExtraFee-.+-(?:additional-work|reserve))-(.+)$/.exec(withoutMethod)
if (!mainMatch) continue
const mainStorageKey = String(mainMatch[1] || '').trim()
const rowId = String(mainMatch[2] || '').trim()
if (!mainStorageKey || !rowId) continue
return {
key,
mainStorageKey,
rowId,
method
}
}
return null
}
export const useZxFwPricingStore = defineStore('zxFwPricing', () => { export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const contracts = ref<Record<string, ZxFwState>>({}) const contracts = ref<Record<string, ZxFwState>>({})
const contractVersions = ref<Record<string, number>>({}) const contractVersions = ref<Record<string, number>>({})
const contractLoaded = ref<Record<string, boolean>>({}) const contractLoaded = ref<Record<string, boolean>>({})
const servicePricingStates = ref<Record<string, Record<string, ServicePricingState>>>({}) const servicePricingStates = ref<Record<string, Record<string, ServicePricingState>>>({})
const htFeeMainStates = ref<Record<string, HtFeeMainState>>({})
const htFeeMethodStates = ref<Record<string, Record<string, Partial<Record<HtFeeMethodType, HtFeeMethodPayload>>>>>({}) const keysStore = useZxFwPricingKeysStore()
const keyedStates = ref<Record<string, unknown>>({}) const htFeeStore = useZxFwPricingHtFeeStore()
const keyedLoaded = ref<Record<string, boolean>>({})
const keyVersions = ref<Record<string, number>>({}) const htFeeMainStates = htFeeStore.htFeeMainStates
const keySnapshots = ref<Record<string, string>>({}) const htFeeMethodStates = htFeeStore.htFeeMethodStates
const keyedStates = keysStore.keyedStates
const keyVersions = keysStore.keyVersions
const touchVersion = (contractId: string) => { const touchVersion = (contractId: string) => {
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1 contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
} }
const touchKeyVersion = (key: string) => {
keyVersions.value[key] = (keyVersions.value[key] || 0) + 1
}
const getKvStoreSafely = () => { const getKvStoreSafely = () => {
try { try {
return useKvStore() return useKvStore()
@ -334,13 +292,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return state[method] || null return state[method] || null
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getServicePricingMethodState = <TRow = unknown>( const getServicePricingMethodState = <TRow = unknown>(
contractIdRaw: string | number, contractIdRaw: string | number,
serviceIdRaw: string | number, serviceIdRaw: string | number,
@ -352,13 +303,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState<TRow> | undefined) || null return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState<TRow> | undefined) || null
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const setServicePricingMethodState = <TRow = unknown>( const setServicePricingMethodState = <TRow = unknown>(
contractIdRaw: string | number, contractIdRaw: string | number,
serviceIdRaw: string | number, serviceIdRaw: string | number,
@ -385,27 +329,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (syncKeyState) { if (syncKeyState) {
if (normalizedPayload == null) { if (normalizedPayload == null) {
delete keyedStates.value[storageKey] keysStore.removeKeyState(storageKey)
keyedLoaded.value[storageKey] = true
keySnapshots.value[storageKey] = toKeySnapshot(null)
touchKeyVersion(storageKey)
} else { } else {
keyedStates.value[storageKey] = cloneAny(normalizedPayload) keysStore.setKeyState(storageKey, cloneAny(normalizedPayload), { force: true })
keyedLoaded.value[storageKey] = true
keySnapshots.value[storageKey] = toKeySnapshot(normalizedPayload)
touchKeyVersion(storageKey)
} }
} }
return true return true
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const loadServicePricingMethodState = async <TRow = unknown>( const loadServicePricingMethodState = async <TRow = unknown>(
contractIdRaw: string | number, contractIdRaw: string | number,
serviceIdRaw: string | number, serviceIdRaw: string | number,
@ -431,13 +362,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return getServicePricingMethodState<TRow>(contractId, serviceId, method) return getServicePricingMethodState<TRow>(contractId, serviceId, method)
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const removeServicePricingMethodState = ( const removeServicePricingMethodState = (
contractIdRaw: string | number, contractIdRaw: string | number,
serviceIdRaw: string | number, serviceIdRaw: string | number,
@ -449,20 +373,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method) const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
const had = getServicePricingMethodState(contractId, serviceId, method) != null const had = getServicePricingMethodState(contractId, serviceId, method) != null
setServiceMethodStateInMemory(contractId, serviceId, method, null) setServiceMethodStateInMemory(contractId, serviceId, method, null)
delete keyedStates.value[storageKey] keysStore.removeKeyState(storageKey)
keyedLoaded.value[storageKey] = true
keySnapshots.value[storageKey] = toKeySnapshot(null)
touchKeyVersion(storageKey)
return had return had
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getServicePricingStorageKey = ( const getServicePricingStorageKey = (
contractIdRaw: string | number, contractIdRaw: string | number,
serviceIdRaw: string | number, serviceIdRaw: string | number,
@ -474,13 +388,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return serviceMethodDbKeyOf(contractId, serviceId, method) return serviceMethodDbKeyOf(contractId, serviceId, method)
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => { const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
const contractId = toKey(contractIdRaw) const contractId = toKey(contractIdRaw)
const serviceId = toServiceKey(serviceIdRaw) const serviceId = toServiceKey(serviceIdRaw)
@ -490,13 +397,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
) )
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => { const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
let changed = false let changed = false
for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) { for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
@ -505,262 +405,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return changed return changed
} }
/** const getHtFeeMainState = htFeeStore.getHtFeeMainState
* @Author: wintsa const setHtFeeMainState = htFeeStore.setHtFeeMainState
* @Date: 2026-03-13 const loadHtFeeMainState = htFeeStore.loadHtFeeMainState
* @LastEditors: wintsa const removeHtFeeMainState = htFeeStore.removeHtFeeMainState
* @Description: const getHtFeeMethodStorageKey = htFeeStore.getHtFeeMethodStorageKey
* @returns {*} const getHtFeeMethodState = htFeeStore.getHtFeeMethodState
*/ const setHtFeeMethodState = htFeeStore.setHtFeeMethodState
const getHtFeeMainState = <TRow = unknown>(mainStorageKeyRaw: string | number): HtFeeMainState<TRow> | null => { const loadHtFeeMethodState = htFeeStore.loadHtFeeMethodState
const mainStorageKey = toKey(mainStorageKeyRaw) const removeHtFeeMethodState = htFeeStore.removeHtFeeMethodState
if (!mainStorageKey) return null
return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState<TRow> | undefined) || null
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const setHtFeeMainState = <TRow = unknown>(
mainStorageKeyRaw: string | number,
payload: Partial<HtFeeMainState<TRow>> | null | undefined,
options?: {
force?: boolean
syncKeyState?: boolean
}
) => {
const mainStorageKey = toKey(mainStorageKeyRaw)
if (!mainStorageKey) return false
const force = options?.force === true
const syncKeyState = options?.syncKeyState !== false
const normalized = payload == null ? null : normalizeHtFeeMainState(payload)
const prevSnapshot = toKeySnapshot(getHtFeeMainState(mainStorageKey))
const nextSnapshot = toKeySnapshot(normalized)
if (!force && prevSnapshot === nextSnapshot) return false
if (normalized == null) {
delete htFeeMainStates.value[mainStorageKey]
} else {
htFeeMainStates.value[mainStorageKey] = normalized
}
if (syncKeyState) {
if (normalized == null) {
delete keyedStates.value[mainStorageKey]
keyedLoaded.value[mainStorageKey] = true
keySnapshots.value[mainStorageKey] = toKeySnapshot(null)
} else {
keyedStates.value[mainStorageKey] = cloneAny(normalized)
keyedLoaded.value[mainStorageKey] = true
keySnapshots.value[mainStorageKey] = toKeySnapshot(normalized)
}
touchKeyVersion(mainStorageKey)
}
return true
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const loadHtFeeMainState = async <TRow = unknown>(
mainStorageKeyRaw: string | number,
force = false
): Promise<HtFeeMainState<TRow> | null> => {
const mainStorageKey = toKey(mainStorageKeyRaw)
if (!mainStorageKey) return null
if (!force) {
const existing = getHtFeeMainState<TRow>(mainStorageKey)
if (existing) return existing
}
const payload = await loadKeyState<HtFeeMainState<TRow>>(mainStorageKey, force)
if (!payload) {
setHtFeeMainState(mainStorageKey, null, { force: true, syncKeyState: false })
return null
}
setHtFeeMainState(mainStorageKey, payload, { force: true, syncKeyState: false })
return getHtFeeMainState<TRow>(mainStorageKey)
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const removeHtFeeMainState = (mainStorageKeyRaw: string | number) =>
setHtFeeMainState(mainStorageKeyRaw, null)
const ensureHtFeeMethodStateContainer = (mainStorageKeyRaw: string | number, rowIdRaw: string | number) => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return null
if (!htFeeMethodStates.value[mainStorageKey]) {
htFeeMethodStates.value[mainStorageKey] = {}
}
if (!htFeeMethodStates.value[mainStorageKey][rowId]) {
htFeeMethodStates.value[mainStorageKey][rowId] = {}
}
return htFeeMethodStates.value[mainStorageKey][rowId]
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getHtFeeMethodStorageKey = (
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType
) => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return ''
return `${mainStorageKey}-${rowId}-${method}`
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType
): TPayload | null => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return null
const value = htFeeMethodStates.value[mainStorageKey]?.[rowId]?.[method]
return value == null ? null : (cloneAny(value) as TPayload)
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const setHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType,
payload: TPayload | null | undefined,
options?: {
force?: boolean
syncKeyState?: boolean
}
) => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return false
const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
if (!storageKey) return false
const force = options?.force === true
const syncKeyState = options?.syncKeyState !== false
const prevSnapshot = toKeySnapshot(getHtFeeMethodState(mainStorageKey, rowId, method))
const nextSnapshot = toKeySnapshot(payload ?? null)
if (!force && prevSnapshot === nextSnapshot) return false
if (payload == null) {
const byRow = htFeeMethodStates.value[mainStorageKey]?.[rowId]
if (byRow) {
delete byRow[method]
if (Object.keys(byRow).length === 0) {
delete htFeeMethodStates.value[mainStorageKey][rowId]
if (Object.keys(htFeeMethodStates.value[mainStorageKey]).length === 0) {
delete htFeeMethodStates.value[mainStorageKey]
}
}
}
} else {
const container = ensureHtFeeMethodStateContainer(mainStorageKey, rowId)
if (!container) return false
container[method] = cloneAny(payload)
}
if (syncKeyState) {
if (payload == null) {
delete keyedStates.value[storageKey]
keyedLoaded.value[storageKey] = true
keySnapshots.value[storageKey] = toKeySnapshot(null)
} else {
keyedStates.value[storageKey] = cloneAny(payload)
keyedLoaded.value[storageKey] = true
keySnapshots.value[storageKey] = toKeySnapshot(payload)
}
touchKeyVersion(storageKey)
}
return true
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const loadHtFeeMethodState = async <TPayload = HtFeeMethodPayload>(
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType,
force = false
): Promise<TPayload | null> => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return null
if (!force) {
const existing = getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
if (existing != null) return existing
}
const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
const payload = await loadKeyState<TPayload>(storageKey, force)
if (payload == null) {
setHtFeeMethodState(mainStorageKey, rowId, method, null, { force: true, syncKeyState: false })
return null
}
setHtFeeMethodState(mainStorageKey, rowId, method, payload, { force: true, syncKeyState: false })
return getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const removeHtFeeMethodState = (
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType
) => setHtFeeMethodState(mainStorageKeyRaw, rowIdRaw, method, null)
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getKeyState = <T = unknown>(keyRaw: string | number): T | null => { const getKeyState = <T = unknown>(keyRaw: string | number): T | null => {
const key = toKey(keyRaw) const key = toKey(keyRaw)
if (!key) return null if (!key) return null
@ -787,85 +441,45 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const mainState = getHtFeeMainState(htMainMeta.mainStorageKey) const mainState = getHtFeeMainState(htMainMeta.mainStorageKey)
if (mainState != null) return cloneAny(mainState as T) if (mainState != null) return cloneAny(mainState as T)
} }
if (!Object.prototype.hasOwnProperty.call(keyedStates.value, key)) return null return keysStore.getKeyState<T>(key)
return cloneAny(keyedStates.value[key] as T)
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const loadKeyState = async <T = unknown>(keyRaw: string | number, force = false): Promise<T | null> => { const loadKeyState = async <T = unknown>(keyRaw: string | number, force = false): Promise<T | null> => {
const key = toKey(keyRaw) const key = toKey(keyRaw)
if (!key) return null if (!key) return null
const hasState = Object.prototype.hasOwnProperty.call(keyedStates.value, key)
if (!force && hasState) {
keyedLoaded.value[key] = true
if (!keySnapshots.value[key]) {
keySnapshots.value[key] = toKeySnapshot(keyedStates.value[key])
}
return getKeyState<T>(key)
}
// 注意当内存中没有该key时不应仅凭keyedLoaded短路返回null。
// 该key可能被其他逻辑直接写入了IndexedDB例如默认明细生成
if (!force && keyLoadTasks.has(key)) return keyLoadTasks.get(key) as Promise<T | null>
const task = (async () => { const raw = await keysStore.loadKeyState<T>(key, force)
const kvStore = getKvStoreSafely()
const raw = kvStore ? await kvStore.getItem<T>(key) : null
const nextSnapshot = toKeySnapshot(raw)
const prevSnapshot = keySnapshots.value[key]
keyedLoaded.value[key] = true
if (prevSnapshot !== nextSnapshot || !Object.prototype.hasOwnProperty.call(keyedStates.value, key)) {
keyedStates.value[key] = cloneAny(raw)
keySnapshots.value[key] = nextSnapshot
touchKeyVersion(key)
}
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)
})()
keyLoadTasks.set(key, task) const serviceMeta = parseServiceMethodStorageKey(key)
try { if (serviceMeta) {
return await task setServicePricingMethodState(
} finally { serviceMeta.contractId,
keyLoadTasks.delete(key) 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)
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const setKeyState = <T = unknown>( const setKeyState = <T = unknown>(
keyRaw: string | number, keyRaw: string | number,
value: T, value: T,
@ -875,11 +489,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
) => { ) => {
const key = toKey(keyRaw) const key = toKey(keyRaw)
if (!key) return false if (!key) return false
const force = options?.force === true
const nextSnapshot = toKeySnapshot(value)
const prevSnapshot = keySnapshots.value[key]
keyedLoaded.value[key] = true
if (!force && prevSnapshot === nextSnapshot) return false
const serviceMeta = parseServiceMethodStorageKey(key) const serviceMeta = parseServiceMethodStorageKey(key)
if (serviceMeta) { if (serviceMeta) {
setServicePricingMethodState( setServicePricingMethodState(
@ -901,30 +511,24 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
{ force: true, syncKeyState: false } { force: true, syncKeyState: false }
) )
} }
const htMainMeta = parseHtFeeMainStorageKey(key) const htMainMeta = parseHtFeeMainStorageKey(key)
if (htMainMeta) { if (htMainMeta) {
setHtFeeMainState(htMainMeta.mainStorageKey, value as Partial<HtFeeMainState>, { force: true, syncKeyState: false }) setHtFeeMainState(htMainMeta.mainStorageKey, value as Partial<HtFeeMainState>, { force: true, syncKeyState: false })
} }
keyedStates.value[key] = cloneAny(value)
keySnapshots.value[key] = nextSnapshot return keysStore.setKeyState(key, value, options)
touchKeyVersion(key)
return true
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const removeKeyState = (keyRaw: string | number) => { const removeKeyState = (keyRaw: string | number) => {
const key = toKey(keyRaw) const key = toKey(keyRaw)
if (!key) return false if (!key) return false
const serviceMeta = parseServiceMethodStorageKey(key) const serviceMeta = parseServiceMethodStorageKey(key)
if (serviceMeta) { if (serviceMeta) {
setServiceMethodStateInMemory(serviceMeta.contractId, serviceMeta.serviceId, serviceMeta.method, null) setServiceMethodStateInMemory(serviceMeta.contractId, serviceMeta.serviceId, serviceMeta.method, null)
} }
const htMethodMeta = parseHtFeeMethodStorageKey(key) const htMethodMeta = parseHtFeeMethodStorageKey(key)
if (htMethodMeta) { if (htMethodMeta) {
setHtFeeMethodState(htMethodMeta.mainStorageKey, htMethodMeta.rowId, htMethodMeta.method, null, { setHtFeeMethodState(htMethodMeta.mainStorageKey, htMethodMeta.rowId, htMethodMeta.method, null, {
@ -932,38 +536,17 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
syncKeyState: false syncKeyState: false
}) })
} }
const htMainMeta = parseHtFeeMainStorageKey(key) const htMainMeta = parseHtFeeMainStorageKey(key)
if (htMainMeta) { if (htMainMeta) {
setHtFeeMainState(htMainMeta.mainStorageKey, null, { force: true, syncKeyState: false }) setHtFeeMainState(htMainMeta.mainStorageKey, null, { force: true, syncKeyState: false })
} }
const hadValue = Object.prototype.hasOwnProperty.call(keyedStates.value, key)
delete keyedStates.value[key] return keysStore.removeKeyState(key)
keyedLoaded.value[key] = true
keySnapshots.value[key] = toKeySnapshot(null)
touchKeyVersion(key)
return hadValue
} }
/** const getKeyVersion = (keyRaw: string | number) => keysStore.getKeyVersion(keyRaw)
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getKeyVersion = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return 0
return keyVersions.value[key] || 0
}
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getContractState = (contractIdRaw: string | number) => { const getContractState = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw) const contractId = toKey(contractIdRaw)
if (!contractId) return null if (!contractId) return null
@ -971,13 +554,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return data ? cloneState(data) : null return data ? cloneState(data) : null
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const loadContract = async (contractIdRaw: string | number, force = false) => { const loadContract = async (contractIdRaw: string | number, force = false) => {
const contractId = toKey(contractIdRaw) const contractId = toKey(contractIdRaw)
if (!contractId) return null if (!contractId) return null
@ -992,10 +568,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
: null : null
const current = contracts.value[contractId] const current = contracts.value[contractId]
if (raw) { if (raw) {
console.log(raw,'init')
const normalized = normalizeState(raw) const normalized = normalizeState(raw)
console.log(normalized)
if (!current || !isSameState(current, normalized)) { if (!current || !isSameState(current, normalized)) {
contracts.value[contractId] = normalized contracts.value[contractId] = normalized
touchVersion(contractId) touchVersion(contractId)
@ -1016,13 +589,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
} }
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => { const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
const contractId = toKey(contractIdRaw) const contractId = toKey(contractIdRaw)
if (!contractId) return false if (!contractId) return false
@ -1035,23 +601,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return true return true
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const updatePricingField = async (params: { const updatePricingField = async (params: {
contractId: string contractId: string
serviceId: string | number serviceId: string | number
field: ZxFwPricingField field: ZxFwPricingField
value: number | null | undefined value: number | null | undefined
}) => { }) => {
const contractId = toKey(params.contractId) const contractId = toKey(params.contractId)
if (!contractId) return false if (!contractId) return false
const current = contracts.value[contractId] const current = contracts.value[contractId]
if (!current?.detailRows?.length) return false if (!current?.detailRows?.length) return false
@ -1085,13 +642,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return true return true
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const getBaseSubtotal = (contractIdRaw: string | number): number | null => { const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
const contractId = toKey(contractIdRaw) const contractId = toKey(contractIdRaw)
if (!contractId) return null if (!contractId) return null
@ -1111,13 +661,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return hasValid ? round3(sum) : null return hasValid ? round3(sum) : null
} }
/**
* @Author: wintsa
* @Date: 2026-03-13
* @LastEditors: wintsa
* @Description:
* @returns {*}
*/
const removeContractData = (contractIdRaw: string | number) => { const removeContractData = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw) const contractId = toKey(contractIdRaw)
if (!contractId) return false if (!contractId) return false
@ -1141,49 +684,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
} }
loadTasks.delete(contractId) loadTasks.delete(contractId)
changed = htFeeStore.removeContractHtFeeData(contractId) || changed
const htMainPrefix = `htExtraFee-${contractId}-` const htMainPrefix = `htExtraFee-${contractId}-`
for (const key of Object.keys(htFeeMainStates.value)) { changed = keysStore.removeKeysByPrefix(htMainPrefix) || changed
if (!key.startsWith(htMainPrefix)) continue
delete htFeeMainStates.value[key] for (const prefix of Object.values(METHOD_STORAGE_PREFIX_MAP)) {
changed = true changed = keysStore.removeKeysByPrefix(`${prefix}-${contractId}-`) || changed
}
for (const key of Object.keys(htFeeMethodStates.value)) {
if (!key.startsWith(htMainPrefix)) continue
delete htFeeMethodStates.value[key]
changed = true
} }
const methodPrefixes = Object.values(METHOD_STORAGE_PREFIX_MAP) changed = keysStore.removeKeyState(dbKeyOf(contractId)) || changed
const isContractRelatedKey = (key: string) => {
if (key === dbKeyOf(contractId)) return true
if (key.startsWith(htMainPrefix)) return true
if (methodPrefixes.some(prefix => key.startsWith(`${prefix}-${contractId}-`))) return true
return false
}
const keySet = new Set<string>([
...Object.keys(keyedStates.value),
...Object.keys(keyedLoaded.value),
...Object.keys(keyVersions.value),
...Object.keys(keySnapshots.value)
])
for (const key of keySet) {
if (!isContractRelatedKey(key)) continue
if (Object.prototype.hasOwnProperty.call(keyedStates.value, key)) {
delete keyedStates.value[key]
changed = true
}
if (Object.prototype.hasOwnProperty.call(keyedLoaded.value, key)) {
delete keyedLoaded.value[key]
}
if (Object.prototype.hasOwnProperty.call(keyVersions.value, key)) {
delete keyVersions.value[key]
}
if (Object.prototype.hasOwnProperty.call(keySnapshots.value, key)) {
delete keySnapshots.value[key]
}
keyLoadTasks.delete(key)
}
return changed return changed
} }

View File

@ -0,0 +1,287 @@
/**
* Store
*
* zxFwPricing
* additional-work reserve
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys'
import type { HtFeeMainState, HtFeeMethodType, HtFeeMethodPayload } from '@/types/pricing'
const HT_FEE_MAIN_KEY_PATTERN = /^htExtraFee-(.+)-(additional-work|reserve)$/
const HT_FEE_METHOD_TYPES: HtFeeMethodType[] = ['rate-fee', 'hourly-fee', 'quantity-unit-price-fee']
const toKey = (keyRaw: string | number) => String(keyRaw || '').trim()
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 normalizeHtFeeMainState = (
payload: Partial<HtFeeMainState> | null | undefined
): HtFeeMainState => ({
detailRows: Array.isArray(payload?.detailRows) ? cloneAny(payload.detailRows) : []
})
/** 解析附加费用主表存储键 */
export const parseHtFeeMainStorageKey = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return null
const match = HT_FEE_MAIN_KEY_PATTERN.exec(key)
if (!match) return null
const contractId = String(match[1] || '').trim()
const feeType = String(match[2] || '').trim()
if (!contractId || !feeType) return null
return { key, contractId, feeType, mainStorageKey: key }
}
/** 解析附加费用子方法存储键 */
export const parseHtFeeMethodStorageKey = (keyRaw: string | number) => {
const key = toKey(keyRaw)
if (!key) return null
for (const method of HT_FEE_METHOD_TYPES) {
const suffix = `-${method}`
if (!key.endsWith(suffix)) continue
const withoutMethod = key.slice(0, key.length - suffix.length)
const mainMatch = /^(htExtraFee-.+-(?:additional-work|reserve))-(.+)$/.exec(withoutMethod)
if (!mainMatch) continue
const mainStorageKey = String(mainMatch[1] || '').trim()
const rowId = String(mainMatch[2] || '').trim()
if (!mainStorageKey || !rowId) continue
return { key, mainStorageKey, rowId, method }
}
return null
}
export const useZxFwPricingHtFeeStore = defineStore('zxFwPricingHtFee', () => {
/** 附加费用主表状态 */
const htFeeMainStates = ref<Record<string, HtFeeMainState>>({})
/** 附加费用子方法状态 */
const htFeeMethodStates = ref<
Record<string, Record<string, Partial<Record<HtFeeMethodType, HtFeeMethodPayload>>>>
>({})
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 获取附加费用主表状态 */
const getHtFeeMainState = <TRow = unknown>(
mainStorageKeyRaw: string | number
): HtFeeMainState<TRow> | null => {
const mainStorageKey = toKey(mainStorageKeyRaw)
if (!mainStorageKey) return null
return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState<TRow> | undefined) || null
}
/** 设置附加费用主表状态并同步版本 */
const setHtFeeMainState = <TRow = unknown>(
mainStorageKeyRaw: string | number,
payload: Partial<HtFeeMainState<TRow>> | null | undefined,
options?: { force?: boolean; syncKeyState?: boolean }
): boolean => {
const mainStorageKey = toKey(mainStorageKeyRaw)
if (!mainStorageKey) return false
const force = options?.force === true
const syncKeyState = options?.syncKeyState !== false
const normalized = payload == null ? null : normalizeHtFeeMainState(payload)
const prevSnapshot = toKeySnapshot(getHtFeeMainState(mainStorageKey))
const nextSnapshot = toKeySnapshot(normalized)
if (!force && prevSnapshot === nextSnapshot) return false
if (normalized == null) {
delete htFeeMainStates.value[mainStorageKey]
} else {
htFeeMainStates.value[mainStorageKey] = normalized
}
if (syncKeyState) {
const keysStore = useZxFwPricingKeysStore()
if (normalized == null) {
keysStore.removeKeyState(mainStorageKey)
} else {
keysStore.setKeyState(mainStorageKey, cloneAny(normalized), { force: true })
}
}
return true
}
/** 从缓存或 IndexedDB 加载附加费用主表状态 */
const loadHtFeeMainState = async <TRow = unknown>(
mainStorageKeyRaw: string | number,
force = false
): Promise<HtFeeMainState<TRow> | null> => {
const mainStorageKey = toKey(mainStorageKeyRaw)
if (!mainStorageKey) return null
if (!force) {
const existing = getHtFeeMainState<TRow>(mainStorageKey)
if (existing) return existing
}
const keysStore = useZxFwPricingKeysStore()
const payload = await keysStore.loadKeyState<HtFeeMainState<TRow>>(mainStorageKey, force)
if (!payload) {
setHtFeeMainState(mainStorageKey, null, { force: true, syncKeyState: false })
return null
}
setHtFeeMainState(mainStorageKey, payload, { force: true, syncKeyState: false })
return getHtFeeMainState<TRow>(mainStorageKey)
}
/** 删除附加费用主表状态 */
const removeHtFeeMainState = (mainStorageKeyRaw: string | number) =>
setHtFeeMainState(mainStorageKeyRaw, null)
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
const ensureMethodContainer = (mainStorageKey: string, rowId: string) => {
if (!htFeeMethodStates.value[mainStorageKey]) {
htFeeMethodStates.value[mainStorageKey] = {}
}
if (!htFeeMethodStates.value[mainStorageKey][rowId]) {
htFeeMethodStates.value[mainStorageKey][rowId] = {}
}
return htFeeMethodStates.value[mainStorageKey][rowId]
}
/** 获取附加费用子方法存储键 */
const getHtFeeMethodStorageKey = (
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType
): string => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return ''
return `${mainStorageKey}-${rowId}-${method}`
}
/** 获取附加费用子方法状态 */
const getHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType
): TPayload | null => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return null
const value = htFeeMethodStates.value[mainStorageKey]?.[rowId]?.[method]
return value == null ? null : (cloneAny(value) as TPayload)
}
/** 设置附加费用子方法状态并同步版本 */
const setHtFeeMethodState = <TPayload = HtFeeMethodPayload>(
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType,
payload: TPayload | null | undefined,
options?: { force?: boolean; syncKeyState?: boolean }
): boolean => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return false
const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
if (!storageKey) return false
const force = options?.force === true
const syncKeyState = options?.syncKeyState !== false
const prevSnapshot = toKeySnapshot(getHtFeeMethodState(mainStorageKey, rowId, method))
const nextSnapshot = toKeySnapshot(payload ?? null)
if (!force && prevSnapshot === nextSnapshot) return false
if (payload == null) {
const byRow = htFeeMethodStates.value[mainStorageKey]?.[rowId]
if (byRow) {
delete byRow[method]
if (Object.keys(byRow).length === 0) {
delete htFeeMethodStates.value[mainStorageKey][rowId]
if (Object.keys(htFeeMethodStates.value[mainStorageKey]).length === 0) {
delete htFeeMethodStates.value[mainStorageKey]
}
}
}
} else {
const container = ensureMethodContainer(mainStorageKey, rowId)
container[method] = cloneAny(payload)
}
if (syncKeyState) {
const keysStore = useZxFwPricingKeysStore()
if (payload == null) {
keysStore.removeKeyState(storageKey)
} else {
keysStore.setKeyState(storageKey, cloneAny(payload), { force: true })
}
}
return true
}
/** 从缓存或 IndexedDB 加载附加费用子方法状态 */
const loadHtFeeMethodState = async <TPayload = HtFeeMethodPayload>(
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType,
force = false
): Promise<TPayload | null> => {
const mainStorageKey = toKey(mainStorageKeyRaw)
const rowId = toKey(rowIdRaw)
if (!mainStorageKey || !rowId) return null
if (!force) {
const existing = getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
if (existing != null) return existing
}
const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method)
const keysStore = useZxFwPricingKeysStore()
const payload = await keysStore.loadKeyState<TPayload>(storageKey, force)
if (payload == null) {
setHtFeeMethodState(mainStorageKey, rowId, method, null, { force: true, syncKeyState: false })
return null
}
setHtFeeMethodState(mainStorageKey, rowId, method, payload, { force: true, syncKeyState: false })
return getHtFeeMethodState<TPayload>(mainStorageKey, rowId, method)
}
/** 删除附加费用子方法状态 */
const removeHtFeeMethodState = (
mainStorageKeyRaw: string | number,
rowIdRaw: string | number,
method: HtFeeMethodType
) => setHtFeeMethodState(mainStorageKeyRaw, rowIdRaw, method, null)
/** 清除指定合同的全部附加费用数据 */
const removeContractHtFeeData = (contractId: string): boolean => {
const prefix = `htExtraFee-${contractId}-`
let changed = false
for (const key of Object.keys(htFeeMainStates.value)) {
if (!key.startsWith(prefix)) continue
delete htFeeMainStates.value[key]
changed = true
}
for (const key of Object.keys(htFeeMethodStates.value)) {
if (!key.startsWith(prefix)) continue
delete htFeeMethodStates.value[key]
changed = true
}
return changed
}
return {
htFeeMainStates,
htFeeMethodStates,
getHtFeeMainState,
setHtFeeMainState,
loadHtFeeMainState,
removeHtFeeMainState,
getHtFeeMethodStorageKey,
getHtFeeMethodState,
setHtFeeMethodState,
loadHtFeeMethodState,
removeHtFeeMethodState,
removeContractHtFeeData
}
}, {
persist: true
})

View File

@ -0,0 +1,176 @@
/**
* Store
*
* zxFwPricing IndexedDB
*
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useKvStore } from '@/pinia/kv'
const toKey = (keyRaw: string | number) => String(keyRaw || '').trim()
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 keyLoadTasks = new Map<string, Promise<unknown>>()
const getKvStoreSafely = () => {
try {
return useKvStore()
} catch {
return null
}
}
export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
/** 通用键值状态 */
const keyedStates = ref<Record<string, unknown>>({})
/** 键是否已从 IndexedDB 加载 */
const keyedLoaded = ref<Record<string, boolean>>({})
/** 键版本号(每次变更递增) */
const keyVersions = ref<Record<string, number>>({})
/** 键快照(用于比对是否变更) */
const keySnapshots = ref<Record<string, string>>({})
const touchKeyVersion = (key: string) => {
keyVersions.value[key] = (keyVersions.value[key] || 0) + 1
}
/** 获取指定键的版本号 */
const getKeyVersion = (keyRaw: string | number): number => {
const key = toKey(keyRaw)
if (!key) return 0
return keyVersions.value[key] || 0
}
/** 按通用键获取状态 */
const getKeyState = <T = unknown>(keyRaw: string | number): T | null => {
const key = toKey(keyRaw)
if (!key) return null
if (!Object.prototype.hasOwnProperty.call(keyedStates.value, key)) return null
return cloneAny(keyedStates.value[key] as T)
}
/** 按通用键从缓存或 IndexedDB 加载状态 */
const loadKeyState = async <T = unknown>(keyRaw: string | number, force = false): Promise<T | null> => {
const key = toKey(keyRaw)
if (!key) return null
const hasState = Object.prototype.hasOwnProperty.call(keyedStates.value, key)
if (!force && hasState) {
keyedLoaded.value[key] = true
if (!keySnapshots.value[key]) {
keySnapshots.value[key] = toKeySnapshot(keyedStates.value[key])
}
return getKeyState<T>(key)
}
if (!force && keyLoadTasks.has(key)) return keyLoadTasks.get(key) as Promise<T | null>
const task = (async () => {
const kvStore = getKvStoreSafely()
const raw = kvStore ? await kvStore.getItem<T>(key) : null
const nextSnapshot = toKeySnapshot(raw)
const prevSnapshot = keySnapshots.value[key]
keyedLoaded.value[key] = true
if (prevSnapshot !== nextSnapshot || !Object.prototype.hasOwnProperty.call(keyedStates.value, key)) {
keyedStates.value[key] = cloneAny(raw)
keySnapshots.value[key] = nextSnapshot
touchKeyVersion(key)
}
return getKeyState<T>(key)
})()
keyLoadTasks.set(key, task)
try {
return await task
} finally {
keyLoadTasks.delete(key)
}
}
/** 按通用键设置状态并同步版本 */
const setKeyState = <T = unknown>(
keyRaw: string | number,
value: T,
options?: { force?: boolean }
): boolean => {
const key = toKey(keyRaw)
if (!key) return false
const force = options?.force === true
const nextSnapshot = toKeySnapshot(value)
const prevSnapshot = keySnapshots.value[key]
keyedLoaded.value[key] = true
if (!force && prevSnapshot === nextSnapshot) return false
keyedStates.value[key] = cloneAny(value)
keySnapshots.value[key] = nextSnapshot
touchKeyVersion(key)
return true
}
/** 按通用键删除状态 */
const removeKeyState = (keyRaw: string | number): boolean => {
const key = toKey(keyRaw)
if (!key) return false
const hadValue = Object.prototype.hasOwnProperty.call(keyedStates.value, key)
delete keyedStates.value[key]
keyedLoaded.value[key] = true
keySnapshots.value[key] = toKeySnapshot(null)
touchKeyVersion(key)
return hadValue
}
/** 批量清除指定前缀的键 */
const removeKeysByPrefix = (prefix: string): boolean => {
let changed = false
const allKeys = new Set<string>([
...Object.keys(keyedStates.value),
...Object.keys(keyedLoaded.value),
...Object.keys(keyVersions.value),
...Object.keys(keySnapshots.value)
])
for (const key of allKeys) {
if (!key.startsWith(prefix)) continue
if (Object.prototype.hasOwnProperty.call(keyedStates.value, key)) {
delete keyedStates.value[key]
changed = true
}
delete keyedLoaded.value[key]
delete keyVersions.value[key]
delete keySnapshots.value[key]
keyLoadTasks.delete(key)
}
return changed
}
/**
*
* store
*/
const setKeyStateSilent = <T = unknown>(key: string, value: T): void => {
keyedStates.value[key] = cloneAny(value)
keyedLoaded.value[key] = true
keySnapshots.value[key] = toKeySnapshot(value)
}
return {
keyedStates,
keyedLoaded,
keyVersions,
keySnapshots,
getKeyVersion,
getKeyState,
loadKeyState,
setKeyState,
removeKeyState,
removeKeysByPrefix,
setKeyStateSilent,
touchKeyVersion
}
}, {
persist: true
})

262
src/types/pricing.ts Normal file
View File

@ -0,0 +1,262 @@
/**
* -
*
*
* zxFw 4Store
*/
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 4种计价方法字段名对应 zxFw 主表列) */
export type PricingMethodField = 'investScale' | 'landScale' | 'workload' | 'hourly'
/** 规模法类型:投资规模 / 用地规模 */
export type ScaleType = 'invest' | 'land'
/** 合同附加费用计费方式 */
export type HtFeeMethodType = 'rate-fee' | 'hourly-fee' | 'quantity-unit-price-fee'
/* ----------------------------------------------------------------
* zxFw
* ---------------------------------------------------------------- */
/** zxFw 主表每行数据 —— 一个咨询服务的4种计价法汇总 */
export interface ZxFwDetailRow {
id: string
code?: string
name?: string
/** 是否参与流程1=参与, 0=不参与, null=固定行) */
process?: number | null
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
/** 4种计价法小计 */
subtotal?: number | null
/** 最终费用 */
finalFee?: number | null
actions?: unknown
}
/** zxFw 主表状态(一个合同的全部咨询服务) */
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
}
/* ----------------------------------------------------------------
* +
* ---------------------------------------------------------------- */
/** 规模法 AG Grid 行数据 */
export interface ScaleDetailRow {
id: string
projectIndex?: number
majorDictId?: string
groupCode: string
groupName: string
majorCode: string
majorName: string
hasCost: boolean
hasArea: boolean
amount: number | null
landArea: number | null
benchmarkBudget: number | null
benchmarkBudgetBasic: number | null
benchmarkBudgetOptional: number | null
benchmarkBudgetBasicChecked: boolean
benchmarkBudgetOptionalChecked: boolean
basicFormula: string | null
optionalFormula: string | null
consultCategoryFactor: number | null
majorFactor: number | null
workStageFactor: number | null
workRatio: number | null
budgetFee: number | null
budgetFeeBasic: number | null
budgetFeeOptional: number | null
remark: string
path: string[]
}
/** 规模法计算用精简行pricingMethodTotals 使用) */
export interface ScaleCalcRow {
id: string
amount: number | null
landArea: number | null
consultCategoryFactor: number | null
majorFactor: number | null
workStageFactor: number | null
workRatio: number | null
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 工作量法 AG Grid 行数据 */
export interface WorkloadDetailRow {
id: string
taskCode: string
taskName: string
unit: string
conversion: number | null
workload: number | null
basicFee: number | null
budgetBase: string
budgetReferenceUnitPrice: string
budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null
serviceFee: number | null
remark: string
path: string[]
}
/** 工作量法计算用精简行 */
export interface WorkloadCalcRow {
id: string
conversion: number | null
workload: number | null
basicFee: number | null
budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 工时法 AG Grid 行数据 */
export interface HourlyDetailRow {
id: string
adoptedBudgetUnitPrice: number | null
personnelCount: number | null
workdayCount: number | null
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 4种计价法的汇总金额 */
export interface PricingMethodTotals {
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 合同附加费用主表状态 */
export interface HtFeeMainState<TRow = unknown> {
detailRows: TRow[]
}
/** 合同附加费用子方法载荷 */
export type HtFeeMethodPayload = unknown
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 专业字典叶子节点 */
export interface MajorDictLeaf {
id: string
code: string
name: string
hasCost: boolean
hasArea: boolean
}
/** 专业字典分组 */
export interface MajorDictGroup {
id: string
code: string
name: string
children: MajorDictLeaf[]
}
/** 专业字典精简信息 */
export interface MajorLite {
code: string
name: string
defCoe: number | null
hasCost?: boolean
hasArea?: boolean
industryId?: string | number | null
}
/** 咨询服务字典精简信息 */
export interface ServiceLite {
defCoe: number | null
onlyCostScale?: boolean | null
mutiple?: boolean | null
}
/** 工作量法任务字典 */
export interface TaskLite {
serviceID: number
code?: string
ref?: string
name: string
basicParam: string
unit: string
conversion: number | null
maxPrice: number | null
minPrice: number | null
defPrice: number | null
desc: string | null
}
/** 工时法专家字典 */
export interface ExpertLite {
defPrice: number | null
manageCoe: number | null
}
/* ----------------------------------------------------------------
*
* ---------------------------------------------------------------- */
/** 存储的明细行状态(通用) */
export interface StoredDetailRowsState<T = unknown> {
detailRows?: T[]
}
/** 存储的系数状态 */
export interface StoredFactorState {
detailRows?: Array<{
id: string
standardFactor?: number | null
budgetValue?: number | null
}>
}
/** 项目基础信息 */
export interface XmBaseInfoState {
projectIndustry?: string
}