Files
writer-work-flow/apps/web/lib/autosave/useAutosave.ts
Yaojia Wang c51623836f fix+feat(ux): 已存草稿显示已保存(Q6) + AI 推理动画指示器(A)
- Q6 bug: useAutosave 加 initialSavedText,进页种入已存草稿时初始即「已保存」,修「加载已存草稿却显示尚未保存」的误导。
- A: 新 ThinkingIndicator(三点波动,motion-safe 尊重 reduced-motion)接入审稿进行中/验收中/回炉中三处静态文案,长等待看得见在工作。
2026-06-21 18:05:55 +02:00

85 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import {
AUTOSAVE_DELAY_MS,
createDebouncedSaver,
formatSavedAt,
type DebouncedSaver,
} from "./autosave";
export type SaveStatus = "idle" | "saving" | "saved" | "error";
export interface UseAutosave {
status: SaveStatus;
savedLabel: string | null;
onChange: (text: string) => void;
flush: () => void;
}
// 防抖自动保存草稿:乐观置 saving → PUT 成功置 saved落「保存于 hh:mm」
// 失败回滚状态 + toast不丢正文已在编辑器里
// `initialSavedText`:进页种入的已存草稿——非空时初始即「已保存」,修「加载已存草稿却显示尚未保存」的误导。
export function useAutosave(
projectId: string,
chapterNo: number,
initialSavedText?: string,
): UseAutosave {
const hasInitialDraft = (initialSavedText ?? "").length > 0;
const [status, setStatus] = useState<SaveStatus>(
hasInitialDraft ? "saved" : "idle",
);
const [savedLabel, setSavedLabel] = useState<string | null>(
hasInitialDraft ? "已保存" : null,
);
const toast = useToast();
// 种入已存草稿为基线用户未改动时不触发冗余保存save 早退)。
const lastSavedRef = useRef<string | null>(
hasInitialDraft ? (initialSavedText as string) : null,
);
const save = useCallback(
async (text: string): Promise<void> => {
if (text === lastSavedRef.current) return;
setStatus("saving");
const { error } = await api.PUT(
"/projects/{project_id}/chapters/{chapter_no}/draft",
{
params: { path: { project_id: projectId, chapter_no: chapterNo } },
body: { text },
},
);
if (error) {
setStatus("error");
toast("自动保存失败,稍后重试(正文未丢失)", "error");
return;
}
lastSavedRef.current = text;
setSavedLabel(formatSavedAt(new Date()));
setStatus("saved");
},
[projectId, chapterNo, toast],
);
const saver: DebouncedSaver = useMemo(
() => createDebouncedSaver(save, AUTOSAVE_DELAY_MS),
[save],
);
useEffect(() => () => saver.cancel(), [saver]);
const onChange = useCallback(
(text: string) => {
saver.schedule(text);
},
[saver],
);
const flush = useCallback(() => saver.flush(), [saver]);
return { status, savedLabel, onChange, flush };
}