calculator2026/src/features/tab/importExport.ts
2026-03-25 10:40:19 +08:00

132 lines
4.1 KiB
TypeScript

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<typeof localforage.createInstance>
export type ForageStore = Pick<ForageInstance, 'keys' | 'getItem' | 'setItem' | 'clear'>
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<DataEntry[]> => {
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<DataPackage>
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
}