Files
writer-work-flow/apps/web/lib/start/useDeferredDraft.ts
Yaojia Wang 8389572cbb feat(frontend): 进来即写——落地页「直接开始写」直达空白编辑器,敲字后延迟落库
Phase 0(写作工作台重构):新增 /write 草稿编辑器入口,只有敲入实质正文并停顿后
才懒建占位「未命名草稿」项目并种入首章草稿(useDeferredDraft,双重幂等闸 + 草稿落库
非致命降级),随即 replace 换入完整工作台;空进空出不落库,杜绝孤儿草稿。
落地页 CTA 改为「直接开始写」为主、「先立项」(立项向导)为辅。
2026-07-07 18:18:18 +02:00

96 lines
3.2 KiB
TypeScript
Raw Permalink 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, useRef, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
// 进来即写(延迟落库):落地页「直接开始写」直达空白编辑器,**只有敲入实质正文后**
// 才懒建占位项目并把已敲入正文种进首章草稿。空进空出不落库,杜绝孤儿「未命名草稿」。
export const DRAFT_PLACEHOLDER_TITLE = "未命名草稿";
export const DRAFT_FIRST_CHAPTER = 1;
export type DeferredStatus = "idle" | "creating" | "created" | "error";
export interface UseDeferredDraft {
status: DeferredStatus;
// 首次有实质正文时调用:懒建占位项目 + 落首章草稿,返回新项目 id调用方据此跳转编辑器
// 幂等:已建/在建时复用同一结果,绝不重复 POST。失败返回 null不跳转
ensureCreated: (text: string) => Promise<string | null>;
}
export function useDeferredDraft(): UseDeferredDraft {
const [status, setStatus] = useState<DeferredStatus>("idle");
const toast = useToast();
// 已建项目 id 与在途 Promise双重幂等闸防抖多次触发只落一次库。
const createdIdRef = useRef<string | null>(null);
const inflightRef = useRef<Promise<string | null> | null>(null);
const ensureCreated = useCallback(
(text: string): Promise<string | null> => {
if (createdIdRef.current) return Promise.resolve(createdIdRef.current);
if (inflightRef.current) return inflightRef.current;
const run = createDraftProject(text, toast, setStatus).then((id) => {
inflightRef.current = null;
if (id) createdIdRef.current = id;
return id;
});
inflightRef.current = run;
return run;
},
[toast],
);
return { status, ensureCreated };
}
type Toast = ReturnType<typeof useToast>;
// 懒建项目 + 落首章草稿(草稿失败为非致命:项目已建,正文仍在前端,进编辑器后自动重存)。
async function createDraftProject(
text: string,
toast: Toast,
setStatus: (s: DeferredStatus) => void,
): Promise<string | null> {
setStatus("creating");
try {
const { data, error } = await api.POST("/projects", {
body: { title: DRAFT_PLACEHOLDER_TITLE },
});
if (error || !data) {
setStatus("error");
toast("创建草稿失败,请重试", "error");
return null;
}
await seedFirstChapter(data.id, text, toast);
setStatus("created");
return data.id;
} catch {
setStatus("error");
toast("创建草稿异常,请检查网络", "error");
return null;
}
}
// 把已敲入正文种进首章草稿;空白正文跳过。落库失败仅提示、不抛(项目已建,不阻断进入编辑器)。
async function seedFirstChapter(
projectId: string,
text: string,
toast: Toast,
): Promise<void> {
if (text.trim().length === 0) return;
const { error } = await api.PUT(
"/projects/{project_id}/chapters/{chapter_no}/draft",
{
params: {
path: { project_id: projectId, chapter_no: DRAFT_FIRST_CHAPTER },
},
body: { text },
},
);
if (error) {
toast("首段草稿保存失败,进入编辑器后会自动重试", "info");
}
}