You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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数据转换成前端可用的树形结构,有两种常用方式:

  1. 递归查询:从顶级页面(parent_id为NULL)开始,递归查询每个页面的子页面并组装成树
  2. 数据库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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 12:01:02