/** * 规模法通用计算(投资规模法 + 用地规模法共用) * * 提供行默认值构建、行合并、费用计算等纯函数, * 供 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 }