1
This commit is contained in:
parent
66069ef0f1
commit
4f46b23769
@ -474,6 +474,7 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
landScale: sanitizedTotals.landScale,
|
||||
workload: sanitizedTotals.workload,
|
||||
hourly: sanitizedTotals.hourly,
|
||||
subtotal: newSubtotal != null ? roundTo(newSubtotal, 2) : null,
|
||||
finalFee: newSubtotal != null ? roundTo(newSubtotal, 2) : null
|
||||
}
|
||||
})
|
||||
@ -727,16 +728,16 @@ const columnDefs: ColDef<DetailRow>[] = [
|
||||
editable: params => !isFixedRow(params.data),
|
||||
valueGetter: params => {
|
||||
if (!params.data) return null
|
||||
|
||||
console.log(detailRows.value)
|
||||
return params.data.finalFee
|
||||
},
|
||||
valueSetter: params => {
|
||||
const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
|
||||
const val = parsed != null ? roundTo(parsed, 2) : null
|
||||
if (params.data.finalFee === val) return false
|
||||
params.data.finalFee = val
|
||||
return true
|
||||
},
|
||||
// valueSetter: params => {
|
||||
// const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
|
||||
// const val = parsed != null ? roundTo(parsed, 2) : null
|
||||
// if (params.data.finalFee === val) return false
|
||||
// params.data.finalFee = val
|
||||
// return true
|
||||
// },
|
||||
valueParser: params => {
|
||||
const parsed = parseNumberOrNull(params.newValue, { precision: 2 })
|
||||
return parsed != null ? roundTo(parsed, 2) : null
|
||||
|
||||
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 { toFiniteNumberOrNull } from '@/lib/number'
|
||||
import { useKvStore } from '@/pinia/kv'
|
||||
import {
|
||||
parseHtFeeMainStorageKey,
|
||||
parseHtFeeMethodStorageKey,
|
||||
useZxFwPricingHtFeeStore
|
||||
} from '@/pinia/zxFwPricingHtFee'
|
||||
import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys'
|
||||
|
||||
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||
export type ServicePricingMethod = ZxFwPricingField
|
||||
@ -57,8 +63,6 @@ const METHOD_STORAGE_PREFIX_MAP: Record<ServicePricingMethod, string> = {
|
||||
const STORAGE_PREFIX_METHOD_MAP = new Map<string, 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 toServiceKey = (serviceId: string | number) => String(serviceId || '').trim()
|
||||
@ -81,6 +85,11 @@ const normalizeProcessValue = (value: unknown, rowId: string) => {
|
||||
if (rowId === FIXED_ROW_ID) return null
|
||||
return Number(value) === 1 ? 1 : 0
|
||||
}
|
||||
const toKeySnapshot = (value: unknown) => JSON.stringify(value ?? null)
|
||||
const cloneAny = <T>(value: T): T => {
|
||||
if (value == null) return value
|
||||
return JSON.parse(JSON.stringify(value)) as T
|
||||
}
|
||||
|
||||
const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
|
||||
(Array.isArray(rows) ? rows : []).map(item => {
|
||||
@ -118,8 +127,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
|
||||
workload: round3Nullable(totalWorkload),
|
||||
hourly: round3Nullable(totalHourly),
|
||||
subtotal: round3Nullable(fixedSubtotal),
|
||||
finalFee: row.finalFee,
|
||||
|
||||
finalFee: row.finalFee
|
||||
}
|
||||
}
|
||||
const subtotal = sumNullableNumbers([
|
||||
@ -131,8 +139,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
|
||||
return {
|
||||
...row,
|
||||
subtotal: round3Nullable(subtotal),
|
||||
finalFee: round3Nullable(subtotal),
|
||||
|
||||
finalFee: round3Nullable(subtotal)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -198,15 +205,6 @@ const isSameState = (a: ZxFwState | null | undefined, b: ZxFwState | null | unde
|
||||
return isSameRows(a.detailRows, b.detailRows)
|
||||
}
|
||||
|
||||
const loadTasks = new Map<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 numeric = Number(value)
|
||||
if (!Number.isFinite(numeric)) return null
|
||||
@ -237,66 +235,26 @@ const parseServiceMethodStorageKey = (keyRaw: string | number) => {
|
||||
return { key, method, contractId, serviceId }
|
||||
}
|
||||
|
||||
const normalizeHtFeeMainState = (payload: Partial<HtFeeMainState> | null | undefined): HtFeeMainState => ({
|
||||
detailRows: Array.isArray(payload?.detailRows) ? cloneAny(payload.detailRows) : []
|
||||
})
|
||||
|
||||
const parseHtFeeMainStorageKey = (keyRaw: string | number) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return null
|
||||
const match = HT_FEE_MAIN_KEY_PATTERN.exec(key)
|
||||
if (!match) return null
|
||||
const contractId = String(match[1] || '').trim()
|
||||
const feeType = String(match[2] || '').trim()
|
||||
if (!contractId || !feeType) return null
|
||||
return {
|
||||
key,
|
||||
contractId,
|
||||
feeType,
|
||||
mainStorageKey: key
|
||||
}
|
||||
}
|
||||
|
||||
const parseHtFeeMethodStorageKey = (keyRaw: string | number) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return null
|
||||
for (const method of HT_FEE_METHOD_TYPES) {
|
||||
const suffix = `-${method}`
|
||||
if (!key.endsWith(suffix)) continue
|
||||
const withoutMethod = key.slice(0, key.length - suffix.length)
|
||||
const mainMatch = /^(htExtraFee-.+-(?:additional-work|reserve))-(.+)$/.exec(withoutMethod)
|
||||
if (!mainMatch) continue
|
||||
const mainStorageKey = String(mainMatch[1] || '').trim()
|
||||
const rowId = String(mainMatch[2] || '').trim()
|
||||
if (!mainStorageKey || !rowId) continue
|
||||
return {
|
||||
key,
|
||||
mainStorageKey,
|
||||
rowId,
|
||||
method
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const loadTasks = new Map<string, Promise<ZxFwState | null>>()
|
||||
|
||||
export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
const contracts = ref<Record<string, ZxFwState>>({})
|
||||
const contractVersions = ref<Record<string, number>>({})
|
||||
const contractLoaded = ref<Record<string, boolean>>({})
|
||||
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 keyedStates = ref<Record<string, unknown>>({})
|
||||
const keyedLoaded = ref<Record<string, boolean>>({})
|
||||
const keyVersions = ref<Record<string, number>>({})
|
||||
const keySnapshots = ref<Record<string, string>>({})
|
||||
|
||||
const keysStore = useZxFwPricingKeysStore()
|
||||
const htFeeStore = useZxFwPricingHtFeeStore()
|
||||
|
||||
const htFeeMainStates = htFeeStore.htFeeMainStates
|
||||
const htFeeMethodStates = htFeeStore.htFeeMethodStates
|
||||
const keyedStates = keysStore.keyedStates
|
||||
const keyVersions = keysStore.keyVersions
|
||||
|
||||
const touchVersion = (contractId: string) => {
|
||||
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
|
||||
}
|
||||
const touchKeyVersion = (key: string) => {
|
||||
keyVersions.value[key] = (keyVersions.value[key] || 0) + 1
|
||||
}
|
||||
|
||||
const getKvStoreSafely = () => {
|
||||
try {
|
||||
return useKvStore()
|
||||
@ -334,13 +292,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return state[method] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取咨询服务计价方法状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const getServicePricingMethodState = <TRow = unknown>(
|
||||
contractIdRaw: 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
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 设置咨询服务计价方法状态并同步版本
|
||||
* @returns {*}
|
||||
*/
|
||||
const setServicePricingMethodState = <TRow = unknown>(
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
@ -385,27 +329,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
|
||||
if (syncKeyState) {
|
||||
if (normalizedPayload == null) {
|
||||
delete keyedStates.value[storageKey]
|
||||
keyedLoaded.value[storageKey] = true
|
||||
keySnapshots.value[storageKey] = toKeySnapshot(null)
|
||||
touchKeyVersion(storageKey)
|
||||
keysStore.removeKeyState(storageKey)
|
||||
} else {
|
||||
keyedStates.value[storageKey] = cloneAny(normalizedPayload)
|
||||
keyedLoaded.value[storageKey] = true
|
||||
keySnapshots.value[storageKey] = toKeySnapshot(normalizedPayload)
|
||||
touchKeyVersion(storageKey)
|
||||
keysStore.setKeyState(storageKey, cloneAny(normalizedPayload), { force: true })
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 从缓存或持久化存储加载咨询服务计价方法状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const loadServicePricingMethodState = async <TRow = unknown>(
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
@ -431,13 +362,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return getServicePricingMethodState<TRow>(contractId, serviceId, method)
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 删除单个咨询服务计价方法状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const removeServicePricingMethodState = (
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
@ -449,20 +373,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||
const had = getServicePricingMethodState(contractId, serviceId, method) != null
|
||||
setServiceMethodStateInMemory(contractId, serviceId, method, null)
|
||||
delete keyedStates.value[storageKey]
|
||||
keyedLoaded.value[storageKey] = true
|
||||
keySnapshots.value[storageKey] = toKeySnapshot(null)
|
||||
touchKeyVersion(storageKey)
|
||||
keysStore.removeKeyState(storageKey)
|
||||
return had
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取单个咨询服务计价方法存储键
|
||||
* @returns {*}
|
||||
*/
|
||||
const getServicePricingStorageKey = (
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
@ -474,13 +388,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取咨询服务全部计价方法存储键
|
||||
* @returns {*}
|
||||
*/
|
||||
const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
@ -490,13 +397,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 删除咨询服务全部计价方法状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||
let changed = false
|
||||
for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
|
||||
@ -505,262 +405,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return changed
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取合同附加费用主表状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const getHtFeeMainState = <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 getHtFeeMainState = htFeeStore.getHtFeeMainState
|
||||
const setHtFeeMainState = htFeeStore.setHtFeeMainState
|
||||
const loadHtFeeMainState = htFeeStore.loadHtFeeMainState
|
||||
const removeHtFeeMainState = htFeeStore.removeHtFeeMainState
|
||||
const getHtFeeMethodStorageKey = htFeeStore.getHtFeeMethodStorageKey
|
||||
const getHtFeeMethodState = htFeeStore.getHtFeeMethodState
|
||||
const setHtFeeMethodState = htFeeStore.setHtFeeMethodState
|
||||
const loadHtFeeMethodState = htFeeStore.loadHtFeeMethodState
|
||||
const removeHtFeeMethodState = htFeeStore.removeHtFeeMethodState
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 设置合同附加费用主表状态并同步版本
|
||||
* @returns {*}
|
||||
*/
|
||||
const setHtFeeMainState = <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 key = toKey(keyRaw)
|
||||
if (!key) return null
|
||||
@ -787,43 +441,15 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
const mainState = getHtFeeMainState(htMainMeta.mainStorageKey)
|
||||
if (mainState != null) return cloneAny(mainState as T)
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(keyedStates.value, key)) return null
|
||||
return cloneAny(keyedStates.value[key] as T)
|
||||
return keysStore.getKeyState<T>(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 按通用键从缓存或持久化存储加载状态
|
||||
* @returns {*}
|
||||
*/
|
||||
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)
|
||||
}
|
||||
// 注意:当内存中没有该key时,不应仅凭keyedLoaded短路返回null。
|
||||
// 该key可能被其他逻辑直接写入了IndexedDB(例如默认明细生成)。
|
||||
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)
|
||||
}
|
||||
const raw = await keysStore.loadKeyState<T>(key, force)
|
||||
|
||||
const serviceMeta = parseServiceMethodStorageKey(key)
|
||||
if (serviceMeta) {
|
||||
setServicePricingMethodState(
|
||||
@ -834,6 +460,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
{ force: true, syncKeyState: false }
|
||||
)
|
||||
}
|
||||
|
||||
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
||||
if (htMethodMeta) {
|
||||
setHtFeeMethodState(
|
||||
@ -844,28 +471,15 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
{ 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)
|
||||
try {
|
||||
return await task
|
||||
} finally {
|
||||
keyLoadTasks.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 按通用键设置状态并同步版本
|
||||
* @returns {*}
|
||||
*/
|
||||
const setKeyState = <T = unknown>(
|
||||
keyRaw: string | number,
|
||||
value: T,
|
||||
@ -875,11 +489,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return false
|
||||
const force = options?.force === true
|
||||
const nextSnapshot = toKeySnapshot(value)
|
||||
const prevSnapshot = keySnapshots.value[key]
|
||||
keyedLoaded.value[key] = true
|
||||
if (!force && prevSnapshot === nextSnapshot) return false
|
||||
|
||||
const serviceMeta = parseServiceMethodStorageKey(key)
|
||||
if (serviceMeta) {
|
||||
setServicePricingMethodState(
|
||||
@ -901,30 +511,24 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
{ force: true, syncKeyState: false }
|
||||
)
|
||||
}
|
||||
|
||||
const htMainMeta = parseHtFeeMainStorageKey(key)
|
||||
if (htMainMeta) {
|
||||
setHtFeeMainState(htMainMeta.mainStorageKey, value as Partial<HtFeeMainState>, { force: true, syncKeyState: false })
|
||||
}
|
||||
keyedStates.value[key] = cloneAny(value)
|
||||
keySnapshots.value[key] = nextSnapshot
|
||||
touchKeyVersion(key)
|
||||
return true
|
||||
|
||||
return keysStore.setKeyState(key, value, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 按通用键删除状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const removeKeyState = (keyRaw: string | number) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return false
|
||||
|
||||
const serviceMeta = parseServiceMethodStorageKey(key)
|
||||
if (serviceMeta) {
|
||||
setServiceMethodStateInMemory(serviceMeta.contractId, serviceMeta.serviceId, serviceMeta.method, null)
|
||||
}
|
||||
|
||||
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
||||
if (htMethodMeta) {
|
||||
setHtFeeMethodState(htMethodMeta.mainStorageKey, htMethodMeta.rowId, htMethodMeta.method, null, {
|
||||
@ -932,38 +536,17 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
syncKeyState: false
|
||||
})
|
||||
}
|
||||
|
||||
const htMainMeta = parseHtFeeMainStorageKey(key)
|
||||
if (htMainMeta) {
|
||||
setHtFeeMainState(htMainMeta.mainStorageKey, null, { force: true, syncKeyState: false })
|
||||
}
|
||||
const hadValue = Object.prototype.hasOwnProperty.call(keyedStates.value, key)
|
||||
delete keyedStates.value[key]
|
||||
keyedLoaded.value[key] = true
|
||||
keySnapshots.value[key] = toKeySnapshot(null)
|
||||
touchKeyVersion(key)
|
||||
return hadValue
|
||||
|
||||
return keysStore.removeKeyState(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取指定键的版本号
|
||||
* @returns {*}
|
||||
*/
|
||||
const getKeyVersion = (keyRaw: string | number) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return 0
|
||||
return keyVersions.value[key] || 0
|
||||
}
|
||||
const getKeyVersion = (keyRaw: string | number) => keysStore.getKeyVersion(keyRaw)
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取合同咨询服务主表状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const getContractState = (contractIdRaw: string | number) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return null
|
||||
@ -971,13 +554,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return data ? cloneState(data) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 从缓存或持久化存储加载合同咨询服务主表状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return null
|
||||
@ -992,10 +568,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
: null
|
||||
const current = contracts.value[contractId]
|
||||
if (raw) {
|
||||
console.log(raw,'init')
|
||||
|
||||
const normalized = normalizeState(raw)
|
||||
console.log(normalized)
|
||||
if (!current || !isSameState(current, normalized)) {
|
||||
contracts.value[contractId] = normalized
|
||||
touchVersion(contractId)
|
||||
@ -1016,13 +589,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 设置合同咨询服务主表状态
|
||||
* @returns {*}
|
||||
*/
|
||||
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return false
|
||||
@ -1035,24 +601,15 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 更新合同咨询服务行的计价汇总字段
|
||||
* @returns {*}
|
||||
*/
|
||||
const updatePricingField = async (params: {
|
||||
contractId: string
|
||||
serviceId: string | number
|
||||
field: ZxFwPricingField
|
||||
value: number | null | undefined
|
||||
}) => {
|
||||
|
||||
const contractId = toKey(params.contractId)
|
||||
if (!contractId) return false
|
||||
|
||||
|
||||
const current = contracts.value[contractId]
|
||||
if (!current?.detailRows?.length) return false
|
||||
|
||||
@ -1085,13 +642,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 获取合同咨询服务基础小计
|
||||
* @returns {*}
|
||||
*/
|
||||
const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return null
|
||||
@ -1111,13 +661,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return hasValid ? round3(sum) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @Author: wintsa
|
||||
* @Date: 2026-03-13
|
||||
* @LastEditors: wintsa
|
||||
* @Description: 删除合同关联的咨询服务、附加费用和键状态数据
|
||||
* @returns {*}
|
||||
*/
|
||||
const removeContractData = (contractIdRaw: string | number) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return false
|
||||
@ -1141,49 +684,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
}
|
||||
loadTasks.delete(contractId)
|
||||
|
||||
changed = htFeeStore.removeContractHtFeeData(contractId) || changed
|
||||
|
||||
const htMainPrefix = `htExtraFee-${contractId}-`
|
||||
for (const key of Object.keys(htFeeMainStates.value)) {
|
||||
if (!key.startsWith(htMainPrefix)) continue
|
||||
delete htFeeMainStates.value[key]
|
||||
changed = true
|
||||
}
|
||||
for (const key of Object.keys(htFeeMethodStates.value)) {
|
||||
if (!key.startsWith(htMainPrefix)) continue
|
||||
delete htFeeMethodStates.value[key]
|
||||
changed = true
|
||||
changed = keysStore.removeKeysByPrefix(htMainPrefix) || changed
|
||||
|
||||
for (const prefix of Object.values(METHOD_STORAGE_PREFIX_MAP)) {
|
||||
changed = keysStore.removeKeysByPrefix(`${prefix}-${contractId}-`) || changed
|
||||
}
|
||||
|
||||
const methodPrefixes = Object.values(METHOD_STORAGE_PREFIX_MAP)
|
||||
const isContractRelatedKey = (key: string) => {
|
||||
if (key === dbKeyOf(contractId)) return true
|
||||
if (key.startsWith(htMainPrefix)) return true
|
||||
if (methodPrefixes.some(prefix => key.startsWith(`${prefix}-${contractId}-`))) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const keySet = new Set<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)
|
||||
}
|
||||
changed = keysStore.removeKeyState(dbKeyOf(contractId)) || 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