feat(frontend): 进来即写——落地页「直接开始写」直达空白编辑器,敲字后延迟落库

Phase 0(写作工作台重构):新增 /write 草稿编辑器入口,只有敲入实质正文并停顿后
才懒建占位「未命名草稿」项目并种入首章草稿(useDeferredDraft,双重幂等闸 + 草稿落库
非致命降级),随即 replace 换入完整工作台;空进空出不落库,杜绝孤儿草稿。
落地页 CTA 改为「直接开始写」为主、「先立项」(立项向导)为辅。
This commit is contained in:
Yaojia Wang
2026-07-07 18:18:18 +02:00
parent e703d0a65c
commit 8389572cbb
5 changed files with 327 additions and 16 deletions

View File

@@ -0,0 +1,58 @@
"use client";
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { AppShell } from "@/components/AppShell";
import { TextArea } from "@/components/ui/TextArea";
import { useDeferredDraft } from "@/lib/start/useDeferredDraft";
// 敲入正文后停顿多久才懒建项目(落库)——在停顿点落库,快照即所见,避免打断连续输入。
const CREATE_DEBOUNCE_MS = 700;
// 进来即写:空白编辑器直达。敲入实质正文并停顿后懒建占位项目 + 落首章草稿,随即换入完整工作台。
// 空进空出不落库(从不敲字 → 从不 POST。落库逻辑在 useDeferredDraft已单测此处只管交互。
export function DraftComposer() {
const router = useRouter();
const { ensureCreated } = useDeferredDraft();
const [text, setText] = useState("");
const textRef = useRef("");
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const redirectingRef = useRef(false);
const tryCreate = async (): Promise<void> => {
if (redirectingRef.current) return;
const snapshot = textRef.current;
if (snapshot.trim().length === 0) return;
const id = await ensureCreated(snapshot);
if (id && !redirectingRef.current) {
redirectingRef.current = true;
router.replace(`/projects/${id}/write`);
}
};
const onChange = (value: string): void => {
setText(value);
textRef.current = value;
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => void tryCreate(), CREATE_DEBOUNCE_MS);
};
return (
<AppShell>
<div className="mx-auto flex min-h-[calc(100vh-var(--chrome,4rem))] max-w-3xl flex-col px-6 py-10 sm:px-8">
<p className="mb-4 text-sm text-ink-soft">
</p>
<TextArea
value={text}
onChange={(e) => onChange(e.target.value)}
className="min-h-[60vh] flex-1 resize-none text-lg leading-8"
placeholder="从这里开始写你的第一章……"
autoFocus
aria-label="正文"
/>
</div>
</AppShell>
);
}