```markdown # 阶段 1.5:精确克隆 Wolai「转为页面」+「子页面嵌入块」功能(从纯 Stage1 结束状态开始) **当前前提**: 你刚完成 Stage1(递归侧边栏 + BlockNote 编辑器 + 实时保存 + 面包屑 + 底部栏),还没有任何自定义块、没有 /api/documents/create、没有转页面逻辑。 **目标**:3-4 小时内(AI 助手 < 90 分钟)100% 还原 Wolai 以下三个核心交互(见你提供的最新三张图): 1. 点击块左侧 ::: 柄 → 弹出菜单 → 最上方「转换为」子菜单 → 出现「页面」选项 → 点击后当前块内容变成独立子页面,并在原位置留下蓝色可点击子页面块 2. / 斜杠命令菜单中出现「页面」选项(直接插入或转换) 3. 父页面中所有子页面块正确显示为蓝色标题 + 文件图标,点击跳转,侧边栏自动缩进显示 **验收标准**(运行后必须全部通过): - 任意段落块点击左侧 ::: → 菜单里有「转换为 → 页面」 - 点击「页面」后:当前块内容立即变成新子页面,父页面该位置出现蓝色子页面块(带图标) - 侧边栏实时出现新子页面(缩进正确) - 点击子页面块 → 正常跳转到子页面编辑 - 所有操作实时、无刷新 ## 1. 先创建 API:创建子页面(必须第一步) ```ts // src/app/api/documents/create-child/route.ts (POST) import { createSupabaseServer } from '@/lib/supabase/server'; import { NextRequest } from 'next/server'; export async function POST(req: NextRequest) { const supabase = createSupabaseServer(); const { data: { user } } = await supabase.auth.getUser(); if (!user) return new Response('Unauthorized', { status: 401 }); const { parentId, title, blocks } = await req.json(); // blocks = 当前块内容 const { data: newDoc, error } = await supabase .from('documents') .insert({ user_id: user.id, parent_id: parentId, // 关键:建立父子关系 title: title || '未命名页面', content: { blocks: blocks ?? [] }, // 把原块内容整个迁移过去 }) .select('id, title') .single(); if (error) return new Response(error.message, { status: 500 }); return Response.json({ pageId: newDoc.id, title: newDoc.title, }); } ``` ## 2. 自定义「子页面块」渲染(精确 Wolai 蓝色样式) ```tsx // src/components/editor/blocks/PageReferenceBlock.tsx import { RiFileTextFill } from "react-icons/ri"; import { useRouter } from "next/navigation"; export const pageReferenceBlock = { type: "pageReference" as const, propSchema: { pageId: { default: "" }, title: { default: "未命名页面" }, }, render: (block: any) => { const router = useRouter(); const title = block.props.title; const pageId = block.props.pageId; return (
router.push(`/documents/${pageId}`)} className="flex items-center gap-3 px-4 py-3 my-2 bg-blue-50 border-l-4 border-[#2563eb] rounded-r cursor-pointer hover:bg-blue-100 transition-colors group" > {title} 点击进入 →
); }, }; ``` ## 3. 在 BlockNote Schema 中注册自定义块 ```tsx // src/components/editor/schema.ts (新建文件) import { defaultBlockSchema } from "@blocknote/core"; import { pageReferenceBlock } from "./blocks/PageReferenceBlock"; export const customSchema = { ...defaultBlockSchema, pageReference: pageReferenceBlock, }; ``` ```tsx // src/components/editor/BlockNoteEditor.tsx (修改 useCreateBlockNote) import { customSchema } from "./schema"; const editor = useCreateBlockNote({ initialContent: initialContent, schema: customSchema, // ← 关键 }); ``` ## 4. 核心:自定义 SideMenu(::: 柄菜单)添加「转换为 → 页面」 BlockNote 官方支持完全自定义 SideMenu: ```tsx // src/components/editor/menus/CustomSideMenu.tsx 'use client'; import { BlockNoteSideMenu from "@blocknote/react/side-menu"; import { HiOutlineDocumentDuplicate, HiOutlineTrash, HiOutlineColorSwatch } from "react-icons/hi"; import { MdOutlineSubdirectoryArrowRight } from "react-icons/md"; export const CustomSideMenu = (props: { editor: any; currentDocumentId: string }) => { const { editor, currentDocumentId } = props; const turnToPage = async () => { const block = editor.getSelectedBlock(); if (!block) return; const res = await fetch("/api/documents/create-child", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parentId: currentDocumentId, title: block.content?.[0]?.text || "未命名页面", blocks: [block], // 把整个块内容传过去 }), }); const { pageId, title } = await res.json(); // 替换当前块为子页面引用块 editor.updateBlock(block, { type: "pageReference", props: { pageId, title }, }); }; return ( {/* 默认项 */} {/* 自定义「转换为」子菜单 */}
转换为
{/* 可继续加 标题1-6、清单、代码块 等 */}
); }; ``` 然后在 BlockNoteView 中替换: ```tsx {/* 关闭默认 */} ``` ## 5. 在 / 斜杠菜单也加上「页面」选项(双保险) ```tsx // src/components/editor/menus/CustomSlashMenu.tsx (类似上面) const pageSlashItem = { title: "页面", onItemClick: async () => { // 和 turnToPage 完全一样的逻辑,只是插入新块而不是替换 const res = await fetch("/api/documents/create-child", { ... }); const { pageId, title } = await res.json(); editor.insertBlocks([{ type: "pageReference", props: { pageId, title }, }], editor.getTextCursorPosition().block, "after"); }, aliases: ["page", "子页面", "嵌入页面块"], group: "嵌入", icon: , }; editor.slashMenu.addItems([pageSlashItem]); ``` ## 6. 交给 AI 编码助手的完整 Prompt(直接复制) ``` 你现在接手一个刚完成 Stage1 的 Wolai 克隆项目(BlockNote + Supabase + 递归侧边栏 + 实时保存)。 请精确克隆 Wolai「转为页面」功能: 1. 新建 /api/documents/create-child/route.ts (POST)接收 parentId + title + blocks,创建新 document 并返回 pageId + title 2. 创建自定义块 type="pageReference",渲染为蓝色带文件图标的可点击块,点击跳转 /documents/[pageId] 3. 自定义 SideMenu(::: 柄菜单):在默认 Delete/Duplicate/Colors 下面加一个「转换为」分组,里面有「页面」选项,点击后调用 API 创建子页面并把当前块替换为 pageReference 块 4. 同时在 SlashMenu(/ 菜单)添加「页面」项,执行相同创建+插入逻辑 5. 所有样式严格遵循 ui_react.md:蓝色 #2563eb、圆角 4px、hover #f5f5f5、Inter 字体 6. 确保侧边栏实时刷新(已有 recursive CTE + Realtime 订阅即可自动) 请输出完整文件路径 + 完整代码,不要省略任何细节。 ``` 执行完以上步骤后,你的项目就拥有了 Wolai 最灵魂的「块 ↔ 页面」双向转换能力,父页面中会自动出现所有子页面块,侧边栏也完美缩进。 存为 `PHASE_1.5_TURN_TO_PAGE_EXACT_CLONE.md` ```