flnxi-Wu/lib/draft-storage.ts
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

396 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 草稿自动保存工具
*
* 功能:
* - 表单填写时自动保存到localStorage
* - 页面刷新时提示恢复数据
* - 提交失败时保留用户输入
* - 检测多标签页冲突
* - 自动清理过期草稿
*/
/**
* 草稿数据结构
*/
export interface DraftData<T = Record<string, unknown>> {
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<DraftStorageConfig> = {
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<T>(
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<T>(draftId, config)
let version = 1
if (existingDraft && existingDraft.tabId !== tabId) {
// 其他标签页的草稿,增加版本号
version = existingDraft.version + 1
} else if (existingDraft) {
version = existingDraft.version
}
const draft: DraftData<T> = {
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<T>(
draftId: string,
config: DraftStorageConfig = {}
): DraftData<T> | 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<T>
// 检查是否过期
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<T>(
draftId: string,
config: DraftStorageConfig = {}
): { hasConflict: boolean; otherTabDraft?: DraftData<T> } {
if (typeof window === 'undefined') {
return { hasConflict: false }
}
const draft = getDraft<T>(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<T>(
formType?: string,
config: DraftStorageConfig = {}
): DraftData<T>[] {
if (typeof window === 'undefined') {
return []
}
const prefix = config.prefix || DEFAULT_CONFIG.prefix
const drafts: DraftData<T>[] = []
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<T>
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')
}
}