如何在Prisma中建模支持无限子页面的Page模型?
实现Notion式无限层级侧边栏方案
一、先搞定数据模型的层级关联
你的现有模型缺少父页面关联字段,要实现无限层级,只需要给Page模型加一个parent_id字段,让它关联自身的主键(id),就能形成树形结构:
- parent_id为
NULL时,代表这是顶级页面 - parent_id指向某个Page的id时,代表这是该页面的子页面
以常见ORM模型为例:
# Django 示例 class Page(models.Model): title = models.CharField(max_length=255) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') # 其他字段:内容、创建时间等
// TypeORM 示例 @Entity() export class Page { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToOne(() => Page, page => page.children) parent: Page; @OneToMany(() => Page, page => page.parent) children: Page[]; // 其他字段 }
二、后端获取树形结构数据
要把扁平的Page数据转换成前端可用的树形结构,有两种常用方式:
- 递归查询:从顶级页面(parent_id为NULL)开始,递归查询每个页面的子页面并组装成树
- 数据库CTE查询:用SQL公共表表达式一次性查出所有层级数据,再在后端组装成树
以PostgreSQL的CTE为例:
WITH RECURSIVE page_tree AS ( SELECT id, title, parent_id, 1 AS level FROM page WHERE parent_id IS NULL UNION ALL SELECT p.id, p.title, p.parent_id, pt.level + 1 FROM page p JOIN page_tree pt ON p.parent_id = pt.id ) SELECT * FROM page_tree ORDER BY level, id;
后端拿到数据后,用递归函数转成嵌套结构:
function buildTree(nodes, parentId = null) { return nodes .filter(node => node.parent_id === parentId) .map(node => ({ ...node, children: buildTree(nodes, node.id) })); }
三、前端渲染无限层级侧边栏
核心是用递归组件渲染嵌套结构,同时处理展开/折叠、新增子页面的交互。
以React为例,写一个递归的PageItem组件:
function PageItem({ page, expanded, onToggle, onAddChild }) { return ( <div className="page-item"> <div className="page-header" onClick={() => onToggle(page.id)}> {/* 展开/折叠图标 */} {page.children.length > 0 && ( <span>{expanded ? '▼' : '▶'}</span> )} <span>{page.title}</span> {/* 新增子页面按钮 */} <button onClick={(e) => { e.stopPropagation(); onAddChild(page.id); }}>+</button> </div> {/* 递归渲染子页面 */} {expanded && page.children.length > 0 && ( <div className="child-pages"> {page.children.map(child => ( <PageItem key={child.id} page={child} expanded={expanded} onToggle={onToggle} onAddChild={onAddChild} /> ))} </div> )} </div> ); } // 主侧边栏组件 function Sidebar({ pageTree }) { const [expandedIds, setExpandedIds] = useState(new Set()); const handleToggle = (pageId) => { setExpandedIds(prev => { const newSet = new Set(prev); newSet.has(pageId) ? newSet.delete(pageId) : newSet.add(pageId); return newSet; }); }; const handleAddChild = (parentId) => { // 调用后端接口创建子页面,之后刷新树结构 createPage({ title: '新页面', parent_id: parentId }).then(() => { // 重新获取pageTree }); }; return ( <div className="sidebar"> {pageTree.map(page => ( <PageItem key={page.id} page={page} expanded={expandedIds.has(page.id)} onToggle={handleToggle} onAddChild={handleAddChild} /> ))} </div> ); }
四、关键交互细节
- 展开/折叠状态:用Set维护展开的页面ID,避免层级状态冲突
- 新增子页面:点击新增按钮时,传入当前页面ID作为parent_id,调用接口后刷新树形数据
- 样式适配:给不同层级页面加缩进,模拟Notion侧边栏的视觉层级
内容的提问来源于stack exchange,提问作者Millenial2020
相关产品推荐
相关产品推荐

