/** * 草稿自动保存工具 * * 功能: * - 表单填写时自动保存到localStorage * - 页面刷新时提示恢复数据 * - 提交失败时保留用户输入 * - 检测多标签页冲突 * - 自动清理过期草稿 */ /** * 草稿数据结构 */ export interface DraftData> { id: string // 草稿唯一标识 formType: string // 表单类型(如 'material-upload', 'template-edit') data: T // 表单数据 createdAt: string // 创建时间 updatedAt: string // 最后更新时间 tabId: string // 创建此草稿的标签页ID version: number // 版本号(用于冲突检测) } /** * 草稿存储配置 */ export interface DraftStorageConfig { prefix?: string // localStorage key前缀 maxAge?: number // 草稿最大保存时间(毫秒),默认7天 autoSaveInterval?: number // 自动保存间隔(毫秒),默认3秒 maxDrafts?: number // 最大草稿数量,默认10 } const DEFAULT_CONFIG: Required = { prefix: 'flnxi_draft_', maxAge: 7 * 24 * 60 * 60 * 1000, // 7天 autoSaveInterval: 3000, // 3秒 maxDrafts: 10, } /** * 生成唯一的标签页ID */ export function generateTabId(): string { // 使用sessionStorage存储标签页ID,确保每个标签页有唯一ID const storageKey = 'flnxi_tab_id' if (typeof window === 'undefined') { return 'server' } let tabId = sessionStorage.getItem(storageKey) if (!tabId) { tabId = `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` sessionStorage.setItem(storageKey, tabId) } return tabId } /** * 生成草稿ID */ export function generateDraftId(formType: string, entityId?: string): string { if (entityId) { return `${formType}_${entityId}` } return `${formType}_new_${Date.now()}` } /** * 获取localStorage key */ function getStorageKey(draftId: string, config: DraftStorageConfig): string { const prefix = config.prefix || DEFAULT_CONFIG.prefix return `${prefix}${draftId}` } /** * 保存草稿 */ export function saveDraft( draftId: string, formType: string, data: T, config: DraftStorageConfig = {} ): { success: boolean; error?: string } { if (typeof window === 'undefined') { return { success: false, error: '服务端不支持草稿保存' } } try { const key = getStorageKey(draftId, config) const tabId = generateTabId() // 检查是否存在其他标签页的草稿 const existingDraft = getDraft(draftId, config) let version = 1 if (existingDraft && existingDraft.tabId !== tabId) { // 其他标签页的草稿,增加版本号 version = existingDraft.version + 1 } else if (existingDraft) { version = existingDraft.version } const draft: DraftData = { id: draftId, formType, data, createdAt: existingDraft?.createdAt || new Date().toISOString(), updatedAt: new Date().toISOString(), tabId, version, } localStorage.setItem(key, JSON.stringify(draft)) // 清理过期草稿 cleanExpiredDrafts(config) return { success: true } } catch (error) { console.error('[DraftStorage] Save failed:', error) return { success: false, error: error instanceof Error ? error.message : '保存草稿失败' } } } /** * 获取草稿 */ export function getDraft( draftId: string, config: DraftStorageConfig = {} ): DraftData | null { if (typeof window === 'undefined') { return null } try { const key = getStorageKey(draftId, config) const stored = localStorage.getItem(key) if (!stored) { return null } const draft = JSON.parse(stored) as DraftData // 检查是否过期 const maxAge = config.maxAge || DEFAULT_CONFIG.maxAge const updatedAt = new Date(draft.updatedAt).getTime() if (Date.now() - updatedAt > maxAge) { // 草稿已过期,删除并返回null deleteDraft(draftId, config) return null } return draft } catch (error) { console.error('[DraftStorage] Get failed:', error) return null } } /** * 删除草稿 */ export function deleteDraft( draftId: string, config: DraftStorageConfig = {} ): boolean { if (typeof window === 'undefined') { return false } try { const key = getStorageKey(draftId, config) localStorage.removeItem(key) return true } catch (error) { console.error('[DraftStorage] Delete failed:', error) return false } } /** * 检查是否存在草稿 */ export function hasDraft( draftId: string, config: DraftStorageConfig = {} ): boolean { return getDraft(draftId, config) !== null } /** * 检测多标签页冲突 */ export function checkConflict( draftId: string, config: DraftStorageConfig = {} ): { hasConflict: boolean; otherTabDraft?: DraftData } { if (typeof window === 'undefined') { return { hasConflict: false } } const draft = getDraft(draftId, config) if (!draft) { return { hasConflict: false } } const currentTabId = generateTabId() if (draft.tabId !== currentTabId) { return { hasConflict: true, otherTabDraft: draft } } return { hasConflict: false } } /** * 获取所有草稿 */ export function getAllDrafts( formType?: string, config: DraftStorageConfig = {} ): DraftData[] { if (typeof window === 'undefined') { return [] } const prefix = config.prefix || DEFAULT_CONFIG.prefix const drafts: DraftData[] = [] try { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) if (key && key.startsWith(prefix)) { const stored = localStorage.getItem(key) if (stored) { try { const draft = JSON.parse(stored) as DraftData if (!formType || draft.formType === formType) { drafts.push(draft) } } catch { // 忽略解析错误 } } } } } catch (error) { console.error('[DraftStorage] GetAll failed:', error) } // 按更新时间排序 return drafts.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() ) } /** * 清理过期草稿 */ export function cleanExpiredDrafts(config: DraftStorageConfig = {}): number { if (typeof window === 'undefined') { return 0 } const prefix = config.prefix || DEFAULT_CONFIG.prefix const maxAge = config.maxAge || DEFAULT_CONFIG.maxAge const maxDrafts = config.maxDrafts || DEFAULT_CONFIG.maxDrafts const now = Date.now() let cleanedCount = 0 try { const allDrafts: Array<{ key: string; updatedAt: number }> = [] // 收集所有草稿 for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) if (key && key.startsWith(prefix)) { const stored = localStorage.getItem(key) if (stored) { try { const draft = JSON.parse(stored) as DraftData const updatedAt = new Date(draft.updatedAt).getTime() // 删除过期草稿 if (now - updatedAt > maxAge) { localStorage.removeItem(key) cleanedCount++ } else { allDrafts.push({ key, updatedAt }) } } catch { // 删除无效数据 localStorage.removeItem(key) cleanedCount++ } } } } // 如果草稿数量超过限制,删除最旧的 if (allDrafts.length > maxDrafts) { allDrafts.sort((a, b) => b.updatedAt - a.updatedAt) const toRemove = allDrafts.slice(maxDrafts) for (const item of toRemove) { localStorage.removeItem(item.key) cleanedCount++ } } } catch (error) { console.error('[DraftStorage] Clean failed:', error) } return cleanedCount } /** * 清除所有草稿 */ export function clearAllDrafts( formType?: string, config: DraftStorageConfig = {} ): number { if (typeof window === 'undefined') { return 0 } const prefix = config.prefix || DEFAULT_CONFIG.prefix let clearedCount = 0 try { const keysToRemove: string[] = [] for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) if (key && key.startsWith(prefix)) { if (formType) { const stored = localStorage.getItem(key) if (stored) { try { const draft = JSON.parse(stored) as DraftData if (draft.formType === formType) { keysToRemove.push(key) } } catch { keysToRemove.push(key) } } } else { keysToRemove.push(key) } } } for (const key of keysToRemove) { localStorage.removeItem(key) clearedCount++ } } catch (error) { console.error('[DraftStorage] ClearAll failed:', error) } return clearedCount } /** * 格式化草稿时间 */ export function formatDraftTime(isoString: string): string { const date = new Date(isoString) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMins < 1) { return '刚刚' } else if (diffMins < 60) { return `${diffMins}分钟前` } else if (diffHours < 24) { return `${diffHours}小时前` } else if (diffDays < 7) { return `${diffDays}天前` } else { return date.toLocaleDateString('zh-CN') } }