1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
189 lines
4.5 KiB
TypeScript
189 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
import {
|
|
saveDraft,
|
|
getDraft,
|
|
deleteDraft,
|
|
hasDraft,
|
|
checkConflict,
|
|
generateDraftId,
|
|
formatDraftTime,
|
|
DraftData,
|
|
DraftStorageConfig,
|
|
} from '@/lib/draft-storage'
|
|
|
|
interface UseDraftStorageOptions<T> extends DraftStorageConfig {
|
|
formType: string
|
|
entityId?: string
|
|
onRestore?: (data: T) => void
|
|
onConflict?: (otherDraft: DraftData<T>) => void
|
|
autoSave?: boolean
|
|
}
|
|
|
|
interface UseDraftStorageReturn<T> {
|
|
// 状态
|
|
draftId: string
|
|
hasDraft: boolean
|
|
lastSaved: string | null
|
|
isConflict: boolean
|
|
conflictDraft: DraftData<T> | null
|
|
|
|
// 操作
|
|
save: (data: T) => boolean
|
|
restore: () => T | null
|
|
clear: () => void
|
|
checkForConflict: () => boolean
|
|
|
|
// 格式化
|
|
formatLastSaved: () => string
|
|
}
|
|
|
|
/**
|
|
* 草稿自动保存 Hook
|
|
*/
|
|
export function useDraftStorage<T extends Record<string, unknown>>(
|
|
options: UseDraftStorageOptions<T>
|
|
): UseDraftStorageReturn<T> {
|
|
const {
|
|
formType,
|
|
entityId,
|
|
onRestore,
|
|
onConflict,
|
|
autoSave = true,
|
|
...config
|
|
} = options
|
|
|
|
// 生成草稿ID
|
|
const draftId = generateDraftId(formType, entityId)
|
|
|
|
// 状态
|
|
const [hasDraftState, setHasDraftState] = useState(false)
|
|
const [lastSaved, setLastSaved] = useState<string | null>(null)
|
|
const [isConflict, setIsConflict] = useState(false)
|
|
const [conflictDraft, setConflictDraft] = useState<DraftData<T> | null>(null)
|
|
|
|
// 自动保存定时器
|
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null)
|
|
const pendingDataRef = useRef<T | null>(null)
|
|
|
|
// 检查是否存在草稿
|
|
useEffect(() => {
|
|
const exists = hasDraft(draftId, config)
|
|
setHasDraftState(exists)
|
|
|
|
if (exists) {
|
|
const draft = getDraft<T>(draftId, config)
|
|
if (draft) {
|
|
setLastSaved(draft.updatedAt)
|
|
|
|
// 检查冲突
|
|
const conflict = checkConflict<T>(draftId, config)
|
|
if (conflict.hasConflict && conflict.otherTabDraft) {
|
|
setIsConflict(true)
|
|
setConflictDraft(conflict.otherTabDraft)
|
|
onConflict?.(conflict.otherTabDraft)
|
|
}
|
|
}
|
|
}
|
|
}, [draftId])
|
|
|
|
// 保存草稿
|
|
const save = useCallback((data: T): boolean => {
|
|
const result = saveDraft(draftId, formType, data, config)
|
|
if (result.success) {
|
|
setHasDraftState(true)
|
|
setLastSaved(new Date().toISOString())
|
|
setIsConflict(false)
|
|
setConflictDraft(null)
|
|
}
|
|
return result.success
|
|
}, [draftId, formType, config])
|
|
|
|
// 恢复草稿
|
|
const restore = useCallback((): T | null => {
|
|
const draft = getDraft<T>(draftId, config)
|
|
if (draft) {
|
|
onRestore?.(draft.data)
|
|
return draft.data
|
|
}
|
|
return null
|
|
}, [draftId, config, onRestore])
|
|
|
|
// 清除草稿
|
|
const clear = useCallback(() => {
|
|
deleteDraft(draftId, config)
|
|
setHasDraftState(false)
|
|
setLastSaved(null)
|
|
setIsConflict(false)
|
|
setConflictDraft(null)
|
|
}, [draftId, config])
|
|
|
|
// 检查冲突
|
|
const checkForConflict = useCallback((): boolean => {
|
|
const conflict = checkConflict<T>(draftId, config)
|
|
setIsConflict(conflict.hasConflict)
|
|
if (conflict.hasConflict && conflict.otherTabDraft) {
|
|
setConflictDraft(conflict.otherTabDraft)
|
|
}
|
|
return conflict.hasConflict
|
|
}, [draftId, config])
|
|
|
|
// 格式化最后保存时间
|
|
const formatLastSaved = useCallback((): string => {
|
|
if (!lastSaved) return ''
|
|
return formatDraftTime(lastSaved)
|
|
}, [lastSaved])
|
|
|
|
// 自动保存逻辑
|
|
const scheduleAutoSave = useCallback((data: T) => {
|
|
if (!autoSave) return
|
|
|
|
pendingDataRef.current = data
|
|
|
|
if (autoSaveTimerRef.current) {
|
|
clearTimeout(autoSaveTimerRef.current)
|
|
}
|
|
|
|
autoSaveTimerRef.current = setTimeout(() => {
|
|
if (pendingDataRef.current) {
|
|
save(pendingDataRef.current)
|
|
pendingDataRef.current = null
|
|
}
|
|
}, config.autoSaveInterval || 3000)
|
|
}, [autoSave, save, config.autoSaveInterval])
|
|
|
|
// 清理定时器
|
|
useEffect(() => {
|
|
return () => {
|
|
if (autoSaveTimerRef.current) {
|
|
clearTimeout(autoSaveTimerRef.current)
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
return {
|
|
draftId,
|
|
hasDraft: hasDraftState,
|
|
lastSaved,
|
|
isConflict,
|
|
conflictDraft,
|
|
save,
|
|
restore,
|
|
clear,
|
|
checkForConflict,
|
|
formatLastSaved,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 草稿恢复提示组件的 Props
|
|
*/
|
|
export interface DraftRecoveryPromptProps<T> {
|
|
hasDraft: boolean
|
|
lastSaved: string | null
|
|
onRestore: () => void
|
|
onDiscard: () => void
|
|
formatLastSaved: () => string
|
|
}
|