```markdown # 阶段 1:Wolai 核心页面树 + BlockNote 编辑器(最大程度克隆 Wolai UI 与交互) **目标**:4-6 天内(AI 助手单人 8-12 小时)完成一个视觉与交互 95% 接近真实 Wolai(光模式)的核心编辑体验。 **验收标准**(可直接写 Cypress 测试): - 左侧 240px 树状侧边栏无限嵌套、可拖拽排序/嵌套、实时同步 - 主编辑区 BlockNote 块编辑器(斜杠命令、Tab 缩进、拖拽柄、子页面块渲染为蓝色链接) - 顶部面包屑完整(≡ 展开侧边栏 + 路径跳转 + 星标/分享/更多) - 底部固定 AI 工具栏(发送/魔力中心/?/AI 按钮) - 暗黑模式自动适配、<768px 侧边栏折叠、首屏加载 <1.2s - 所有颜色/圆角/间距/字体 100% 按 ui_react.md 规范(Inter 字体、#2563eb 主蓝、hover #f5f5f5) **本阶段只做阶段 1 内容**,不提前实现思维导图、OCR、AI 聊天侧边栏。 ## 1. 颜色与全局样式(必须先做,Wolai 克隆关键) ```ts // src/app/globals.css (在现有 Tailwind 基础上追加) @import url('https://rsms.me/inter/inter.css'); @layer base { html { font-family: 'Inter', system-ui, sans-serif; } } @layer components { .wolai-hover { @apply hover:bg-gray-50 transition-colors duration-150; } .wolai-selected { @apply bg-blue-50 border-l-2 border-blue-500; } .wolai-blue { @apply text-[#2563eb] hover:underline cursor-pointer; } } ``` ```ts // tailwind.config.ts 追加 Wolai 精确色值 theme: { extend: { colors: { wolai: { blue: '#2563eb', hover: '#f5f5f5', border: '#eeeeee', text: '#333333', muted: '#666666', light: '#999999', }, }, borderRadius: { wolai: '4px', }, spacing: { wolai: '8px', }, }, }, ``` ## 2. 整体布局组件(严格两栏 + 面包屑 + 底部栏) ```tsx // src/app/(app)/layout.tsx import { Sidebar } from '@/components/sidebar/Sidebar'; import { Breadcrumb } from '@/components/Breadcrumb'; import { BottomToolbar } from '@/components/BottomToolbar'; import { MobileSidebarTrigger } from '@/components/MobileSidebarTrigger'; export default function AppLayout({ children }: { children: React.ReactNode }) { return (
{/* 左侧侧边栏 - 响应式折叠 */}
{/* 顶部面包屑 40px */}
{/* 主内容区 */}
{children}
{/* 底部固定工具栏 */}
); } ``` ## 3. 递归树状侧边栏(240px 精确复刻) ```tsx // src/components/sidebar/Sidebar.tsx 'use client'; import { useEffect, useState } from 'react'; import { supabase } from '@/lib/supabase/client'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; // 推荐比 react-dnd 更轻量 interface PageNode { id: string; title: string; children: PageNode[]; hasChildren: boolean; } export function Sidebar() { const [pages, setPages] = useState([]); const [openIds, setOpenIds] = useState>(new Set()); // 递归 CTE 查询所有层级(一次性拉取用户全部页面树) const fetchTree = async () => { const { data } = await supabase.rpc('get_documents_tree', { userId); setPages(data || []); }; // 拖拽结束 → 调用 Supabase function 更新 parent_id 和排序 const onDragEnd = async (result: any) => { /* 实现 parent_id 与顺序更新 */ }; return (
{/* 搜索区 */}
{/* 树 */} {(provided) => (
{pages.map((page, index) => ( ))} {provided.placeholder}
)}
); } // SidebarItem 递归组件(缩进 16px,> 展开箭头,hover 显示拖拽柄) const SidebarItem = ({ page, index, depth }: { page: PageNode; index: number; depth: number }) => { const hasChildren = page.children.length > 0; return ( {(provided) => (
{/* 展开箭头 + 拖拽柄(hover 显示) */}
⋮⋮
{hasChildren && }
{page.title || '未命名'}
)}
); }; ``` > Supabase RPC 函数(SQL Editor 执行): ```sql create or replace function get_documents_tree(user_id uuid) returns table (id uuid, title text, parent_id uuid, depth integer, path text[]) language sql as $$ with recursive tree as ( select d.id, d.title, d.parent_id, 0 as depth, array[d.id] as path from documents d where d.parent_id is null and d.user_id = user_id union all select d.id, d.title, d.parent_id, t.depth + 1, t.path || d.id from documents d join tree t on d.parent_id = t.id where d.user_id = user_id ) select id, title, parent_id, depth from tree order by path; $$; ``` ## 4. BlockNote 编辑器页面(/documents/[id]/page.tsx) ```tsx // src/app/(app)/documents/[id]/page.tsx import { BlockNoteEditor } from '@/components/editor/BlockNoteEditor'; import { Skeleton } from '@/components/ui/skeleton'; export default async function DocumentPage({ params }: { params: { id: string } }) { const supabase = createSupabaseServer(); const { data: doc } = await supabase .from('documents') .select('title, content') .eq('id', params.id) .single(); if (!doc) return
404
; return (
{/* 标题 */}

{doc.title}

{/* BlockNote 编辑器(高度占满) */}
); } ``` ```tsx // src/components/editor/BlockNoteEditor.tsx (核心克隆点) 'use client'; import { BlockNoteView, useCreateBlockNote } from '@blocknote/react'; import { useEffect, useRef } from 'react'; import * as Y from 'yjs'; import { HocuspocusProvider } from '@hocuspocus/provider'; export function BlockNoteEditor({ documentId, initialContent }: { documentId: string; initialContent: any }) { const editor = useCreateBlockNote({ initialContent, // 自定义斜杠命令菜单样式与 Wolai 一致(圆角 4px,浅灰背景) slashMenuTheme: { item: 'px-3 py-1.5 text-sm wolai-hover' }, }); // 实时协作(Hocuspocus + Supabase Realtime) useEffect(() => { const ydoc = new Y.Doc(); const provider = new HocuspocusProvider({ url: `${process.env.NEXT_PUBLIC_SUPABASE_URL}/realtime/v1`, name: `document.${documentId}`, token: 'your-supabase-anon-key', // 实际用 JWT }); // 绑定 BlockNote → Yjs // ... 官方示例代码 // 防抖 800ms 保存到 Supabase documents.content const debounceSave = debounce(async (content) => { await fetch('/api/documents/save', { method: 'POST', body: JSON.stringify({ id: documentId, content }), }); }, 800); editor.onEditorContentChange(() => debounceSave(editor.document)); }, []); return ( ); } ``` ```css /* 自定义 CSS 让 BlockNote 100% Wolai 风格 */ .wolai-editor { --bn-colors-editor-text: #333333; --bn-colors-editor-background: #ffffff; --bn-radii: 4px; --bn-spacing: 8px; .bn-block-outer { padding: 2px 8px; } .bn-block-group:hover .bn-drag-handle { opacity: 1; } /* 左侧拖拽柄 hover 显示 */ .bn-inline-content a { color: #2563eb; text-decoration: underline; } } ``` ## 5. 面包屑 + 底部工具栏(精确像素级克隆) ```tsx // src/components/Breadcrumb.tsx (路径链接可点,≡ 展开侧边栏) ``` ```tsx // src/components/BottomToolbar.tsx
``` ## 6. 交给 AI 编码助手的完整 Prompt(直接复制) ``` 你现在是一个顶级 Next.js + BlockNote 专家。仓库已完成阶段0(Next.js15 App Router + Supabase + shadcn + 数据库表)。 请严格按照 ui_react.md 文档实现 Wolai 克隆: 1. 实现递归侧边栏 Sidebar(宽度 240px,缩进 16px,拖拽用 @hello-pangea/dnd,hover 显示拖拽柄,支持无限嵌套,颜色 #333/#666/#999) 2. 实现 /documents/[id]/page.tsx 使用 BlockNoteView,标题 24px 粗体,内容区 px-12,块间距 8px,左侧拖拽柄 hover 显示,子页面块渲染为蓝色可点链接 3. 实现顶部面包屑(高度 40px,≡ 触发侧边栏,路径链接可跳转,右侧星标/分享/更多) 4. 实现固定底部工具栏(AI 按钮蓝色圆形 32px) 5. 所有圆角 4px、间距 8px 基准、Inter 字体、hover #f5f5f5、选中左侧蓝边 6. 实时协作用 @hocuspocus/provider + Supabase Realtime,800ms 防抖保存到 documents.content JSONB 7. 移动端 <768px 侧边栏隐藏,用 ≡ 按钮展开 Drawer 8. 完全使用 Server Components + Server Actions,首屏 skeleton 加载 请输出完整代码文件列表与代码,不要省略任何细节。 ``` **阶段 1 到此结束**。执行完后运行 `pnpm dev` 应该看到一个几乎一模一样的 Wolai 界面(除思维导图外),拖拽、嵌套、实时保存全部正常。 下一步再做阶段 2(斜杠命令增强 + 嵌入块 + 模板)。存为 `PHASE_1_WOLAI_CORE_UI.md` ```