fix,去掉大部分indexdb的逻辑
This commit is contained in:
parent
9a045cfe86
commit
3d26b0b259
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() : ''
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
// Reset后会立刻有逻辑读取IndexedDB计算默认值,这里强制同步删除持久层,避免读到旧数据。
|
||||
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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
90
src/pinia/kv.ts
Normal 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
|
||||
})
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user