fix,去掉大部分indexdb的逻辑

This commit is contained in:
wintsa 2026-03-11 12:01:16 +08:00
parent 9a045cfe86
commit 3d26b0b259
19 changed files with 416 additions and 103 deletions

View File

@ -172,10 +172,10 @@ const hydrateRowsFromMethodStores = async (rows: FeeMethodRow[]): Promise<FeeMet
const storedRateFee = toFiniteUnknown(rateData?.budgetFee)
const rateValue = toFiniteUnknown(rateData?.rate)
const rateFee =
storedRateFee != null
? round3(storedRateFee)
: contractBase != null && rateValue != null
contractBase != null && rateValue != null
? round3(contractBase * rateValue)
: storedRateFee != null
? round3(storedRateFee)
: null
const hourlyFee = sumHourlyMethodFee(hourlyData)
const quantityUnitPriceFee = sumQuantityMethodFee(quantityData)

View File

@ -2,10 +2,10 @@
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, FirstDataRenderedEvent, GridApi, GridReadyEvent, GridSizeChangedEvent } from 'ag-grid-community'
import localforage from 'localforage'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { parseNumberOrNull } from '@/lib/number'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { useKvStore } from '@/pinia/kv'
interface DictItem {
code: string
@ -42,6 +42,7 @@ const props = defineProps<{
initBudgetValueFromStandard?: boolean
}>()
const kvStore = useKvStore()
const detailRows = ref<FactorRow[]>([])
const gridApi = ref<GridApi<FactorRow> | null>(null)
@ -227,7 +228,7 @@ const saveToIndexedDB = async () => {
const payload: GridState = {
detailRows: JSON.parse(JSON.stringify(detailRows.value))
}
await localforage.setItem(props.storageKey, payload)
await kvStore.setItem(props.storageKey, payload)
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
@ -235,7 +236,7 @@ const saveToIndexedDB = async () => {
const loadGridState = async (storageKey: string): Promise<GridState | null> => {
if (!storageKey) return null
const data = await localforage.getItem<GridState>(storageKey)
const data = await kvStore.getItem<GridState>(storageKey)
if (!data?.detailRows || !Array.isArray(data.detailRows)) return null
return data
}

View File

@ -4,11 +4,11 @@ import { AgGridVue } from 'ag-grid-vue3'
import type { CellValueChangedEvent, ColDef, GridApi, GridReadyEvent } from 'ag-grid-community'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import localforage from 'localforage'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { industryTypeList, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import { SwitchRoot, SwitchThumb } from 'reka-ui'
import { useKvStore } from '@/pinia/kv'
@ -42,6 +42,7 @@ interface XmBaseInfoState {
const BASE_INFO_KEY = 'xm-base-info-v1'
type MajorLite = { code: string; name: string; hasCost?: boolean; hasArea?: boolean }
const kvStore = useKvStore()
const detailRows = ref<DetailRow[]>([])
const detailDict = ref<DictGroup[]>([])
@ -157,8 +158,8 @@ const applyPinnedTotalAmount = (
const loadFromIndexedDB = async (api: GridApi<DetailRow>) => {
try {
const [baseInfo, contractData] = await Promise.all([
localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY),
localforage.getItem<XmScaleState>(props.dbKey)
kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY),
kvStore.getItem<XmScaleState>(props.dbKey)
])
activeIndustryId.value =
@ -204,7 +205,7 @@ const loadFromIndexedDB = async (api: GridApi<DetailRow>) => {
if (props.xmInfoKey) {
// id
const xmData = await localforage.getItem<XmScaleState>(props.xmInfoKey)
const xmData = await kvStore.getItem<XmScaleState>(props.xmInfoKey)
roughCalcEnabled.value = Boolean(xmData?.roughCalcEnabled)
applyPinnedTotalAmount(api, xmData?.totalAmount)
@ -405,7 +406,7 @@ const saveToIndexedDB = async () => {
}
payload.roughCalcEnabled = roughCalcEnabled.value
payload.totalAmount = pinnedTopRowData.value[0].amount
await localforage.setItem(props.dbKey, payload)
await kvStore.setItem(props.dbKey, payload)
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}

View File

@ -1,13 +1,13 @@
<script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import draggable from 'vuedraggable'
import localforage from 'localforage'
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { ArrowUp, Edit3, GripVertical, MoreHorizontal, Plus, Trash2, X } from 'lucide-vue-next'
import { decodeZwArchive, encodeZwArchive } from '@/lib/zwArchive'
import { industryTypeList } from '@/sql'
@ -73,6 +73,7 @@ const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly
const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
@ -186,7 +187,7 @@ const closeContractDataMenu = () => {
const loadProjectBaseState = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
canManageContracts.value = Boolean(typeof data?.projectIndustry === 'string' && data.projectIndustry.trim())
} catch (error) {
console.error('load project base state failed:', error)
@ -195,7 +196,7 @@ const loadProjectBaseState = async () => {
}
const getCurrentProjectIndustry = async (): Promise<string> => {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
return typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
}
@ -354,7 +355,7 @@ const getContractCardStyle = (index: number) => {
const saveContracts = async () => {
try {
contracts.value = normalizeOrder(contracts.value)
await localforage.setItem(STORAGE_KEY, JSON.parse(JSON.stringify(contracts.value)))
await kvStore.setItem(STORAGE_KEY, JSON.parse(JSON.stringify(contracts.value)))
} catch (error) {
console.error('save contracts failed:', error)
}
@ -509,7 +510,7 @@ const isContractRelatedForageKey = (key: string, contractId: string) => {
}
const readContractRelatedForageEntries = async (contractIds: string[]) => {
const keys = await localforage.keys()
const keys = await kvStore.keys()
const idSet = new Set(contractIds)
const targetKeys = keys.filter(key => {
for (const id of idSet) {
@ -520,7 +521,7 @@ const readContractRelatedForageEntries = async (contractIds: string[]) => {
return Promise.all(
targetKeys.map(async key => ({
key,
value: await localforage.getItem(key)
value: await kvStore.getItem(key)
}))
)
}
@ -669,7 +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 => kvStore.setItem(entry.key, entry.value)))
await applyImportedContractPiniaPayload(payload.piniaState, oldToNewIdMap)
contracts.value = [...contracts.value, ...nextContracts]
@ -697,7 +698,7 @@ const importContractSegments = async (event: Event) => {
}
}
const removeForageKeysByContractId = async (store: typeof localforage, contractId: string) => {
const removeForageKeysByContractId = async (store: typeof kvStore, contractId: string) => {
try {
const keys = await store.keys()
const targetKeys = keys.filter(key => key.includes(contractId))
@ -725,14 +726,16 @@ const removeRelatedTabsByContractId = (contractId: string) => {
}
const cleanupContractRelatedData = async (contractId: string) => {
zxFwPricingStore.removeContractData(contractId)
await Promise.all([
removeForageKeysByContractId(localforage, contractId),
removeForageKeysByContractId(kvStore, contractId),
])
await zxFwPricingStore.$persistNow?.()
}
const loadContracts = async () => {
try {
const saved = await localforage.getItem<ContractItem[]>(STORAGE_KEY)
const saved = await kvStore.getItem<ContractItem[]>(STORAGE_KEY)
if (!saved || saved.length === 0) {
contracts.value = buildDefaultContracts()
await saveContracts()

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import { useKvStore } from '@/pinia/kv'
const props = defineProps<{
contractId: string
@ -22,10 +22,11 @@ type ServiceItem = {
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const projectIndustry = ref('')
const kvStore = useKvStore()
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import { useKvStore } from '@/pinia/kv'
interface XmBaseInfoState {
projectIndustry?: string
@ -22,10 +22,11 @@ const props = defineProps<{
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const projectIndustry = ref('')
const kvStore = useKvStore()
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import { useKvStore } from '@/pinia/kv'
interface XmBaseInfoState {
projectIndustry?: string
@ -18,10 +18,11 @@ type ServiceItem = {
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const projectIndustry = ref('')
const kvStore = useKvStore()
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {

View File

@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import XmFactorGrid from '@/components/common/XmFactorGrid.vue'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
import { useKvStore } from '@/pinia/kv'
interface XmBaseInfoState {
projectIndustry?: string
@ -20,10 +20,11 @@ type MajorItem = {
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const projectIndustry = ref('')
const hasProjectBaseInfo = ref(false)
const kvStore = useKvStore()
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
hasProjectBaseInfo.value = Boolean(data)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''

View File

@ -1,7 +1,5 @@
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import localforage from 'localforage'
import { getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import { computed } from 'vue'
import CommonAgGrid from '@/components/common/xmCommonAgGrid.vue'

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { parseDate } from '@internationalized/date'
import { onMounted, ref, watch } from 'vue'
import localforage from 'localforage'
import { industryTypeList } from '@/sql'
import { useKvStore } from '@/pinia/kv'
import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import {
@ -99,6 +99,7 @@ const majorParentNodes: MajorParentNode[] = industryTypeList.map(item => ({
}))
const majorParentCodeSet = new Set(majorParentNodes.map(item => item.id))
const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || ''
const kvStore = useKvStore()
const saveToIndexedDB = async () => {
try {
@ -110,7 +111,7 @@ const saveToIndexedDB = async () => {
preparedCompany: preparedCompany.value,
preparedDate: preparedDate.value
}
await localforage.setItem(DB_KEY, payload)
await kvStore.setItem(DB_KEY, payload)
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
@ -118,7 +119,7 @@ const saveToIndexedDB = async () => {
const loadFromIndexedDB = async () => {
try {
const data = await localforage.getItem<XmInfoState>(DB_KEY)
const data = await kvStore.getItem<XmInfoState>(DB_KEY)
if (data) {
isProjectInitialized.value = true
projectIndustry.value =

View File

@ -2,13 +2,13 @@
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
@ -94,6 +94,7 @@ const props = defineProps<{
serviceId: string | number
}>()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
const DB_KEY = computed(() => `tzGMF-${props.contractId}-${props.serviceId}`)
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
@ -1058,7 +1059,7 @@ const buildRowsFromImportDefaultSource = async (
): Promise<DetailRow[]> => {
// 使
await loadFactorDefaults()
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
if (isOnlyCostScaleService.value) {
return hasContractRows
@ -1129,14 +1130,14 @@ const applyProjectCountChange = async (nextValue: unknown) => {
const loadFromIndexedDB = async () => {
try {
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
activeIndustryCode.value =
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
projectCount.value = 1
await ensureFactorDefaultsLoaded()
const applyContractDefaultRows = async () => {
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
const targetProjectCount = getTargetProjectCount()
if (isOnlyCostScaleService.value) {
@ -1194,13 +1195,13 @@ const loadFromIndexedDB = async () => {
const importContractData = async () => {
try {
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
activeIndustryCode.value =
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
// 使退
await loadFactorDefaults()
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
const targetProjectCount = getTargetProjectCount()
if (isOnlyCostScaleService.value) {

View File

@ -2,13 +2,13 @@
import { computed, onActivated, onBeforeUnmount, onMounted, ref } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, ColGroupDef } from 'ag-grid-community'
import localforage from 'localforage'
import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorIdInIndustryScope } from '@/sql'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
@ -94,6 +94,7 @@ const props = defineProps<{
serviceId: string | number
}>()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
const DB_KEY = computed(() => `ydGMF-${props.contractId}-${props.serviceId}`)
const HT_DB_KEY = computed(() => `ht-info-v3-${props.contractId}`)
const HT_CONSULT_FACTOR_KEY = computed(() => `ht-consult-category-factor-v1-${props.contractId}`)
@ -911,7 +912,7 @@ const buildRowsFromImportDefaultSource = async (
): Promise<DetailRow[]> => {
// 使
await loadFactorDefaults()
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
return hasContractRows
? mergeWithDictRows(htData!.detailRows, {
@ -975,14 +976,14 @@ const applyProjectCountChange = async (nextValue: unknown) => {
const loadFromIndexedDB = async () => {
try {
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
activeIndustryCode.value =
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
projectCount.value = 1
await ensureFactorDefaultsLoaded()
const applyContractDefaultRows = async () => {
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
const targetProjectCount = getTargetProjectCount()
detailRows.value = hasContractRows
@ -1027,13 +1028,13 @@ const loadFromIndexedDB = async () => {
const importContractData = async () => {
try {
const baseInfo = await localforage.getItem<XmBaseInfoState>(BASE_INFO_KEY)
const baseInfo = await kvStore.getItem<XmBaseInfoState>(BASE_INFO_KEY)
activeIndustryCode.value =
typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
// 使退
await loadFactorDefaults()
const htData = await localforage.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
const targetProjectCount = getTargetProjectCount()
detailRows.value = hasContractRows

View File

@ -10,8 +10,8 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw, onBeforeUnmount, onMounted, ref } from 'vue'
import localforage from 'localforage'
import TypeLine from '@/layout/typeLine.vue'
import { useKvStore } from '@/pinia/kv'
const infoView = markRaw(defineAsyncComponent(() => import('@/components/views/info.vue')))
const scaleInfoView = markRaw(defineAsyncComponent(() => import('@/components/views/xmInfo.vue')))
@ -27,6 +27,7 @@ const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
const hasProjectBaseInfo = ref(false)
const kvStore = useKvStore()
const fullXmCategories = [
{ key: 'info', label: '基础信息', component: infoView },
@ -42,7 +43,7 @@ const xmCategories = computed(() =>
const refreshProjectBaseInfoState = async () => {
try {
const data = await localforage.getItem(PROJECT_INFO_KEY)
const data = await kvStore.getItem(PROJECT_INFO_KEY)
hasProjectBaseInfo.value = Boolean(data)
} catch (error) {
console.error('read project base info failed:', error)

View File

@ -3,7 +3,6 @@ import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, o
import type { ComponentPublicInstance, PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community'
import localforage from 'localforage'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { addNumbers } from '@/lib/decimal'
@ -32,6 +31,7 @@ import { TooltipProvider } from '@/components/ui/tooltip'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
interface ServiceItem {
@ -76,6 +76,7 @@ const props = defineProps<{
}>()
const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
@ -423,7 +424,7 @@ const clearPricingPaneValues = async (serviceId: string) => {
}
zxFwPricingStore.removeAllServicePricingMethodStates(props.contractId, serviceId)
// ResetIndexedDB
await Promise.all(keys.map(key => localforage.removeItem(key)))
await Promise.all(keys.map(key => kvStore.removeItem(key)))
}
const clearRowValues = async (row: DetailRow) => {
@ -995,7 +996,7 @@ const initializeContractState = async () => {
const loadProjectIndustry = async () => {
try {
const data = await localforage.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
const data = await kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
projectIndustry.value =
typeof data?.projectIndustry === 'string' ? data.projectIndustry.trim() : ''
} catch (error) {

View File

@ -1,15 +1,14 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, defineAsyncComponent, markRaw, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import draggable from 'vuedraggable'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
import { ChevronDown, CircleHelp, RotateCcw, X } from 'lucide-vue-next'
import { createPinia } from 'pinia';
import localforage from 'localforage'
import {
AlertDialogAction,
@ -30,12 +29,18 @@ interface DataEntry {
value: any
}
interface ForageStoreSnapshot {
storeName: string
entries: DataEntry[]
}
interface DataPackage {
version: number
exportedAt: string
localStorage: DataEntry[]
sessionStorage: DataEntry[]
localforageDefault: DataEntry[]
localforageStores?: ForageStoreSnapshot[]
}
interface UserGuideStep {
@ -268,6 +273,9 @@ const PROJECT_INFO_DB_KEY = 'xm-base-info-v1'
const LEGACY_PROJECT_DB_KEY = 'xm-info-v3'
const CONSULT_CATEGORY_FACTOR_DB_KEY = 'xm-consult-category-factor-v1'
const MAJOR_FACTOR_DB_KEY = 'xm-major-factor-v1'
const PINIA_PERSIST_DB_NAME = 'DB'
const PINIA_PERSIST_BASE_STORE_NAME = 'pinia'
const PINIA_PERSIST_STORE_IDS = ['tabs', 'zxFwPricing', 'kv'] as const
const userGuideSteps: UserGuideStep[] = [
{
title: '欢迎使用',
@ -352,6 +360,7 @@ const componentMap: Record<string, any> = {
const tabStore = useTabStore()
const zxFwPricingStore = useZxFwPricingStore()
const kvStore = useKvStore()
@ -365,7 +374,7 @@ const dataMenuOpen = ref(false)
const dataMenuRef = ref<HTMLElement | null>(null)
const importFileRef = ref<HTMLInputElement | null>(null)
const importConfirmOpen = ref(false)
const pendingImportPayload = ref<DataPackage | null>(null)
const pendingImportPayload = shallowRef<DataPackage | null>(null)
const pendingImportFileName = ref('')
const userGuideOpen = ref(false)
const userGuideStepIndex = ref(0)
@ -450,10 +459,10 @@ const shouldAutoOpenGuide = async () => {
if (hasGuideCompleted()) return false
if (hasNonDefaultTabState()) return false
try {
const keys = await localforage.keys()
const keys = await kvStore.keys()
return keys.length === 0
} catch (error) {
console.error('read localforage keys failed:', error)
console.error('read kv keys failed:', error)
return false
}
}
@ -734,20 +743,49 @@ const writeWebStorage = (storageObj: Storage, entries: DataEntry[]) => {
}
}
const readForage = async (store: typeof localforage): Promise<DataEntry[]> => {
type ForageInstance = ReturnType<typeof localforage.createInstance>
type ForageStore = Pick<ForageInstance, 'keys' | 'getItem' | 'setItem' | 'clear'>
const createForageStore = (storeName: string): ForageInstance =>
localforage.createInstance({
name: PINIA_PERSIST_DB_NAME,
storeName
})
const getPiniaPersistStoreName = (storeId: string) => `${PINIA_PERSIST_BASE_STORE_NAME}-${storeId}`
const getPiniaPersistStores = () =>
PINIA_PERSIST_STORE_IDS.map(storeId => {
const storeName = getPiniaPersistStoreName(storeId)
return {
storeName,
store: createForageStore(storeName)
}
})
const readForage = async (store: ForageStore): Promise<DataEntry[]> => {
const keys = await store.keys()
const entries: DataEntry[] = []
for (const key of keys) {
const value = await store.getItem(key)
entries.push({ key, value })
entries.push({ key, value: toPersistableValue(value) })
}
return entries
}
const writeForage = async (store: typeof localforage, entries: DataEntry[]) => {
const toPersistableValue = (value: unknown) => {
try {
return JSON.parse(JSON.stringify(value))
} catch (error) {
console.error('normalize persist value failed, fallback to null:', error)
return null
}
}
const writeForage = async (store: ForageStore, entries: DataEntry[]) => {
await store.clear()
for (const entry of entries || []) {
await store.setItem(entry.key, entry.value)
await store.setItem(entry.key, toPersistableValue(entry.value))
}
}
@ -758,6 +796,21 @@ const normalizeEntries = (value: unknown): DataEntry[] => {
.map(item => ({ key: String((item as any).key), value: (item as any).value }))
}
const normalizeForageStoreSnapshots = (value: unknown): ForageStoreSnapshot[] => {
if (!Array.isArray(value)) return []
return value
.filter(item =>
item
&& typeof item === 'object'
&& typeof (item as any).storeName === 'string'
&& Array.isArray((item as any).entries)
)
.map(item => ({
storeName: String((item as any).storeName),
entries: normalizeEntries((item as any).entries)
}))
}
const sanitizeFileNamePart = (value: string): string => {
const cleaned = value
.replace(/[\\/:*?"<>|]/g, '_')
@ -1058,11 +1111,11 @@ const buildServiceFee = (
const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([
localforage.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
localforage.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
localforage.getItem<DetailRowsStorageLike<FactorRowLike>>(CONSULT_CATEGORY_FACTOR_DB_KEY),
localforage.getItem<DetailRowsStorageLike<FactorRowLike>>(MAJOR_FACTOR_DB_KEY),
localforage.getItem<ContractCardItem[]>('ht-card-v1')
kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
kvStore.getItem<XmInfoStorageLike>(LEGACY_PROJECT_DB_KEY),
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(CONSULT_CATEGORY_FACTOR_DB_KEY),
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(MAJOR_FACTOR_DB_KEY),
kvStore.getItem<ContractCardItem[]>('ht-card-v1')
])
const projectInfo = projectInfoRaw || {}
@ -1087,8 +1140,8 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const contract = contractCards[index]
const contractId = contract.id
const [htInfoRaw, zxFwRaw] = await Promise.all([
localforage.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
localforage.getItem<ZxFwStorageLike>(`zxFW-${contractId}`)
kvStore.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`)
])
const zxRows = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : []
@ -1179,12 +1232,19 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const exportData = async () => {
try {
const now = new Date()
const piniaForageStores = await Promise.all(
getPiniaPersistStores().map(async ({ storeName, store }) => ({
storeName,
entries: await readForage(store)
}))
)
const payload: DataPackage = {
version: 1,
version: 2,
exportedAt: now.toISOString(),
localStorage: readWebStorage(localStorage),
sessionStorage: readWebStorage(sessionStorage),
localforageDefault: await readForage(localforage),
localforageStores: piniaForageStores
}
const content = await encodeZwArchive(payload)
@ -1261,9 +1321,44 @@ const confirmImportOverride = async () => {
writeWebStorage(localStorage, normalizeEntries(payload.localStorage))
writeWebStorage(sessionStorage, normalizeEntries(payload.sessionStorage))
await writeForage(localforage, normalizeEntries(payload.localforageDefault))
const piniaSnapshots = normalizeForageStoreSnapshots(payload.localforageStores)
const snapshotMap = new Map(piniaSnapshots.map(item => [item.storeName, item.entries]))
await Promise.all(
getPiniaPersistStores().map(async ({ storeName, store }) => {
const entries = snapshotMap.get(storeName) || []
await writeForage(store, entries)
})
)
const readPersistedState = (storeId: string) => {
const storeName = getPiniaPersistStoreName(storeId)
const entries = snapshotMap.get(storeName) || []
const hit = entries.find(entry => entry.key === storeName)
return hit && hit.value && typeof hit.value === 'object' ? hit.value : null
}
const tabsState = readPersistedState('tabs')
if (tabsState) {
tabStore.$patch(tabsState as any)
} else {
tabStore.resetTabs()
await tabStore.$persistNow?.()
}
const zxFwPricingState = readPersistedState('zxFwPricing')
if (zxFwPricingState) {
zxFwPricingStore.$patch(zxFwPricingState as any)
}
const kvState = readPersistedState('kv')
if (kvState) {
kvStore.$patch(kvState as any)
}
await Promise.all([
tabStore.$persistNow?.(),
zxFwPricingStore.$persistNow?.(),
kvStore.$persistNow?.()
])
dataMenuOpen.value = false
window.location.reload()
} catch (error) {
@ -1279,6 +1374,11 @@ const handleReset = async () => {
localStorage.clear()
sessionStorage.clear()
await localforage.clear()
await Promise.all(
getPiniaPersistStores().map(async ({ store }) => {
await store.clear()
})
)
localStorage.setItem(USER_GUIDE_COMPLETED_KEY, '1')
} catch (error) {
console.error('reset failed:', error)

View File

@ -1,4 +1,3 @@
import localforage from 'localforage'
import {
expertList,
getMajorDictEntries,
@ -10,6 +9,7 @@ import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv'
interface StoredDetailRowsState<T = any> {
detailRows?: T[]
@ -123,6 +123,26 @@ const getZxFwStoreSafely = () => {
}
}
const getKvStoreSafely = () => {
try {
return useKvStore()
} catch {
return null
}
}
const kvGetItem = async <T = unknown>(key: string): Promise<T | null> => {
const store = getKvStoreSafely()
if (!store) return null
return store.getItem<T>(key)
}
const kvSetItem = async <T = unknown>(key: string, value: T): Promise<void> => {
const store = getKvStoreSafely()
if (!store) return
await store.setItem(key, value)
}
const toStoredDetailRowsState = <TRow = unknown>(state: { detailRows?: TRow[] } | null | undefined): StoredDetailRowsState<TRow> | null => {
if (!state || !Array.isArray(state.detailRows)) return null
return {
@ -594,10 +614,10 @@ const loadPricingMethodDefaultBuildContext = async (
const baseInfoDbKey = 'xm-base-info-v1'
const [htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
localforage.getItem<StoredDetailRowsState>(htDbKey),
localforage.getItem<StoredFactorState>(consultFactorDbKey),
localforage.getItem<StoredFactorState>(majorFactorDbKey),
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
kvGetItem<StoredDetailRowsState>(htDbKey),
kvGetItem<StoredFactorState>(consultFactorDbKey),
kvGetItem<StoredFactorState>(majorFactorDbKey),
kvGetItem<XmBaseInfoState>(baseInfoDbKey)
])
return {
@ -668,10 +688,10 @@ export const persistDefaultPricingMethodDetailRowsForServices = async (params: {
}
}
await Promise.all([
localforage.setItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
localforage.setItem(dbKeys.landScale, { detailRows: defaultRows.landScale }),
localforage.setItem(dbKeys.workload, { detailRows: defaultRows.workload }),
localforage.setItem(dbKeys.hourly, { detailRows: defaultRows.hourly })
kvSetItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
kvSetItem(dbKeys.landScale, { detailRows: defaultRows.landScale }),
kvSetItem(dbKeys.workload, { detailRows: defaultRows.workload }),
kvSetItem(dbKeys.hourly, { detailRows: defaultRows.hourly })
])
})
)
@ -695,17 +715,17 @@ export const getPricingMethodTotalsForService = async (params: {
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'landScale') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'workload') || Promise.resolve(null),
store?.loadServicePricingMethodState<Record<string, unknown>>(params.contractId, serviceId, 'hourly') || Promise.resolve(null),
localforage.getItem<StoredDetailRowsState>(htDbKey),
localforage.getItem<StoredFactorState>(consultFactorDbKey),
localforage.getItem<StoredFactorState>(majorFactorDbKey),
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
kvGetItem<StoredDetailRowsState>(htDbKey),
kvGetItem<StoredFactorState>(consultFactorDbKey),
kvGetItem<StoredFactorState>(majorFactorDbKey),
kvGetItem<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)
storeInvestData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.investScale),
storeLandData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.landScale),
storeWorkloadData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.workload),
storeHourlyData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.hourly)
])
const investData = toStoredDetailRowsState(storeInvestData) || investDataFallback
@ -799,10 +819,10 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
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)
storeInvestData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.investScale),
storeLandData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.landScale),
storeWorkloadData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.workload),
storeHourlyData ? Promise.resolve(null) : kvGetItem<StoredDetailRowsState>(dbKeys.hourly)
])
const investData = toStoredDetailRowsState(storeInvestData) || investDataFallback
const landData = toStoredDetailRowsState(storeLandData) || landDataFallback
@ -829,7 +849,7 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
detailRows: getDefaultRows().investScale
}, { force: true })
}
writeTasks.push(localforage.setItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
writeTasks.push(kvSetItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
}
if (shouldInitLand) {
@ -838,7 +858,7 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
detailRows: getDefaultRows().landScale
}, { force: true })
}
writeTasks.push(localforage.setItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
writeTasks.push(kvSetItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
}
if (shouldInitWorkload) {
@ -847,7 +867,7 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
detailRows: getDefaultRows().workload
}, { force: true })
}
writeTasks.push(localforage.setItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
writeTasks.push(kvSetItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
}
if (shouldInitHourly) {
@ -856,7 +876,7 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
detailRows: getDefaultRows().hourly
}, { force: true })
}
writeTasks.push(localforage.setItem(dbKeys.hourly, { detailRows: getDefaultRows().hourly }))
writeTasks.push(kvSetItem(dbKeys.hourly, { detailRows: getDefaultRows().hourly }))
}
if (writeTasks.length > 0) {

View File

@ -1,6 +1,6 @@
import localforage from 'localforage'
import { getMajorDictById, getMajorIdAliasMap, getServiceDictById } from '@/sql'
import { toFiniteNumberOrNull } from '@/lib/number'
import { useKvStore } from '@/pinia/kv'
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
@ -41,12 +41,21 @@ const resolveFactorValue = (
return fallbackStandard
}
const getKvStoreSafely = () => {
try {
return useKvStore()
} catch {
return null
}
}
const loadFactorMap = async (
storageKey: string,
dict: FactorDict,
aliases?: Map<string, string>
): Promise<Map<string, number | null>> => {
const data = await localforage.getItem<XmFactorState>(storageKey)
const kvStore = getKvStoreSafely()
const data = kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null
const map = buildStandardFactorMap(dict)
for (const row of data?.detailRows || []) {
if (!row?.id) continue

90
src/pinia/kv.ts Normal file
View File

@ -0,0 +1,90 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
export const useKvStore = defineStore('kv', () => {
const entries = ref<Record<string, unknown>>({})
const ready = ref(false)
const loading = ref<Promise<void> | null>(null)
const ensureReady = async () => {
if (ready.value) return
if (loading.value) {
await loading.value
return
}
loading.value = (async () => {
ready.value = true
})()
await loading.value
loading.value = null
}
const getItem = async <T = unknown>(keyRaw: string | number): Promise<T | null> => {
await ensureReady()
const key = String(keyRaw || '').trim()
if (!key) return null
if (!Object.prototype.hasOwnProperty.call(entries.value, key)) return null
return cloneJson(entries.value[key] as T)
}
const setItem = async <T = unknown>(keyRaw: string | number, value: T): Promise<void> => {
await ensureReady()
const key = String(keyRaw || '').trim()
if (!key) return
entries.value[key] = cloneJson(value)
}
const removeItem = async (keyRaw: string | number): Promise<void> => {
await ensureReady()
const key = String(keyRaw || '').trim()
if (!key) return
delete entries.value[key]
}
const keys = async (): Promise<string[]> => {
await ensureReady()
return Object.keys(entries.value)
}
const clear = async (): Promise<void> => {
await ensureReady()
entries.value = {}
}
const exportEntries = async () => {
await ensureReady()
return Object.entries(entries.value).map(([key, value]) => ({ key, value: cloneJson(value) }))
}
const importEntries = async (
incoming: Array<{ key: string; value: unknown }>,
options?: { replace?: boolean }
) => {
await ensureReady()
if (options?.replace !== false) {
entries.value = {}
}
for (const item of incoming || []) {
const key = String(item?.key || '').trim()
if (!key) continue
entries.value[key] = cloneJson(item.value)
}
}
return {
entries,
ready,
ensureReady,
getItem,
setItem,
removeItem,
keys,
clear,
exportEntries,
importEntries
}
}, {
persist: true
})

View File

@ -1,8 +1,8 @@
import localforage from 'localforage'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { addNumbers } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { useKvStore } from '@/pinia/kv'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
export type ServicePricingMethod = ZxFwPricingField
@ -276,6 +276,13 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const touchKeyVersion = (key: string) => {
keyVersions.value[key] = (keyVersions.value[key] || 0) + 1
}
const getKvStoreSafely = () => {
try {
return useKvStore()
} catch {
return null
}
}
const ensureServicePricingState = (contractIdRaw: string | number, serviceIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
@ -658,7 +665,8 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (!force && keyLoadTasks.has(key)) return keyLoadTasks.get(key) as Promise<T | null>
const task = (async () => {
const raw = await localforage.getItem<T>(key)
const kvStore = getKvStoreSafely()
const raw = kvStore ? await kvStore.getItem<T>(key) : null
const nextSnapshot = toKeySnapshot(raw)
const prevSnapshot = keySnapshots.value[key]
keyedLoaded.value[key] = true
@ -793,7 +801,10 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
if (!force && loadTasks.has(contractId)) return loadTasks.get(contractId) as Promise<ZxFwState | null>
const task = (async () => {
const raw = await localforage.getItem<ZxFwState>(dbKeyOf(contractId))
const kvStore = getKvStoreSafely()
const raw = kvStore
? await kvStore.getItem<ZxFwState>(dbKeyOf(contractId))
: null
const current = contracts.value[contractId]
if (raw) {
const normalized = normalizeState(raw)
@ -892,6 +903,76 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
return round3(sum)
}
const removeContractData = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return false
let changed = false
if (Object.prototype.hasOwnProperty.call(contracts.value, contractId)) {
delete contracts.value[contractId]
changed = true
}
if (Object.prototype.hasOwnProperty.call(servicePricingStates.value, contractId)) {
delete servicePricingStates.value[contractId]
changed = true
}
if (Object.prototype.hasOwnProperty.call(contractLoaded.value, contractId)) {
delete contractLoaded.value[contractId]
changed = true
}
if (Object.prototype.hasOwnProperty.call(contractVersions.value, contractId)) {
delete contractVersions.value[contractId]
changed = true
}
loadTasks.delete(contractId)
const htMainPrefix = `htExtraFee-${contractId}-`
for (const key of Object.keys(htFeeMainStates.value)) {
if (!key.startsWith(htMainPrefix)) continue
delete htFeeMainStates.value[key]
changed = true
}
for (const key of Object.keys(htFeeMethodStates.value)) {
if (!key.startsWith(htMainPrefix)) continue
delete htFeeMethodStates.value[key]
changed = true
}
const methodPrefixes = Object.values(METHOD_STORAGE_PREFIX_MAP)
const isContractRelatedKey = (key: string) => {
if (key === dbKeyOf(contractId)) return true
if (key.startsWith(htMainPrefix)) return true
if (methodPrefixes.some(prefix => key.startsWith(`${prefix}-${contractId}-`))) return true
return false
}
const keySet = new Set<string>([
...Object.keys(keyedStates.value),
...Object.keys(keyedLoaded.value),
...Object.keys(keyVersions.value),
...Object.keys(keySnapshots.value)
])
for (const key of keySet) {
if (!isContractRelatedKey(key)) continue
if (Object.prototype.hasOwnProperty.call(keyedStates.value, key)) {
delete keyedStates.value[key]
changed = true
}
if (Object.prototype.hasOwnProperty.call(keyedLoaded.value, key)) {
delete keyedLoaded.value[key]
}
if (Object.prototype.hasOwnProperty.call(keyVersions.value, key)) {
delete keyVersions.value[key]
}
if (Object.prototype.hasOwnProperty.call(keySnapshots.value, key)) {
delete keySnapshots.value[key]
}
keyLoadTasks.delete(key)
}
return changed
}
return {
contracts,
contractVersions,
@ -906,6 +987,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
setContractState,
updatePricingField,
getBaseSubtotal,
removeContractData,
getKeyState,
loadKeyState,
setKeyState,