1
This commit is contained in:
parent
66069ef0f1
commit
4f46b23769
@ -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
|
||||||
|
|||||||
69
src/lib/pricingHourlyCalc.ts
Normal file
69
src/lib/pricingHourlyCalc.ts
Normal 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)
|
||||||
|
}
|
||||||
70
src/lib/pricingPersistControl.ts
Normal file
70
src/lib/pricingPersistControl.ts
Normal 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
218
src/lib/pricingScaleCalc.ts
Normal 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
|
||||||
|
}
|
||||||
99
src/lib/pricingWorkloadCalc.ts
Normal file
99
src/lib/pricingWorkloadCalc.ts
Normal 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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
287
src/pinia/zxFwPricingHtFee.ts
Normal file
287
src/pinia/zxFwPricingHtFee.ts
Normal 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
|
||||||
|
})
|
||||||
176
src/pinia/zxFwPricingKeys.ts
Normal file
176
src/pinia/zxFwPricingKeys.ts
Normal 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
262
src/types/pricing.ts
Normal file
@ -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<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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user