This commit is contained in:
wintsa 2026-03-19 10:23:24 +08:00
parent d52765604e
commit 303d6d6185
7 changed files with 274 additions and 470 deletions

View File

@ -205,19 +205,23 @@ const projectTotalBudget = computed(() => {
const budgetRefreshSignature = computed(() => {
const ids = contracts.value.map(item => String(item.id || '').trim()).filter(Boolean)
if (ids.length === 0) return ''
const keyVersionEntries = Object.entries(zxFwPricingStore.keyVersions)
return ids
.map(id => {
const contractVersion = zxFwPricingStore.contractVersions[id] || 0
const additionalMainKey = `htExtraFee-${id}-additional-work`
const reserveMainKey = `htExtraFee-${id}-reserve`
const mainKeySig = `${zxFwPricingStore.getKeyVersion(additionalMainKey)}:${zxFwPricingStore.getKeyVersion(reserveMainKey)}`
const methodKeySig = keyVersionEntries
.filter(([key]) => key.startsWith(`${additionalMainKey}-`) || key.startsWith(`${reserveMainKey}-`))
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([key, version]) => `${key}:${version}`)
.join(',')
return `${id}:${contractVersion}:${mainKeySig}:${methodKeySig}`
const contractState = zxFwPricingStore.contracts[id] || null
const addMain = zxFwPricingStore.htFeeMainStates[additionalMainKey] || null
const reserveMain = zxFwPricingStore.htFeeMainStates[reserveMainKey] || null
const addMethods = zxFwPricingStore.htFeeMethodStates[additionalMainKey] || null
const reserveMethods = zxFwPricingStore.htFeeMethodStates[reserveMainKey] || null
return JSON.stringify({
id,
contractState,
addMain,
reserveMain,
addMethods,
reserveMethods
})
})
.join('|')
})
@ -1881,3 +1885,4 @@ watch(budgetRefreshSignature, (next, prev) => {
}
}
</style>

View File

@ -36,17 +36,17 @@ const isReserveFee = computed(() => {
if (mainKey) return mainKey.endsWith('-reserve')
return String(props.storageKey || '').includes('-reserve')
})
const contractVersion = computed(() => {
const contractStateSignature = computed(() => {
const contractId = contractIdText.value
if (!contractId) return 0
return zxFwPricingStore.contractVersions[contractId] || 0
if (!contractId) return ''
return JSON.stringify(zxFwPricingStore.contracts[contractId] || null)
})
const additionalWorkKeyVersion = computed(() => {
if (!isReserveFee.value) return 0
const additionalWorkStateSignature = computed(() => {
if (!isReserveFee.value) return ''
const contractId = contractIdText.value
if (!contractId) return 0
if (!contractId) return ''
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
return zxFwPricingStore.getKeyVersion(additionalStorageKey)
return JSON.stringify(zxFwPricingStore.htFeeMainStates[additionalStorageKey] || null)
})
const baseLabel = computed(() =>
isReserveFee.value ? '基数(咨询服务总计 + 附加工作费总计)' : '基数(所有服务费预算合计)'
@ -180,8 +180,8 @@ watch(
}
)
watch([contractVersion, additionalWorkKeyVersion], ([nextContract, nextAdditional], [prevContract, prevAdditional]) => {
if (nextContract === prevContract && nextAdditional === prevAdditional) return
watch([contractStateSignature, additionalWorkStateSignature], ([nextContractSig, nextAdditionalSig], [prevContractSig, prevAdditionalSig]) => {
if (nextContractSig === prevContractSig && nextAdditionalSig === prevAdditionalSig) return
void ensureContractLoaded()
})
@ -230,3 +230,4 @@ onBeforeUnmount(() => {
</div>
</div>
</template>

View File

@ -159,16 +159,15 @@ const refreshContractBudget = async () => {
}
const budgetRefreshSignature = computed(() => {
const contractVersion = zxFwPricingStore.contractVersions[props.contractId] || 0
const additionalMainKey = `htExtraFee-${props.contractId}-additional-work`
const reserveMainKey = `htExtraFee-${props.contractId}-reserve`
const keyVersionEntries = Object.entries(zxFwPricingStore.keyVersions)
const methodKeySig = keyVersionEntries
.filter(([key]) => key.startsWith(`${additionalMainKey}-`) || key.startsWith(`${reserveMainKey}-`))
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([key, version]) => `${key}:${version}`)
.join(',')
return `${contractVersion}:${zxFwPricingStore.getKeyVersion(additionalMainKey)}:${zxFwPricingStore.getKeyVersion(reserveMainKey)}:${methodKeySig}`
return JSON.stringify({
contractState: zxFwPricingStore.contracts[props.contractId] || null,
addMain: zxFwPricingStore.htFeeMainStates[additionalMainKey] || null,
reserveMain: zxFwPricingStore.htFeeMainStates[reserveMainKey] || null,
addMethods: zxFwPricingStore.htFeeMethodStates[additionalMainKey] || null,
reserveMethods: zxFwPricingStore.htFeeMethodStates[reserveMainKey] || null
})
})
const scheduleRefreshContractBudget = () => {
@ -349,3 +348,4 @@ onBeforeUnmount(() => {
if (budgetRefreshTimer) clearTimeout(budgetRefreshTimer)
})
</script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import type { ComponentPublicInstance, PropType } from 'vue'
import { computed, defineComponent, h, nextTick, onActivated, onMounted, ref, shallowRef, watch } from 'vue'
import type { PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions'
@ -24,7 +24,6 @@ import {
AlertDialogPortal,
AlertDialogRoot,
AlertDialogTitle,
} from 'reka-ui'
import { Button } from '@/components/ui/button'
import { TooltipProvider } from '@/components/ui/tooltip'
@ -101,9 +100,11 @@ type ServiceListItem = {
workDay?: boolean | null
}
/** 仅保留明确的 boolean其他值统一转 null。 */
const toNullableBoolean = (value: unknown): boolean | null =>
typeof value === 'boolean' ? value : null
/** 解析方法开关,未配置时返回默认值。 */
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
typeof value === 'boolean' ? value : fallback
@ -141,44 +142,44 @@ const serviceById = computed(() => new Map(serviceDict.value.map(item => [item.i
const serviceIdByCode = computed(() => new Map(serviceDict.value.map(item => [item.code, item.id])))
const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id).join('|'))
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: '', name: '小计' }
/** 判断是否固定汇总行(小计行)。 */
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
/**
* 统一把 store/raw row 转成页面使用的 DetailRow 结构
* detailRows 计算属性和 getCurrentContractState 复用避免重复映射逻辑
*/
const normalizeDetailRow = (row: Partial<DetailRow>): DetailRow => ({
id: String(row.id || ''),
code: row.code || '',
name: row.name || '',
process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
investScale: typeof row.investScale === 'number' ? row.investScale : null,
landScale: typeof row.landScale === 'number' ? row.landScale : null,
workload: typeof row.workload === 'number' ? row.workload : null,
hourly: typeof row.hourly === 'number' ? row.hourly : null,
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
finalFee: typeof row.finalFee === 'number' ? row.finalFee : null,
actions: row.actions
})
const selectedIds = computed<string[]>(() => zxFwPricingStore.contracts[props.contractId]?.selectedIds || [])
const detailRows = computed<DetailRow[]>(() =>
(zxFwPricingStore.contracts[props.contractId]?.detailRows || []).map(row => ({
id: String(row.id || ''),
code: row.code || '',
name: row.name || '',
process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
investScale: typeof row.investScale === 'number' ? row.investScale : null,
landScale: typeof row.landScale === 'number' ? row.landScale : null,
workload: typeof row.workload === 'number' ? row.workload : null,
hourly: typeof row.hourly === 'number' ? row.hourly : null,
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
finalFee: typeof (row as any).finalFee === 'number' ? (row as any).finalFee : null,
actions: row.actions
}))
(zxFwPricingStore.contracts[props.contractId]?.detailRows || []).map(row => normalizeDetailRow(row as Partial<DetailRow>))
)
/**
* 获取当前合同状态的可变副本
* 所有写回前都先读这里避免直接修改 store 引用对象
*/
const getCurrentContractState = (): ZxFwViewState => {
const current = zxFwPricingStore.getContractState(props.contractId)
if (current) {
return {
selectedIds: Array.isArray(current.selectedIds) ? [...current.selectedIds] : [],
selectedCodes: Array.isArray(current.selectedCodes) ? [...current.selectedCodes] : [],
detailRows: (current.detailRows || []).map(row => ({
id: String(row.id || ''),
code: row.code || '',
name: row.name || '',
process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
investScale: typeof row.investScale === 'number' ? row.investScale : null,
landScale: typeof row.landScale === 'number' ? row.landScale : null,
workload: typeof row.workload === 'number' ? row.workload : null,
hourly: typeof row.hourly === 'number' ? row.hourly : null,
subtotal: typeof row.subtotal === 'number' ? row.subtotal : null,
finalFee: typeof (row as any).finalFee === 'number' ? (row as any).finalFee : null,
actions: row.actions
}))
detailRows: (current.detailRows || []).map(row => normalizeDetailRow(row as Partial<DetailRow>))
}
}
return {
@ -187,94 +188,21 @@ const getCurrentContractState = (): ZxFwViewState => {
}
}
/** 统一写回当前合同状态到 store。 */
const setCurrentContractState = async (nextState: ZxFwViewState) => {
await zxFwPricingStore.setContractState(props.contractId, nextState)
}
const gridApi = shallowRef<GridApi<DetailRow> | null>(null)
/** 记录 grid api供编辑后局部刷新固定行使用。 */
const onGridReady = (event: GridReadyEvent<DetailRow>) => {
gridApi.value = event.api
}
const pickerOpen = ref(false)
const pickerTempIds = ref<string[]>([])
const pickerSearch = ref('')
const pickerListScrollRef = ref<HTMLElement | null>(null)
const clearConfirmOpen = ref(false)
const pendingClearServiceId = ref<string | null>(null)
const deleteConfirmOpen = ref(false)
const pendingDeleteServiceId = ref<string | null>(null)
const dragSelecting = ref(false)
const dragMoved = ref(false)
let dragSelectChecked = false
const dragBaseIds = ref<string[]>([])
const dragStartPoint = ref({ x: 0, y: 0 })
const dragCurrentPoint = ref({ x: 0, y: 0 })
const pickerItemElMap = new Map<string, HTMLElement>()
let dragTouchedIds = new Set<string>()
let dragAutoScrollRafId: number | null = null
const DRAG_AUTO_SCROLL_EDGE = 36
const DRAG_AUTO_SCROLL_MAX_STEP = 16
const stopDragAutoScroll = () => {
if (dragAutoScrollRafId !== null) {
cancelAnimationFrame(dragAutoScrollRafId)
dragAutoScrollRafId = null
}
}
const resolveDragAutoScrollStep = () => {
const scroller = pickerListScrollRef.value
if (!scroller) return 0
const rect = scroller.getBoundingClientRect()
const pointerY = dragCurrentPoint.value.y
let step = 0
if (pointerY > rect.bottom - DRAG_AUTO_SCROLL_EDGE) {
const ratio = Math.min(2, (pointerY - (rect.bottom - DRAG_AUTO_SCROLL_EDGE)) / DRAG_AUTO_SCROLL_EDGE)
step = Math.max(2, Math.round(ratio * DRAG_AUTO_SCROLL_MAX_STEP))
} else if (pointerY < rect.top + DRAG_AUTO_SCROLL_EDGE) {
const ratio = Math.min(2, ((rect.top + DRAG_AUTO_SCROLL_EDGE) - pointerY) / DRAG_AUTO_SCROLL_EDGE)
step = -Math.max(2, Math.round(ratio * DRAG_AUTO_SCROLL_MAX_STEP))
}
return step
}
const runDragAutoScroll = () => {
if (!dragSelecting.value) {
stopDragAutoScroll()
return
}
const scroller = pickerListScrollRef.value
if (scroller) {
const step = resolveDragAutoScrollStep()
if (step !== 0) {
const prev = scroller.scrollTop
scroller.scrollTop += step
if (scroller.scrollTop !== prev) {
applyDragSelectionByRect()
}
}
}
dragAutoScrollRafId = requestAnimationFrame(runDragAutoScroll)
}
const startDragAutoScroll = () => {
if (dragAutoScrollRafId !== null) return
dragAutoScrollRafId = requestAnimationFrame(runDragAutoScroll)
}
const selectedServiceText = computed(() => {
if (selectedIds.value.length === 0) return ''
const names = selectedIds.value
.map(id => serviceById.value.get(id)?.name || '')
.filter(Boolean)
if (names.length <= 2) return names.join('、')
return `${names.slice(0, 2).join('、')}${names.length}`
})
const pendingClearServiceName = computed(() => {
if (!pendingClearServiceId.value) return ''
@ -294,20 +222,24 @@ const pendingDeleteServiceName = computed(() => {
return pendingDeleteServiceId.value
})
/** 清空确认框开关回调。 */
const handleClearConfirmOpenChange = (open: boolean) => {
clearConfirmOpen.value = open
}
/** 删除确认框开关回调。 */
const handleDeleteConfirmOpenChange = (open: boolean) => {
deleteConfirmOpen.value = open
}
/** 触发“恢复默认”二次确认。 */
const requestClearRow = (row: DetailRow) => {
if (isFixedRow(row)) return
pendingClearServiceId.value = row.id
clearConfirmOpen.value = true
}
/** 执行“恢复默认”并关闭确认框。 */
const confirmClearRow = async () => {
const id = pendingClearServiceId.value
if (!id) return
@ -322,12 +254,14 @@ const confirmClearRow = async () => {
pendingClearServiceId.value = null
}
/** 触发“删除服务”二次确认。 */
const requestDeleteRow = (row: DetailRow) => {
if (isFixedRow(row)) return
pendingDeleteServiceId.value = row.id
deleteConfirmOpen.value = true
}
/** 执行删除并关闭确认框。 */
const confirmDeleteRow = async () => {
const id = pendingDeleteServiceId.value
if (!id) return
@ -342,39 +276,33 @@ const confirmDeleteRow = async () => {
pendingDeleteServiceId.value = null
}
const filteredServiceDict = computed(() => {
const keyword = pickerSearch.value.trim()
if (!keyword) return serviceDict.value
return serviceDict.value.filter(item => item.code.includes(keyword) || item.name.includes(keyword))
})
const dragRectStyle = computed(() => {
if (!dragSelecting.value) return {}
const left = Math.min(dragStartPoint.value.x, dragCurrentPoint.value.x)
const top = Math.min(dragStartPoint.value.y, dragCurrentPoint.value.y)
const width = Math.abs(dragCurrentPoint.value.x - dragStartPoint.value.x)
const height = Math.abs(dragCurrentPoint.value.y - dragStartPoint.value.y)
return {
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`
}
})
/** 表格数字输入解析器(保留 3 位小数)。 */
const numericParser = (newValue: any): number | null => {
return parseNumberOrNull(newValue, { precision: 3 })
}
/** 类型守卫:有限数字。 */
const isFiniteNumberValue = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value)
/** 可空数字求和:全为空返回 null。 */
const sumNullableNumbers = (values: Array<number | null | undefined>): number | null => {
const validValues = values.filter(isFiniteNumberValue)
if (validValues.length === 0) return null
return addNumbers(...validValues)
}
const isSameNullableNumber = (a: number | null | undefined, b: number | null | undefined, precision = 3) => {
if (a == null && b == null) return true
if (a == null || b == null) return false
return roundTo(a, precision) === roundTo(b, precision)
}
/**
* 读取服务词典中的计价方法开关规模/工作量/工时并应用默认值
* 该方法被 sanitizePricingTotalsByService 复用
*/
const getServiceMethodTypeById = (serviceId: string) => {
const type = serviceById.value.get(serviceId)?.type
const scale = resolveMethodEnabled(type?.scale, defaultServiceMethodType.scale)
@ -384,6 +312,10 @@ const getServiceMethodTypeById = (serviceId: string) => {
return { scale, onlyCostScale, amount, workDay }
}
/**
* 按服务方法开关过滤计价合计
* clearRowValues/fillPricingTotalsForServiceIds 等入口复用
*/
const sanitizePricingTotalsByService = (serviceId: string, totals: PricingMethodTotals): PricingMethodTotals => {
const methodType = getServiceMethodTypeById(serviceId)
const isScaleEnabled = methodType.scale
@ -396,6 +328,10 @@ const sanitizePricingTotalsByService = (serviceId: string, totals: PricingMethod
}
}
/**
* 4 个计价字段做开关过滤例如 onlyCostScale 会禁用用地规模法
* 主要在选择服务加载历史值时复用
*/
const sanitizePricingFieldsByService = (
serviceId: string,
values: Pick<DetailRow, 'investScale' | 'landScale' | 'workload' | 'hourly'>
@ -409,29 +345,58 @@ const sanitizePricingFieldsByService = (
}
}
type PricingMethodField = 'investScale' | 'landScale' | 'workload' | 'hourly'
/**
* 计算某列在非固定行上的合计
* getMethodTotal 和固定行汇总逻辑复用
*/
const getMethodTotalFromRows = (
rows: DetailRow[],
field: 'investScale' | 'landScale' | 'workload' | 'hourly'
field: PricingMethodField
) => sumNullableNumbers(
rows
.filter(row => !isFixedRow(row))
.map(row => row[field])
)
const getMethodTotal = (field: 'investScale' | 'landScale' | 'workload' | 'hourly') =>
/** 当前页面行数据中某计价列总计(仅非固定行)。 */
const getMethodTotal = (field: PricingMethodField) =>
getMethodTotalFromRows(detailRows.value, field)
const getFixedRowSubtotal = () =>
sumNullableNumbers([
getMethodTotal('investScale'),
getMethodTotal('landScale'),
getMethodTotal('workload'),
getMethodTotal('hourly')
])
/**
* 生成 4 个计价法列的公共配置减少重复定义
* 值来源统一固定行取列合计普通行取自身字段
*/
const createMethodColumn = (
headerName: string,
field: PricingMethodField,
minWidth: number
): ColDef<DetailRow> => ({
headerName,
field,
headerClass: 'ag-right-aligned-header',
minWidth,
flex: 1.5,
cellClass: 'ag-right-aligned-cell',
editable: false,
valueGetter: params => {
if (!params.data) return null
if (isFixedRow(params.data)) return getMethodTotal(field)
return params.data[field]
},
valueParser: params => numericParser(params.newValue),
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
})
/** 获取某服务对应四个计价页的存储键。 */
const getPricingPaneStorageKeys = (serviceId: string) =>
zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId)
/**
* 清空某服务在 4 个计价页的缓存与持久化数据
* 会写入短期 skip/force 标记避免刚删完又被旧数据回填
*/
const clearPricingPaneValues = async (serviceId: string) => {
const keys = getPricingPaneStorageKeys(serviceId)
const clearIssuedAt = Date.now()
@ -446,6 +411,10 @@ const clearPricingPaneValues = async (serviceId: string) => {
await Promise.all(keys.map(key => kvStore.removeItem(key)))
}
/**
* 恢复单行到默认计价结果恢复默认按钮
* 执行顺序关闭编辑页 -> 清缓存/持久层 -> 重新生成默认明细 -> 回填合计
*/
const clearRowValues = async (row: DetailRow) => {
if (isFixedRow(row)) return
@ -484,6 +453,7 @@ const clearRowValues = async (row: DetailRow) => {
})
}
/** 打开服务编辑子页。 */
const openEditTab = (row: DetailRow) => {
const serviceType = serviceById.value.get(row.id)?.type
tabStore.openTab({
@ -501,6 +471,7 @@ const openEditTab = (row: DetailRow) => {
})
}
/** 删除单行服务(本质是更新 selectedIds。 */
const removeRow = async (row: DetailRow) => {
if (isFixedRow(row)) return
const nextIds = selectedIds.value.filter(id => id !== row.id)
@ -604,8 +575,7 @@ const columnDefs: ColDef<DetailRow>[] = [
flex: 3,
wrapText: true,
autoHeight: true,
cellStyle:{ 'line-height': 1.6
},
cellStyle: { 'line-height': 1.6 },
valueGetter: params => {
if (!params.data) return ''
if (isFixedRow(params.data)) return ''
@ -634,76 +604,10 @@ const columnDefs: ColDef<DetailRow>[] = [
},
cellRenderer: ProcessCellRenderer
},
{
headerName: '投资规模法',
field: 'investScale',
headerClass: 'ag-right-aligned-header',
minWidth: 100,
flex: 1.5,
cellClass: 'ag-right-aligned-cell',
editable: false,
valueGetter: params => {
if (!params.data) return null
if (isFixedRow(params.data)) return getMethodTotal('investScale')
return params.data.investScale
},
valueParser: params => numericParser(params.newValue),
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
},
{
headerName: '用地规模法',
field: 'landScale',
headerClass: 'ag-right-aligned-header',
minWidth: 100,
flex: 1.5,
cellClass: 'ag-right-aligned-cell',
editable: false,
valueGetter: params => {
if (!params.data) return null
if (isFixedRow(params.data)) return getMethodTotal('landScale')
return params.data.landScale
},
valueParser: params => numericParser(params.newValue),
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
},
{
headerName: '工作量法',
field: 'workload',
headerClass: 'ag-right-aligned-header',
minWidth: 90,
flex: 1.5,
cellClass: 'ag-right-aligned-cell',
editable: false,
valueGetter: params => {
if (!params.data) return null
if (isFixedRow(params.data)) return getMethodTotal('workload')
return params.data.workload
},
// editable: params => !params.node?.rowPinned && !isFixedRow(params.data),
valueParser: params => numericParser(params.newValue),
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
},
{
headerName: '工时法',
field: 'hourly',
headerClass: 'ag-right-aligned-header',
minWidth: 90,
flex: 1.5,
cellClass: 'ag-right-aligned-cell',
editable: false,
valueGetter: params => {
if (!params.data) return null
if (isFixedRow(params.data)) return getMethodTotal('hourly')
return params.data.hourly
},
// editable: params => !params.node?.rowPinned && !isFixedRow(params.data),
valueParser: params => numericParser(params.newValue),
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
},
createMethodColumn('投资规模法', 'investScale', 100),
createMethodColumn('用地规模法', 'landScale', 100),
createMethodColumn('工作量法', 'workload', 90),
createMethodColumn('工时法', 'hourly', 90),
{
headerName: '小计',
field: 'subtotal',
@ -714,7 +618,7 @@ const columnDefs: ColDef<DetailRow>[] = [
editable: false,
valueGetter: params => {
if (!params.data) return null
return params.data.subtotal
return params.data.subtotal
},
valueFormatter: params => (params.value == null ? '' : formatThousands(params.value, 2))
},
@ -802,21 +706,17 @@ const detailGridOptions: GridOptions<DetailRow> = {
}
}
const applyFixedRowTotals = (rows: DetailRow[]) => {
/**
* 只重算固定行小计行汇总不覆盖普通行 finalFee
* 主要用于用户手动编辑 finalFee 后的同步场景
*/
const applyFixedRowSummary = (rows: DetailRow[]) => {
const nextInvestScale = getMethodTotalFromRows(rows, 'investScale')
const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
const nextWorkload = getMethodTotalFromRows(rows, 'workload')
const nextHourly = getMethodTotalFromRows(rows, 'hourly')
// finalFee null
const updatedRows = rows.map(row => {
if (isFixedRow(row)) return row
const rowSubtotal = sumNullableNumbers([row.investScale, row.landScale, row.workload, row.hourly])
const nextFinalFee = row.finalFee != null ? row.finalFee : (rowSubtotal != null ? roundTo(rowSubtotal, 2) : null)
return { ...row, finalFee: nextFinalFee }
})
//
const totalFinalFee = sumNullableNumbers(updatedRows.filter(r => !isFixedRow(r)).map(r => r.finalFee))
return updatedRows.map(row =>
const totalFinalFee = sumNullableNumbers(rows.filter(r => !isFixedRow(r)).map(r => r.finalFee))
return rows.map(row =>
isFixedRow(row)
? {
...row,
@ -831,11 +731,33 @@ const applyFixedRowTotals = (rows: DetailRow[]) => {
)
}
/**
* 计价法金额发生变化时调用
* 1) 普通行 finalFee 强制同步 subtotal
* 2) 固定行汇总四列+确认金额统一重算
*/
const applyFixedRowTotals = (rows: DetailRow[]) => {
const syncedRows = rows.map(row => {
if (isFixedRow(row)) return row
const rowSubtotal = sumNullableNumbers([row.investScale, row.landScale, row.workload, row.hourly])
return {
...row,
finalFee: rowSubtotal != null ? roundTo(rowSubtotal, 2) : null
}
})
return applyFixedRowSummary(syncedRows)
}
/** 获取当前已选服务 id排除固定小计行。 */
const getSelectedServiceIdsWithoutFixed = () =>
detailRows.value
.filter(row => !isFixedRow(row))
.map(row => String(row.id))
/**
* 为当前选中服务确保计价明细行存在必要时创建默认明细
* 初始化勾选变化后都会调用
*/
const ensurePricingDetailRowsForCurrentSelection = async () => {
const serviceIds = getSelectedServiceIdsWithoutFixed()
if (serviceIds.length === 0) return
@ -846,7 +768,12 @@ const ensurePricingDetailRowsForCurrentSelection = async () => {
})
}
/**
* 刷新指定服务的 4 个计价法合计并回写到 zxFw 明细
* 计价法变更场景统一走这里最终会触发 applyFixedRowTotals
*/
const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
const currentState = getCurrentContractState()
const targetIds = Array.from(
new Set(
@ -859,7 +786,7 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
if (targetIds.length === 0) {
await setCurrentContractState({
...currentState,
detailRows: applyFixedRowTotals(currentState.detailRows)
detailRows: applyFixedRowSummary(currentState.detailRows)
})
return
}
@ -883,26 +810,34 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
if (!totals) return row
const newSubtotal = sumNullableNumbers([totals.investScale, totals.landScale, totals.workload, totals.hourly])
// finalFee
const oldSubtotal = sumNullableNumbers([row.investScale, row.landScale, row.workload, row.hourly])
const userEdited = row.finalFee != null && oldSubtotal != null
&& roundTo(row.finalFee, 2) !== roundTo(oldSubtotal, 2)
const methodChanged = !(
isSameNullableNumber(row.investScale, totals.investScale)
&& isSameNullableNumber(row.landScale, totals.landScale)
&& isSameNullableNumber(row.workload, totals.workload)
&& isSameNullableNumber(row.hourly, totals.hourly)
)
return {
...row,
investScale: totals.investScale,
landScale: totals.landScale,
workload: totals.workload,
hourly: totals.hourly,
finalFee: userEdited ? row.finalFee : (newSubtotal != null ? roundTo(newSubtotal, 2) : null)
finalFee: methodChanged
? (newSubtotal != null ? roundTo(newSubtotal, 2) : null)
: row.finalFee
}
})
await setCurrentContractState({
...currentState,
detailRows: applyFixedRowTotals(nextRows)
detailRows: applyFixedRowSummary(nextRows)
})
}
/**
* 应用服务勾选结果新增/删除服务
* 会保留已有行可复用信息并确保固定行始终存在
*/
const applySelection = async (codes: string[]) => {
const currentState = getCurrentContractState()
const prevSelectedSet = new Set(currentState.selectedIds || [])
@ -917,6 +852,7 @@ const applySelection = async (codes: string[]) => {
if (!dictItem) return null
const old = existingMap.get(id)
const nextValues = sanitizePricingFieldsByService(id, {
investScale: old?.investScale ?? null,
landScale: old?.landScale ?? null,
@ -932,6 +868,8 @@ const applySelection = async (codes: string[]) => {
landScale: nextValues.landScale,
workload: nextValues.workload,
hourly: nextValues.hourly,
subtotal: typeof old?.subtotal === 'number' ? old.subtotal : null,
finalFee: typeof old?.finalFee === 'number' ? old.finalFee : null
}
})
@ -950,8 +888,8 @@ const applySelection = async (codes: string[]) => {
landScale: typeof fixedOld?.landScale === 'number' ? fixedOld.landScale : null,
workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null,
hourly: typeof fixedOld?.hourly === 'number' ? fixedOld.hourly : null,
subtotal: null,
finalFee: null,
subtotal: typeof fixedOld?.subtotal === 'number' ? fixedOld.subtotal : null,
finalFee: typeof fixedOld?.finalFee === 'number' ? fixedOld.finalFee : null,
actions: null
}
@ -963,10 +901,13 @@ const applySelection = async (codes: string[]) => {
await setCurrentContractState({
...currentState,
selectedIds: uniqueIds,
detailRows: applyFixedRowTotals([...baseRows, fixedRow])
detailRows: applyFixedRowSummary([...baseRows, fixedRow])
})
}
/**
* 服务勾选变化入口先更新行再刷新新增服务的计价汇总
*/
const handleServiceSelectionChange = async (ids: string[]) => {
const prevIds = [...selectedIds.value]
await applySelection(ids)
@ -977,133 +918,9 @@ const handleServiceSelectionChange = async (ids: string[]) => {
await ensurePricingDetailRowsForCurrentSelection()
}
const preparePickerOpen = () => {
pickerTempIds.value = [...selectedIds.value]
pickerSearch.value = ''
}
const closePicker = () => {
stopDragSelect()
pickerOpen.value = false
}
const handlePickerOpenChange = (open: boolean) => {
if (open) {
preparePickerOpen()
} else {
stopDragSelect()
}
pickerOpen.value = open
}
const confirmPicker = async () => {
await applySelection(pickerTempIds.value)
}
const clearPickerSelection = () => {
pickerTempIds.value = []
}
const applyTempChecked = (code: string, checked: boolean) => {
const exists = pickerTempIds.value.includes(code)
if (checked && !exists) {
pickerTempIds.value = [...pickerTempIds.value, code]
return
}
if (!checked && exists) {
pickerTempIds.value = pickerTempIds.value.filter(item => item !== code)
}
}
const setPickerItemRef = (
code: string,
el: Element | ComponentPublicInstance | null
) => {
if (el instanceof HTMLElement) {
pickerItemElMap.set(code, el)
return
}
pickerItemElMap.delete(code)
}
const isRectIntersect = (
a: { left: number; right: number; top: number; bottom: number },
b: { left: number; right: number; top: number; bottom: number }
) => !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom)
const applyDragSelectionByRect = () => {
const rect = {
left: Math.min(dragStartPoint.value.x, dragCurrentPoint.value.x),
right: Math.max(dragStartPoint.value.x, dragCurrentPoint.value.x),
top: Math.min(dragStartPoint.value.y, dragCurrentPoint.value.y),
bottom: Math.max(dragStartPoint.value.y, dragCurrentPoint.value.y)
}
const nextSelectedSet = new Set(dragBaseIds.value)
for (const [code, el] of pickerItemElMap.entries()) {
const itemRect = el.getBoundingClientRect()
const hit = isRectIntersect(rect, itemRect)
if (hit) {
dragTouchedIds.add(code)
}
}
for (const code of dragTouchedIds) {
if (dragSelectChecked) {
nextSelectedSet.add(code)
} else {
nextSelectedSet.delete(code)
}
}
pickerTempIds.value = serviceDict
.value
.map(item => item.id)
.filter(id => nextSelectedSet.has(id))
}
const stopDragSelect = () => {
dragSelecting.value = false
dragMoved.value = false
dragBaseIds.value = []
dragTouchedIds.clear()
stopDragAutoScroll()
window.removeEventListener('mousemove', onDragSelectingMove)
window.removeEventListener('mouseup', stopDragSelect)
}
const onDragSelectingMove = (event: MouseEvent) => {
dragCurrentPoint.value = { x: event.clientX, y: event.clientY }
if (!dragMoved.value) {
const dx = Math.abs(event.clientX - dragStartPoint.value.x)
const dy = Math.abs(event.clientY - dragStartPoint.value.y)
if (dx >= 3 || dy >= 3) {
dragMoved.value = true
}
}
applyDragSelectionByRect()
}
const startDragSelect = (event: MouseEvent, code: string) => {
dragSelecting.value = true
dragMoved.value = false
dragBaseIds.value = [...pickerTempIds.value]
dragTouchedIds = new Set([code])
dragSelectChecked = !pickerTempIds.value.includes(code)
dragStartPoint.value = { x: event.clientX, y: event.clientY }
dragCurrentPoint.value = { x: event.clientX, y: event.clientY }
applyTempChecked(code, dragSelectChecked)
startDragAutoScroll()
window.addEventListener('mousemove', onDragSelectingMove)
window.addEventListener('mouseup', stopDragSelect)
}
const handleDragHover = (_code: string) => {
if (!dragSelecting.value || !dragMoved.value) return
applyDragSelectionByRect()
}
/**
* 页面初始化/激活时入口加载合同套用选择确保明细刷新合计
*/
const initializeContractState = async () => {
try {
await zxFwPricingStore.loadContract(props.contractId)
@ -1138,6 +955,7 @@ const initializeContractState = async () => {
}
}
/** 读取项目行业,用于过滤可选服务词典。 */
const loadProjectIndustry = async () => {
try {
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY.value)
@ -1157,16 +975,21 @@ watch(serviceIdSignature, () => {
}
})
/**
* 处理表格单元格编辑当前只接管 finalFee
* 编辑后仅重算固定行避免覆盖用户刚输入的确认金额
*/
const handleCellValueChanged = async (event: any) => {
if (event.colDef?.field !== 'finalFee') return
const row = event.data as DetailRow | undefined
if (!row || isFixedRow(row)) return
const newValue = event.newValue != null ? roundTo(Number(event.newValue), 2) : null
const currentState = getCurrentContractState()
const nextRows = currentState.detailRows.map(item =>
item.id === row.id ? { ...item, finalFee: newValue } : item
)
const finalRows = applyFixedRowTotals(nextRows)
const finalRows = applyFixedRowSummary(nextRows)
await setCurrentContractState({
...currentState,
detailRows: finalRows
@ -1191,17 +1014,11 @@ onActivated(async () => {
await loadProjectIndustry()
await initializeContractState()
})
onBeforeUnmount(() => {
stopDragSelect()
})
</script>
<template>
<TooltipProvider>
<div class="h-full min-h-0 flex flex-col gap-2">
<!-- 浏览框选择服务实现已抽离并停用改为直接复选框勾选 -->
<!-- <DialogRoot v-model:open="pickerOpen" @update:open="handlePickerOpenChange" /> -->
<ServiceCheckboxSelector :services="serviceDict" :model-value="selectedIds"
@update:model-value="handleServiceSelectionChange" />

View File

@ -624,27 +624,19 @@ onActivated(() => {
})
const storageKeyRef = computed(() => props.storageKey)
const reserveAdditionalWorkKeyVersion = computed(() => {
if (!isReserveStorageKey(props.storageKey)) return 0
const reserveAdditionalWorkStateSignature = computed(() => {
if (!isReserveStorageKey(props.storageKey)) return ''
const contractId = String(props.contractId || '').trim()
if (!contractId) return 0
if (!contractId) return ''
const additionalStorageKey = `htExtraFee-${contractId}-additional-work`
return zxFwPricingStore.getKeyVersion(additionalStorageKey)
return JSON.stringify(zxFwPricingStore.htFeeMainStates[additionalStorageKey] || null)
})
watch(storageKeyRef, () => {
scheduleReloadFromStorage()
})
watch(
() =>
detailRows.value
.map(row => {
const rateKey = zxFwPricingStore.getHtFeeMethodStorageKey(props.storageKey, row.id, 'rate-fee')
const hourlyKey = zxFwPricingStore.getHtFeeMethodStorageKey(props.storageKey, row.id, 'hourly-fee')
const quantityKey = zxFwPricingStore.getHtFeeMethodStorageKey(props.storageKey, row.id, 'quantity-unit-price-fee')
return `${row.id}:${zxFwPricingStore.getKeyVersion(rateKey)}:${zxFwPricingStore.getKeyVersion(hourlyKey)}:${zxFwPricingStore.getKeyVersion(quantityKey)}`
})
.join('|'),
() => JSON.stringify(zxFwPricingStore.htFeeMethodStates[props.storageKey] || null),
(nextSignature, prevSignature) => {
if (!nextSignature && !prevSignature) return
if (nextSignature === prevSignature) return
@ -655,19 +647,19 @@ watch(
watch(
() => {
const contractId = String(props.contractId || '').trim()
if (!contractId) return 0
return zxFwPricingStore.contractVersions[contractId] || 0
if (!contractId) return ''
return JSON.stringify(zxFwPricingStore.contracts[contractId] || null)
},
(nextVersion, prevVersion) => {
if (nextVersion === prevVersion || nextVersion === 0) return
(nextSig, prevSig) => {
if (nextSig === prevSig) return
scheduleReloadFromStorage()
}
)
watch(
reserveAdditionalWorkKeyVersion,
(nextVersion, prevVersion) => {
if (nextVersion === prevVersion) return
reserveAdditionalWorkStateSignature,
(nextSig, prevSig) => {
if (nextSig === prevSig) return
scheduleReloadFromStorage()
}
)

View File

@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
import { addNumbers } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { useKvStore } from '@/pinia/kv'
import { waitForHydration } from '@/pinia/Plugin/indexdb'
import {
parseHtFeeMainStorageKey,
parseHtFeeMethodStorageKey,
@ -66,7 +66,6 @@ const STORAGE_PREFIX_METHOD_MAP = new Map<string, ServicePricingMethod>(
const toKey = (contractId: string | number) => String(contractId || '').trim()
const toServiceKey = (serviceId: string | number) => String(serviceId || '').trim()
const dbKeyOf = (contractId: string) => `zxFW-${contractId}`
const serviceMethodDbKeyOf = (contractId: string, serviceId: string, method: ServicePricingMethod) =>
`${METHOD_STORAGE_PREFIX_MAP[method]}-${contractId}-${serviceId}`
const round3 = (value: number) => Number(value.toFixed(3))
@ -139,7 +138,7 @@ const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
return {
...row,
subtotal: round3Nullable(subtotal),
finalFee: round3Nullable(subtotal)
finalFee: row.finalFee != null ? round3Nullable(row.finalFee) : round3Nullable(subtotal)
}
})
}
@ -238,8 +237,9 @@ const parseServiceMethodStorageKey = (keyRaw: string | number) => {
const loadTasks = new Map<string, Promise<ZxFwState | null>>()
export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
let hydrationReady = false
let hydrationTask: Promise<void> | null = null
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>>>({})
@ -249,18 +249,18 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
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 getKvStoreSafely = () => {
try {
return useKvStore()
} catch {
return null
const ensureHydrated = async () => {
if (hydrationReady) return
if (!hydrationTask) {
hydrationTask = waitForHydration('zxFwPricing')
.catch(() => undefined)
.finally(() => {
hydrationReady = true
hydrationTask = null
})
}
await hydrationTask
}
const ensureServicePricingState = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
@ -552,8 +552,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return keysStore.removeKeyState(key)
}
const getKeyVersion = (keyRaw: string | number) => keysStore.getKeyVersion(keyRaw)
// 对外返回合同咨询服务状态的深拷贝,避免组件直接改写 store 内部引用。
const getContractState = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
@ -567,25 +565,15 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const loadContract = async (contractIdRaw: string | number, force = false) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return null
await ensureHydrated()
if (!force && contractLoaded.value[contractId]) return getContractState(contractId)
if (!force && contracts.value[contractId]) return getContractState(contractId)
if (!force && loadTasks.has(contractId)) return loadTasks.get(contractId) as Promise<ZxFwState | null>
const task = (async () => {
const kvStore = getKvStoreSafely()
const raw = kvStore
? await kvStore.getItem<ZxFwState>(dbKeyOf(contractId))
: null
const current = contracts.value[contractId]
if (raw) {
const normalized = normalizeState(raw)
if (!current || !isSameState(current, normalized)) {
contracts.value[contractId] = normalized
touchVersion(contractId)
}
} else if (!current) {
if (!current) {
contracts.value[contractId] = normalizeState(null)
touchVersion(contractId)
}
contractLoaded.value[contractId] = true
return getContractState(contractId)
@ -609,12 +597,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (current && isSameState(current, normalized)) return false
contracts.value[contractId] = normalized
contractLoaded.value[contractId] = true
touchVersion(contractId)
return true
}
// 只更新某个服务行上的单个汇总字段,适合计费页回写 investScale/landScale/workload/hourly。
// 这里不会额外重算其它派生字段,调用方需要自行决定是否联动更新 subtotal/finalFee。
// 为保证“计价法金额变化 -> 确认金额跟随小计”,这里会同步重算 finalFee
// - 普通行finalFee = 当前四种计价法小计
// - 固定小计行finalFee = 普通行 finalFee 合计
const updatePricingField = async (params: {
contractId: string
serviceId: string | number
@ -632,7 +621,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const nextValue = toFiniteNumberOrNull(params.value)
let changed = false
const nextRows = current.detailRows.map(row => {
const updatedRows = current.detailRows.map(row => {
if (String(row.id || '') !== targetServiceId) return row
const oldValue = toFiniteNumberOrNull(row[params.field])
if (oldValue === nextValue) return row
@ -645,6 +634,37 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (!changed) return false
const rowsWithSyncedFinalFee = updatedRows.map(row => {
if (String(row.id || '') === FIXED_ROW_ID) return row
const rowSubtotal = sumNullableNumbers([
toFiniteNumberOrNull(row.investScale),
toFiniteNumberOrNull(row.landScale),
toFiniteNumberOrNull(row.workload),
toFiniteNumberOrNull(row.hourly)
])
return {
...row,
finalFee: round3Nullable(rowSubtotal)
}
})
const fixedFinalFee = round3Nullable(
sumNullableNumbers(
rowsWithSyncedFinalFee
.filter(row => String(row.id || '') !== FIXED_ROW_ID)
.map(row => toFiniteNumberOrNull(row.finalFee))
)
)
const nextRows = rowsWithSyncedFinalFee.map(row =>
String(row.id || '') === FIXED_ROW_ID
? {
...row,
finalFee: fixedFinalFee
}
: row
)
const nextState = normalizeState({
...current,
detailRows: nextRows
@ -652,7 +672,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (isSameState(current, nextState)) return false
contracts.value[contractId] = nextState
contractLoaded.value[contractId] = true
touchVersion(contractId)
return true
}
@ -693,10 +712,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
delete contractLoaded.value[contractId]
changed = true
}
if (Object.prototype.hasOwnProperty.call(contractVersions.value, contractId)) {
delete contractVersions.value[contractId]
changed = true
}
loadTasks.delete(contractId)
changed = htFeeStore.removeContractHtFeeData(contractId) || changed
@ -708,20 +723,16 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
changed = keysStore.removeKeysByPrefix(`${prefix}-${contractId}-`) || changed
}
changed = keysStore.removeKeyState(dbKeyOf(contractId)) || changed
return changed
}
return {
contracts,
contractVersions,
contractLoaded,
servicePricingStates,
htFeeMainStates,
htFeeMethodStates,
keyedStates,
keyVersions,
getContractState,
loadContract,
setContractState,
@ -732,7 +743,6 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
loadKeyState,
setKeyState,
removeKeyState,
getKeyVersion,
getServicePricingMethodState,
setServicePricingMethodState,
loadServicePricingMethodState,

View File

@ -2,7 +2,7 @@
* Store
*
* zxFwPricing IndexedDB
*
*
*/
import { defineStore } from 'pinia'
@ -31,22 +31,9 @@ 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)
@ -80,7 +67,6 @@ export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
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)
})()
@ -108,7 +94,6 @@ export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
if (!force && prevSnapshot === nextSnapshot) return false
keyedStates.value[key] = cloneAny(value)
keySnapshots.value[key] = nextSnapshot
touchKeyVersion(key)
return true
}
@ -120,7 +105,6 @@ export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
delete keyedStates.value[key]
keyedLoaded.value[key] = true
keySnapshots.value[key] = toKeySnapshot(null)
touchKeyVersion(key)
return hadValue
}
@ -130,7 +114,6 @@ export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
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) {
@ -140,7 +123,6 @@ export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
changed = true
}
delete keyedLoaded.value[key]
delete keyVersions.value[key]
delete keySnapshots.value[key]
keyLoadTasks.delete(key)
}
@ -160,16 +142,13 @@ export const useZxFwPricingKeysStore = defineStore('zxFwPricingKeys', () => {
return {
keyedStates,
keyedLoaded,
keyVersions,
keySnapshots,
getKeyVersion,
getKeyState,
loadKeyState,
setKeyState,
removeKeyState,
removeKeysByPrefix,
setKeyStateSilent,
touchKeyVersion
setKeyStateSilent
}
}, {
persist: true