VetNet-LCPS / vetcopilot-app /src /hooks /useAutoSave.ts
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
4.19 kB
/**
* 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,
}
}