import type localforage from 'localforage' export interface DataEntry { key: string value: any } export interface ForageStoreSnapshot { storeName: string entries: DataEntry[] } export interface DataPackage { version: number packageType?: 'project-snapshot' exportedAt: string projectId?: string localStorage: DataEntry[] sessionStorage: DataEntry[] localforageDefault: DataEntry[] localforageStores?: ForageStoreSnapshot[] } export type ForageInstance = ReturnType export type ForageStore = Pick type XmInfoLike = { projectName?: unknown } export const readWebStorage = (storageObj: Storage): DataEntry[] => { const entries: DataEntry[] = [] for (let i = 0; i < storageObj.length; i++) { const key = storageObj.key(i) if (!key) continue const raw = storageObj.getItem(key) let value: any = raw if (raw != null) { try { value = JSON.parse(raw) } catch { value = raw } } entries.push({ key, value }) } return entries } export const writeWebStorage = (storageObj: Storage, entries: DataEntry[]) => { storageObj.clear() for (const entry of entries || []) { const value = typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value) storageObj.setItem(entry.key, value) } } export 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 } } export const readForage = async (store: ForageStore): Promise => { const keys = await store.keys() const values = await Promise.all(keys.map(key => store.getItem(key))) return keys.map((key, index) => ({ key, value: toPersistableValue(values[index]) })) } export const writeForage = async (store: ForageStore, entries: DataEntry[]) => { await store.clear() await Promise.all((entries || []).map(entry => store.setItem(entry.key, toPersistableValue(entry.value)))) } export const normalizeEntries = (value: unknown): DataEntry[] => { if (!Array.isArray(value)) return [] return value .filter(item => item && typeof item === 'object' && typeof (item as any).key === 'string') .map(item => ({ key: String((item as any).key), value: (item as any).value })) } export 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) })) } export const sanitizeFileNamePart = (value: string): string => { const cleaned = value .replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, ' ') .trim() return cleaned || '造价项目' } export const getExportProjectName = (entries: DataEntry[], projectInfoDbKey: string, legacyProjectDbKey: string) => { const target = entries.find(item => item.key === projectInfoDbKey) || entries.find(item => item.key === legacyProjectDbKey) const data = (target?.value || {}) as XmInfoLike return typeof data.projectName === 'string' ? sanitizeFileNamePart(data.projectName) : '造价项目' } export const isDataPackageLike = (value: unknown): value is DataPackage => { if (!value || typeof value !== 'object') return false const payload = value as Partial const hasRequiredArrays = Array.isArray(payload.localStorage) && Array.isArray(payload.sessionStorage) && Array.isArray(payload.localforageDefault) if (!hasRequiredArrays) return false if (typeof payload.version !== 'number' || !Number.isFinite(payload.version)) return false if (payload.packageType != null && payload.packageType !== 'project-snapshot') return false if (payload.projectId != null && typeof payload.projectId !== 'string') return false return true }