148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
/**
|
||
* 草稿自动保存 composable。
|
||
*
|
||
* 特性:
|
||
* 1. 防抖保存(默认 2 秒)
|
||
* 2. 乐观锁冲突检测
|
||
* 3. 未保存修改提示
|
||
*/
|
||
import { ref, onUnmounted } from 'vue'
|
||
import api from '@/utils/api'
|
||
|
||
export interface AutoSaveOptions {
|
||
/** 保存端点(字符串或返回字符串的函数,支持动态 endpoint) */
|
||
endpoint: string | (() => string)
|
||
/** 防抖延迟(毫秒) */
|
||
debounceMs?: number
|
||
}
|
||
|
||
export function useAutoSave(options: AutoSaveOptions) {
|
||
const { debounceMs = 2000 } = options
|
||
const getEndpoint: () => string = typeof options.endpoint === 'function'
|
||
? options.endpoint
|
||
: () => options.endpoint as string
|
||
|
||
const draftRevision = ref(1)
|
||
const generatedRevision = ref(0)
|
||
const saving = ref(false)
|
||
const lastSaved = ref<Date | null>(null)
|
||
const conflictError = ref<string | null>(null)
|
||
const hasUnsavedChanges = ref(false)
|
||
|
||
let timer: ReturnType<typeof setTimeout> | null = null
|
||
let workspaceGeneration = 0
|
||
|
||
/**
|
||
* 触发防抖保存。
|
||
* 调用方在数据变更时调用此方法。
|
||
*/
|
||
function scheduleSave(data: Record<string, any>) {
|
||
if (timer) clearTimeout(timer)
|
||
hasUnsavedChanges.value = true
|
||
conflictError.value = null
|
||
|
||
timer = setTimeout(() => {
|
||
doSave(data)
|
||
}, debounceMs)
|
||
}
|
||
|
||
/**
|
||
* 立即保存(用于页面离开前)。
|
||
*/
|
||
async function saveNow(data: Record<string, any>): Promise<boolean> {
|
||
if (timer) {
|
||
clearTimeout(timer)
|
||
timer = null
|
||
}
|
||
return await doSave(data)
|
||
}
|
||
|
||
async function doSave(data: Record<string, any>): Promise<boolean> {
|
||
if (saving.value) return false
|
||
|
||
const saveGeneration = workspaceGeneration
|
||
saving.value = true
|
||
conflictError.value = null
|
||
|
||
try {
|
||
const payload = {
|
||
...data,
|
||
expected_revision: draftRevision.value,
|
||
}
|
||
const res: any = await api.post(getEndpoint(), payload)
|
||
if (saveGeneration !== workspaceGeneration) return false
|
||
// 拦截器已解包 Axios 响应,res = {code: 0, data: {...}}
|
||
if (res?.code === 0) {
|
||
draftRevision.value = res.data?.draft_revision || draftRevision.value + 1
|
||
generatedRevision.value = res.data?.generated_revision || 0
|
||
hasUnsavedChanges.value = res.data?.has_unsaved_changes ?? false
|
||
lastSaved.value = new Date()
|
||
return true
|
||
} else if (res?.code === 1001 && res?.message?.includes('冲突')) {
|
||
syncDraftRevision(Number(res.data?.current_revision))
|
||
conflictError.value = res.message
|
||
hasUnsavedChanges.value = true
|
||
return false
|
||
}
|
||
} catch (e: any) {
|
||
if (saveGeneration !== workspaceGeneration) return false
|
||
const response = e?.response?.data
|
||
if (response?.code === 1001 && response?.message?.includes('冲突')) {
|
||
syncDraftRevision(Number(response.data?.current_revision))
|
||
conflictError.value = response.message
|
||
hasUnsavedChanges.value = true
|
||
return false
|
||
}
|
||
console.warn('自动保存失败:', e)
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
return false
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearTimeout(timer)
|
||
})
|
||
|
||
/**
|
||
* 从服务端状态初始化版本号。
|
||
* 在工作区恢复后调用,避免首次保存时版本冲突。
|
||
*/
|
||
function initRevisions(serverDraftRevision: number, serverGeneratedRevision: number) {
|
||
draftRevision.value = serverDraftRevision || 1
|
||
generatedRevision.value = serverGeneratedRevision || 0
|
||
hasUnsavedChanges.value = draftRevision.value > generatedRevision.value
|
||
}
|
||
|
||
function syncDraftRevision(serverDraftRevision: number) {
|
||
if (Number.isInteger(serverDraftRevision) && serverDraftRevision > 0) {
|
||
draftRevision.value = serverDraftRevision
|
||
}
|
||
}
|
||
|
||
function resetForWorkspace(serverDraftRevision: number = 1, serverGeneratedRevision: number = 0) {
|
||
workspaceGeneration += 1
|
||
if (timer) {
|
||
clearTimeout(timer)
|
||
timer = null
|
||
}
|
||
conflictError.value = null
|
||
lastSaved.value = null
|
||
initRevisions(serverDraftRevision, serverGeneratedRevision)
|
||
}
|
||
|
||
return {
|
||
draftRevision,
|
||
generatedRevision,
|
||
saving,
|
||
lastSaved,
|
||
conflictError,
|
||
hasUnsavedChanges,
|
||
scheduleSave,
|
||
saveNow,
|
||
initRevisions,
|
||
syncDraftRevision,
|
||
resetForWorkspace,
|
||
}
|
||
}
|