/** * 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 { /** localStorage 存储键 */ key: string /** 需要自动保存的数据 */ data: T /** 防抖延迟(ms),默认 2000 */ debounceMs?: number /** 自定义保存函数(如保存到后端),返回 true 表示成功 */ onSave?: (data: T) => Promise /** 是否启用,默认 true */ enabled?: boolean } interface UseAutoSaveReturn { /** 是否正在保存 */ isSaving: boolean /** 上次成功保存的时间字符串 */ lastSaved: string | null /** 是否有未保存的更改(自上次保存后) */ hasUnsavedChanges: boolean /** 手动触发保存 */ saveNow: () => Promise /** 清除已保存的草稿 */ clearDraft: () => void /** 从 localStorage 恢复草稿数据 */ restoreDraft: () => T | null } // ================================================================ // Hook // ================================================================ const DRAFT_PREFIX = 'vetcopilot_autosave_' export function useAutoSave(options: UseAutoSaveOptions): UseAutoSaveReturn { const { key, data, debounceMs = 2000, onSave, enabled = true } = options const [isSaving, setIsSaving] = useState(false) const [lastSaved, setLastSaved] = useState(null) const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) const timerRef = useRef | 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, } }