大改,使用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 { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
import type { ColDef, ColGroupDef, GridApi, GridReadyEvent } from 'ag-grid-community'
|
||||||
import localforage from 'localforage'
|
|
||||||
import { expertList } from '@/sql'
|
import { expertList } from '@/sql'
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { syncPricingTotalToZxFw, type ZxFwPricingField } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw, type ZxFwPricingField } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { useZxFwPricingStore, type HtFeeMethodType, type ServicePricingMethod } from '@/pinia/zxFwPricing'
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
|
||||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
|
|
||||||
interface DetailRow {
|
interface DetailRow {
|
||||||
@ -39,8 +37,9 @@ const props = withDefaults(
|
|||||||
serviceId?: string | number
|
serviceId?: string | number
|
||||||
enableZxFwSync?: boolean
|
enableZxFwSync?: boolean
|
||||||
syncField?: ZxFwPricingField
|
syncField?: ZxFwPricingField
|
||||||
syncMainStorageKey?: string
|
htMainStorageKey?: string
|
||||||
syncRowId?: string
|
htRowId?: string
|
||||||
|
htMethodType?: HtFeeMethodType
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
title: '工时法明细',
|
title: '工时法明细',
|
||||||
@ -48,13 +47,11 @@ const props = withDefaults(
|
|||||||
syncField: 'hourly'
|
syncField: 'hourly'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
|
||||||
|
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
||||||
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
|
||||||
const paneInstanceCreatedAt = Date.now()
|
const paneInstanceCreatedAt = Date.now()
|
||||||
const reloadSignal = ref(0)
|
|
||||||
|
|
||||||
const shouldSkipPersist = () => {
|
const shouldSkipPersist = () => {
|
||||||
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${props.storageKey}`
|
const storageKey = `${PRICING_CLEAR_SKIP_PREFIX}${props.storageKey}`
|
||||||
@ -88,8 +85,66 @@ const shouldForceDefaultLoad = () => {
|
|||||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailRows = ref<DetailRow[]>([])
|
const fallbackDetailRows = ref<DetailRow[]>([])
|
||||||
const gridApi = ref<GridApi<DetailRow> | null>(null)
|
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 = {
|
type ExpertLite = {
|
||||||
code: string
|
code: string
|
||||||
@ -393,7 +448,18 @@ const saveToIndexedDB = async () => {
|
|||||||
detailRows: JSON.parse(JSON.stringify(detailRows.value))
|
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) {
|
if (props.enableZxFwSync && props.contractId && props.serviceId != null) {
|
||||||
const synced = await syncPricingTotalToZxFw({
|
const synced = await syncPricingTotalToZxFw({
|
||||||
@ -404,9 +470,6 @@ const saveToIndexedDB = async () => {
|
|||||||
})
|
})
|
||||||
if (!synced) return
|
if (!synced) return
|
||||||
}
|
}
|
||||||
if (props.syncMainStorageKey && props.syncRowId) {
|
|
||||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
@ -420,7 +483,15 @@ const loadFromIndexedDB = async () => {
|
|||||||
syncServiceBudgetToRows()
|
syncServiceBudgetToRows()
|
||||||
return
|
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) {
|
if (data) {
|
||||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||||
syncServiceBudgetToRows()
|
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 = () => {
|
const handleCellValueChanged = () => {
|
||||||
syncServiceBudgetToRows()
|
syncServiceBudgetToRows()
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
void saveToIndexedDB()
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
|
||||||
@ -505,8 +552,6 @@ onDeactivated(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
gridApi.value?.stopEditing()
|
gridApi.value?.stopEditing()
|
||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<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 { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
import type { ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
||||||
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
import localforage from 'localforage'
|
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { roundTo, toDecimal } from '@/lib/decimal'
|
import { roundTo, toDecimal } from '@/lib/decimal'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { Trash2 } from 'lucide-vue-next'
|
import { Trash2 } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -40,10 +39,11 @@ interface FeeGridState {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
storageKey: string
|
storageKey: string
|
||||||
syncMainStorageKey?: string
|
htMainStorageKey?: string
|
||||||
syncRowId?: 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 createRowId = () => `fee-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
||||||
const SUBTOTAL_ROW_ID = 'fee-subtotal-fixed'
|
const SUBTOTAL_ROW_ID = 'fee-subtotal-fixed'
|
||||||
@ -76,11 +76,35 @@ const ensureSubtotalRow = (rows: FeeRow[]) => {
|
|||||||
return [...normalRows, createSubtotalRow()]
|
return [...normalRows, createSubtotalRow()]
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailRows = ref<FeeRow[]>([])
|
const fallbackDetailRows = ref<FeeRow[]>([])
|
||||||
const gridApi = ref<GridApi<FeeRow> | null>(null)
|
const gridApi = ref<GridApi<FeeRow> | null>(null)
|
||||||
const deleteConfirmOpen = ref(false)
|
const deleteConfirmOpen = ref(false)
|
||||||
const pendingDeleteRowId = ref<string | null>(null)
|
const pendingDeleteRowId = ref<string | null>(null)
|
||||||
const pendingDeleteRowName = ref('')
|
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 addRow = () => {
|
||||||
const normalRows = detailRows.value.filter(row => !isSubtotalRow(row))
|
const normalRows = detailRows.value.filter(row => !isSubtotalRow(row))
|
||||||
@ -193,10 +217,15 @@ const saveToIndexedDB = async () => {
|
|||||||
const payload: FeeGridState = {
|
const payload: FeeGridState = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||||
}
|
}
|
||||||
|
if (useHtMethodState.value) {
|
||||||
await localforage.setItem(props.storageKey, payload)
|
zxFwPricingStore.setHtFeeMethodState(
|
||||||
if (props.syncMainStorageKey && props.syncRowId) {
|
props.htMainStorageKey!,
|
||||||
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)
|
props.htRowId!,
|
||||||
|
props.htMethodType!,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
zxFwPricingStore.setKeyState(props.storageKey, payload)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
@ -205,7 +234,13 @@ const saveToIndexedDB = async () => {
|
|||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
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)
|
detailRows.value = mergeWithStoredRows(data?.detailRows)
|
||||||
syncComputedValuesToRows()
|
syncComputedValuesToRows()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -363,8 +398,6 @@ const detailGridOptions: GridOptions<FeeRow> = {
|
|||||||
treeData: false
|
treeData: false
|
||||||
}
|
}
|
||||||
|
|
||||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
||||||
gridApi.value = event.api
|
gridApi.value = event.api
|
||||||
}
|
}
|
||||||
@ -372,10 +405,7 @@ const handleGridReady = (event: GridReadyEvent<FeeRow>) => {
|
|||||||
const handleCellValueChanged = () => {
|
const handleCellValueChanged = () => {
|
||||||
syncComputedValuesToRows()
|
syncComputedValuesToRows()
|
||||||
gridApi.value?.refreshCells({ force: true })
|
gridApi.value?.refreshCells({ force: true })
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
void saveToIndexedDB()
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -394,7 +424,6 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
|
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue'
|
||||||
import { AgGridVue } from 'ag-grid-vue3'
|
import { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { CellValueChangedEvent, ColDef, GridApi, GridOptions, GridReadyEvent, ICellRendererParams } from 'ag-grid-community'
|
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 { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
@ -10,7 +9,6 @@ import { formatThousandsFlexible } from '@/lib/numberFormat'
|
|||||||
import { Pencil, Eraser } from 'lucide-vue-next'
|
import { Pencil, Eraser } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -80,7 +78,6 @@ const props = defineProps<{
|
|||||||
fixedNames?: string[]
|
fixedNames?: string[]
|
||||||
}>()
|
}>()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
|
||||||
const createRowId = () => `fee-method-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
|
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)
|
const numeric = Number(value)
|
||||||
return Number.isFinite(numeric) ? numeric : null
|
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 loadContractServiceFeeBase = async (): Promise<number | null> => {
|
||||||
const contractId = String(props.contractId || '').trim()
|
const contractId = String(props.contractId || '').trim()
|
||||||
if (!contractId) return null
|
if (!contractId) return null
|
||||||
@ -172,9 +164,9 @@ const hydrateRowsFromMethodStores = async (rows: FeeMethodRow[]): Promise<FeeMet
|
|||||||
rows.map(async row => {
|
rows.map(async row => {
|
||||||
if (!row?.id) return row
|
if (!row?.id) return row
|
||||||
const [rateData, hourlyData, quantityData] = await Promise.all([
|
const [rateData, hourlyData, quantityData] = await Promise.all([
|
||||||
localforage.getItem<MethodRateState>(buildMethodStorageKey(row.id, 'rate-fee')),
|
zxFwPricingStore.loadHtFeeMethodState<MethodRateState>(props.storageKey, row.id, 'rate-fee'),
|
||||||
localforage.getItem<MethodHourlyState>(buildMethodStorageKey(row.id, 'hourly-fee')),
|
zxFwPricingStore.loadHtFeeMethodState<MethodHourlyState>(props.storageKey, row.id, 'hourly-fee'),
|
||||||
localforage.getItem<MethodQuantityState>(buildMethodStorageKey(row.id, 'quantity-unit-price-fee'))
|
zxFwPricingStore.loadHtFeeMethodState<MethodQuantityState>(props.storageKey, row.id, 'quantity-unit-price-fee')
|
||||||
])
|
])
|
||||||
|
|
||||||
const storedRateFee = toFiniteUnknown(rateData?.budgetFee)
|
const storedRateFee = toFiniteUnknown(rateData?.budgetFee)
|
||||||
@ -205,7 +197,17 @@ const fixedNames = computed(() =>
|
|||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
const hasFixedNames = computed(() => fixedNames.value.length > 0)
|
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 summaryRow = computed<FeeMethodRow>(() => {
|
||||||
const totals = detailRows.value.reduce(
|
const totals = detailRows.value.reduce(
|
||||||
(acc, row) => {
|
(acc, row) => {
|
||||||
@ -235,6 +237,7 @@ const gridApi = ref<GridApi<FeeMethodRow> | null>(null)
|
|||||||
const clearConfirmOpen = ref(false)
|
const clearConfirmOpen = ref(false)
|
||||||
const pendingClearRowId = ref<string | null>(null)
|
const pendingClearRowId = ref<string | null>(null)
|
||||||
const pendingClearRowName = ref('')
|
const pendingClearRowName = ref('')
|
||||||
|
const lastSavedSnapshot = ref('')
|
||||||
|
|
||||||
const requestClearRow = (id: string, name?: string) => {
|
const requestClearRow = (id: string, name?: string) => {
|
||||||
pendingClearRowId.value = id
|
pendingClearRowId.value = id
|
||||||
@ -325,12 +328,15 @@ const mergeWithStoredRows = (rowsFromDb: unknown): FeeMethodRow[] => {
|
|||||||
|
|
||||||
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
const buildPersistDetailRows = () => detailRows.value.map(row => ({ ...row }))
|
||||||
|
|
||||||
const saveToIndexedDB = async () => {
|
const saveToIndexedDB = async (force = false) => {
|
||||||
try {
|
try {
|
||||||
const payload: FeeMethodState = {
|
const payload: FeeMethodState = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
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) {
|
} catch (error) {
|
||||||
console.error('saveToIndexedDB failed:', error)
|
console.error('saveToIndexedDB failed:', error)
|
||||||
}
|
}
|
||||||
@ -338,17 +344,16 @@ const saveToIndexedDB = async () => {
|
|||||||
|
|
||||||
const loadFromIndexedDB = async () => {
|
const loadFromIndexedDB = async () => {
|
||||||
try {
|
try {
|
||||||
|
const data = await zxFwPricingStore.loadHtFeeMainState<FeeMethodRow>(props.storageKey)
|
||||||
const data = await localforage.getItem<FeeMethodState>(props.storageKey)
|
|
||||||
|
|
||||||
const mergedRows = mergeWithStoredRows(data?.detailRows)
|
const mergedRows = mergeWithStoredRows(data?.detailRows)
|
||||||
detailRows.value = await hydrateRowsFromMethodStores(mergedRows)
|
detailRows.value = await hydrateRowsFromMethodStores(mergedRows)
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('loadFromIndexedDB failed:', error)
|
||||||
const mergedRows = mergeWithStoredRows([])
|
const mergedRows = mergeWithStoredRows([])
|
||||||
detailRows.value = await hydrateRowsFromMethodStores(mergedRows)
|
detailRows.value = await hydrateRowsFromMethodStores(mergedRows)
|
||||||
await saveToIndexedDB()
|
await saveToIndexedDB(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,11 +365,9 @@ const addRow = () => {
|
|||||||
const clearRow = async (id: string) => {
|
const clearRow = async (id: string) => {
|
||||||
tabStore.removeTab(`ht-fee-edit-${props.storageKey}-${id}`)
|
tabStore.removeTab(`ht-fee-edit-${props.storageKey}-${id}`)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await Promise.all([
|
zxFwPricingStore.removeHtFeeMethodState(props.storageKey, id, 'rate-fee')
|
||||||
localforage.removeItem(buildMethodStorageKey(id, 'rate-fee')),
|
zxFwPricingStore.removeHtFeeMethodState(props.storageKey, id, 'hourly-fee')
|
||||||
localforage.removeItem(buildMethodStorageKey(id, 'hourly-fee')),
|
zxFwPricingStore.removeHtFeeMethodState(props.storageKey, id, 'quantity-unit-price-fee')
|
||||||
localforage.removeItem(buildMethodStorageKey(id, 'quantity-unit-price-fee'))
|
|
||||||
])
|
|
||||||
detailRows.value = detailRows.value.map(row =>
|
detailRows.value = detailRows.value.map(row =>
|
||||||
row.id !== id
|
row.id !== id
|
||||||
? row
|
? 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>) => {
|
const handleGridReady = (event: GridReadyEvent<FeeMethodRow>) => {
|
||||||
gridApi.value = event.api
|
gridApi.value = event.api
|
||||||
@ -561,10 +571,7 @@ const handleGridReady = (event: GridReadyEvent<FeeMethodRow>) => {
|
|||||||
|
|
||||||
const handleCellValueChanged = (event: CellValueChangedEvent<FeeMethodRow>) => {
|
const handleCellValueChanged = (event: CellValueChangedEvent<FeeMethodRow>) => {
|
||||||
if (isSummaryRow(event.data)) return
|
if (isSummaryRow(event.data)) return
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
void saveToIndexedDB()
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -572,22 +579,28 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
void loadFromIndexedDB()
|
scheduleReloadFromStorage()
|
||||||
})
|
})
|
||||||
|
|
||||||
const storageKeyRef = computed(() => props.storageKey)
|
const storageKeyRef = computed(() => props.storageKey)
|
||||||
watch(storageKeyRef, () => {
|
watch(storageKeyRef, () => {
|
||||||
void loadFromIndexedDB()
|
scheduleReloadFromStorage()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => htFeeMethodReloadStore.seq,
|
() =>
|
||||||
(nextVersion, prevVersion) => {
|
detailRows.value
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
.map(row => {
|
||||||
const detail = htFeeMethodReloadStore.lastEvent
|
const rateKey = zxFwPricingStore.getHtFeeMethodStorageKey(props.storageKey, row.id, 'rate-fee')
|
||||||
if (!detail) return
|
const hourlyKey = zxFwPricingStore.getHtFeeMethodStorageKey(props.storageKey, row.id, 'hourly-fee')
|
||||||
if (String(detail.mainStorageKey || '').trim() !== String(props.storageKey || '').trim()) return
|
const quantityKey = zxFwPricingStore.getHtFeeMethodStorageKey(props.storageKey, row.id, 'quantity-unit-price-fee')
|
||||||
void loadFromIndexedDB()
|
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) => {
|
(nextVersion, prevVersion) => {
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
if (nextVersion === prevVersion || nextVersion === 0) return
|
||||||
void loadFromIndexedDB()
|
scheduleReloadFromStorage()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -613,9 +626,9 @@ watch([hasFixedNames], () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
if (reloadTimer) clearTimeout(reloadTimer)
|
||||||
gridApi.value = null
|
gridApi.value = null
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB(true)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
|
||||||
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
|
||||||
import { industryTypeList } from '@/sql'
|
import { industryTypeList } from '@/sql'
|
||||||
@ -45,6 +46,14 @@ interface ContractSegmentPackage {
|
|||||||
projectIndustry: string
|
projectIndustry: string
|
||||||
contracts: ContractItem[]
|
contracts: ContractItem[]
|
||||||
localforageEntries: DataEntry[]
|
localforageEntries: DataEntry[]
|
||||||
|
piniaState?: {
|
||||||
|
zxFwPricing?: {
|
||||||
|
contracts?: Record<string, unknown>
|
||||||
|
servicePricingStates?: Record<string, unknown>
|
||||||
|
htFeeMainStates?: Record<string, unknown>
|
||||||
|
htFeeMethodStates?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
interface XmBaseInfoState {
|
interface XmBaseInfoState {
|
||||||
projectIndustry?: string
|
projectIndustry?: string
|
||||||
@ -52,16 +61,18 @@ interface XmBaseInfoState {
|
|||||||
|
|
||||||
const STORAGE_KEY = 'ht-card-v1'
|
const STORAGE_KEY = 'ht-card-v1'
|
||||||
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
|
||||||
const CONTRACT_SEGMENT_VERSION = 1
|
const CONTRACT_SEGMENT_VERSION = 2
|
||||||
const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
const CONTRACT_KEY_PREFIX = 'ht-info-v3-'
|
||||||
const SERVICE_KEY_PREFIX = 'zxFW-'
|
const SERVICE_KEY_PREFIX = 'zxFW-'
|
||||||
const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
|
const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
|
||||||
const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-'
|
const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-'
|
||||||
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-']
|
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-']
|
||||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
|
const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly'] as const
|
||||||
|
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
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 isContractSegmentPackage = (value: unknown): value is ContractSegmentPackage => {
|
||||||
const payload = value as Partial<ContractSegmentPackage> | null
|
const payload = value as Partial<ContractSegmentPackage> | null
|
||||||
return Boolean(payload && typeof payload === 'object' && Array.isArray(payload.contracts))
|
return Boolean(payload && typeof payload === 'object' && Array.isArray(payload.contracts))
|
||||||
@ -452,6 +569,7 @@ const exportSelectedContracts = async () => {
|
|||||||
const localforageEntries = await readContractRelatedForageEntries(
|
const localforageEntries = await readContractRelatedForageEntries(
|
||||||
selectedContracts.map(item => item.id)
|
selectedContracts.map(item => item.id)
|
||||||
)
|
)
|
||||||
|
const piniaPayload = await buildContractPiniaPayload(selectedContracts.map(item => item.id))
|
||||||
|
|
||||||
const projectIndustry = await getCurrentProjectIndustry()
|
const projectIndustry = await getCurrentProjectIndustry()
|
||||||
if (!projectIndustry) {
|
if (!projectIndustry) {
|
||||||
@ -465,7 +583,10 @@ const exportSelectedContracts = async () => {
|
|||||||
exportedAt: now.toISOString(),
|
exportedAt: now.toISOString(),
|
||||||
projectIndustry,
|
projectIndustry,
|
||||||
contracts: selectedContracts,
|
contracts: selectedContracts,
|
||||||
localforageEntries
|
localforageEntries,
|
||||||
|
piniaState: {
|
||||||
|
zxFwPricing: piniaPayload
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await encodeZwArchive(payload)
|
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 Promise.all(rewrittenEntries.map(entry => localforage.setItem(entry.key, entry.value)))
|
||||||
|
await applyImportedContractPiniaPayload(payload.piniaState, oldToNewIdMap)
|
||||||
|
|
||||||
contracts.value = [...contracts.value, ...nextContracts]
|
contracts.value = [...contracts.value, ...nextContracts]
|
||||||
await saveContracts()
|
await saveContracts()
|
||||||
|
|||||||
@ -50,8 +50,9 @@ const quantityUnitPricePane = markRaw(
|
|||||||
h(HtFeeGrid, {
|
h(HtFeeGrid, {
|
||||||
title: '数量单价',
|
title: '数量单价',
|
||||||
storageKey: quantityStorageKey.value,
|
storageKey: quantityStorageKey.value,
|
||||||
syncMainStorageKey: props.storageKey,
|
htMainStorageKey: props.storageKey,
|
||||||
syncRowId: props.rowId
|
htRowId: props.rowId,
|
||||||
|
htMethodType: 'quantity-unit-price-fee'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -66,8 +67,9 @@ const rateFeePane = markRaw(
|
|||||||
h(HtFeeRateMethodForm, {
|
h(HtFeeRateMethodForm, {
|
||||||
storageKey: rateStorageKey.value,
|
storageKey: rateStorageKey.value,
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
syncMainStorageKey: props.storageKey,
|
htMainStorageKey: props.storageKey,
|
||||||
syncRowId: props.rowId
|
htRowId: props.rowId,
|
||||||
|
htMethodType: 'rate-fee'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -82,8 +84,9 @@ const hourlyFeePane = markRaw(
|
|||||||
h(HourlyFeeGrid, {
|
h(HourlyFeeGrid, {
|
||||||
title: '工时法明细',
|
title: '工时法明细',
|
||||||
storageKey: hourlyStorageKey.value,
|
storageKey: hourlyStorageKey.value,
|
||||||
syncMainStorageKey: props.storageKey,
|
htMainStorageKey: props.storageKey,
|
||||||
syncRowId: props.rowId
|
htRowId: props.rowId,
|
||||||
|
htMethodType: 'hourly-fee'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import localforage from 'localforage'
|
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
|
|
||||||
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
interface RateMethodState {
|
interface RateMethodState {
|
||||||
@ -15,71 +13,95 @@ interface RateMethodState {
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
storageKey: string
|
storageKey: string
|
||||||
contractId?: string
|
contractId?: string
|
||||||
syncMainStorageKey?: string
|
htMainStorageKey?: string
|
||||||
syncRowId?: string
|
htRowId?: string
|
||||||
|
htMethodType?: 'rate-fee'
|
||||||
}>()
|
}>()
|
||||||
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
|
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
|
||||||
const base = ref<number | null>(null)
|
|
||||||
const rate = ref<number | null>(null)
|
const rate = ref<number | null>(null)
|
||||||
const remark = ref('')
|
const remark = ref('')
|
||||||
const rateInput = 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()
|
const contractId = String(props.contractId || '').trim()
|
||||||
if (!contractId) return 0
|
return contractId
|
||||||
return zxFwPricingStore.contractVersions[contractId] || 0
|
})
|
||||||
|
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>(() => {
|
const budgetFee = computed<number | null>(() => {
|
||||||
if (base.value == null || rate.value == null) return 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) =>
|
const formatAmount = (value: number | null) =>
|
||||||
value == null ? '' : formatThousandsFlexible(value, 3)
|
value == null ? '' : formatThousandsFlexible(value, 3)
|
||||||
|
|
||||||
const loadBase = async () => {
|
const ensureContractLoaded = async () => {
|
||||||
const contractId = String(props.contractId || '').trim()
|
const contractId = contractIdText.value
|
||||||
if (!contractId) {
|
if (!contractId) return
|
||||||
base.value = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await zxFwPricingStore.loadContract(contractId)
|
await zxFwPricingStore.loadContract(contractId)
|
||||||
const nextBase = zxFwPricingStore.getBaseSubtotal(contractId)
|
|
||||||
base.value = nextBase == null ? null : round3(nextBase)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('load rate base failed:', error)
|
console.error('load contract for rate base failed:', error)
|
||||||
base.value = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadForm = async () => {
|
const loadForm = async () => {
|
||||||
try {
|
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
|
rate.value = typeof data?.rate === 'number' ? data.rate : null
|
||||||
remark.value = typeof data?.remark === 'string' ? data.remark : ''
|
remark.value = typeof data?.remark === 'string' ? data.remark : ''
|
||||||
rateInput.value = rate.value == null ? '' : String(rate.value)
|
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) {
|
} catch (error) {
|
||||||
console.error('load rate form failed:', error)
|
console.error('load rate form failed:', error)
|
||||||
rate.value = null
|
rate.value = null
|
||||||
remark.value = ''
|
remark.value = ''
|
||||||
rateInput.value = ''
|
rateInput.value = ''
|
||||||
|
lastSavedSnapshot.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveForm = async () => {
|
const saveForm = async (force = false) => {
|
||||||
try {
|
try {
|
||||||
await localforage.setItem<RateMethodState>(props.storageKey, {
|
const payload: RateMethodState = {
|
||||||
rate: rate.value,
|
rate: rate.value,
|
||||||
budgetFee: budgetFee.value,
|
budgetFee: budgetFee.value,
|
||||||
remark: remark.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) {
|
} catch (error) {
|
||||||
console.error('save rate form failed:', error)
|
console.error('save rate form failed:', error)
|
||||||
}
|
}
|
||||||
@ -105,26 +127,22 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => contractVersion.value,
|
() => contractIdText.value,
|
||||||
(nextVersion, prevVersion) => {
|
() => {
|
||||||
if (nextVersion === prevVersion || nextVersion === 0) return
|
void ensureContractLoaded()
|
||||||
void loadBase()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadBase(), loadForm()])
|
await Promise.all([ensureContractLoaded(), loadForm()])
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
await Promise.all([loadBase(), loadForm()])
|
await Promise.all([ensureContractLoaded(), loadForm()])
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
void saveForm(true)
|
||||||
void saveForm()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
|||||||
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||||
@ -93,7 +93,7 @@ const props = defineProps<{
|
|||||||
contractId: string,
|
contractId: string,
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
}>()
|
}>()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
|
const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
|
||||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${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())
|
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||||
let factorDefaultsLoaded = false
|
let factorDefaultsLoaded = false
|
||||||
const paneInstanceCreatedAt = Date.now()
|
const paneInstanceCreatedAt = Date.now()
|
||||||
const reloadSignal = ref(0)
|
|
||||||
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||||
const industryNameMap = new Map(
|
const industryNameMap = new Map(
|
||||||
industryTypeList.flatMap(item => [
|
industryTypeList.flatMap(item => [
|
||||||
@ -238,7 +237,22 @@ const shouldForceDefaultLoad = () => {
|
|||||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
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 = {
|
type majorLite = {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
@ -1016,10 +1030,10 @@ const saveToIndexedDB = async () => {
|
|||||||
if (shouldSkipPersist()) return
|
if (shouldSkipPersist()) return
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||||
|
projectCount: getTargetProjectCount()
|
||||||
}
|
}
|
||||||
console.log('Saving to IndexedDB:', payload)
|
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'investScale', payload)
|
||||||
await localforage.setItem(DB_KEY.value, payload)
|
|
||||||
const synced = await syncPricingTotalToZxFw({
|
const synced = await syncPricingTotalToZxFw({
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
serviceId: props.serviceId,
|
serviceId: props.serviceId,
|
||||||
@ -1149,10 +1163,11 @@ const loadFromIndexedDB = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'investScale')
|
||||||
if (data) {
|
if (data) {
|
||||||
if (isMutipleService.value) {
|
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
|
detailRows.value = isOnlyCostScaleService.value
|
||||||
? buildOnlyCostScaleRows(data.detailRows as any, {
|
? 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 = () => {
|
const handleCellValueChanged = () => {
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
void saveToIndexedDB()
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -1262,8 +1251,6 @@ onActivated(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<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 { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
import type { ColDef, ColGroupDef } from 'ag-grid-community'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
|||||||
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
|
||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
|
||||||
@ -93,7 +93,7 @@ const props = defineProps<{
|
|||||||
contractId: string,
|
contractId: string,
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
}>()
|
}>()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
|
||||||
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
|
||||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${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())
|
const majorFactorMap = ref<Map<string, number | null>>(new Map())
|
||||||
let factorDefaultsLoaded = false
|
let factorDefaultsLoaded = false
|
||||||
const paneInstanceCreatedAt = Date.now()
|
const paneInstanceCreatedAt = Date.now()
|
||||||
const reloadSignal = ref(0)
|
|
||||||
const industryNameMap = new Map(
|
const industryNameMap = new Map(
|
||||||
industryTypeList.flatMap(item => [
|
industryTypeList.flatMap(item => [
|
||||||
[String(item.id).trim(), item.name],
|
[String(item.id).trim(), item.name],
|
||||||
@ -182,7 +181,22 @@ const inferProjectCountFromRows = (rows?: Array<Partial<DetailRow>>) => {
|
|||||||
return maxProjectIndex
|
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 = () =>
|
const getDefaultConsultCategoryFactor = () =>
|
||||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
||||||
|
|
||||||
@ -869,10 +883,10 @@ const saveToIndexedDB = async () => {
|
|||||||
if (shouldSkipPersist()) return
|
if (shouldSkipPersist()) return
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())),
|
||||||
|
projectCount: getTargetProjectCount()
|
||||||
}
|
}
|
||||||
console.log('Saving to IndexedDB:', payload)
|
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'landScale', payload)
|
||||||
await localforage.setItem(DB_KEY.value, payload)
|
|
||||||
const synced = await syncPricingTotalToZxFw({
|
const synced = await syncPricingTotalToZxFw({
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
serviceId: props.serviceId,
|
serviceId: props.serviceId,
|
||||||
@ -989,10 +1003,11 @@ const loadFromIndexedDB = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'landScale')
|
||||||
if (data) {
|
if (data) {
|
||||||
if (isMutipleService.value) {
|
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, {
|
detailRows.value = mergeWithDictRows(data.detailRows as any, {
|
||||||
projectCount: getTargetProjectCount(),
|
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 = () => {
|
const handleCellValueChanged = () => {
|
||||||
syncComputedValuesToDetailRows()
|
syncComputedValuesToDetailRows()
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
void saveToIndexedDB()
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -1087,8 +1076,6 @@ onActivated(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<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 { AgGridVue } from 'ag-grid-vue3'
|
||||||
import type { ColDef } from 'ag-grid-community'
|
import type { ColDef } from 'ag-grid-community'
|
||||||
import localforage from 'localforage'
|
|
||||||
import { taskList } from '@/sql'
|
import { taskList } from '@/sql'
|
||||||
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
|
||||||
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||||
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import { parseNumberOrNull } from '@/lib/number'
|
import { parseNumberOrNull } from '@/lib/number'
|
||||||
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
|
||||||
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
}>()
|
}>()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
const DB_KEY = computed(() => `gzlF-${props.contractId}-${props.serviceId}`)
|
||||||
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
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())
|
const consultCategoryFactorMap = ref<Map<string, number | null>>(new Map())
|
||||||
let factorDefaultsLoaded = false
|
let factorDefaultsLoaded = false
|
||||||
const paneInstanceCreatedAt = Date.now()
|
const paneInstanceCreatedAt = Date.now()
|
||||||
const reloadSignal = ref(0)
|
|
||||||
|
|
||||||
const getDefaultConsultCategoryFactor = () =>
|
const getDefaultConsultCategoryFactor = () =>
|
||||||
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
|
||||||
@ -93,7 +91,20 @@ const shouldForceDefaultLoad = () => {
|
|||||||
return Number.isFinite(forceUntil) && Date.now() <= forceUntil
|
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 = {
|
type taskLite = {
|
||||||
serviceID: number
|
serviceID: number
|
||||||
@ -424,8 +435,7 @@ const saveToIndexedDB = async () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
|
||||||
}
|
}
|
||||||
console.log('Saving to IndexedDB:', payload)
|
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'workload', payload)
|
||||||
await localforage.setItem(DB_KEY.value, payload)
|
|
||||||
const synced = await syncPricingTotalToZxFw({
|
const synced = await syncPricingTotalToZxFw({
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
serviceId: props.serviceId,
|
serviceId: props.serviceId,
|
||||||
@ -452,7 +462,7 @@ const loadFromIndexedDB = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await localforage.getItem<XmInfoState>(DB_KEY.value)
|
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'workload')
|
||||||
if (data) {
|
if (data) {
|
||||||
detailRows.value = mergeWithDictRows(data.detailRows)
|
detailRows.value = mergeWithDictRows(data.detailRows)
|
||||||
return
|
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 = () => {
|
const handleCellValueChanged = () => {
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
void saveToIndexedDB()
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -500,8 +484,6 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (persistTimer) clearTimeout(persistTimer)
|
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
void saveToIndexedDB()
|
void saveToIndexedDB()
|
||||||
})
|
})
|
||||||
const processCellForClipboard = (params: any) => {
|
const processCellForClipboard = (params: any) => {
|
||||||
|
|||||||
@ -11,12 +11,10 @@ import { parseNumberOrNull } from '@/lib/number'
|
|||||||
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
import { formatThousandsFlexible } from '@/lib/numberFormat'
|
||||||
import {
|
import {
|
||||||
ensurePricingMethodDetailRowsForServices,
|
ensurePricingMethodDetailRowsForServices,
|
||||||
persistDefaultPricingMethodDetailRowsForServices,
|
|
||||||
getPricingMethodTotalsForService,
|
getPricingMethodTotalsForService,
|
||||||
getPricingMethodTotalsForServices,
|
getPricingMethodTotalsForServices,
|
||||||
type PricingMethodTotals
|
type PricingMethodTotals
|
||||||
} from '@/lib/pricingMethodTotals'
|
} from '@/lib/pricingMethodTotals'
|
||||||
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
|
|
||||||
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -55,7 +53,7 @@ interface DetailRow {
|
|||||||
actions?: unknown
|
actions?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ZxFwState {
|
interface ZxFwViewState {
|
||||||
selectedIds?: string[]
|
selectedIds?: string[]
|
||||||
selectedCodes?: string[]
|
selectedCodes?: string[]
|
||||||
detailRows: DetailRow[]
|
detailRows: DetailRow[]
|
||||||
@ -77,7 +75,6 @@ const props = defineProps<{
|
|||||||
contractName?: string
|
contractName?: string
|
||||||
}>()
|
}>()
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
const pricingPaneReloadStore = usePricingPaneReloadStore()
|
|
||||||
const zxFwPricingStore = useZxFwPricingStore()
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
|
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_CLEAR_SKIP_TTL_MS = 5000
|
||||||
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||||||
const projectIndustry = ref('')
|
const projectIndustry = ref('')
|
||||||
const syncingFromStore = ref(false)
|
|
||||||
const localSavedVersion = ref(0)
|
|
||||||
const zxFwStoreVersion = computed(() => zxFwPricingStore.contractVersions[props.contractId] || 0)
|
|
||||||
|
|
||||||
type ServiceListItem = {
|
type ServiceListItem = {
|
||||||
code?: string
|
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 fixedBudgetRow: Pick<DetailRow, 'id' | 'code' | 'name'> = { id: 'fixed-budget-c', code: '', name: '小计' }
|
||||||
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
const isFixedRow = (row?: DetailRow | null) => row?.id === fixedBudgetRow.id
|
||||||
|
|
||||||
const selectedIds = ref<string[]>([])
|
const selectedIds = computed<string[]>(() => zxFwPricingStore.contracts[props.contractId]?.selectedIds || [])
|
||||||
const detailRows = ref<DetailRow[]>([])
|
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 pickerOpen = ref(false)
|
||||||
const pickerTempIds = ref<string[]>([])
|
const pickerTempIds = ref<string[]>([])
|
||||||
@ -374,12 +409,8 @@ const getFixedRowSubtotal = () =>
|
|||||||
getMethodTotal('hourly')
|
getMethodTotal('hourly')
|
||||||
)
|
)
|
||||||
|
|
||||||
const getPricingPaneStorageKeys = (serviceId: string) => [
|
const getPricingPaneStorageKeys = (serviceId: string) =>
|
||||||
`tzGMF-${props.contractId}-${serviceId}`,
|
zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId)
|
||||||
`ydGMF-${props.contractId}-${serviceId}`,
|
|
||||||
`gzlF-${props.contractId}-${serviceId}`,
|
|
||||||
`hourlyPricing-${props.contractId}-${serviceId}`
|
|
||||||
]
|
|
||||||
|
|
||||||
const clearPricingPaneValues = async (serviceId: string) => {
|
const clearPricingPaneValues = async (serviceId: string) => {
|
||||||
const keys = getPricingPaneStorageKeys(serviceId)
|
const keys = getPricingPaneStorageKeys(serviceId)
|
||||||
@ -390,14 +421,16 @@ const clearPricingPaneValues = async (serviceId: string) => {
|
|||||||
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, skipToken)
|
sessionStorage.setItem(`${PRICING_CLEAR_SKIP_PREFIX}${key}`, skipToken)
|
||||||
sessionStorage.setItem(`${PRICING_FORCE_DEFAULT_PREFIX}${key}`, String(skipUntil))
|
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)))
|
await Promise.all(keys.map(key => localforage.removeItem(key)))
|
||||||
pricingPaneReloadStore.emit(props.contractId, serviceId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearRowValues = async (row: DetailRow) => {
|
const clearRowValues = async (row: DetailRow) => {
|
||||||
if (isFixedRow(row)) return
|
if (isFixedRow(row)) return
|
||||||
|
|
||||||
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回缓? tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
// 若该服务编辑页已打开,先关闭,避免子页面卸载时把旧数据写回。
|
||||||
|
tabStore.removeTab(`zxfw-edit-${props.contractId}-${row.id}`)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
await clearPricingPaneValues(row.id)
|
await clearPricingPaneValues(row.id)
|
||||||
await ensurePricingMethodDetailRowsForServices({
|
await ensurePricingMethodDetailRowsForServices({
|
||||||
@ -411,7 +444,8 @@ const clearRowValues = async (row: DetailRow) => {
|
|||||||
options: PRICING_TOTALS_OPTIONS
|
options: PRICING_TOTALS_OPTIONS
|
||||||
})
|
})
|
||||||
const sanitizedTotals = sanitizePricingTotalsByService(row.id, totals)
|
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.id !== row.id
|
||||||
? item
|
? item
|
||||||
: {
|
: {
|
||||||
@ -426,7 +460,7 @@ const clearRowValues = async (row: DetailRow) => {
|
|||||||
const nextLandScale = getMethodTotalFromRows(clearedRows, 'landScale')
|
const nextLandScale = getMethodTotalFromRows(clearedRows, 'landScale')
|
||||||
const nextWorkload = getMethodTotalFromRows(clearedRows, 'workload')
|
const nextWorkload = getMethodTotalFromRows(clearedRows, 'workload')
|
||||||
const nextHourly = getMethodTotalFromRows(clearedRows, 'hourly')
|
const nextHourly = getMethodTotalFromRows(clearedRows, 'hourly')
|
||||||
detailRows.value = clearedRows.map(item =>
|
const nextRows = clearedRows.map(item =>
|
||||||
isFixedRow(item)
|
isFixedRow(item)
|
||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
@ -438,7 +472,10 @@ const clearRowValues = async (row: DetailRow) => {
|
|||||||
}
|
}
|
||||||
: item
|
: item
|
||||||
)
|
)
|
||||||
await saveToIndexedDB()
|
await setCurrentContractState({
|
||||||
|
...currentState,
|
||||||
|
detailRows: nextRows
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEditTab = (row: DetailRow) => {
|
const openEditTab = (row: DetailRow) => {
|
||||||
@ -682,20 +719,24 @@ const ensurePricingDetailRowsForCurrentSelection = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
||||||
|
const currentState = getCurrentContractState()
|
||||||
const targetIds = Array.from(
|
const targetIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
serviceIds.filter(id =>
|
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) {
|
if (targetIds.length === 0) {
|
||||||
detailRows.value = applyFixedRowTotals(detailRows.value)
|
await setCurrentContractState({
|
||||||
|
...currentState,
|
||||||
|
detailRows: applyFixedRowTotals(currentState.detailRows)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await persistDefaultPricingMethodDetailRowsForServices({
|
await ensurePricingMethodDetailRowsForServices({
|
||||||
contractId: props.contractId,
|
contractId: props.contractId,
|
||||||
serviceIds: targetIds,
|
serviceIds: targetIds,
|
||||||
options: PRICING_TOTALS_OPTIONS
|
options: PRICING_TOTALS_OPTIONS
|
||||||
@ -708,7 +749,7 @@ const fillPricingTotalsForServiceIds = async (serviceIds: string[]) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const targetSet = new Set(targetIds.map(id => String(id)))
|
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
|
if (isFixedRow(row) || !targetSet.has(String(row.id))) return row
|
||||||
const totalsRaw = totalsByServiceId.get(String(row.id))
|
const totalsRaw = totalsByServiceId.get(String(row.id))
|
||||||
const totals = totalsRaw ? sanitizePricingTotalsByService(String(row.id), totalsRaw) : null
|
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 applySelection = async (codes: string[]) => {
|
||||||
const prevSelectedSet = new Set(selectedIds.value)
|
const currentState = getCurrentContractState()
|
||||||
|
const prevSelectedSet = new Set(currentState.selectedIds || [])
|
||||||
const uniqueIds = Array.from(new Set(codes)).filter(
|
const uniqueIds = Array.from(new Set(codes)).filter(
|
||||||
id => serviceById.value.has(id) && id !== fixedBudgetRow.id
|
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
|
const baseRows: DetailRow[] = uniqueIds
|
||||||
.map(id => {
|
.map(id => {
|
||||||
@ -777,18 +822,21 @@ const applySelection = (codes: string[]) => {
|
|||||||
tabStore.removeTab(`zxfw-edit-${props.contractId}-${id}`)
|
tabStore.removeTab(`zxfw-edit-${props.contractId}-${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedIds.value = uniqueIds
|
await setCurrentContractState({
|
||||||
detailRows.value = [...baseRows, fixedRow]
|
...currentState,
|
||||||
|
selectedIds: uniqueIds,
|
||||||
|
detailRows: applyFixedRowTotals([...baseRows, fixedRow])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleServiceSelectionChange = async (ids: string[]) => {
|
const handleServiceSelectionChange = async (ids: string[]) => {
|
||||||
const prevIds = [...selectedIds.value]
|
const prevIds = [...selectedIds.value]
|
||||||
applySelection(ids)
|
await applySelection(ids)
|
||||||
const nextSelectedSet = new Set(selectedIds.value)
|
const nextSelectedIds = getCurrentContractState().selectedIds || []
|
||||||
const addedIds = selectedIds.value.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
|
const nextSelectedSet = new Set(nextSelectedIds)
|
||||||
|
const addedIds = nextSelectedIds.filter(id => !prevIds.includes(id) && nextSelectedSet.has(id))
|
||||||
await fillPricingTotalsForServiceIds(addedIds)
|
await fillPricingTotalsForServiceIds(addedIds)
|
||||||
await ensurePricingDetailRowsForCurrentSelection()
|
await ensurePricingDetailRowsForCurrentSelection()
|
||||||
await saveToIndexedDB()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const preparePickerOpen = () => {
|
const preparePickerOpen = () => {
|
||||||
@ -811,14 +859,7 @@ const handlePickerOpenChange = (open: boolean) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmPicker = async () => {
|
const confirmPicker = async () => {
|
||||||
applySelection(pickerTempIds.value)
|
await applySelection(pickerTempIds.value)
|
||||||
try {
|
|
||||||
// await fillPricingTotalsForSelectedRows()
|
|
||||||
await saveToIndexedDB()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('confirmPicker failed:', error)
|
|
||||||
await saveToIndexedDB()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearPickerSelection = () => {
|
const clearPickerSelection = () => {
|
||||||
@ -925,90 +966,30 @@ const handleDragHover = (_code: string) => {
|
|||||||
applyDragSelectionByRect()
|
applyDragSelectionByRect()
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPersistDetailRows = () => {
|
const initializeContractState = async () => {
|
||||||
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
|
|
||||||
try {
|
try {
|
||||||
await zxFwPricingStore.loadContract(props.contractId)
|
await zxFwPricingStore.loadContract(props.contractId)
|
||||||
const data = zxFwPricingStore.getContractState(props.contractId)
|
const data = zxFwPricingStore.getContractState(props.contractId)
|
||||||
if (!data) {
|
const idsFromStorage = data?.selectedIds
|
||||||
selectedIds.value = []
|
|| (data?.selectedCodes || []).map(code => serviceIdByCode.value.get(code)).filter((id): id is string => Boolean(id))
|
||||||
detailRows.value = []
|
await applySelection(idsFromStorage || [])
|
||||||
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)
|
|
||||||
await ensurePricingDetailRowsForCurrentSelection()
|
await ensurePricingDetailRowsForCurrentSelection()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('loadFromIndexedDB failed:', error)
|
console.error('initializeContractState failed:', error)
|
||||||
selectedIds.value = []
|
await setCurrentContractState({
|
||||||
detailRows.value = []
|
selectedIds: [],
|
||||||
} finally {
|
detailRows: applyFixedRowTotals([{
|
||||||
syncingFromStore.value = false
|
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, () => {
|
watch(serviceIdSignature, () => {
|
||||||
const availableIds = new Set(serviceDict.value.map(item => item.id))
|
const availableIds = new Set(serviceDict.value.map(item => item.id))
|
||||||
const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
|
const nextSelectedIds = selectedIds.value.filter(id => availableIds.has(id))
|
||||||
if (nextSelectedIds.length !== selectedIds.value.length) {
|
if (nextSelectedIds.length !== selectedIds.value.length) {
|
||||||
applySelection(nextSelectedIds)
|
void applySelection(nextSelectedIds)
|
||||||
void saveToIndexedDB()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let gridPersistTimer: ReturnType<typeof setTimeout> | null = null
|
const handleCellValueChanged = () => {}
|
||||||
const handleCellValueChanged = () => {
|
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
gridPersistTimer = setTimeout(() => {
|
|
||||||
void saveToIndexedDB()
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProjectIndustry()
|
await loadProjectIndustry()
|
||||||
await loadFromIndexedDB()
|
await initializeContractState()
|
||||||
})
|
})
|
||||||
|
|
||||||
onActivated(async () => {
|
onActivated(async () => {
|
||||||
await loadProjectIndustry()
|
await loadProjectIndustry()
|
||||||
await loadFromIndexedDB()
|
await initializeContractState()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopDragSelect()
|
stopDragSelect()
|
||||||
if (gridPersistTimer) clearTimeout(gridPersistTimer)
|
|
||||||
void saveToIndexedDB()
|
|
||||||
})
|
})
|
||||||
</script>
|
</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">
|
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>
|
<AlertDialogTitle class="text-base font-semibold">确认删除服务</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
|
||||||
将逻辑删除“{{ pendingDeleteServiceName }}”,已填写的数据不会清楚,重新勾选后会恢复,是否继续?
|
将逻辑删除“{{ pendingDeleteServiceName }}”,已填写的数据不会清空,重新勾选后会恢复,是否继续?
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
<div class="mt-4 flex items-center justify-end gap-2">
|
<div class="mt-4 flex items-center justify-end gap-2">
|
||||||
<AlertDialogCancel as-child>
|
<AlertDialogCancel as-child>
|
||||||
|
|||||||
@ -3,10 +3,13 @@ import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onM
|
|||||||
import type { ComponentPublicInstance } from 'vue'
|
import type { ComponentPublicInstance } from 'vue'
|
||||||
import draggable from 'vuedraggable'
|
import draggable from 'vuedraggable'
|
||||||
import { useTabStore } from '@/pinia/tab'
|
import { useTabStore } from '@/pinia/tab'
|
||||||
|
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
|
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import {
|
import {
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -348,6 +351,7 @@ const componentMap: Record<string, any> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
|
const zxFwPricingStore = useZxFwPricingStore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1098,12 +1102,16 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
const serviceId = toSafeInteger(serviceIdText)
|
const serviceId = toSafeInteger(serviceIdText)
|
||||||
if (serviceId == null) return null
|
if (serviceId == null) return null
|
||||||
|
|
||||||
const [method1Raw, method2Raw, method3Raw, method4Raw] = await Promise.all([
|
const [method1State, method2State, method3State, method4State] = await Promise.all([
|
||||||
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`tzGMF-${contractId}-${serviceIdText}`),
|
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale'),
|
||||||
localforage.getItem<DetailRowsStorageLike<ScaleMethodRowLike>>(`ydGMF-${contractId}-${serviceIdText}`),
|
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'landScale'),
|
||||||
localforage.getItem<DetailRowsStorageLike<WorkloadMethodRowLike>>(`gzlF-${contractId}-${serviceIdText}`),
|
zxFwPricingStore.loadServicePricingMethodState<WorkloadMethodRowLike>(contractId, serviceIdText, 'workload'),
|
||||||
localforage.getItem<DetailRowsStorageLike<HourlyMethodRowLike>>(`hourlyPricing-${contractId}-${serviceIdText}`)
|
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 method1 = buildMethod1(method1Raw?.detailRows)
|
||||||
const method2 = buildMethod2(method2Raw?.detailRows)
|
const method2 = buildMethod2(method2Raw?.detailRows)
|
||||||
@ -1255,6 +1263,7 @@ const confirmImportOverride = async () => {
|
|||||||
await writeForage(localforage, normalizeEntries(payload.localforageDefault))
|
await writeForage(localforage, normalizeEntries(payload.localforageDefault))
|
||||||
|
|
||||||
tabStore.resetTabs()
|
tabStore.resetTabs()
|
||||||
|
await tabStore.$persistNow?.()
|
||||||
dataMenuOpen.value = false
|
dataMenuOpen.value = false
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1275,6 +1284,7 @@ const handleReset = async () => {
|
|||||||
console.error('reset failed:', error)
|
console.error('reset failed:', error)
|
||||||
} finally {
|
} finally {
|
||||||
tabStore.resetTabs()
|
tabStore.resetTabs()
|
||||||
|
await tabStore.$persistNow?.()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||||
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
|
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
|
||||||
|
import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
interface StoredDetailRowsState<T = any> {
|
interface StoredDetailRowsState<T = any> {
|
||||||
detailRows?: T[]
|
detailRows?: T[]
|
||||||
@ -112,6 +113,22 @@ interface PricingMethodDefaultBuildContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
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) =>
|
const hasOwn = (obj: unknown, key: string) =>
|
||||||
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
||||||
@ -550,6 +567,15 @@ export const getPricingMethodDetailDbKeys = (
|
|||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
): PricingMethodDetailDbKeys => {
|
): PricingMethodDetailDbKeys => {
|
||||||
const normalizedServiceId = String(serviceId)
|
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 {
|
return {
|
||||||
investScale: `tzGMF-${contractId}-${normalizedServiceId}`,
|
investScale: `tzGMF-${contractId}-${normalizedServiceId}`,
|
||||||
landScale: `ydGMF-${contractId}-${normalizedServiceId}`,
|
landScale: `ydGMF-${contractId}-${normalizedServiceId}`,
|
||||||
@ -628,12 +654,19 @@ export const persistDefaultPricingMethodDetailRowsForServices = async (params: {
|
|||||||
if (uniqueServiceIds.length === 0) return
|
if (uniqueServiceIds.length === 0) return
|
||||||
|
|
||||||
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
|
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
|
||||||
|
const store = getZxFwStoreSafely()
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
uniqueServiceIds.map(async serviceId => {
|
uniqueServiceIds.map(async serviceId => {
|
||||||
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
||||||
const defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
|
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([
|
await Promise.all([
|
||||||
localforage.setItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
|
localforage.setItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
|
||||||
localforage.setItem(dbKeys.landScale, { detailRows: defaultRows.landScale }),
|
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 consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
|
||||||
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
|
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
|
||||||
const baseInfoDbKey = 'xm-base-info-v1'
|
const baseInfoDbKey = 'xm-base-info-v1'
|
||||||
const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
|
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
||||||
const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
|
const store = getZxFwStoreSafely()
|
||||||
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
|
|
||||||
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
|
|
||||||
|
|
||||||
const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
|
const [storeInvestData, storeLandData, storeWorkloadData, storeHourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
|
||||||
localforage.getItem<StoredDetailRowsState>(investDbKey),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'investScale') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(landDbKey),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'hourly') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(htDbKey),
|
localforage.getItem<StoredDetailRowsState>(htDbKey),
|
||||||
localforage.getItem<StoredFactorState>(consultFactorDbKey),
|
localforage.getItem<StoredFactorState>(consultFactorDbKey),
|
||||||
localforage.getItem<StoredFactorState>(majorFactorDbKey),
|
localforage.getItem<StoredFactorState>(majorFactorDbKey),
|
||||||
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
|
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 consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
|
||||||
const majorFactorMap = buildMajorFactorMap(majorFactorData)
|
const majorFactorMap = buildMajorFactorMap(majorFactorData)
|
||||||
const onlyCostScale = isOnlyCostScaleService(serviceId)
|
const onlyCostScale = isOnlyCostScaleService(serviceId)
|
||||||
@ -744,16 +787,27 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
|
|||||||
if (uniqueServiceIds.length === 0) return
|
if (uniqueServiceIds.length === 0) return
|
||||||
|
|
||||||
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
|
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
|
||||||
|
const store = getZxFwStoreSafely()
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
uniqueServiceIds.map(async serviceId => {
|
uniqueServiceIds.map(async serviceId => {
|
||||||
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
|
||||||
const [investData, landData, workloadData, hourlyData] = await Promise.all([
|
const [storeInvestData, storeLandData, storeWorkloadData, storeHourlyData] = await Promise.all([
|
||||||
localforage.getItem<StoredDetailRowsState>(dbKeys.investScale),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'investScale') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(dbKeys.landScale),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(dbKeys.workload),
|
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
|
||||||
localforage.getItem<StoredDetailRowsState>(dbKeys.hourly)
|
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 shouldInitInvest = !Array.isArray(investData?.detailRows) || investData!.detailRows!.length === 0
|
||||||
const shouldInitLand = !Array.isArray(landData?.detailRows) || landData!.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 (shouldInitInvest) {
|
||||||
|
if (store) {
|
||||||
|
store.setServicePricingMethodState(params.contractId, serviceId, 'investScale', {
|
||||||
|
detailRows: getDefaultRows().investScale
|
||||||
|
}, { force: true })
|
||||||
|
}
|
||||||
writeTasks.push(localforage.setItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
|
writeTasks.push(localforage.setItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldInitLand) {
|
if (shouldInitLand) {
|
||||||
|
if (store) {
|
||||||
|
store.setServicePricingMethodState(params.contractId, serviceId, 'landScale', {
|
||||||
|
detailRows: getDefaultRows().landScale
|
||||||
|
}, { force: true })
|
||||||
|
}
|
||||||
writeTasks.push(localforage.setItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
|
writeTasks.push(localforage.setItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldInitWorkload) {
|
if (shouldInitWorkload) {
|
||||||
|
if (store) {
|
||||||
|
store.setServicePricingMethodState(params.contractId, serviceId, 'workload', {
|
||||||
|
detailRows: getDefaultRows().workload
|
||||||
|
}, { force: true })
|
||||||
|
}
|
||||||
writeTasks.push(localforage.setItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
|
writeTasks.push(localforage.setItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldInitHourly) {
|
if (shouldInitHourly) {
|
||||||
|
if (store) {
|
||||||
|
store.setServicePricingMethodState(params.contractId, serviceId, 'hourly', {
|
||||||
|
detailRows: getDefaultRows().hourly
|
||||||
|
}, { force: true })
|
||||||
|
}
|
||||||
writeTasks.push(localforage.setItem(dbKeys.hourly, { detailRows: getDefaultRows().hourly }))
|
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 type { ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
|
|
||||||
|
|
||||||
export const syncPricingTotalToZxFw = async (params: {
|
export const syncPricingTotalToZxFw = async (params: {
|
||||||
contractId: string
|
contractId: string
|
||||||
serviceId: string | number
|
serviceId: string | number
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
TreeDataModule,ContextMenuModule,ValidationModule
|
TreeDataModule,ContextMenuModule,ValidationModule
|
||||||
} from 'ag-grid-enterprise'
|
} from 'ag-grid-enterprise'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
import piniaPersistedstate from '@/pinia/Plugin/indexdb'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
@ -51,7 +51,12 @@ const AG_GRID_MODULES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPluginPersistedstate)
|
pinia.use(
|
||||||
|
piniaPersistedstate({
|
||||||
|
storeName: 'pinia',
|
||||||
|
mode: 'multiple'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// 在应用启动时一次性注册 AG Grid 运行所需模块。
|
// 在应用启动时一次性注册 AG Grid 运行所需模块。
|
||||||
ModuleRegistry.registerModules(AG_GRID_MODULES)
|
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 = () => {
|
const closeAllTabs = () => {
|
||||||
tabs.value = createDefaultTabs()
|
tabs.value = createDefaultTabs()
|
||||||
|
console.log(tabs.value)
|
||||||
activeTabId.value = HOME_TAB_ID
|
activeTabId.value = HOME_TAB_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,10 +107,6 @@ export const useTabStore = defineStore(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: {
|
persist: true
|
||||||
key: 'tabs',
|
|
||||||
storage: localStorage,
|
|
||||||
pick: ['tabs', 'activeTabId']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { addNumbers } from '@/lib/decimal'
|
|||||||
import { toFiniteNumberOrNull } from '@/lib/number'
|
import { toFiniteNumberOrNull } from '@/lib/number'
|
||||||
|
|
||||||
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||||
|
export type ServicePricingMethod = ZxFwPricingField
|
||||||
|
|
||||||
export interface ZxFwDetailRow {
|
export interface ZxFwDetailRow {
|
||||||
id: string
|
id: string
|
||||||
@ -24,10 +25,44 @@ export interface ZxFwState {
|
|||||||
detailRows: ZxFwDetailRow[]
|
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 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 toKey = (contractId: string | number) => String(contractId || '').trim()
|
||||||
|
const toServiceKey = (serviceId: string | number) => String(serviceId || '').trim()
|
||||||
const dbKeyOf = (contractId: string) => `zxFW-${contractId}`
|
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 round3 = (value: number) => Number(value.toFixed(3))
|
||||||
const toNumberOrZero = (value: unknown) => {
|
const toNumberOrZero = (value: unknown) => {
|
||||||
const numeric = Number(value)
|
const numeric = Number(value)
|
||||||
@ -99,30 +134,648 @@ const cloneState = (state: ZxFwState): ZxFwState => ({
|
|||||||
detailRows: state.detailRows.map(row => ({ ...row }))
|
detailRows: state.detailRows.map(row => ({ ...row }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadTasks = new Map<string, Promise<ZxFwState>>()
|
const isSameStringArray = (a: string[] | undefined, b: string[] | undefined) => {
|
||||||
const persistQueues = new Map<string, Promise<void>>()
|
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', () => {
|
export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
||||||
const contracts = ref<Record<string, ZxFwState>>({})
|
const contracts = ref<Record<string, ZxFwState>>({})
|
||||||
const contractVersions = ref<Record<string, number>>({})
|
const contractVersions = ref<Record<string, number>>({})
|
||||||
|
const contractLoaded = ref<Record<string, boolean>>({})
|
||||||
|
const 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) => {
|
const touchVersion = (contractId: string) => {
|
||||||
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
|
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
|
||||||
}
|
}
|
||||||
|
const touchKeyVersion = (key: string) => {
|
||||||
|
keyVersions.value[key] = (keyVersions.value[key] || 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
const queuePersist = async (contractId: string) => {
|
const ensureServicePricingState = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
|
||||||
const prev = persistQueues.get(contractId) || Promise.resolve()
|
const contractId = toKey(contractIdRaw)
|
||||||
const next = prev
|
const serviceId = toServiceKey(serviceIdRaw)
|
||||||
.then(async () => {
|
if (!contractId || !serviceId) return null
|
||||||
const current = contracts.value[contractId]
|
if (!servicePricingStates.value[contractId]) {
|
||||||
if (!current) return
|
servicePricingStates.value[contractId] = {}
|
||||||
await localforage.setItem<ZxFwState>(dbKeyOf(contractId), cloneState(current))
|
}
|
||||||
|
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)
|
const htMainMeta = parseHtFeeMainStorageKey(key)
|
||||||
})
|
if (htMainMeta) {
|
||||||
persistQueues.set(contractId, next)
|
setHtFeeMainState(htMainMeta.mainStorageKey, null, { force: true, syncKeyState: false })
|
||||||
await next
|
}
|
||||||
|
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) => {
|
const getContractState = (contractIdRaw: string | number) => {
|
||||||
@ -135,15 +788,25 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
const loadContract = async (contractIdRaw: string | number, force = false) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return null
|
if (!contractId) return null
|
||||||
|
if (!force && contractLoaded.value[contractId]) return getContractState(contractId)
|
||||||
if (!force && contracts.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 task = (async () => {
|
||||||
const raw = await localforage.getItem<ZxFwState>(dbKeyOf(contractId))
|
const raw = await localforage.getItem<ZxFwState>(dbKeyOf(contractId))
|
||||||
const normalized = normalizeState(raw)
|
const current = contracts.value[contractId]
|
||||||
contracts.value[contractId] = normalized
|
if (raw) {
|
||||||
touchVersion(contractId)
|
const normalized = normalizeState(raw)
|
||||||
return cloneState(normalized)
|
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)
|
loadTasks.set(contractId, task)
|
||||||
|
|
||||||
@ -156,10 +819,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
|
|
||||||
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
|
||||||
const contractId = toKey(contractIdRaw)
|
const contractId = toKey(contractIdRaw)
|
||||||
if (!contractId) return
|
if (!contractId) return false
|
||||||
contracts.value[contractId] = normalizeState(state)
|
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)
|
touchVersion(contractId)
|
||||||
await queuePersist(contractId)
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePricingField = async (params: {
|
const updatePricingField = async (params: {
|
||||||
@ -196,12 +863,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
|
|
||||||
if (!changed) return false
|
if (!changed) return false
|
||||||
|
|
||||||
contracts.value[contractId] = normalizeState({
|
const nextState = normalizeState({
|
||||||
...current,
|
...current,
|
||||||
detailRows: nextRows
|
detailRows: nextRows
|
||||||
})
|
})
|
||||||
|
if (isSameState(current, nextState)) return false
|
||||||
|
contracts.value[contractId] = nextState
|
||||||
|
contractLoaded.value[contractId] = true
|
||||||
touchVersion(contractId)
|
touchVersion(contractId)
|
||||||
await queuePersist(contractId)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,10 +895,39 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
|
|||||||
return {
|
return {
|
||||||
contracts,
|
contracts,
|
||||||
contractVersions,
|
contractVersions,
|
||||||
|
contractLoaded,
|
||||||
|
servicePricingStates,
|
||||||
|
htFeeMainStates,
|
||||||
|
htFeeMethodStates,
|
||||||
|
keyedStates,
|
||||||
|
keyVersions,
|
||||||
getContractState,
|
getContractState,
|
||||||
loadContract,
|
loadContract,
|
||||||
setContractState,
|
setContractState,
|
||||||
updatePricingField,
|
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