大改,使用pinia传值,indexdb做持久化
This commit is contained in:
parent
3ad7bae1a9
commit
9a045cfe86
@ -2,15 +2,13 @@
|
||||
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { expertList } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { syncPricingTotalToZxFw, type ZxFwPricingField } from '@/lib/zxFwPricingSync'
|
||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||
import { useZxFwPricingStore, type HtFeeMethodType, type ServicePricingMethod } from '@/pinia/zxFwPricing'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
|
||||
interface DetailRow {
|
||||
@ -39,8 +37,9 @@ const props = withDefaults(
|
||||
serviceId?: string | number
|
||||
enableZxFwSync?: boolean
|
||||
syncField?: ZxFwPricingField
|
||||
syncMainStorageKey?: string
|
||||
syncRowId?: string
|
||||
htMainStorageKey?: string
|
||||
htRowId?: string
|
||||
htMethodType?: HtFeeMethodType
|
||||
}>(),
|
||||
{
|
||||
title: '工时法明细',
|
||||
@ -48,13 +47,11 @@ const props = withDefaults(
|
||||
syncField: 'hourly'
|
||||
}
|
||||
)
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
const reloadSignal = ref(0)
|
||||
|
||||
const shouldSkipPersist = () => {
|
||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${props.storageKey}`
|
||||
@ -88,8 +85,66 @@ const shouldForceDefaultLoad = () => {
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
const fallbackDetailRows = ref<DetailRow[]>([])
|
||||
const gridApi = ref<GridApi<DetailRow> | null>(null)
|
||||
const serviceMethod = computed<ServicePricingMethod | null>(() => {
|
||||
if (props.syncField === 'investScale') return 'investScale'
|
||||
if (props.syncField === 'landScale') return 'landScale'
|
||||
if (props.syncField === 'workload') return 'workload'
|
||||
if (props.syncField === 'hourly') return 'hourly'
|
||||
return null
|
||||
})
|
||||
const useServicePricingState = computed(
|
||||
() => Boolean(props.enableZxFwSync && props.contractId && props.serviceId != null && serviceMethod.value)
|
||||
)
|
||||
const useHtMethodState = computed(
|
||||
() => Boolean(props.htMainStorageKey && props.htRowId && props.htMethodType)
|
||||
)
|
||||
const getServiceMethodState = () => {
|
||||
if (!useServicePricingState.value || !serviceMethod.value) return null
|
||||
return zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId!, props.serviceId!, serviceMethod.value)
|
||||
}
|
||||
const getHtMethodState = () => {
|
||||
if (!useHtMethodState.value) return null
|
||||
return zxFwPricingStore.getHtFeeMethodState<GridState>(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!
|
||||
)
|
||||
}
|
||||
const detailRows = computed<DetailRow[]>({
|
||||
get: () => {
|
||||
if (useServicePricingState.value) {
|
||||
const rows = getServiceMethodState()?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
}
|
||||
if (useHtMethodState.value) {
|
||||
const rows = getHtMethodState()?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
}
|
||||
return fallbackDetailRows.value
|
||||
},
|
||||
set: rows => {
|
||||
if (useServicePricingState.value && serviceMethod.value) {
|
||||
const currentState = getServiceMethodState()
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId!, props.serviceId!, serviceMethod.value, {
|
||||
detailRows: rows,
|
||||
projectCount: currentState?.projectCount ?? null
|
||||
})
|
||||
return
|
||||
}
|
||||
if (useHtMethodState.value) {
|
||||
zxFwPricingStore.setHtFeeMethodState(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!,
|
||||
{ detailRows: rows }
|
||||
)
|
||||
return
|
||||
}
|
||||
fallbackDetailRows.value = rows
|
||||
}
|
||||
})
|
||||
|
||||
type ExpertLite = {
|
||||
code: string
|
||||
@ -393,7 +448,18 @@ const saveToIndexedDB = async () => {
|
||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
||||
}
|
||||
|
||||
await localforage.setItem(props.storageKey, payload)
|
||||
if (useServicePricingState.value && serviceMethod.value) {
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId!, props.serviceId!, serviceMethod.value, payload)
|
||||
} else if (useHtMethodState.value) {
|
||||
zxFwPricingStore.setHtFeeMethodState(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!,
|
||||
payload
|
||||
)
|
||||
} else {
|
||||
zxFwPricingStore.setKeyState(props.storageKey, payload)
|
||||
}
|
||||
|
||||
if (props.enableZxFwSync && props.contractId && props.serviceId != null) {
|
||||
const synced = await syncPricingTotalToZxFw({
|
||||
@ -404,9 +470,6 @@ const saveToIndexedDB = async () => {
|
||||
})
|
||||
if (!synced) return
|
||||
}
|
||||
if (props.syncMainStorageKey && props.syncRowId) {
|
||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
@ -420,7 +483,15 @@ const loadFromIndexedDB = async () => {
|
||||
syncServiceBudgetToRows()
|
||||
return
|
||||
}
|
||||
const data = await localforage.getItem<GridState>(props.storageKey)
|
||||
const data = useServicePricingState.value && serviceMethod.value
|
||||
? await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId!, props.serviceId!, serviceMethod.value)
|
||||
: useHtMethodState.value
|
||||
? await zxFwPricingStore.loadHtFeeMethodState<GridState>(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!
|
||||
)
|
||||
: await zxFwPricingStore.loadKeyState<GridState>(props.storageKey)
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
syncServiceBudgetToRows()
|
||||
@ -435,34 +506,10 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.seq,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
if (!props.contractId || props.serviceId == null) return
|
||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, props.serviceId)) return
|
||||
reloadSignal.value += 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reloadSignal.value,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const handleCellValueChanged = () => {
|
||||
syncServiceBudgetToRows()
|
||||
gridApi.value?.refreshCells({ force: true })
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||
@ -505,8 +552,6 @@ onDeactivated(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridApi.value?.stopEditing()
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
|
||||
import { computed, defineComponent, h, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import localforage from 'localforage'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { roundTo, toDecimal } from '@/lib/decimal'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
@ -40,10 +39,11 @@ interface FeeGridState {
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
storageKey: string
|
||||
syncMainStorageKey?: string
|
||||
syncRowId?: string
|
||||
htMainStorageKey?: string
|
||||
htRowId?: string
|
||||
htMethodType?: 'quantity-unit-price-fee'
|
||||
}>()
|
||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
const createRowId = () => `fee-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||
const SUBTOTAL_ROW_ID = 'fee-subtotal-fixed'
|
||||
@ -76,11 +76,35 @@ const ensureSubtotalRow = (rows: FeeRow[]) => {
|
||||
return [...normalRows, createSubtotalRow()]
|
||||
}
|
||||
|
||||
const detailRows = ref<FeeRow[]>([])
|
||||
const fallbackDetailRows = ref<FeeRow[]>([])
|
||||
const gridApi = ref<GridApi<FeeRow> | null>(null)
|
||||
const deleteConfirmOpen = ref(false)
|
||||
const pendingDeleteRowId = ref<string | null>(null)
|
||||
const pendingDeleteRowName = ref('')
|
||||
const useHtMethodState = computed(
|
||||
() => Boolean(props.htMainStorageKey && props.htRowId && props.htMethodType)
|
||||
)
|
||||
const detailRows = computed<FeeRow[]>({
|
||||
get: () => {
|
||||
if (!useHtMethodState.value) return fallbackDetailRows.value
|
||||
const state = zxFwPricingStore.getHtFeeMethodState<FeeGridState>(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!
|
||||
)
|
||||
const rows = state?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
},
|
||||
set: rows => {
|
||||
if (!useHtMethodState.value) {
|
||||
fallbackDetailRows.value = rows
|
||||
return
|
||||
}
|
||||
zxFwPricingStore.setHtFeeMethodState(props.htMainStorageKey!, props.htRowId!, props.htMethodType!, {
|
||||
detailRows: rows
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const addRow = () => {
|
||||
const normalRows = detailRows.value.filter(row => !isSubtotalRow(row))
|
||||
@ -193,10 +217,15 @@ const saveToIndexedDB = async () => {
|
||||
const payload: FeeGridState = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
}
|
||||
|
||||
await localforage.setItem(props.storageKey, payload)
|
||||
if (props.syncMainStorageKey && props.syncRowId) {
|
||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
||||
if (useHtMethodState.value) {
|
||||
zxFwPricingStore.setHtFeeMethodState(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!,
|
||||
payload
|
||||
)
|
||||
} else {
|
||||
zxFwPricingStore.setKeyState(props.storageKey, payload)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
@ -205,7 +234,13 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
const data = await localforage.getItem<FeeGridState>(props.storageKey)
|
||||
const data = useHtMethodState.value
|
||||
? await zxFwPricingStore.loadHtFeeMethodState<FeeGridState>(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!
|
||||
)
|
||||
: await zxFwPricingStore.loadKeyState<FeeGridState>(props.storageKey)
|
||||
detailRows.value = mergeWithStoredRows(data?.detailRows)
|
||||
syncComputedValuesToRows()
|
||||
} catch (error) {
|
||||
@ -363,8 +398,6 @@ const detailGridOptions: GridOptions<FeeRow> = {
|
||||
treeData: false
|
||||
}
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
||||
gridApi.value = event.api
|
||||
}
|
||||
@ -372,10 +405,7 @@ const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToRows()
|
||||
gridApi.value?.refreshCells({ force: true })
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -394,7 +424,6 @@ watch(
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { CellValueChangedEvent, ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
@ -10,7 +9,6 @@ import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { Pencil, Eraser } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
@ -80,7 +78,6 @@ const props = defineProps<{
|
||||
fixedNames?: string[]
|
||||
}>()
|
||||
const tabStore = useTabStore()
|
||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
const createRowId = () => `fee-method-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||
@ -102,11 +99,6 @@ const toFiniteUnknown = (value: unknown): number | null => {
|
||||
const numeric = Number(value)
|
||||
return Number.isFinite(numeric) ? numeric : null
|
||||
}
|
||||
const buildMethodStorageKey = (
|
||||
rowId: string,
|
||||
method: 'rate-fee' | 'hourly-fee' | 'quantity-unit-price-fee'
|
||||
) => `${props.storageKey}-${rowId}-${method}`
|
||||
|
||||
const loadContractServiceFeeBase = async (): Promise<number | null> => {
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
if (!contractId) return null
|
||||
@ -172,9 +164,9 @@ const hydrateRowsFromMethodStores = async (rows: FeeMethodRow[]): Promise<FeeMet
|
||||
rows.map(async row => {
|
||||
if (!row?.id) return row
|
||||
const [rateData, hourlyData, quantityData] = await Promise.all([
|
||||
localforage.getItem<MethodRateState>(buildMethodStorageKey(row.id, 'rate-fee')),
|
||||
localforage.getItem<MethodHourlyState>(buildMethodStorageKey(row.id, 'hourly-fee')),
|
||||
localforage.getItem<MethodQuantityState>(buildMethodStorageKey(row.id, 'quantity-unit-price-fee'))
|
||||
zxFwPricingStore.loadHtFeeMethodState<MethodRateState>(props.storageKey, row.id, 'rate-fee'),
|
||||
zxFwPricingStore.loadHtFeeMethodState<MethodHourlyState>(props.storageKey, row.id, 'hourly-fee'),
|
||||
zxFwPricingStore.loadHtFeeMethodState<MethodQuantityState>(props.storageKey, row.id, 'quantity-unit-price-fee')
|
||||
])
|
||||
|
||||
const storedRateFee = toFiniteUnknown(rateData?.budgetFee)
|
||||
@ -205,7 +197,17 @@ const fixedNames = computed(() =>
|
||||
: []
|
||||
)
|
||||
const hasFixedNames = computed(() => fixedNames.value.length > 0)
|
||||
const detailRows = ref<FeeMethodRow[]>([createDefaultRow()])
|
||||
const detailRows = computed<FeeMethodRow[]>({
|
||||
get: () => {
|
||||
const rows = zxFwPricingStore.getHtFeeMainState<FeeMethodRow>(props.storageKey)?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
},
|
||||
set: rows => {
|
||||
zxFwPricingStore.setHtFeeMainState(props.storageKey, {
|
||||
detailRows: rows
|
||||
})
|
||||
}
|
||||
})
|
||||
const summaryRow = computed<FeeMethodRow>(() => {
|
||||
const totals = detailRows.value.reduce(
|
||||
(acc, row) => {
|
||||
@ -235,6 +237,7 @@ const gridApi = ref<GridApi<FeeMethodRow> | null>(null)
|
||||
const clearConfirmOpen = ref(false)
|
||||
const pendingClearRowId = ref<string | null>(null)
|
||||
const pendingClearRowName = ref('')
|
||||
const lastSavedSnapshot = ref('')
|
||||
|
||||
const requestClearRow = (id: string, name?: string) => {
|
||||
pendingClearRowId.value = id
|
||||
@ -325,12 +328,15 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => {
|
||||
|
||||
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
const saveToIndexedDB = async (force = false) => {
|
||||
try {
|
||||
const payload: FeeMethodState = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
}
|
||||
await localforage.setItem(props.storageKey, payload)
|
||||
const snapshot = JSON.stringify(payload.detailRows)
|
||||
if (!force && snapshot === lastSavedSnapshot.value) return
|
||||
zxFwPricingStore.setHtFeeMainState(props.storageKey, payload, { force })
|
||||
lastSavedSnapshot.value = snapshot
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
}
|
||||
@ -338,17 +344,16 @@ const saveToIndexedDB = async () => {
|
||||
|
||||
const loadFromIndexedDB = async () => {
|
||||
try {
|
||||
|
||||
const data = await localforage.getItem<FeeMethodState>(props.storageKey)
|
||||
const data = await zxFwPricingStore.loadHtFeeMainState<FeeMethodRow>(props.storageKey)
|
||||
|
||||
const mergedRows = mergeWithStoredRows(data?.detailRows)
|
||||
detailRows.value = await hydrateRowsFromMethodStores(mergedRows)
|
||||
await saveToIndexedDB()
|
||||
await saveToIndexedDB(true)
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
const mergedRows = mergeWithStoredRows([])
|
||||
detailRows.value = await hydrateRowsFromMethodStores(mergedRows)
|
||||
await saveToIndexedDB()
|
||||
await saveToIndexedDB(true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -360,11 +365,9 @@ const addRow = () => {
|
||||
const clearRow = async (id: string) => {
|
||||
tabStore.removeTab(`ht-fee-edit-${props.storageKey}-${id}`)
|
||||
await nextTick()
|
||||
await Promise.all([
|
||||
localforage.removeItem(buildMethodStorageKey(id, 'rate-fee')),
|
||||
localforage.removeItem(buildMethodStorageKey(id, 'hourly-fee')),
|
||||
localforage.removeItem(buildMethodStorageKey(id, 'quantity-unit-price-fee'))
|
||||
])
|
||||
zxFwPricingStore.removeHtFeeMethodState(props.storageKey, id, 'rate-fee')
|
||||
zxFwPricingStore.removeHtFeeMethodState(props.storageKey, id, 'hourly-fee')
|
||||
zxFwPricingStore.removeHtFeeMethodState(props.storageKey, id, 'quantity-unit-price-fee')
|
||||
detailRows.value = detailRows.value.map(row =>
|
||||
row.id !== id
|
||||
? row
|
||||
@ -553,7 +556,14 @@ const detailGridOptions: GridOptions<FeeMethodRow> = {
|
||||
}
|
||||
}
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let reloadTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const scheduleReloadFromStorage = () => {
|
||||
if (reloadTimer) clearTimeout(reloadTimer)
|
||||
reloadTimer = setTimeout(() => {
|
||||
void loadFromIndexedDB()
|
||||
}, 80)
|
||||
}
|
||||
|
||||
const handleGridReady = (event: GridReadyEvent<FeeMethodRow>) => {
|
||||
gridApi.value = event.api
|
||||
@ -561,10 +571,7 @@ const handleGridReady = (event: GridReadyEvent<FeeMethodRow>) => {
|
||||
|
||||
const handleCellValueChanged = (event: CellValueChangedEvent<FeeMethodRow>) => {
|
||||
if (isSummaryRow(event.data)) return
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -572,22 +579,28 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void loadFromIndexedDB()
|
||||
scheduleReloadFromStorage()
|
||||
})
|
||||
|
||||
const storageKeyRef = computed(() => props.storageKey)
|
||||
watch(storageKeyRef, () => {
|
||||
void loadFromIndexedDB()
|
||||
scheduleReloadFromStorage()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => htFeeMethodReloadStore.seq,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
const detail = htFeeMethodReloadStore.lastEvent
|
||||
if (!detail) return
|
||||
if (String(detail.mainStorageKey || '').trim() !== String(props.storageKey || '').trim()) return
|
||||
void loadFromIndexedDB()
|
||||
() =>
|
||||
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('|'),
|
||||
(nextSignature, prevSignature) => {
|
||||
if (!nextSignature && !prevSignature) return
|
||||
if (nextSignature === prevSignature) return
|
||||
scheduleReloadFromStorage()
|
||||
}
|
||||
)
|
||||
|
||||
@ -599,7 +612,7 @@ watch(
|
||||
},
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
scheduleReloadFromStorage()
|
||||
}
|
||||
)
|
||||
|
||||
@ -613,9 +626,9 @@ watch([hasFixedNames], () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
if (reloadTimer) clearTimeout(reloadTimer)
|
||||
gridApi.value = null
|
||||
void saveToIndexedDB()
|
||||
void saveToIndexedDB(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
||||
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
||||
import { industryTypeList } from '@/sql'
|
||||
@ -45,6 +46,14 @@ interface ContractSegmentPackage {
|
||||
projectIndustry: string
|
||||
contracts: ContractItem[]
|
||||
localforageEntries: DataEntry[]
|
||||
piniaState?: {
|
||||
zxFwPricing?: {
|
||||
contracts?: Record<string, unknown>
|
||||
servicePricingStates?: Record<string, unknown>
|
||||
htFeeMainStates?: Record<string, unknown>
|
||||
htFeeMethodStates?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
}
|
||||
interface XmBaseInfoState {
|
||||
projectIndustry?: string
|
||||
@ -52,16 +61,18 @@ interface XmBaseInfoState {
|
||||
|
||||
const STORAGE_KEY = 'ht-card-v1'
|
||||
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
||||
const CONTRACT_SEGMENT_VERSION = 1
|
||||
const CONTRACT_SEGMENT_VERSION = 2
|
||||
const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
||||
const SERVICE_KEY_PREFIX = 'zxFW-'
|
||||
const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
|
||||
const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-'
|
||||
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-']
|
||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||
const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly'] as const
|
||||
|
||||
|
||||
const tabStore = useTabStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
|
||||
|
||||
@ -377,6 +388,112 @@ const normalizeDataEntries = (value: unknown): DataEntry[] => {
|
||||
}))
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||
|
||||
const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
|
||||
|
||||
const buildContractPiniaPayload = async (contractIds: string[]) => {
|
||||
const idSet = new Set(contractIds.map(id => String(id || '').trim()).filter(Boolean))
|
||||
const payload = {
|
||||
contracts: {} as Record<string, unknown>,
|
||||
servicePricingStates: {} as Record<string, unknown>,
|
||||
htFeeMainStates: {} as Record<string, unknown>,
|
||||
htFeeMethodStates: {} as Record<string, unknown>
|
||||
}
|
||||
if (idSet.size === 0) return payload
|
||||
|
||||
await Promise.all(Array.from(idSet).map(id => zxFwPricingStore.loadContract(id)))
|
||||
|
||||
for (const contractId of idSet) {
|
||||
const contractState = zxFwPricingStore.getContractState(contractId)
|
||||
if (contractState) {
|
||||
payload.contracts[contractId] = cloneJson(contractState)
|
||||
}
|
||||
|
||||
const servicePricingState = zxFwPricingStore.servicePricingStates[contractId]
|
||||
if (isRecord(servicePricingState)) {
|
||||
payload.servicePricingStates[contractId] = cloneJson(servicePricingState)
|
||||
}
|
||||
|
||||
const mainPrefix = `htExtraFee-${contractId}-`
|
||||
for (const [mainKey, mainState] of Object.entries(zxFwPricingStore.htFeeMainStates)) {
|
||||
if (!mainKey.startsWith(mainPrefix)) continue
|
||||
payload.htFeeMainStates[mainKey] = cloneJson(mainState)
|
||||
}
|
||||
|
||||
for (const [mainKey, methodState] of Object.entries(zxFwPricingStore.htFeeMethodStates)) {
|
||||
if (!mainKey.startsWith(mainPrefix)) continue
|
||||
payload.htFeeMethodStates[mainKey] = cloneJson(methodState)
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
const applyImportedContractPiniaPayload = async (
|
||||
piniaPayload: unknown,
|
||||
oldToNewIdMap: Map<string, string>
|
||||
) => {
|
||||
if (!isRecord(piniaPayload)) return
|
||||
const zxFwPayload = isRecord(piniaPayload.zxFwPricing) ? piniaPayload.zxFwPricing : null
|
||||
if (!zxFwPayload) return
|
||||
|
||||
const contractsMap = isRecord(zxFwPayload.contracts) ? zxFwPayload.contracts : {}
|
||||
const servicePricingStatesMap = isRecord(zxFwPayload.servicePricingStates) ? zxFwPayload.servicePricingStates : {}
|
||||
const htFeeMainStatesMap = isRecord(zxFwPayload.htFeeMainStates) ? zxFwPayload.htFeeMainStates : {}
|
||||
const htFeeMethodStatesMap = isRecord(zxFwPayload.htFeeMethodStates) ? zxFwPayload.htFeeMethodStates : {}
|
||||
|
||||
for (const [oldId, newId] of oldToNewIdMap.entries()) {
|
||||
const rawContractState = contractsMap[oldId]
|
||||
if (isRecord(rawContractState) && Array.isArray(rawContractState.detailRows)) {
|
||||
await zxFwPricingStore.setContractState(newId, rawContractState as any)
|
||||
}
|
||||
|
||||
const rawServicePricingByService = servicePricingStatesMap[oldId]
|
||||
if (isRecord(rawServicePricingByService)) {
|
||||
for (const [serviceId, rawServiceMethods] of Object.entries(rawServicePricingByService)) {
|
||||
if (!isRecord(rawServiceMethods)) continue
|
||||
for (const method of SERVICE_PRICING_METHODS) {
|
||||
const methodState = rawServiceMethods[method]
|
||||
if (!isRecord(methodState) || !Array.isArray(methodState.detailRows)) continue
|
||||
zxFwPricingStore.setServicePricingMethodState(newId, serviceId, method, methodState as any, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldMainPrefix = `htExtraFee-${oldId}-`
|
||||
const newMainPrefix = `htExtraFee-${newId}-`
|
||||
for (const [oldMainKey, rawMainState] of Object.entries(htFeeMainStatesMap)) {
|
||||
if (!oldMainKey.startsWith(oldMainPrefix)) continue
|
||||
if (!isRecord(rawMainState) || !Array.isArray(rawMainState.detailRows)) continue
|
||||
const newMainKey = oldMainKey.replace(oldMainPrefix, newMainPrefix)
|
||||
zxFwPricingStore.setHtFeeMainState(newMainKey, rawMainState as any, { force: true })
|
||||
}
|
||||
|
||||
for (const [oldMainKey, rawByRow] of Object.entries(htFeeMethodStatesMap)) {
|
||||
if (!oldMainKey.startsWith(oldMainPrefix)) continue
|
||||
if (!isRecord(rawByRow)) continue
|
||||
const newMainKey = oldMainKey.replace(oldMainPrefix, newMainPrefix)
|
||||
for (const [rowId, rawByMethod] of Object.entries(rawByRow)) {
|
||||
if (!isRecord(rawByMethod)) continue
|
||||
const ratePayload = rawByMethod['rate-fee']
|
||||
const hourlyPayload = rawByMethod['hourly-fee']
|
||||
const quantityPayload = rawByMethod['quantity-unit-price-fee']
|
||||
if (ratePayload != null) {
|
||||
zxFwPricingStore.setHtFeeMethodState(newMainKey, rowId, 'rate-fee', cloneJson(ratePayload), { force: true })
|
||||
}
|
||||
if (hourlyPayload != null) {
|
||||
zxFwPricingStore.setHtFeeMethodState(newMainKey, rowId, 'hourly-fee', cloneJson(hourlyPayload), { force: true })
|
||||
}
|
||||
if (quantityPayload != null) {
|
||||
zxFwPricingStore.setHtFeeMethodState(newMainKey, rowId, 'quantity-unit-price-fee', cloneJson(quantityPayload), { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isContractSegmentPackage = (value: unknown): value is ContractSegmentPackage => {
|
||||
const payload = value as Partial<ContractSegmentPackage> | null
|
||||
return Boolean(payload && typeof payload === 'object' && Array.isArray(payload.contracts))
|
||||
@ -452,6 +569,7 @@ const exportSelectedContracts = async () => {
|
||||
const localforageEntries = await readContractRelatedForageEntries(
|
||||
selectedContracts.map(item => item.id)
|
||||
)
|
||||
const piniaPayload = await buildContractPiniaPayload(selectedContracts.map(item => item.id))
|
||||
|
||||
const projectIndustry = await getCurrentProjectIndustry()
|
||||
if (!projectIndustry) {
|
||||
@ -465,7 +583,10 @@ const exportSelectedContracts = async () => {
|
||||
exportedAt: now.toISOString(),
|
||||
projectIndustry,
|
||||
contracts: selectedContracts,
|
||||
localforageEntries
|
||||
localforageEntries,
|
||||
piniaState: {
|
||||
zxFwPricing: piniaPayload
|
||||
}
|
||||
}
|
||||
|
||||
const content = await encodeZwArchive(payload)
|
||||
@ -549,6 +670,7 @@ const importContractSegments = async (event: Event) => {
|
||||
})
|
||||
|
||||
await Promise.all(rewrittenEntries.map(entry => localforage.setItem(entry.key, entry.value)))
|
||||
await applyImportedContractPiniaPayload(payload.piniaState, oldToNewIdMap)
|
||||
|
||||
contracts.value = [...contracts.value, ...nextContracts]
|
||||
await saveContracts()
|
||||
|
||||
@ -50,8 +50,9 @@ const quantityUnitPricePane = markRaw(
|
||||
h(HtFeeGrid, {
|
||||
title: '数量单价',
|
||||
storageKey: quantityStorageKey.value,
|
||||
syncMainStorageKey: props.storageKey,
|
||||
syncRowId: props.rowId
|
||||
htMainStorageKey: props.storageKey,
|
||||
htRowId: props.rowId,
|
||||
htMethodType: 'quantity-unit-price-fee'
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -66,8 +67,9 @@ const rateFeePane = markRaw(
|
||||
h(HtFeeRateMethodForm, {
|
||||
storageKey: rateStorageKey.value,
|
||||
contractId: props.contractId,
|
||||
syncMainStorageKey: props.storageKey,
|
||||
syncRowId: props.rowId
|
||||
htMainStorageKey: props.storageKey,
|
||||
htRowId: props.rowId,
|
||||
htMethodType: 'rate-fee'
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -82,8 +84,9 @@ const hourlyFeePane = markRaw(
|
||||
h(HourlyFeeGrid, {
|
||||
title: '工时法明细',
|
||||
storageKey: hourlyStorageKey.value,
|
||||
syncMainStorageKey: props.storageKey,
|
||||
syncRowId: props.rowId
|
||||
htMainStorageKey: props.storageKey,
|
||||
htRowId: props.rowId,
|
||||
htMethodType: 'hourly-fee'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import localforage from 'localforage'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
|
||||
interface RateMethodState {
|
||||
@ -15,71 +13,95 @@ interface RateMethodState {
|
||||
const props = defineProps<{
|
||||
storageKey: string
|
||||
contractId?: string
|
||||
syncMainStorageKey?: string
|
||||
syncRowId?: string
|
||||
htMainStorageKey?: string
|
||||
htRowId?: string
|
||||
htMethodType?: 'rate-fee'
|
||||
}>()
|
||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
const base = ref<number | null>(null)
|
||||
const rate = ref<number | null>(null)
|
||||
const remark = ref('')
|
||||
const rateInput = ref('')
|
||||
const contractVersion = computed(() => {
|
||||
const lastSavedSnapshot = ref('')
|
||||
const useHtMethodState = computed(
|
||||
() => Boolean(props.htMainStorageKey && props.htRowId && props.htMethodType)
|
||||
)
|
||||
const contractIdText = computed(() => {
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
if (!contractId) return 0
|
||||
return zxFwPricingStore.contractVersions[contractId] || 0
|
||||
return contractId
|
||||
})
|
||||
const base = computed<number | null>(() => {
|
||||
const contractId = contractIdText.value
|
||||
if (!contractId) return null
|
||||
return zxFwPricingStore.getBaseSubtotal(contractId)
|
||||
})
|
||||
|
||||
const round3 = (value: number) => Number(value.toFixed(3))
|
||||
const budgetFee = computed<number | null>(() => {
|
||||
if (base.value == null || rate.value == null) return null
|
||||
return round3(base.value * rate.value)
|
||||
return Number((base.value * rate.value).toFixed(3))
|
||||
})
|
||||
|
||||
const formatAmount = (value: number | null) =>
|
||||
value == null ? '' : formatThousandsFlexible(value, 3)
|
||||
|
||||
const loadBase = async () => {
|
||||
const contractId = String(props.contractId || '').trim()
|
||||
if (!contractId) {
|
||||
base.value = null
|
||||
return
|
||||
}
|
||||
const ensureContractLoaded = async () => {
|
||||
const contractId = contractIdText.value
|
||||
if (!contractId) return
|
||||
try {
|
||||
await zxFwPricingStore.loadContract(contractId)
|
||||
const nextBase = zxFwPricingStore.getBaseSubtotal(contractId)
|
||||
base.value = nextBase == null ? null : round3(nextBase)
|
||||
} catch (error) {
|
||||
console.error('load rate base failed:', error)
|
||||
base.value = null
|
||||
console.error('load contract for rate base failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadForm = async () => {
|
||||
try {
|
||||
const data = await localforage.getItem<RateMethodState>(props.storageKey)
|
||||
const data = useHtMethodState.value
|
||||
? await zxFwPricingStore.loadHtFeeMethodState<RateMethodState>(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!
|
||||
)
|
||||
: await zxFwPricingStore.loadKeyState<RateMethodState>(props.storageKey)
|
||||
rate.value = typeof data?.rate === 'number' ? data.rate : null
|
||||
remark.value = typeof data?.remark === 'string' ? data.remark : ''
|
||||
rateInput.value = rate.value == null ? '' : String(rate.value)
|
||||
const snapshot: RateMethodState = {
|
||||
rate: rate.value,
|
||||
budgetFee: budgetFee.value,
|
||||
remark: remark.value
|
||||
}
|
||||
lastSavedSnapshot.value = JSON.stringify(snapshot)
|
||||
} catch (error) {
|
||||
console.error('load rate form failed:', error)
|
||||
rate.value = null
|
||||
remark.value = ''
|
||||
rateInput.value = ''
|
||||
lastSavedSnapshot.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const saveForm = async () => {
|
||||
const saveForm = async (force = false) => {
|
||||
try {
|
||||
await localforage.setItem<RateMethodState>(props.storageKey, {
|
||||
const payload: RateMethodState = {
|
||||
rate: rate.value,
|
||||
budgetFee: budgetFee.value,
|
||||
remark: remark.value
|
||||
})
|
||||
if (props.syncMainStorageKey && props.syncRowId) {
|
||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
||||
}
|
||||
const snapshot = JSON.stringify(payload)
|
||||
if (!force && snapshot === lastSavedSnapshot.value) return
|
||||
if (useHtMethodState.value) {
|
||||
zxFwPricingStore.setHtFeeMethodState(
|
||||
props.htMainStorageKey!,
|
||||
props.htRowId!,
|
||||
props.htMethodType!,
|
||||
payload,
|
||||
{ force }
|
||||
)
|
||||
} else {
|
||||
zxFwPricingStore.setKeyState(props.storageKey, payload, { force })
|
||||
}
|
||||
lastSavedSnapshot.value = snapshot
|
||||
} catch (error) {
|
||||
console.error('save rate form failed:', error)
|
||||
}
|
||||
@ -105,26 +127,22 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => contractVersion.value,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadBase()
|
||||
() => contractIdText.value,
|
||||
() => {
|
||||
void ensureContractLoaded()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadBase(), loadForm()])
|
||||
|
||||
await Promise.all([ensureContractLoaded(), loadForm()])
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await Promise.all([loadBase(), loadForm()])
|
||||
|
||||
await Promise.all([ensureContractLoaded(), loadForm()])
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
void saveForm()
|
||||
void saveForm(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||
@ -93,7 +93,7 @@ const props = defineProps<{
|
||||
contractId: string,
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||
@ -106,7 +106,6 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
let factorDefaultsLoaded = false
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
const reloadSignal = ref(0)
|
||||
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||
const industryNameMap = new Map(
|
||||
industryTypeList.flatMap(item => [
|
||||
@ -238,7 +237,22 @@ const shouldForceDefaultLoad = () => {
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
const getMethodState = () =>
|
||||
zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'investScale')
|
||||
|
||||
const detailRows = computed<DetailRow[]>({
|
||||
get: () => {
|
||||
const rows = getMethodState()?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
},
|
||||
set: rows => {
|
||||
const currentState = getMethodState()
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'investScale', {
|
||||
detailRows: rows,
|
||||
projectCount: currentState?.projectCount ?? getTargetProjectCount()
|
||||
})
|
||||
}
|
||||
})
|
||||
type majorLite = {
|
||||
code: string
|
||||
name: string
|
||||
@ -1016,10 +1030,10 @@ const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||
projectCount: getTargetProjectCount()
|
||||
}
|
||||
console.log('Saving to IndexedDB:', payload)
|
||||
await localforage.setItem(DB_KEY.value, payload)
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'investScale', payload)
|
||||
const synced = await syncPricingTotalToZxFw({
|
||||
contractId: props.contractId,
|
||||
serviceId: props.serviceId,
|
||||
@ -1149,10 +1163,11 @@ const loadFromIndexedDB = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'investScale')
|
||||
if (data) {
|
||||
if (isMutipleService.value) {
|
||||
projectCount.value = inferProjectCountFromRows(data.detailRows as any)
|
||||
const storedProjectCount = normalizeProjectCount(data.projectCount)
|
||||
projectCount.value = storedProjectCount || inferProjectCountFromRows(data.detailRows as any)
|
||||
}
|
||||
detailRows.value = isOnlyCostScaleService.value
|
||||
? buildOnlyCostScaleRows(data.detailRows as any, {
|
||||
@ -1222,35 +1237,9 @@ const clearAllData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.seq,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, props.serviceId)) return
|
||||
reloadSignal.value += 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reloadSignal.value,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -1262,8 +1251,6 @@ onActivated(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
const processCellForClipboard = (params: any) => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||
@ -93,7 +93,7 @@ const props = defineProps<{
|
||||
contractId: string,
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||
@ -106,7 +106,6 @@ const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
let factorDefaultsLoaded = false
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
const reloadSignal = ref(0)
|
||||
const industryNameMap = new Map(
|
||||
industryTypeList.flatMap(item => [
|
||||
[String(item.id).trim(), item.name],
|
||||
@ -182,7 +181,22 @@ const inferProjectCountFromRows = (rows?: Array<Partial<DetailRow>>) => {
|
||||
return maxProjectIndex
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
const getMethodState = () =>
|
||||
zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'landScale')
|
||||
|
||||
const detailRows = computed<DetailRow[]>({
|
||||
get: () => {
|
||||
const rows = getMethodState()?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
},
|
||||
set: rows => {
|
||||
const currentState = getMethodState()
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'landScale', {
|
||||
detailRows: rows,
|
||||
projectCount: currentState?.projectCount ?? getTargetProjectCount()
|
||||
})
|
||||
}
|
||||
})
|
||||
const getDefaultConsultCategoryFactor = () =>
|
||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
||||
|
||||
@ -869,10 +883,10 @@ const saveToIndexedDB = async () => {
|
||||
if (shouldSkipPersist()) return
|
||||
try {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||
projectCount: getTargetProjectCount()
|
||||
}
|
||||
console.log('Saving to IndexedDB:', payload)
|
||||
await localforage.setItem(DB_KEY.value, payload)
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'landScale', payload)
|
||||
const synced = await syncPricingTotalToZxFw({
|
||||
contractId: props.contractId,
|
||||
serviceId: props.serviceId,
|
||||
@ -989,10 +1003,11 @@ const loadFromIndexedDB = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'landScale')
|
||||
if (data) {
|
||||
if (isMutipleService.value) {
|
||||
projectCount.value = inferProjectCountFromRows(data.detailRows as any)
|
||||
const storedProjectCount = normalizeProjectCount(data.projectCount)
|
||||
projectCount.value = storedProjectCount || inferProjectCountFromRows(data.detailRows as any)
|
||||
}
|
||||
detailRows.value = mergeWithDictRows(data.detailRows as any, {
|
||||
projectCount: getTargetProjectCount(),
|
||||
@ -1047,35 +1062,9 @@ const clearAllData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.seq,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, props.serviceId)) return
|
||||
reloadSignal.value += 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reloadSignal.value,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
syncComputedValuesToDetailRows()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -1087,8 +1076,6 @@ onActivated(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
const processCellForClipboard = (params: any) => {
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { AgGridVue } from 'ag-grid-vue3'
|
||||
import type { ColDef } from 'ag-grid-community'
|
||||
import localforage from 'localforage'
|
||||
import { taskList } from '@/sql'
|
||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import { parseNumberOrNull } from '@/lib/number'
|
||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||
|
||||
@ -42,7 +41,7 @@ const props = defineProps<{
|
||||
|
||||
serviceId: string | number
|
||||
}>()
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
@ -50,7 +49,6 @@ const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||
let factorDefaultsLoaded = false
|
||||
const paneInstanceCreatedAt = Date.now()
|
||||
const reloadSignal = ref(0)
|
||||
|
||||
const getDefaultConsultCategoryFactor = () =>
|
||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
||||
@ -93,7 +91,20 @@ const shouldForceDefaultLoad = () => {
|
||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||
}
|
||||
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
const getMethodState = () =>
|
||||
zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'workload')
|
||||
|
||||
const detailRows = computed<DetailRow[]>({
|
||||
get: () => {
|
||||
const rows = getMethodState()?.detailRows
|
||||
return Array.isArray(rows) ? rows : []
|
||||
},
|
||||
set: rows => {
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'workload', {
|
||||
detailRows: rows
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type taskLite = {
|
||||
serviceID: number
|
||||
@ -424,8 +435,7 @@ const saveToIndexedDB = async () => {
|
||||
const payload = {
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
}
|
||||
console.log('Saving to IndexedDB:', payload)
|
||||
await localforage.setItem(DB_KEY.value, payload)
|
||||
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'workload', payload)
|
||||
const synced = await syncPricingTotalToZxFw({
|
||||
contractId: props.contractId,
|
||||
serviceId: props.serviceId,
|
||||
@ -452,7 +462,7 @@ const loadFromIndexedDB = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
||||
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'workload')
|
||||
if (data) {
|
||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||
return
|
||||
@ -465,34 +475,8 @@ const loadFromIndexedDB = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pricingPaneReloadStore.seq,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, props.serviceId)) return
|
||||
reloadSignal.value += 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reloadSignal.value,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
let persistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
|
||||
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
void saveToIndexedDB()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@ -500,8 +484,6 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (persistTimer) clearTimeout(persistTimer)
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
const processCellForClipboard = (params: any) => {
|
||||
|
||||
@ -11,12 +11,10 @@ import { parseNumberOrNull } from '@/lib/number'
|
||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||
import {
|
||||
ensurePricingMethodDetailRowsForServices,
|
||||
persistDefaultPricingMethodDetailRowsForServices,
|
||||
getPricingMethodTotalsForService,
|
||||
getPricingMethodTotalsForServices,
|
||||
type PricingMethodTotals
|
||||
} from '@/lib/pricingMethodTotals'
|
||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
||||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
@ -55,7 +53,7 @@ interface DetailRow {
|
||||
actions?: unknown
|
||||
}
|
||||
|
||||
interface ZxFwState {
|
||||
interface ZxFwViewState {
|
||||
selectedIds?: string[]
|
||||
selectedCodes?: string[]
|
||||
detailRows: DetailRow[]
|
||||
@ -77,7 +75,6 @@ const props = defineProps<{
|
||||
contractName?: string
|
||||
}>()
|
||||
const tabStore = useTabStore()
|
||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||
@ -85,9 +82,6 @@ const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||
const PRICING_CLEAR_SKIP_TTL_MS = 5000
|
||||
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||||
const projectIndustry = ref('')
|
||||
const syncingFromStore = ref(false)
|
||||
const localSavedVersion = ref(0)
|
||||
const zxFwStoreVersion = computed(() => zxFwPricingStore.contractVersions[props.contractId] || 0)
|
||||
|
||||
type ServiceListItem = {
|
||||
code?: string
|
||||
@ -145,8 +139,49 @@ const serviceIdSignature = computed(() => serviceDict.value.map(item => item.id)
|
||||
const fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: '', name: '小计' }
|
||||
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
||||
|
||||
const selectedIds = ref<string[]>([])
|
||||
const detailRows = ref<DetailRow[]>([])
|
||||
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 || '',
|
||||
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,
|
||||
actions: row.actions
|
||||
}))
|
||||
)
|
||||
|
||||
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 || '',
|
||||
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,
|
||||
actions: row.actions
|
||||
}))
|
||||
}
|
||||
}
|
||||
return {
|
||||
selectedIds: [],
|
||||
detailRows: []
|
||||
}
|
||||
}
|
||||
|
||||
const setCurrentContractState = async (nextState: ZxFwViewState) => {
|
||||
await zxFwPricingStore.setContractState(props.contractId, nextState)
|
||||
}
|
||||
|
||||
const pickerOpen = ref(false)
|
||||
const pickerTempIds = ref<string[]>([])
|
||||
@ -374,12 +409,8 @@ const getFixedRowSubtotal = () =>
|
||||
getMethodTotal('hourly')
|
||||
)
|
||||
|
||||
const getPricingPaneStorageKeys = (serviceId: string) => [
|
||||
`tzGMF-${props.contractId}-${serviceId}`,
|
||||
`ydGMF-${props.contractId}-${serviceId}`,
|
||||
`gzlF-${props.contractId}-${serviceId}`,
|
||||
`hourlyPricing-${props.contractId}-${serviceId}`
|
||||
]
|
||||
const getPricingPaneStorageKeys = (serviceId: string) =>
|
||||
zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId)
|
||||
|
||||
const clearPricingPaneValues = async (serviceId: string) => {
|
||||
const keys = getPricingPaneStorageKeys(serviceId)
|
||||
@ -390,14 +421,16 @@ const clearPricingPaneValues = async (serviceId: string) => {
|
||||
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, skipToken)
|
||||
sessionStorage.setItem(`${PRICING_FORCE_DEFAULT_PREFIX}${key}`, String(skipUntil))
|
||||
}
|
||||
zxFwPricingStore.removeAllServicePricingMethodStates(props.contractId, serviceId)
|
||||
// Reset后会立刻有逻辑读取IndexedDB计算默认值,这里强制同步删除持久层,避免读到旧数据。
|
||||
await Promise.all(keys.map(key => localforage.removeItem(key)))
|
||||
pricingPaneReloadStore.emit(props.contractId, serviceId)
|
||||
}
|
||||
|
||||
const clearRowValues = async (row: DetailRow) => {
|
||||
if (isFixedRow(row)) return
|
||||
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回。
|
||||
tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||
await nextTick()
|
||||
await clearPricingPaneValues(row.id)
|
||||
await ensurePricingMethodDetailRowsForServices({
|
||||
@ -411,7 +444,8 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
options: PRICING_TOTALS_OPTIONS
|
||||
})
|
||||
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
||||
const clearedRows = detailRows.value.map(item =>
|
||||
const currentState = getCurrentContractState()
|
||||
const clearedRows = currentState.detailRows.map(item =>
|
||||
item.id !== row.id
|
||||
? item
|
||||
: {
|
||||
@ -426,7 +460,7 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
const nextLandScale = getMethodTotalFromRows(clearedRows, 'landScale')
|
||||
const nextWorkload = getMethodTotalFromRows(clearedRows, 'workload')
|
||||
const nextHourly = getMethodTotalFromRows(clearedRows, 'hourly')
|
||||
detailRows.value = clearedRows.map(item =>
|
||||
const nextRows = clearedRows.map(item =>
|
||||
isFixedRow(item)
|
||||
? {
|
||||
...item,
|
||||
@ -438,7 +472,10 @@ const clearRowValues = async (row: DetailRow) => {
|
||||
}
|
||||
: item
|
||||
)
|
||||
await saveToIndexedDB()
|
||||
await setCurrentContractState({
|
||||
...currentState,
|
||||
detailRows: nextRows
|
||||
})
|
||||
}
|
||||
|
||||
const openEditTab = (row: DetailRow) => {
|
||||
@ -682,20 +719,24 @@ const ensurePricingDetailRowsForCurrentSelection = async () => {
|
||||
}
|
||||
|
||||
const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||
const currentState = getCurrentContractState()
|
||||
const targetIds = Array.from(
|
||||
new Set(
|
||||
serviceIds.filter(id =>
|
||||
detailRows.value.some(row => !isFixedRow(row) && String(row.id) === String(id))
|
||||
currentState.detailRows.some(row => !isFixedRow(row) && String(row.id) === String(id))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (targetIds.length === 0) {
|
||||
detailRows.value = applyFixedRowTotals(detailRows.value)
|
||||
await setCurrentContractState({
|
||||
...currentState,
|
||||
detailRows: applyFixedRowTotals(currentState.detailRows)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await persistDefaultPricingMethodDetailRowsForServices({
|
||||
await ensurePricingMethodDetailRowsForServices({
|
||||
contractId: props.contractId,
|
||||
serviceIds: targetIds,
|
||||
options: PRICING_TOTALS_OPTIONS
|
||||
@ -708,7 +749,7 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||
})
|
||||
|
||||
const targetSet = new Set(targetIds.map(id => String(id)))
|
||||
const nextRows = detailRows.value.map(row => {
|
||||
const nextRows = currentState.detailRows.map(row => {
|
||||
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
||||
const totalsRaw = totalsByServiceId.get(String(row.id))
|
||||
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
||||
@ -722,15 +763,19 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||
}
|
||||
})
|
||||
|
||||
detailRows.value = applyFixedRowTotals(nextRows)
|
||||
await setCurrentContractState({
|
||||
...currentState,
|
||||
detailRows: applyFixedRowTotals(nextRows)
|
||||
})
|
||||
}
|
||||
|
||||
const applySelection = (codes: string[]) => {
|
||||
const prevSelectedSet = new Set(selectedIds.value)
|
||||
const applySelection = async (codes: string[]) => {
|
||||
const currentState = getCurrentContractState()
|
||||
const prevSelectedSet = new Set(currentState.selectedIds || [])
|
||||
const uniqueIds = Array.from(new Set(codes)).filter(
|
||||
id => serviceById.value.has(id) && id !== fixedBudgetRow.id
|
||||
)
|
||||
const existingMap = new Map(detailRows.value.map(row => [row.id, row]))
|
||||
const existingMap = new Map(currentState.detailRows.map(row => [row.id, row]))
|
||||
|
||||
const baseRows: DetailRow[] = uniqueIds
|
||||
.map(id => {
|
||||
@ -777,18 +822,21 @@ const applySelection = (codes: string[]) => {
|
||||
tabStore.removeTab(`zxfw-edit-${props.contractId}-${id}`)
|
||||
}
|
||||
|
||||
selectedIds.value = uniqueIds
|
||||
detailRows.value = [...baseRows, fixedRow]
|
||||
await setCurrentContractState({
|
||||
...currentState,
|
||||
selectedIds: uniqueIds,
|
||||
detailRows: applyFixedRowTotals([...baseRows, fixedRow])
|
||||
})
|
||||
}
|
||||
|
||||
const handleServiceSelectionChange = async (ids: string[]) => {
|
||||
const prevIds = [...selectedIds.value]
|
||||
applySelection(ids)
|
||||
const nextSelectedSet = new Set(selectedIds.value)
|
||||
const addedIds = selectedIds.value.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
|
||||
await applySelection(ids)
|
||||
const nextSelectedIds = getCurrentContractState().selectedIds || []
|
||||
const nextSelectedSet = new Set(nextSelectedIds)
|
||||
const addedIds = nextSelectedIds.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
|
||||
await fillPricingTotalsForServiceIds(addedIds)
|
||||
await ensurePricingDetailRowsForCurrentSelection()
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
|
||||
const preparePickerOpen = () => {
|
||||
@ -811,14 +859,7 @@ const handlePickerOpenChange = (open: boolean) => {
|
||||
}
|
||||
|
||||
const confirmPicker = async () => {
|
||||
applySelection(pickerTempIds.value)
|
||||
try {
|
||||
// await fillPricingTotalsForSelectedRows()
|
||||
await saveToIndexedDB()
|
||||
} catch (error) {
|
||||
console.error('confirmPicker failed:', error)
|
||||
await saveToIndexedDB()
|
||||
}
|
||||
await applySelection(pickerTempIds.value)
|
||||
}
|
||||
|
||||
const clearPickerSelection = () => {
|
||||
@ -925,90 +966,30 @@ const handleDragHover = (_code: string) => {
|
||||
applyDragSelectionByRect()
|
||||
}
|
||||
|
||||
const buildPersistDetailRows = () => {
|
||||
const rows = detailRows.value.map(row => ({ ...row }))
|
||||
const nextInvestScale = getMethodTotalFromRows(rows, 'investScale')
|
||||
const nextLandScale = getMethodTotalFromRows(rows, 'landScale')
|
||||
const nextWorkload = getMethodTotalFromRows(rows, 'workload')
|
||||
const nextHourly = getMethodTotalFromRows(rows, 'hourly')
|
||||
const fixedSubtotal = addNumbers(nextInvestScale, nextLandScale, nextWorkload, nextHourly)
|
||||
|
||||
return rows.map(row =>
|
||||
isFixedRow(row)
|
||||
? {
|
||||
...row,
|
||||
investScale: nextInvestScale,
|
||||
landScale: nextLandScale,
|
||||
workload: nextWorkload,
|
||||
hourly: nextHourly,
|
||||
subtotal: fixedSubtotal
|
||||
}
|
||||
: {
|
||||
...row,
|
||||
subtotal: addNumbers(
|
||||
valueOrZero(row.investScale),
|
||||
valueOrZero(row.landScale),
|
||||
valueOrZero(row.workload),
|
||||
valueOrZero(row.hourly)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const saveToIndexedDB = async () => {
|
||||
try {
|
||||
const payload: ZxFwState = {
|
||||
selectedIds: [...selectedIds.value],
|
||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||
}
|
||||
await zxFwPricingStore.setContractState(props.contractId, payload)
|
||||
localSavedVersion.value = zxFwPricingStore.contractVersions[props.contractId] || 0
|
||||
} catch (error) {
|
||||
console.error('saveToIndexedDB failed:', error)
|
||||
}
|
||||
}
|
||||
const loadFromIndexedDB = async () => {
|
||||
if (syncingFromStore.value) return
|
||||
syncingFromStore.value = true
|
||||
const initializeContractState = async () => {
|
||||
try {
|
||||
await zxFwPricingStore.loadContract(props.contractId)
|
||||
const data = zxFwPricingStore.getContractState(props.contractId)
|
||||
if (!data) {
|
||||
selectedIds.value = []
|
||||
detailRows.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const idsFromStorage = data.selectedIds
|
||||
|| (data.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
|
||||
applySelection(idsFromStorage)
|
||||
|
||||
const savedRowMap = new Map((data.detailRows || []).map(row => [row.id, row]))
|
||||
detailRows.value = detailRows.value.map(row => {
|
||||
const old = savedRowMap.get(row.id)
|
||||
if (!old) return row
|
||||
const nextValues = sanitizePricingFieldsByService(row.id, {
|
||||
investScale: typeof old.investScale === 'number' ? old.investScale : null,
|
||||
landScale: typeof old.landScale === 'number' ? old.landScale : null,
|
||||
workload: typeof old.workload === 'number' ? old.workload : null,
|
||||
hourly: typeof old.hourly === 'number' ? old.hourly : null
|
||||
})
|
||||
return {
|
||||
...row,
|
||||
investScale: nextValues.investScale,
|
||||
landScale: nextValues.landScale,
|
||||
workload: nextValues.workload,
|
||||
hourly: nextValues.hourly
|
||||
}
|
||||
})
|
||||
detailRows.value = applyFixedRowTotals(detailRows.value)
|
||||
const idsFromStorage = data?.selectedIds
|
||||
|| (data?.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
|
||||
await applySelection(idsFromStorage || [])
|
||||
await ensurePricingDetailRowsForCurrentSelection()
|
||||
} catch (error) {
|
||||
console.error('loadFromIndexedDB failed:', error)
|
||||
selectedIds.value = []
|
||||
detailRows.value = []
|
||||
} finally {
|
||||
syncingFromStore.value = false
|
||||
console.error('initializeContractState failed:', error)
|
||||
await setCurrentContractState({
|
||||
selectedIds: [],
|
||||
detailRows: applyFixedRowTotals([{
|
||||
id: fixedBudgetRow.id,
|
||||
code: fixedBudgetRow.code,
|
||||
name: fixedBudgetRow.name,
|
||||
investScale: null,
|
||||
landScale: null,
|
||||
workload: null,
|
||||
hourly: null,
|
||||
subtotal: null,
|
||||
actions: null
|
||||
}])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1023,46 +1004,28 @@ const loadProjectIndustry = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => zxFwStoreVersion.value,
|
||||
(nextVersion, prevVersion) => {
|
||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||
if (nextVersion === localSavedVersion.value) return
|
||||
void loadFromIndexedDB()
|
||||
}
|
||||
)
|
||||
|
||||
watch(serviceIdSignature, () => {
|
||||
const availableIds = new Set(serviceDict.value.map(item => item.id))
|
||||
const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
|
||||
if (nextSelectedIds.length !== selectedIds.value.length) {
|
||||
applySelection(nextSelectedIds)
|
||||
void saveToIndexedDB()
|
||||
void applySelection(nextSelectedIds)
|
||||
}
|
||||
})
|
||||
|
||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleCellValueChanged = () => {
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
gridPersistTimer = setTimeout(() => {
|
||||
void saveToIndexedDB()
|
||||
}, 300)
|
||||
}
|
||||
const handleCellValueChanged = () => {}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadProjectIndustry()
|
||||
await loadFromIndexedDB()
|
||||
await initializeContractState()
|
||||
})
|
||||
|
||||
onActivated(async () => {
|
||||
await loadProjectIndustry()
|
||||
await loadFromIndexedDB()
|
||||
await initializeContractState()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopDragSelect()
|
||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
||||
void saveToIndexedDB()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1118,7 +1081,7 @@ onBeforeUnmount(() => {
|
||||
class="fixed left-1/2 top-1/2 z-[70] w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
|
||||
<AlertDialogTitle class="text-base font-semibold">确认删除服务</AlertDialogTitle>
|
||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||
将逻辑删除“{{ pendingDeleteServiceName }}”,已填写的数据不会清楚,重新勾选后会恢复,是否继续?
|
||||
将逻辑删除“{{ pendingDeleteServiceName }}”,已填写的数据不会清空,重新勾选后会恢复,是否继续?
|
||||
</AlertDialogDescription>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<AlertDialogCancel as-child>
|
||||
|
||||
@ -3,10 +3,13 @@ import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onM
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { useTabStore } from '@/pinia/tab'
|
||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import localforage from 'localforage'
|
||||
import {
|
||||
AlertDialogAction,
|
||||
@ -348,6 +351,7 @@ const componentMap: Record<string, any> = {
|
||||
}
|
||||
|
||||
const tabStore = useTabStore()
|
||||
const zxFwPricingStore = useZxFwPricingStore()
|
||||
|
||||
|
||||
|
||||
@ -1098,12 +1102,16 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
||||
const serviceId = toSafeInteger(serviceIdText)
|
||||
if (serviceId == null) return null
|
||||
|
||||
const [method1Raw, method2Raw, method3Raw, method4Raw] = await Promise.all([
|
||||
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`tzGMF-${contractId}-${serviceIdText}`),
|
||||
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`ydGMF-${contractId}-${serviceIdText}`),
|
||||
localforage.getItem<DetailRowsStorageLike<WorkloadMethodRowLike>>(`gzlF-${contractId}-${serviceIdText}`),
|
||||
localforage.getItem<DetailRowsStorageLike<HourlyMethodRowLike>>(`hourlyPricing-${contractId}-${serviceIdText}`)
|
||||
const [method1State, method2State, method3State, method4State] = await Promise.all([
|
||||
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale'),
|
||||
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'landScale'),
|
||||
zxFwPricingStore.loadServicePricingMethodState<WorkloadMethodRowLike>(contractId, serviceIdText, 'workload'),
|
||||
zxFwPricingStore.loadServicePricingMethodState<HourlyMethodRowLike>(contractId, serviceIdText, 'hourly')
|
||||
])
|
||||
const method1Raw = method1State ? { detailRows: method1State.detailRows } : null
|
||||
const method2Raw = method2State ? { detailRows: method2State.detailRows } : null
|
||||
const method3Raw = method3State ? { detailRows: method3State.detailRows } : null
|
||||
const method4Raw = method4State ? { detailRows: method4State.detailRows } : null
|
||||
|
||||
const method1 = buildMethod1(method1Raw?.detailRows)
|
||||
const method2 = buildMethod2(method2Raw?.detailRows)
|
||||
@ -1255,6 +1263,7 @@ const confirmImportOverride = async () => {
|
||||
await writeForage(localforage, normalizeEntries(payload.localforageDefault))
|
||||
|
||||
tabStore.resetTabs()
|
||||
await tabStore.$persistNow?.()
|
||||
dataMenuOpen.value = false
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
@ -1275,6 +1284,7 @@ const handleReset = async () => {
|
||||
console.error('reset failed:', error)
|
||||
} finally {
|
||||
tabStore.resetTabs()
|
||||
await tabStore.$persistNow?.()
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
|
||||
import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing'
|
||||
|
||||
interface StoredDetailRowsState<T = any> {
|
||||
detailRows?: T[]
|
||||
@ -112,6 +113,22 @@ interface PricingMethodDefaultBuildContext {
|
||||
}
|
||||
|
||||
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||
const SERVICE_PRICING_METHODS: ServicePricingMethod[] = ['investScale', 'landScale', 'workload', 'hourly']
|
||||
|
||||
const getZxFwStoreSafely = () => {
|
||||
try {
|
||||
return useZxFwPricingStore()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const toStoredDetailRowsState = <TRow = unknown>(state: { detailRows?: TRow[] } | null | undefined): StoredDetailRowsState<TRow> | null => {
|
||||
if (!state || !Array.isArray(state.detailRows)) return null
|
||||
return {
|
||||
detailRows: JSON.parse(JSON.stringify(state.detailRows))
|
||||
}
|
||||
}
|
||||
|
||||
const hasOwn = (obj: unknown, key: string) =>
|
||||
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
||||
@ -550,6 +567,15 @@ export const getPricingMethodDetailDbKeys = (
|
||||
serviceId: string | number
|
||||
): PricingMethodDetailDbKeys => {
|
||||
const normalizedServiceId = String(serviceId)
|
||||
const store = getZxFwStoreSafely()
|
||||
if (store) {
|
||||
return {
|
||||
investScale: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'investScale'),
|
||||
landScale: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'landScale'),
|
||||
workload: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'workload'),
|
||||
hourly: store.getServicePricingStorageKey(contractId, normalizedServiceId, 'hourly')
|
||||
}
|
||||
}
|
||||
return {
|
||||
investScale: `tzGMF-${contractId}-${normalizedServiceId}`,
|
||||
landScale: `ydGMF-${contractId}-${normalizedServiceId}`,
|
||||
@ -628,12 +654,19 @@ export const persistDefaultPricingMethodDetailRowsForServices = async (params: {
|
||||
if (uniqueServiceIds.length === 0) return
|
||||
|
||||
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
|
||||
const store = getZxFwStoreSafely()
|
||||
|
||||
await Promise.all(
|
||||
uniqueServiceIds.map(async serviceId => {
|
||||
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
||||
const defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
|
||||
console.log(dbKeys,defaultRows)
|
||||
if (store) {
|
||||
for (const method of SERVICE_PRICING_METHODS) {
|
||||
store.setServicePricingMethodState(params.contractId, serviceId, method, {
|
||||
detailRows: defaultRows[method]
|
||||
}, { force: true })
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
localforage.setItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
|
||||
localforage.setItem(dbKeys.landScale, { detailRows: defaultRows.landScale }),
|
||||
@ -654,22 +687,32 @@ export const getPricingMethodTotalsForService = async (params: {
|
||||
const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
|
||||
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
|
||||
const baseInfoDbKey = 'xm-base-info-v1'
|
||||
const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
|
||||
const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
|
||||
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
|
||||
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
|
||||
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
||||
const store = getZxFwStoreSafely()
|
||||
|
||||
const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
|
||||
localforage.getItem<StoredDetailRowsState>(investDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(landDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
|
||||
const [storeInvestData, storeLandData, storeWorkloadData, storeHourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'investScale') || Promise.resolve(null),
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'hourly') || Promise.resolve(null),
|
||||
localforage.getItem<StoredDetailRowsState>(htDbKey),
|
||||
localforage.getItem<StoredFactorState>(consultFactorDbKey),
|
||||
localforage.getItem<StoredFactorState>(majorFactorDbKey),
|
||||
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
|
||||
])
|
||||
|
||||
const [investDataFallback, landDataFallback, workloadDataFallback, hourlyDataFallback] = await Promise.all([
|
||||
storeInvestData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.investScale),
|
||||
storeLandData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.landScale),
|
||||
storeWorkloadData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.workload),
|
||||
storeHourlyData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.hourly)
|
||||
])
|
||||
|
||||
const investData = toStoredDetailRowsState(storeInvestData) || investDataFallback
|
||||
const landData = toStoredDetailRowsState(storeLandData) || landDataFallback
|
||||
const workloadData = toStoredDetailRowsState(storeWorkloadData) || workloadDataFallback
|
||||
const hourlyData = toStoredDetailRowsState(storeHourlyData) || hourlyDataFallback
|
||||
|
||||
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
|
||||
const majorFactorMap = buildMajorFactorMap(majorFactorData)
|
||||
const onlyCostScale = isOnlyCostScaleService(serviceId)
|
||||
@ -744,16 +787,27 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
|
||||
if (uniqueServiceIds.length === 0) return
|
||||
|
||||
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
|
||||
const store = getZxFwStoreSafely()
|
||||
|
||||
await Promise.all(
|
||||
uniqueServiceIds.map(async serviceId => {
|
||||
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
||||
const [investData, landData, workloadData, hourlyData] = await Promise.all([
|
||||
localforage.getItem<StoredDetailRowsState>(dbKeys.investScale),
|
||||
localforage.getItem<StoredDetailRowsState>(dbKeys.landScale),
|
||||
localforage.getItem<StoredDetailRowsState>(dbKeys.workload),
|
||||
localforage.getItem<StoredDetailRowsState>(dbKeys.hourly)
|
||||
const [storeInvestData, storeLandData, storeWorkloadData, storeHourlyData] = await Promise.all([
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'investScale') || Promise.resolve(null),
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
|
||||
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'hourly') || Promise.resolve(null)
|
||||
])
|
||||
const [investDataFallback, landDataFallback, workloadDataFallback, hourlyDataFallback] = await Promise.all([
|
||||
storeInvestData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.investScale),
|
||||
storeLandData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.landScale),
|
||||
storeWorkloadData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.workload),
|
||||
storeHourlyData ? Promise.resolve(null) : localforage.getItem<StoredDetailRowsState>(dbKeys.hourly)
|
||||
])
|
||||
const investData = toStoredDetailRowsState(storeInvestData) || investDataFallback
|
||||
const landData = toStoredDetailRowsState(storeLandData) || landDataFallback
|
||||
const workloadData = toStoredDetailRowsState(storeWorkloadData) || workloadDataFallback
|
||||
const hourlyData = toStoredDetailRowsState(storeHourlyData) || hourlyDataFallback
|
||||
|
||||
const shouldInitInvest = !Array.isArray(investData?.detailRows) || investData!.detailRows!.length === 0
|
||||
const shouldInitLand = !Array.isArray(landData?.detailRows) || landData!.detailRows!.length === 0
|
||||
@ -770,18 +824,38 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
|
||||
}
|
||||
|
||||
if (shouldInitInvest) {
|
||||
if (store) {
|
||||
store.setServicePricingMethodState(params.contractId, serviceId, 'investScale', {
|
||||
detailRows: getDefaultRows().investScale
|
||||
}, { force: true })
|
||||
}
|
||||
writeTasks.push(localforage.setItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
|
||||
}
|
||||
|
||||
if (shouldInitLand) {
|
||||
if (store) {
|
||||
store.setServicePricingMethodState(params.contractId, serviceId, 'landScale', {
|
||||
detailRows: getDefaultRows().landScale
|
||||
}, { force: true })
|
||||
}
|
||||
writeTasks.push(localforage.setItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
|
||||
}
|
||||
|
||||
if (shouldInitWorkload) {
|
||||
if (store) {
|
||||
store.setServicePricingMethodState(params.contractId, serviceId, 'workload', {
|
||||
detailRows: getDefaultRows().workload
|
||||
}, { force: true })
|
||||
}
|
||||
writeTasks.push(localforage.setItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
|
||||
}
|
||||
|
||||
if (shouldInitHourly) {
|
||||
if (store) {
|
||||
store.setServicePricingMethodState(params.contractId, serviceId, 'hourly', {
|
||||
detailRows: getDefaultRows().hourly
|
||||
}, { force: true })
|
||||
}
|
||||
writeTasks.push(localforage.setItem(dbKeys.hourly, { detailRows: getDefaultRows().hourly }))
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,6 @@ import { useZxFwPricingStore, type ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||
|
||||
export type { ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||
|
||||
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
|
||||
|
||||
export const syncPricingTotalToZxFw = async (params: {
|
||||
contractId: string
|
||||
serviceId: string | number
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
TreeDataModule,ContextMenuModule,ValidationModule
|
||||
} from 'ag-grid-enterprise'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
import piniaPersistedstate from '@/pinia/Plugin/indexdb'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
@ -51,7 +51,12 @@ const AG_GRID_MODULES = [
|
||||
]
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
pinia.use(
|
||||
piniaPersistedstate({
|
||||
storeName: 'pinia',
|
||||
mode: 'multiple'
|
||||
})
|
||||
)
|
||||
|
||||
// 在应用启动时一次性注册 AG Grid 运行所需模块。
|
||||
ModuleRegistry.registerModules(AG_GRID_MODULES)
|
||||
|
||||
124
src/pinia/Plugin/indexdb.ts
Normal file
124
src/pinia/Plugin/indexdb.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import type { PiniaPluginContext } from 'pinia'
|
||||
import localforage from 'localforage'
|
||||
|
||||
export type PersistOption = boolean | {
|
||||
key?: string
|
||||
debounce?: number
|
||||
}
|
||||
|
||||
type PersistStoreExt = {
|
||||
$persistNow?: () => Promise<void>
|
||||
$clearPersisted?: () => Promise<void>
|
||||
}
|
||||
|
||||
type PiniaStorageConfig = LocalForageOptions & {
|
||||
mode?: 'single' | 'multiple'
|
||||
debounce?: number
|
||||
}
|
||||
|
||||
const baseConfig: PiniaStorageConfig = {
|
||||
driver: localforage.INDEXEDDB,
|
||||
name: 'DB',
|
||||
version: 1,
|
||||
description: 'pinia persisted storage',
|
||||
storeName: 'pinia-storage',
|
||||
debounce: 300
|
||||
}
|
||||
|
||||
let singleStore: LocalForage | null = null
|
||||
const multipleStores = new Map<string, LocalForage>()
|
||||
|
||||
const getStore = (mode: 'single' | 'multiple', storeName: string, config: LocalForageOptions) => {
|
||||
if (mode === 'single') {
|
||||
if (!singleStore) {
|
||||
singleStore = localforage.createInstance({ ...baseConfig, ...config })
|
||||
}
|
||||
return singleStore
|
||||
}
|
||||
if (!multipleStores.has(storeName)) {
|
||||
multipleStores.set(
|
||||
storeName,
|
||||
localforage.createInstance({ ...baseConfig, ...config, storeName })
|
||||
)
|
||||
}
|
||||
return multipleStores.get(storeName) as LocalForage
|
||||
}
|
||||
|
||||
const resolvePersistOptions = (persist: PersistOption | undefined) => {
|
||||
if (!persist) return null
|
||||
if (persist === true) return {}
|
||||
return persist
|
||||
}
|
||||
|
||||
export default (config?: PiniaStorageConfig) => {
|
||||
const finalConfig = { ...baseConfig, ...(config || {}) }
|
||||
const { mode = 'single', debounce = 300, ...forageConfig } = finalConfig
|
||||
|
||||
return (context: PiniaPluginContext) => {
|
||||
const persistOptions = resolvePersistOptions(context.options.persist as PersistOption | undefined)
|
||||
if (!persistOptions) return
|
||||
|
||||
const storeId = context.store.$id
|
||||
const baseStoreName = forageConfig.storeName || baseConfig.storeName || 'pinia-storage'
|
||||
const resolvedStoreName = mode === 'multiple' ? `${baseStoreName}-${storeId}` : baseStoreName
|
||||
const key = persistOptions.key || (mode === 'single' ? `${baseStoreName}-${storeId}` : resolvedStoreName)
|
||||
const lf = getStore(mode, resolvedStoreName, forageConfig)
|
||||
const persistDebounce = persistOptions.debounce ?? debounce
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
let hydrating = false
|
||||
let userMutatedBeforeHydrate = false
|
||||
const writeState = (state: unknown) => lf.setItem(key, JSON.parse(JSON.stringify(state)))
|
||||
const storeExt = context.store as typeof context.store & PersistStoreExt
|
||||
|
||||
storeExt.$persistNow = async () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
try {
|
||||
await writeState(context.store.$state)
|
||||
} catch (error) {
|
||||
console.error('pinia persist failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
storeExt.$clearPersisted = async () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
try {
|
||||
await lf.removeItem(key)
|
||||
} catch (error) {
|
||||
console.error('pinia clear persisted failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
context.store.$subscribe(
|
||||
(_mutation, state) => {
|
||||
if (!hydrating) userMutatedBeforeHydrate = true
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
void writeState(state).catch(error => {
|
||||
console.error('pinia persist failed:', error)
|
||||
})
|
||||
}, Math.max(0, persistDebounce))
|
||||
},
|
||||
{ detached: true }
|
||||
)
|
||||
|
||||
void lf.getItem<Record<string, unknown>>(key)
|
||||
.then(state => {
|
||||
if (!state || typeof state !== 'object') return
|
||||
// 若在异步hydrate返回前,store已被用户修改(如removeTab),不再回填旧缓存覆盖当前状态。
|
||||
if (userMutatedBeforeHydrate) return
|
||||
hydrating = true
|
||||
context.store.$patch(state as any)
|
||||
hydrating = false
|
||||
})
|
||||
.catch(error => {
|
||||
hydrating = false
|
||||
console.error('pinia hydrate failed:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
13
src/pinia/Plugin/types.d.ts
vendored
Normal file
13
src/pinia/Plugin/types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import 'pinia'
|
||||
import type { PersistOption } from './indexdb'
|
||||
|
||||
declare module 'pinia' {
|
||||
export interface DefineStoreOptionsBase<S, Store> {
|
||||
persist?: PersistOption
|
||||
}
|
||||
|
||||
export interface PiniaCustomProperties {
|
||||
$persistNow?: () => Promise<void>
|
||||
$clearPersisted?: () => Promise<void>
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface HtFeeMethodReloadDetail {
|
||||
mainStorageKey: string
|
||||
rowId: string
|
||||
at: number
|
||||
}
|
||||
|
||||
interface HtFeeMethodReloadState {
|
||||
seq: number
|
||||
lastEvent: HtFeeMethodReloadDetail | null
|
||||
}
|
||||
|
||||
export const useHtFeeMethodReloadStore = defineStore('htFeeMethodReload', {
|
||||
state: (): HtFeeMethodReloadState => ({
|
||||
seq: 0,
|
||||
lastEvent: null
|
||||
}),
|
||||
actions: {
|
||||
emit(mainStorageKey: string, rowId: string) {
|
||||
|
||||
const normalizedMainStorageKey = String(mainStorageKey || '').trim()
|
||||
const normalizedRowId = String(rowId || '').trim()
|
||||
if (!normalizedMainStorageKey || !normalizedRowId) return
|
||||
this.lastEvent = {
|
||||
mainStorageKey: normalizedMainStorageKey,
|
||||
rowId: normalizedRowId,
|
||||
at: Date.now()
|
||||
}
|
||||
this.seq += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface PricingPaneReloadDetail {
|
||||
contractId: string
|
||||
serviceId: string
|
||||
at: number
|
||||
}
|
||||
|
||||
interface PricingPaneReloadState {
|
||||
seq: number
|
||||
lastEvent: PricingPaneReloadDetail | null
|
||||
persistedSeq: number
|
||||
lastPersistedEvent: PricingPaneReloadDetail | null
|
||||
}
|
||||
|
||||
const toKey = (value: string | number) => String(value)
|
||||
|
||||
export const usePricingPaneReloadStore = defineStore('pricingPaneReload', {
|
||||
state: (): PricingPaneReloadState => ({
|
||||
seq: 0,
|
||||
lastEvent: null,
|
||||
persistedSeq: 0,
|
||||
lastPersistedEvent: null
|
||||
}),
|
||||
actions: {
|
||||
emit(contractId: string, serviceId: string | number) {
|
||||
this.lastEvent = {
|
||||
contractId: toKey(contractId),
|
||||
serviceId: toKey(serviceId),
|
||||
at: Date.now()
|
||||
}
|
||||
this.seq += 1
|
||||
},
|
||||
emitPersisted(contractId: string, serviceId: string | number) {
|
||||
this.lastPersistedEvent = {
|
||||
contractId: toKey(contractId),
|
||||
serviceId: toKey(serviceId),
|
||||
at: Date.now()
|
||||
}
|
||||
this.persistedSeq += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const matchPricingPaneReload = (
|
||||
detail: PricingPaneReloadDetail | null | undefined,
|
||||
contractId: string,
|
||||
serviceId: string | number
|
||||
) => {
|
||||
if (!detail) return false
|
||||
return detail.contractId === toKey(contractId) && detail.serviceId === toKey(serviceId)
|
||||
}
|
||||
@ -65,6 +65,7 @@ export const useTabStore = defineStore(
|
||||
|
||||
const closeAllTabs = () => {
|
||||
tabs.value = createDefaultTabs()
|
||||
console.log(tabs.value)
|
||||
activeTabId.value = HOME_TAB_ID
|
||||
}
|
||||
|
||||
@ -106,10 +107,6 @@ export const useTabStore = defineStore(
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'tabs',
|
||||
storage: localStorage,
|
||||
pick: ['tabs', 'activeTabId']
|
||||
}
|
||||
persist: true
|
||||
}
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ import { addNumbers } from '@/lib/decimal'
|
||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||
|
||||
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||
export type ServicePricingMethod = ZxFwPricingField
|
||||
|
||||
export interface ZxFwDetailRow {
|
||||
id: string
|
||||
@ -24,10 +25,44 @@ export interface ZxFwState {
|
||||
detailRows: ZxFwDetailRow[]
|
||||
}
|
||||
|
||||
export interface ServicePricingMethodState<TRow = unknown> {
|
||||
detailRows: TRow[]
|
||||
projectCount?: number | null
|
||||
}
|
||||
|
||||
export interface ServicePricingState {
|
||||
investScale?: ServicePricingMethodState
|
||||
landScale?: ServicePricingMethodState
|
||||
workload?: ServicePricingMethodState
|
||||
hourly?: ServicePricingMethodState
|
||||
}
|
||||
|
||||
export type HtFeeMethodType = 'rate-fee' | 'hourly-fee' | 'quantity-unit-price-fee'
|
||||
|
||||
export interface HtFeeMainState<TRow = unknown> {
|
||||
detailRows: TRow[]
|
||||
}
|
||||
|
||||
export type HtFeeMethodPayload = unknown
|
||||
|
||||
const FIXED_ROW_ID = 'fixed-budget-c'
|
||||
const METHOD_STORAGE_PREFIX_MAP: Record<ServicePricingMethod, string> = {
|
||||
investScale: 'tzGMF',
|
||||
landScale: 'ydGMF',
|
||||
workload: 'gzlF',
|
||||
hourly: 'hourlyPricing'
|
||||
}
|
||||
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()
|
||||
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))
|
||||
const toNumberOrZero = (value: unknown) => {
|
||||
const numeric = Number(value)
|
||||
@ -99,30 +134,648 @@ const cloneState = (state: ZxFwState): ZxFwState => ({
|
||||
detailRows: state.detailRows.map(row => ({ ...row }))
|
||||
})
|
||||
|
||||
const loadTasks = new Map<string, Promise<ZxFwState>>()
|
||||
const persistQueues = new Map<string, Promise<void>>()
|
||||
const isSameStringArray = (a: string[] | undefined, b: string[] | undefined) => {
|
||||
const left = Array.isArray(a) ? a : []
|
||||
const right = Array.isArray(b) ? b : []
|
||||
if (left.length !== right.length) return false
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
if (left[i] !== right[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const isSameNullableNumber = (a: number | null | undefined, b: number | null | undefined) => {
|
||||
const left = toFiniteNumberOrNull(a)
|
||||
const right = toFiniteNumberOrNull(b)
|
||||
return left === right
|
||||
}
|
||||
|
||||
const isSameRows = (a: ZxFwDetailRow[] | undefined, b: ZxFwDetailRow[] | undefined) => {
|
||||
const left = Array.isArray(a) ? a : []
|
||||
const right = Array.isArray(b) ? b : []
|
||||
if (left.length !== right.length) return false
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
const l = left[i]
|
||||
const r = right[i]
|
||||
if (!l || !r) return false
|
||||
if (l.id !== r.id) return false
|
||||
if ((l.code || '') !== (r.code || '')) return false
|
||||
if ((l.name || '') !== (r.name || '')) return false
|
||||
if (!isSameNullableNumber(l.investScale, r.investScale)) return false
|
||||
if (!isSameNullableNumber(l.landScale, r.landScale)) return false
|
||||
if (!isSameNullableNumber(l.workload, r.workload)) return false
|
||||
if (!isSameNullableNumber(l.hourly, r.hourly)) return false
|
||||
if (!isSameNullableNumber(l.subtotal, r.subtotal)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const isSameState = (a: ZxFwState | null | undefined, b: ZxFwState | null | undefined) => {
|
||||
if (!a || !b) return false
|
||||
if (!isSameStringArray(a.selectedIds, b.selectedIds)) return false
|
||||
if (!isSameStringArray(a.selectedCodes, b.selectedCodes)) return false
|
||||
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
|
||||
return Math.max(1, Math.floor(numeric))
|
||||
}
|
||||
|
||||
const normalizeServiceMethodState = (
|
||||
payload: Partial<ServicePricingMethodState> | null | undefined
|
||||
): ServicePricingMethodState => ({
|
||||
detailRows: Array.isArray(payload?.detailRows) ? cloneAny(payload.detailRows) : [],
|
||||
projectCount: normalizeProjectCount(payload?.projectCount)
|
||||
})
|
||||
|
||||
const parseServiceMethodStorageKey = (keyRaw: string | number) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return null
|
||||
const firstDash = key.indexOf('-')
|
||||
if (firstDash <= 0 || firstDash >= key.length - 1) return null
|
||||
const prefix = key.slice(0, firstDash)
|
||||
const method = STORAGE_PREFIX_METHOD_MAP.get(prefix)
|
||||
if (!method) return null
|
||||
const rest = key.slice(firstDash + 1)
|
||||
const splitIndex = rest.lastIndexOf('-')
|
||||
if (splitIndex <= 0 || splitIndex >= rest.length - 1) return null
|
||||
const contractId = rest.slice(0, splitIndex).trim()
|
||||
const serviceId = rest.slice(splitIndex + 1).trim()
|
||||
if (!contractId || !serviceId) return null
|
||||
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
|
||||
}
|
||||
|
||||
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 touchVersion = (contractId: string) => {
|
||||
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
|
||||
}
|
||||
const touchKeyVersion = (key: string) => {
|
||||
keyVersions.value[key] = (keyVersions.value[key] || 0) + 1
|
||||
}
|
||||
|
||||
const queuePersist = async (contractId: string) => {
|
||||
const prev = persistQueues.get(contractId) || Promise.resolve()
|
||||
const next = prev
|
||||
.then(async () => {
|
||||
const current = contracts.value[contractId]
|
||||
if (!current) return
|
||||
await localforage.setItem<ZxFwState>(dbKeyOf(contractId), cloneState(current))
|
||||
const ensureServicePricingState = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return null
|
||||
if (!servicePricingStates.value[contractId]) {
|
||||
servicePricingStates.value[contractId] = {}
|
||||
}
|
||||
if (!servicePricingStates.value[contractId][serviceId]) {
|
||||
servicePricingStates.value[contractId][serviceId] = {}
|
||||
}
|
||||
return servicePricingStates.value[contractId][serviceId]
|
||||
}
|
||||
|
||||
const setServiceMethodStateInMemory = (
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
method: ServicePricingMethod,
|
||||
payload: Partial<ServicePricingMethodState> | null | undefined
|
||||
) => {
|
||||
const state = ensureServicePricingState(contractIdRaw, serviceIdRaw)
|
||||
if (!state) return null
|
||||
if (!payload) {
|
||||
delete state[method]
|
||||
return null
|
||||
}
|
||||
state[method] = normalizeServiceMethodState(payload)
|
||||
return state[method] || null
|
||||
}
|
||||
|
||||
const getServicePricingMethodState = <TRow = unknown>(
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
method: ServicePricingMethod
|
||||
) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return null
|
||||
return (servicePricingStates.value[contractId]?.[serviceId]?.[method] as ServicePricingMethodState<TRow> | undefined) || null
|
||||
}
|
||||
|
||||
const setServicePricingMethodState = <TRow = unknown>(
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
method: ServicePricingMethod,
|
||||
payload: Partial<ServicePricingMethodState<TRow>> | null | undefined,
|
||||
options?: {
|
||||
force?: boolean
|
||||
syncKeyState?: boolean
|
||||
}
|
||||
) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return false
|
||||
|
||||
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||
const force = options?.force === true
|
||||
const syncKeyState = options?.syncKeyState !== false
|
||||
const normalizedPayload = payload == null ? null : normalizeServiceMethodState(payload)
|
||||
const prevSnapshot = toKeySnapshot(getServicePricingMethodState(contractId, serviceId, method))
|
||||
const nextSnapshot = toKeySnapshot(normalizedPayload)
|
||||
if (!force && prevSnapshot === nextSnapshot) return false
|
||||
|
||||
setServiceMethodStateInMemory(contractId, serviceId, method, normalizedPayload)
|
||||
|
||||
if (syncKeyState) {
|
||||
if (normalizedPayload == null) {
|
||||
delete keyedStates.value[storageKey]
|
||||
keyedLoaded.value[storageKey] = true
|
||||
keySnapshots.value[storageKey] = toKeySnapshot(null)
|
||||
touchKeyVersion(storageKey)
|
||||
} else {
|
||||
keyedStates.value[storageKey] = cloneAny(normalizedPayload)
|
||||
keyedLoaded.value[storageKey] = true
|
||||
keySnapshots.value[storageKey] = toKeySnapshot(normalizedPayload)
|
||||
touchKeyVersion(storageKey)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const loadServicePricingMethodState = async <TRow = unknown>(
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
method: ServicePricingMethod,
|
||||
force = false
|
||||
): Promise<ServicePricingMethodState<TRow> | null> => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return null
|
||||
|
||||
if (!force) {
|
||||
const existing = getServicePricingMethodState<TRow>(contractId, serviceId, method)
|
||||
if (existing) return existing
|
||||
}
|
||||
|
||||
const storageKey = serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||
const payload = await loadKeyState<ServicePricingMethodState<TRow>>(storageKey, force)
|
||||
if (!payload) {
|
||||
setServiceMethodStateInMemory(contractId, serviceId, method, null)
|
||||
return null
|
||||
}
|
||||
setServicePricingMethodState(contractId, serviceId, method, payload, { force: true, syncKeyState: false })
|
||||
return getServicePricingMethodState<TRow>(contractId, serviceId, method)
|
||||
}
|
||||
|
||||
const removeServicePricingMethodState = (
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
method: ServicePricingMethod
|
||||
) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return false
|
||||
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)
|
||||
return had
|
||||
}
|
||||
|
||||
const getServicePricingStorageKey = (
|
||||
contractIdRaw: string | number,
|
||||
serviceIdRaw: string | number,
|
||||
method: ServicePricingMethod
|
||||
) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return ''
|
||||
return serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||
}
|
||||
|
||||
const getServicePricingStorageKeys = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
const serviceId = toServiceKey(serviceIdRaw)
|
||||
if (!contractId || !serviceId) return [] as string[]
|
||||
return (Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]).map(method =>
|
||||
serviceMethodDbKeyOf(contractId, serviceId, method)
|
||||
)
|
||||
}
|
||||
|
||||
const removeAllServicePricingMethodStates = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||
let changed = false
|
||||
for (const method of Object.keys(METHOD_STORAGE_PREFIX_MAP) as ServicePricingMethod[]) {
|
||||
changed = removeServicePricingMethodState(contractIdRaw, serviceIdRaw, method) || changed
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
) => {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
) => {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const removeHtFeeMethodState = (
|
||||
mainStorageKeyRaw: string | number,
|
||||
rowIdRaw: string | number,
|
||||
method: HtFeeMethodType
|
||||
) => setHtFeeMethodState(mainStorageKeyRaw, rowIdRaw, method, null)
|
||||
|
||||
const getKeyState = <T = unknown>(keyRaw: string | number): T | null => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return null
|
||||
const serviceMeta = parseServiceMethodStorageKey(key)
|
||||
if (serviceMeta) {
|
||||
const methodState = getServicePricingMethodState(
|
||||
serviceMeta.contractId,
|
||||
serviceMeta.serviceId,
|
||||
serviceMeta.method
|
||||
)
|
||||
if (methodState != null) return cloneAny(methodState as T)
|
||||
}
|
||||
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
||||
if (htMethodMeta) {
|
||||
const methodState = getHtFeeMethodState(
|
||||
htMethodMeta.mainStorageKey,
|
||||
htMethodMeta.rowId,
|
||||
htMethodMeta.method
|
||||
)
|
||||
if (methodState != null) return cloneAny(methodState as T)
|
||||
}
|
||||
const htMainMeta = parseHtFeeMainStorageKey(key)
|
||||
if (htMainMeta) {
|
||||
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)
|
||||
}
|
||||
|
||||
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 raw = await localforage.getItem<T>(key)
|
||||
const nextSnapshot = toKeySnapshot(raw)
|
||||
const prevSnapshot = keySnapshots.value[key]
|
||||
keyedLoaded.value[key] = true
|
||||
if (prevSnapshot !== nextSnapshot || !Object.prototype.hasOwnProperty.call(keyedStates.value, key)) {
|
||||
keyedStates.value[key] = cloneAny(raw)
|
||||
keySnapshots.value[key] = nextSnapshot
|
||||
touchKeyVersion(key)
|
||||
}
|
||||
const serviceMeta = parseServiceMethodStorageKey(key)
|
||||
if (serviceMeta) {
|
||||
setServicePricingMethodState(
|
||||
serviceMeta.contractId,
|
||||
serviceMeta.serviceId,
|
||||
serviceMeta.method,
|
||||
raw as Partial<ServicePricingMethodState>,
|
||||
{ force: true, syncKeyState: false }
|
||||
)
|
||||
}
|
||||
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
||||
if (htMethodMeta) {
|
||||
setHtFeeMethodState(
|
||||
htMethodMeta.mainStorageKey,
|
||||
htMethodMeta.rowId,
|
||||
htMethodMeta.method,
|
||||
raw,
|
||||
{ force: true, syncKeyState: false }
|
||||
)
|
||||
}
|
||||
const htMainMeta = parseHtFeeMainStorageKey(key)
|
||||
if (htMainMeta) {
|
||||
setHtFeeMainState(htMainMeta.mainStorageKey, raw as Partial<HtFeeMainState>, { force: true, syncKeyState: false })
|
||||
}
|
||||
return getKeyState<T>(key)
|
||||
})()
|
||||
|
||||
keyLoadTasks.set(key, task)
|
||||
try {
|
||||
return await task
|
||||
} finally {
|
||||
keyLoadTasks.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
const setKeyState = <T = unknown>(
|
||||
keyRaw: string | number,
|
||||
value: T,
|
||||
options?: {
|
||||
force?: 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
|
||||
const serviceMeta = parseServiceMethodStorageKey(key)
|
||||
if (serviceMeta) {
|
||||
setServicePricingMethodState(
|
||||
serviceMeta.contractId,
|
||||
serviceMeta.serviceId,
|
||||
serviceMeta.method,
|
||||
value as Partial<ServicePricingMethodState>,
|
||||
{ force: true, syncKeyState: false }
|
||||
)
|
||||
}
|
||||
const htMethodMeta = parseHtFeeMethodStorageKey(key)
|
||||
if (htMethodMeta) {
|
||||
setHtFeeMethodState(
|
||||
htMethodMeta.mainStorageKey,
|
||||
htMethodMeta.rowId,
|
||||
htMethodMeta.method,
|
||||
value,
|
||||
{ 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
|
||||
}
|
||||
|
||||
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, {
|
||||
force: true,
|
||||
syncKeyState: false
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('zxFwPricing persist failed:', error)
|
||||
})
|
||||
persistQueues.set(contractId, next)
|
||||
await next
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
const getKeyVersion = (keyRaw: string | number) => {
|
||||
const key = toKey(keyRaw)
|
||||
if (!key) return 0
|
||||
return keyVersions.value[key] || 0
|
||||
}
|
||||
|
||||
const getContractState = (contractIdRaw: string | number) => {
|
||||
@ -135,15 +788,25 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return null
|
||||
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>
|
||||
if (!force && loadTasks.has(contractId)) return loadTasks.get(contractId) as Promise<ZxFwState | null>
|
||||
|
||||
const task = (async () => {
|
||||
const raw = await localforage.getItem<ZxFwState>(dbKeyOf(contractId))
|
||||
const normalized = normalizeState(raw)
|
||||
contracts.value[contractId] = normalized
|
||||
touchVersion(contractId)
|
||||
return cloneState(normalized)
|
||||
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) {
|
||||
contracts.value[contractId] = normalizeState(null)
|
||||
touchVersion(contractId)
|
||||
}
|
||||
contractLoaded.value[contractId] = true
|
||||
return getContractState(contractId)
|
||||
})()
|
||||
loadTasks.set(contractId, task)
|
||||
|
||||
@ -156,10 +819,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
|
||||
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
||||
const contractId = toKey(contractIdRaw)
|
||||
if (!contractId) return
|
||||
contracts.value[contractId] = normalizeState(state)
|
||||
if (!contractId) return false
|
||||
const normalized = normalizeState(state)
|
||||
const current = contracts.value[contractId]
|
||||
if (current && isSameState(current, normalized)) return false
|
||||
contracts.value[contractId] = normalized
|
||||
contractLoaded.value[contractId] = true
|
||||
touchVersion(contractId)
|
||||
await queuePersist(contractId)
|
||||
return true
|
||||
}
|
||||
|
||||
const updatePricingField = async (params: {
|
||||
@ -196,12 +863,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
|
||||
if (!changed) return false
|
||||
|
||||
contracts.value[contractId] = normalizeState({
|
||||
const nextState = normalizeState({
|
||||
...current,
|
||||
detailRows: nextRows
|
||||
})
|
||||
if (isSameState(current, nextState)) return false
|
||||
contracts.value[contractId] = nextState
|
||||
contractLoaded.value[contractId] = true
|
||||
touchVersion(contractId)
|
||||
await queuePersist(contractId)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -226,10 +895,39 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||
return {
|
||||
contracts,
|
||||
contractVersions,
|
||||
contractLoaded,
|
||||
servicePricingStates,
|
||||
htFeeMainStates,
|
||||
htFeeMethodStates,
|
||||
keyedStates,
|
||||
keyVersions,
|
||||
getContractState,
|
||||
loadContract,
|
||||
setContractState,
|
||||
updatePricingField,
|
||||
getBaseSubtotal
|
||||
getBaseSubtotal,
|
||||
getKeyState,
|
||||
loadKeyState,
|
||||
setKeyState,
|
||||
removeKeyState,
|
||||
getKeyVersion,
|
||||
getServicePricingMethodState,
|
||||
setServicePricingMethodState,
|
||||
loadServicePricingMethodState,
|
||||
removeServicePricingMethodState,
|
||||
getServicePricingStorageKey,
|
||||
getServicePricingStorageKeys,
|
||||
removeAllServicePricingMethodStates,
|
||||
getHtFeeMainState,
|
||||
setHtFeeMainState,
|
||||
loadHtFeeMainState,
|
||||
removeHtFeeMainState,
|
||||
getHtFeeMethodStorageKey,
|
||||
getHtFeeMethodState,
|
||||
setHtFeeMethodState,
|
||||
loadHtFeeMethodState,
|
||||
removeHtFeeMethodState
|
||||
}
|
||||
}, {
|
||||
persist: true
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user