From 4f46b237691e6ffc5c74ae22ff7e4b0f93eb36ef Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Wed, 18 Mar 2026 18:46:58 +0800 Subject: [PATCH] 1 --- src/components/ht/zxFw.vue | 17 +- src/lib/pricingHourlyCalc.ts | 69 ++++ src/lib/pricingPersistControl.ts | 70 ++++ src/lib/pricingScaleCalc.ts | 218 +++++++++++ src/lib/pricingWorkloadCalc.ts | 99 +++++ src/pinia/zxFwPricing.ts | 652 ++++--------------------------- src/pinia/zxFwPricingHtFee.ts | 287 ++++++++++++++ src/pinia/zxFwPricingKeys.ts | 176 +++++++++ src/types/pricing.ts | 262 +++++++++++++ 9 files changed, 1271 insertions(+), 579 deletions(-) create mode 100644 src/lib/pricingHourlyCalc.ts create mode 100644 src/lib/pricingPersistControl.ts create mode 100644 src/lib/pricingScaleCalc.ts create mode 100644 src/lib/pricingWorkloadCalc.ts create mode 100644 src/pinia/zxFwPricingHtFee.ts create mode 100644 src/pinia/zxFwPricingKeys.ts create mode 100644 src/types/pricing.ts diff --git a/src/components/ht/zxFw.vue b/src/components/ht/zxFw.vue index a369152..a616d58 100644 --- a/src/components/ht/zxFw.vue +++ b/src/components/ht/zxFw.vue @@ -474,6 +474,7 @@ const clearRowValues = async (row: DetailRow) => { landScale: sanitizedTotals.landScale, workload: sanitizedTotals.workload, hourly: sanitizedTotals.hourly, + subtotal: newSubtotal != null ? roundTo(newSubtotal, 2) : null, finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null } }) @@ -727,16 +728,16 @@ const columnDefs: ColDef[] = [ editable: params => !isFixedRow(params.data), valueGetter: params => { if (!params.data) return null - + console.log(detailRows.value) return params.data.finalFee }, - valueSetter: params => { - const parsed = parseNumberOrNull(params.newValue, { precision: 2 }) - const val = parsed != null ? roundTo(parsed, 2) : null - if (params.data.finalFee === val) return false - params.data.finalFee = val - return true - }, + // valueSetter: params => { + // const parsed = parseNumberOrNull(params.newValue, { precision: 2 }) + // const val = parsed != null ? roundTo(parsed, 2) : null + // if (params.data.finalFee === val) return false + // params.data.finalFee = val + // return true + // }, valueParser: params => { const parsed = parseNumberOrNull(params.newValue, { precision: 2 }) return parsed != null ? roundTo(parsed, 2) : null diff --git a/src/lib/pricingHourlyCalc.ts b/src/lib/pricingHourlyCalc.ts new file mode 100644 index 0000000..4c48d9a --- /dev/null +++ b/src/lib/pricingHourlyCalc.ts @@ -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) + .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 & Pick> | undefined +): HourlyDetailRow[] => { + const dbMap = new Map & Pick>() + 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) +} diff --git a/src/lib/pricingPersistControl.ts b/src/lib/pricingPersistControl.ts new file mode 100644 index 0000000..dc22b4b --- /dev/null +++ b/src/lib/pricingPersistControl.ts @@ -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)) +} diff --git a/src/lib/pricingScaleCalc.ts b/src/lib/pricingScaleCalc.ts new file mode 100644 index 0000000..aba09b5 --- /dev/null +++ b/src/lib/pricingScaleCalc.ts @@ -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(serviceId)] + return toFiniteNumberOrNull(service?.defCoe) +} + +/** 判断是否为仅投资规模服务 */ +export const isOnlyCostScaleService = (serviceId: string | number): boolean => { + const service = (getServiceDictById() as Record)[String(serviceId)] + return service?.onlyCostScale === true +} + +/* ---------------------------------------------------------------- + * 行构建与合并 + * ---------------------------------------------------------------- */ + +/** 判断是否为分组汇总行(AG Grid tree 用) */ +export const isGroupScaleRow = (row: unknown): boolean => + Boolean(row && typeof row === 'object' && (row as Record).isGroupRow === true) + +/** 过滤掉分组汇总行 */ +export const stripGroupScaleRows = (rows: TRow[] | undefined): TRow[] => + (rows || []).filter(row => !isGroupScaleRow(row)) + +/** 构建规模法默认行(全部专业叶子) */ +export const buildDefaultScaleRows = ( + serviceId: string | number, + consultCategoryFactorMap?: Map, + majorFactorMap?: Map +): 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 = (rows?: TRow[]) => { + const map = new Map() + for (const row of rows || []) map.set(String(row.id), row) + return map +} + +/** 合并持久化行与默认行(保留用户编辑值,补全缺失字段) */ +export const mergeScaleRows = ( + serviceId: string | number, + rowsFromDb: Array & Pick> | undefined, + consultCategoryFactorMap?: Map, + majorFactorMap?: Map +): 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 = (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 +} diff --git a/src/lib/pricingWorkloadCalc.ts b/src/lib/pricingWorkloadCalc.ts new file mode 100644 index 0000000..715dc3e --- /dev/null +++ b/src/lib/pricingWorkloadCalc.ts @@ -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) + .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 +): 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 & Pick> | undefined, + consultCategoryFactorMap?: Map +): WorkloadCalcRow[] => { + const dbMap = new Map & Pick>() + 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) +} diff --git a/src/pinia/zxFwPricing.ts b/src/pinia/zxFwPricing.ts index 7f70598..4826a0d 100644 --- a/src/pinia/zxFwPricing.ts +++ b/src/pinia/zxFwPricing.ts @@ -3,6 +3,12 @@ import { ref } from 'vue' import { addNumbers } from '@/lib/decimal' import { toFiniteNumberOrNull } from '@/lib/number' 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 ServicePricingMethod = ZxFwPricingField @@ -57,8 +63,6 @@ const METHOD_STORAGE_PREFIX_MAP: Record = { const STORAGE_PREFIX_METHOD_MAP = new Map( 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 toServiceKey = (serviceId: string | number) => String(serviceId || '').trim() @@ -81,6 +85,11 @@ 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 = (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 => { @@ -118,8 +127,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => { workload: round3Nullable(totalWorkload), hourly: round3Nullable(totalHourly), subtotal: round3Nullable(fixedSubtotal), - finalFee: row.finalFee, - + finalFee: row.finalFee } } const subtotal = sumNullableNumbers([ @@ -131,8 +139,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => { return { ...row, 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) } -const loadTasks = new Map>() -const keyLoadTasks = new Map>() - -const toKeySnapshot = (value: unknown) => JSON.stringify(value ?? null) -const cloneAny = (value: T): T => { - if (value == null) return value - return JSON.parse(JSON.stringify(value)) as T -} - const normalizeProjectCount = (value: unknown) => { const numeric = Number(value) if (!Number.isFinite(numeric)) return null @@ -237,66 +235,26 @@ const parseServiceMethodStorageKey = (keyRaw: string | number) => { return { key, method, contractId, serviceId } } -const normalizeHtFeeMainState = (payload: Partial | null | undefined): HtFeeMainState => ({ - 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 -} +const loadTasks = new Map>() export const useZxFwPricingStore = defineStore('zxFwPricing', () => { const contracts = ref>({}) const contractVersions = ref>({}) const contractLoaded = ref>({}) const servicePricingStates = ref>>({}) - const htFeeMainStates = ref>({}) - const htFeeMethodStates = ref>>>>({}) - const keyedStates = ref>({}) - const keyedLoaded = ref>({}) - const keyVersions = ref>({}) - const keySnapshots = ref>({}) + + const keysStore = useZxFwPricingKeysStore() + const htFeeStore = useZxFwPricingHtFeeStore() + + const htFeeMainStates = htFeeStore.htFeeMainStates + const htFeeMethodStates = htFeeStore.htFeeMethodStates + const keyedStates = keysStore.keyedStates + const keyVersions = keysStore.keyVersions const touchVersion = (contractId: string) => { contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1 } - const touchKeyVersion = (key: string) => { - keyVersions.value[key] = (keyVersions.value[key] || 0) + 1 - } + const getKvStoreSafely = () => { try { return useKvStore() @@ -334,13 +292,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return state[method] || null } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 获取咨询服务计价方法状态 - * @returns {*} - */ const getServicePricingMethodState = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -352,13 +303,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState | undefined) || null } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 设置咨询服务计价方法状态并同步版本 - * @returns {*} - */ const setServicePricingMethodState = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -385,27 +329,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { if (syncKeyState) { if (normalizedPayload == null) { - delete keyedStates.value[storageKey] - keyedLoaded.value[storageKey] = true - keySnapshots.value[storageKey] = toKeySnapshot(null) - touchKeyVersion(storageKey) + keysStore.removeKeyState(storageKey) } else { - keyedStates.value[storageKey] = cloneAny(normalizedPayload) - keyedLoaded.value[storageKey] = true - keySnapshots.value[storageKey] = toKeySnapshot(normalizedPayload) - touchKeyVersion(storageKey) + keysStore.setKeyState(storageKey, cloneAny(normalizedPayload), { force: true }) } } return true } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 从缓存或持久化存储加载咨询服务计价方法状态 - * @returns {*} - */ const loadServicePricingMethodState = async ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -431,13 +362,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return getServicePricingMethodState(contractId, serviceId, method) } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 删除单个咨询服务计价方法状态 - * @returns {*} - */ const removeServicePricingMethodState = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -449,20 +373,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method) const had = getServicePricingMethodState(contractId, serviceId, method) != null setServiceMethodStateInMemory(contractId, serviceId, method, null) - delete keyedStates.value[storageKey] - keyedLoaded.value[storageKey] = true - keySnapshots.value[storageKey] = toKeySnapshot(null) - touchKeyVersion(storageKey) + keysStore.removeKeyState(storageKey) return had } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 获取单个咨询服务计价方法存储键 - * @returns {*} - */ const getServicePricingStorageKey = ( contractIdRaw: string | number, serviceIdRaw: string | number, @@ -474,13 +388,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return serviceMethodDbKeyOf(contractId, serviceId, method) } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 获取咨询服务全部计价方法存储键 - * @returns {*} - */ const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => { const contractId = toKey(contractIdRaw) 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) => { let changed = false for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) { @@ -505,262 +405,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return changed } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 获取合同附加费用主表状态 - * @returns {*} - */ - const getHtFeeMainState = (mainStorageKeyRaw: string | number): HtFeeMainState | null => { - const mainStorageKey = toKey(mainStorageKeyRaw) - if (!mainStorageKey) return null - return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState | undefined) || null - } + 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 - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 设置合同附加费用主表状态并同步版本 - * @returns {*} - */ - const setHtFeeMainState = ( - mainStorageKeyRaw: string | number, - payload: Partial> | 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 ( - mainStorageKeyRaw: string | number, - force = false - ): Promise | null> => { - - const mainStorageKey = toKey(mainStorageKeyRaw) - if (!mainStorageKey) return null - if (!force) { - const existing = getHtFeeMainState(mainStorageKey) - if (existing) return existing - } - const payload = await loadKeyState>(mainStorageKey, force) - if (!payload) { - setHtFeeMainState(mainStorageKey, null, { force: true, syncKeyState: false }) - return null - } - setHtFeeMainState(mainStorageKey, payload, { force: true, syncKeyState: false }) - return getHtFeeMainState(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 = ( - 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 = ( - 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 ( - mainStorageKeyRaw: string | number, - rowIdRaw: string | number, - method: HtFeeMethodType, - force = false - ): Promise => { - const mainStorageKey = toKey(mainStorageKeyRaw) - const rowId = toKey(rowIdRaw) - if (!mainStorageKey || !rowId) return null - if (!force) { - const existing = getHtFeeMethodState(mainStorageKey, rowId, method) - if (existing != null) return existing - } - const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method) - const payload = await loadKeyState(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(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 = (keyRaw: string | number): T | null => { const key = toKey(keyRaw) if (!key) return null @@ -787,85 +441,45 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { const mainState = getHtFeeMainState(htMainMeta.mainStorageKey) if (mainState != null) return cloneAny(mainState as T) } - if (!Object.prototype.hasOwnProperty.call(keyedStates.value, key)) return null - return cloneAny(keyedStates.value[key] as T) + return keysStore.getKeyState(key) } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 按通用键从缓存或持久化存储加载状态 - * @returns {*} - */ const loadKeyState = async (keyRaw: string | number, force = false): Promise => { 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(key) - } - // 注意:当内存中没有该key时,不应仅凭keyedLoaded短路返回null。 - // 该key可能被其他逻辑直接写入了IndexedDB(例如默认明细生成)。 - if (!force && keyLoadTasks.has(key)) return keyLoadTasks.get(key) as Promise - const task = (async () => { - const kvStore = getKvStoreSafely() - const raw = kvStore ? await kvStore.getItem(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, - { 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, { force: true, syncKeyState: false }) - } - return getKeyState(key) - })() + const raw = await keysStore.loadKeyState(key, force) - keyLoadTasks.set(key, task) - try { - return await task - } finally { - keyLoadTasks.delete(key) + const serviceMeta = parseServiceMethodStorageKey(key) + if (serviceMeta) { + setServicePricingMethodState( + serviceMeta.contractId, + serviceMeta.serviceId, + serviceMeta.method, + raw as Partial, + { 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, { force: true, syncKeyState: false }) + } + + return getKeyState(key) } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 按通用键设置状态并同步版本 - * @returns {*} - */ const setKeyState = ( keyRaw: string | number, value: T, @@ -875,11 +489,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { ) => { 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 + const serviceMeta = parseServiceMethodStorageKey(key) if (serviceMeta) { setServicePricingMethodState( @@ -901,30 +511,24 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { { force: true, syncKeyState: false } ) } + const htMainMeta = parseHtFeeMainStorageKey(key) if (htMainMeta) { setHtFeeMainState(htMainMeta.mainStorageKey, value as Partial, { force: true, syncKeyState: false }) } - keyedStates.value[key] = cloneAny(value) - keySnapshots.value[key] = nextSnapshot - touchKeyVersion(key) - return true + + return keysStore.setKeyState(key, value, options) } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 按通用键删除状态 - * @returns {*} - */ 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, { @@ -932,38 +536,17 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { syncKeyState: false }) } + const htMainMeta = parseHtFeeMainStorageKey(key) if (htMainMeta) { setHtFeeMainState(htMainMeta.mainStorageKey, null, { force: true, syncKeyState: 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 + + return keysStore.removeKeyState(key) } - /** - * @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 - } + const getKeyVersion = (keyRaw: string | number) => keysStore.getKeyVersion(keyRaw) - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 获取合同咨询服务主表状态 - * @returns {*} - */ const getContractState = (contractIdRaw: string | number) => { const contractId = toKey(contractIdRaw) if (!contractId) return null @@ -971,13 +554,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return data ? cloneState(data) : null } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 从缓存或持久化存储加载合同咨询服务主表状态 - * @returns {*} - */ const loadContract = async (contractIdRaw: string | number, force = false) => { const contractId = toKey(contractIdRaw) if (!contractId) return null @@ -992,10 +568,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { : null const current = contracts.value[contractId] if (raw) { - console.log(raw,'init') - const normalized = normalizeState(raw) - console.log(normalized) if (!current || !isSameState(current, normalized)) { contracts.value[contractId] = normalized 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 contractId = toKey(contractIdRaw) if (!contractId) return false @@ -1035,23 +601,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 更新合同咨询服务行的计价汇总字段 - * @returns {*} - */ 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 @@ -1085,13 +642,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return true } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 获取合同咨询服务基础小计 - * @returns {*} - */ const getBaseSubtotal = (contractIdRaw: string | number): number | null => { const contractId = toKey(contractIdRaw) if (!contractId) return null @@ -1111,13 +661,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { return hasValid ? round3(sum) : null } - /** - * @Author: wintsa - * @Date: 2026-03-13 - * @LastEditors: wintsa - * @Description: 删除合同关联的咨询服务、附加费用和键状态数据 - * @returns {*} - */ const removeContractData = (contractIdRaw: string | number) => { const contractId = toKey(contractIdRaw) if (!contractId) return false @@ -1141,49 +684,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => { } loadTasks.delete(contractId) + changed = htFeeStore.removeContractHtFeeData(contractId) || changed + const htMainPrefix = `htExtraFee-${contractId}-` - for (const key of Object.keys(htFeeMainStates.value)) { - if (!key.startsWith(htMainPrefix)) continue - delete htFeeMainStates.value[key] - changed = true - } - for (const key of Object.keys(htFeeMethodStates.value)) { - if (!key.startsWith(htMainPrefix)) continue - delete htFeeMethodStates.value[key] - changed = true + changed = keysStore.removeKeysByPrefix(htMainPrefix) || changed + + for (const prefix of Object.values(METHOD_STORAGE_PREFIX_MAP)) { + changed = keysStore.removeKeysByPrefix(`${prefix}-${contractId}-`) || changed } - const methodPrefixes = Object.values(METHOD_STORAGE_PREFIX_MAP) - 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([ - ...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) - } + changed = keysStore.removeKeyState(dbKeyOf(contractId)) || changed return changed } diff --git a/src/pinia/zxFwPricingHtFee.ts b/src/pinia/zxFwPricingHtFee.ts new file mode 100644 index 0000000..f6be824 --- /dev/null +++ b/src/pinia/zxFwPricingHtFee.ts @@ -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 = (value: T): T => { + if (value == null) return value + return JSON.parse(JSON.stringify(value)) as T +} + +const normalizeHtFeeMainState = ( + payload: Partial | 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>({}) + /** 附加费用子方法状态 */ + const htFeeMethodStates = ref< + Record>>> + >({}) + + /* ---------------------------------------------------------------- + * 主表操作 + * ---------------------------------------------------------------- */ + + /** 获取附加费用主表状态 */ + const getHtFeeMainState = ( + mainStorageKeyRaw: string | number + ): HtFeeMainState | null => { + const mainStorageKey = toKey(mainStorageKeyRaw) + if (!mainStorageKey) return null + return (htFeeMainStates.value[mainStorageKey] as HtFeeMainState | undefined) || null + } + + /** 设置附加费用主表状态并同步版本 */ + const setHtFeeMainState = ( + mainStorageKeyRaw: string | number, + payload: Partial> | 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 ( + mainStorageKeyRaw: string | number, + force = false + ): Promise | null> => { + const mainStorageKey = toKey(mainStorageKeyRaw) + if (!mainStorageKey) return null + if (!force) { + const existing = getHtFeeMainState(mainStorageKey) + if (existing) return existing + } + const keysStore = useZxFwPricingKeysStore() + const payload = await keysStore.loadKeyState>(mainStorageKey, force) + if (!payload) { + setHtFeeMainState(mainStorageKey, null, { force: true, syncKeyState: false }) + return null + } + setHtFeeMainState(mainStorageKey, payload, { force: true, syncKeyState: false }) + return getHtFeeMainState(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 = ( + 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 = ( + 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 ( + mainStorageKeyRaw: string | number, + rowIdRaw: string | number, + method: HtFeeMethodType, + force = false + ): Promise => { + const mainStorageKey = toKey(mainStorageKeyRaw) + const rowId = toKey(rowIdRaw) + if (!mainStorageKey || !rowId) return null + if (!force) { + const existing = getHtFeeMethodState(mainStorageKey, rowId, method) + if (existing != null) return existing + } + const storageKey = getHtFeeMethodStorageKey(mainStorageKey, rowId, method) + const keysStore = useZxFwPricingKeysStore() + const payload = await keysStore.loadKeyState(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(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 +}) diff --git a/src/pinia/zxFwPricingKeys.ts b/src/pinia/zxFwPricingKeys.ts new file mode 100644 index 0000000..94b023e --- /dev/null +++ b/src/pinia/zxFwPricingKeys.ts @@ -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 = (value: T): T => { + if (value == null) return value + return JSON.parse(JSON.stringify(value)) as T +} + +const keyLoadTasks = new Map>() + +const getKvStoreSafely = () => { + try { + return useKvStore() + } catch { + return null + } +} + +export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => { + /** 通用键值状态 */ + const keyedStates = ref>({}) + /** 键是否已从 IndexedDB 加载 */ + const keyedLoaded = ref>({}) + /** 键版本号(每次变更递增) */ + const keyVersions = ref>({}) + /** 键快照(用于比对是否变更) */ + const keySnapshots = ref>({}) + + 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 = (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 (keyRaw: string | number, force = false): Promise => { + 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(key) + } + + if (!force && keyLoadTasks.has(key)) return keyLoadTasks.get(key) as Promise + + const task = (async () => { + const kvStore = getKvStoreSafely() + const raw = kvStore ? await kvStore.getItem(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(key) + })() + + keyLoadTasks.set(key, task) + try { + return await task + } finally { + keyLoadTasks.delete(key) + } + } + + /** 按通用键设置状态并同步版本 */ + const setKeyState = ( + 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([ + ...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 = (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 +}) diff --git a/src/types/pricing.ts b/src/types/pricing.ts new file mode 100644 index 0000000..2611036 --- /dev/null +++ b/src/types/pricing.ts @@ -0,0 +1,262 @@ +/** + * 咨询服务计价体系 - 统一类型定义 + * + * 本文件集中定义所有计价相关的接口和类型, + * 供 zxFw 主表、4个计价法组件、Store、计算模块共用。 + */ + +/* ---------------------------------------------------------------- + * 计价方法枚举 + * ---------------------------------------------------------------- */ + +/** 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 { + 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 { + 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 { + detailRows?: T[] +} + +/** 存储的系数状态 */ +export interface StoredFactorState { + detailRows?: Array<{ + id: string + standardFactor?: number | null + budgetValue?: number | null + }> +} + +/** 项目基础信息 */ +export interface XmBaseInfoState { + projectIndustry?: string +}