File size: 4,187 Bytes
793e953 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | /**
* useAutoSave Hook
*
* 表单内容变化后自动保存草稿到 localStorage。
* 使用 debounce 避免频繁写入,提供保存状态指示器。
*
* 用法:
* const { isSaving, lastSaved, hasUnsavedChanges, saveNow } = useAutoSave({
* key: 'consultation_draft',
* data: formData,
* debounceMs: 3000,
* onSave: async (data) => { await api.saveDraft(data) },
* })
*
* // 显示保存状态
* {isSaving && '正在保存...'}
* {lastSaved && !isSaving && `上次保存:${lastSaved}`}
*/
import { useState, useEffect, useCallback, useRef } from 'react'
// ================================================================
// 类型定义
// ================================================================
interface UseAutoSaveOptions<T> {
/** localStorage 存储键 */
key: string
/** 需要自动保存的数据 */
data: T
/** 防抖延迟(ms),默认 2000 */
debounceMs?: number
/** 自定义保存函数(如保存到后端),返回 true 表示成功 */
onSave?: (data: T) => Promise<boolean>
/** 是否启用,默认 true */
enabled?: boolean
}
interface UseAutoSaveReturn<T> {
/** 是否正在保存 */
isSaving: boolean
/** 上次成功保存的时间字符串 */
lastSaved: string | null
/** 是否有未保存的更改(自上次保存后) */
hasUnsavedChanges: boolean
/** 手动触发保存 */
saveNow: () => Promise<void>
/** 清除已保存的草稿 */
clearDraft: () => void
/** 从 localStorage 恢复草稿数据 */
restoreDraft: () => T | null
}
// ================================================================
// Hook
// ================================================================
const DRAFT_PREFIX = 'vetcopilot_autosave_'
export function useAutoSave<T>(options: UseAutoSaveOptions<T>): UseAutoSaveReturn<T> {
const { key, data, debounceMs = 2000, onSave, enabled = true } = options
const [isSaving, setIsSaving] = useState(false)
const [lastSaved, setLastSaved] = useState<string | null>(null)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const storageKey = `${DRAFT_PREFIX}${key}`
const dataRef = useRef(data)
dataRef.current = data
const doSave = useCallback(async () => {
setIsSaving(true)
try {
const currentData = dataRef.current
// 保存到 localStorage
localStorage.setItem(
storageKey,
JSON.stringify({
data: currentData,
timestamp: Date.now(),
})
)
// 调用自定义保存函数
if (onSave) {
const success = await onSave(currentData)
if (!success) throw new Error('Save failed')
}
const now = new Date().toLocaleTimeString()
setLastSaved(now)
setHasUnsavedChanges(false)
} catch {
// 保存失败时不清除 hasUnsavedChanges
} finally {
setIsSaving(false)
}
}, [storageKey, onSave])
// 数据变化时标记未保存,并启动防抖定时器
useEffect(() => {
if (!enabled) return
setHasUnsavedChanges(true)
if (timerRef.current) {
clearTimeout(timerRef.current)
}
timerRef.current = setTimeout(() => {
doSave()
}, debounceMs)
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [data, debounceMs, enabled, doSave])
const saveNow = useCallback(async () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
await doSave()
}, [doSave])
const clearDraft = useCallback(() => {
try {
localStorage.removeItem(storageKey)
} catch {
// ignore
}
setHasUnsavedChanges(false)
}, [storageKey])
const restoreDraft = useCallback((): T | null => {
try {
const raw = localStorage.getItem(storageKey)
if (!raw) return null
const parsed = JSON.parse(raw)
return parsed.data as T
} catch {
return null
}
}, [storageKey])
return {
isSaving,
lastSaved,
hasUnsavedChanges,
saveNow,
clearDraft,
restoreDraft,
}
}
|