ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

外包项目管理平台全栈实战课04搭建左侧部门树

外包项目管理平台全栈实战课04搭建左侧部门树 目录前言一、 需求梳理及方案设计1. 需求拆解与知识识别2. 状态职责划分与链路推导3. 从方案推导文件结构二、 实操步骤第一步在数据库准备测试数据第二步编写后端服务与组装算法第三步定义共享数据类型第四步实现服务端页面入口第五步实现页面客户端容器第六步实现递归树节点组件运行与闭环验证前言上一课我们建好了第一张业务表在 PostgreSQL 里创建Dept用 Prisma 把model Dept映射成db.dept并第一次让 Next.js 从数据库里读出了部门数据。也就是说“前端能读到数据库”这条路上一课已经打通了。这一课我们要在这条路上放一个真正的页面部门管理页面左侧显示公司的组织架构树。做完之后打开http://localhost:3000/dept你会看到一棵三层部门树——能展开、能收起、能点选数据全部来自数据库而不是写死在页面里。一、 需求梳理及方案设计1. 需求拆解与知识识别表面上看产品经理只说了一句话“做个部门管理页面左侧显示部门树。” 但在动手前我们需要把这句简单的话拆解为 4 个底层的技术问题数据模型与存储数据库里保存的是平铺的部门记录带parentId如何设计“自关联”结构数据结构转换如何把数据库返回的扁平数组转换成前端需要的层级树形结构children: TreeNode[]组件架构与递归前端树状节点的结构是无限层级嵌套的如何编写递归组件并处理逻辑状态管理与数据流节点的“展开/收起”局部状态与“当前选中部门”全局/容器状态应该分别由谁管理2. 状态职责划分与链路推导为了让代码职责清晰、避免状态混乱我们将状态与数据流明确分工数据提供者Server Component / API负责从 PostgreSQL 读取原始数据并通过buildTree()组装成树形 JSON。页面容器DeptWorkspace负责管理页面级状态如selectedId当前选中的部门 ID。树节点组件TreeNodeItem负责管理节点级状态如expanded展开/收起以及自身的递归渲染。系统整体运行链路PostgreSQL (扁平数据) ⟶ Prisma / Service (buildTree 转换为树) ⟶ DeptPage (服务端获取) ⟶ DeptWorkspace (选中状态) ⟶ TreeNodeItem (展开状态/递归渲染) ⟶ 浏览器视图 \text{PostgreSQL (扁平数据)} \longrightarrow \text{Prisma / Service (buildTree 转换为树)} \longrightarrow \text{DeptPage (服务端获取)} \longrightarrow \text{DeptWorkspace (选中状态)} \longrightarrow \text{TreeNodeItem (展开状态/递归渲染)} \longrightarrow \text{浏览器视图}PostgreSQL (扁平数据)⟶Prisma / Service (buildTree转换为树)⟶DeptPage (服务端获取)⟶DeptWorkspace (选中状态)⟶TreeNodeItem (展开状态/递归渲染)⟶浏览器视图3. 从方案推导文件结构根据上面的职责划分我们不需要盲目新建文件而是顺理成章地推导出每个文件具体的职责server/ dept/ service.ts # 1. 数据库查询 扁平数据转树算法 (buildTree) app/ dept/ dept-types.ts # 2. 共享数据类型定义 (TreeNode) page.tsx # 3. 服务端页面入口 (负责 Fetch 数据) dept-workspace.tsx # 4. 客户端容器 (管理 selectedId 选中状态) components/ tree-node-item.tsx # 5. 递归节点组件 (管理 expanded 展开状态 UI 渲染)有了这个清晰的推导图谱接下来的实现步骤就非常明确了。我们将按照“底层数据→ \rightarrow→后端算法→ \rightarrow→前端类型→ \rightarrow→容器组件→ \rightarrow→渲染细节”的时间顺序一步步把方案变成代码。二、 实操步骤按照前面推导的架构设计与文件职责下面是完整的实操步骤第一步到第六步。我们遵循“数据源→ \rightarrow→服务层与算法→ \rightarrow→前端类型→ \rightarrow→页面入口→ \rightarrow→交互容器→ \rightarrow→递归组件”的顺序逐步构建这套部门树系统。第一步在数据库准备测试数据树形结构的核心在于节点之间的父子连接parentId。我们需要在数据库中插入三条具备父子嵌套关系的数据。在终端运行pnpmprisma studio打开浏览器访问http://localhost:51212进入Dept表添加三条记录总公司code HQ,parentId null技术部code DEV,parentId 总公司的 id前端组code FE,parentId 技术部的 id数据关系推导示意总公司 (iddept_1, parentIdnull) └── 技术部 (iddept_2, parentIddept_1) └── 前端组 (iddept_3, parentIddept_2)第二步编写后端服务与组装算法创建文件server/dept/service.ts这个文件的职责有两个listDepts从数据库全量读取扁平部门列表。buildTree通过递归算法将“扁平数组”组装为“树形结构”。import{db}from/lib/db;importtype{TreeNode}from/app/dept/dept-types;// 1. 从数据库读取原始扁平数据exportasyncfunctionlistDepts(){returndb.dept.findMany({orderBy:[{sort:asc},{name:asc}],});}// 2. 算法核心将扁平数组转换为 Tree 结构exportfunctionbuildTree(rows:AwaitedReturnTypetypeoflistDepts,parentId:string|nullnull):TreeNode[]{returnrows.filter((row)row.parentIdparentId)// 步骤 A找出当前层级的节点.map((row)({id:row.id,name:row.name,code:row.code,parentId:row.parentId,status:row.status,// 步骤 B递归寻找当前节点的所有子节点children:buildTree(rows,row.id),}));}深度剖析buildTree算法的执行链路假设数据库拿到的是平铺数据[总公司(id:A, p:null), 技术部(id:B, p:A), 前端组(id:C, p:B)]首次调用buildTree(rows, null)filter匹配到parentId null的节点总公司 A。生成子树为总公司生成children触发递归调用buildTree(rows, A)。第二层递归filter匹配到parentId A的节点技术部 B。继续为技术部递归调用buildTree(rows, B)。第三层递归filter匹配到parentId B的节点前端组 C。继续调用buildTree(rows, C)由于没有parentId C的数据filter返回空数组[]递归在此终止并逐层向上返回。第三步定义共享数据类型创建文件app/dept/dept-types.ts我们需要定义前后端统一下发与接收的数据结构。children属性包含自身类型的数组这就是典型的递归数据类型。exporttypeTreeNode{id:string;name:string;code:string;parentId:string|null;status:number;children:TreeNode[];// 递归嵌套结构};第四步实现服务端页面入口创建文件app/dept/page.tsx在 Next.js App Router 中默认不写use client的文件均为服务端组件Server Component。页面在服务端直接调用数据库和算法获取组装好的tree数据然后向下传递给客户端组件。import{buildTree,listDepts}from/server/dept/service;import{DeptWorkspace}from./dept-workspace;exportdefaultasyncfunctionDeptPage(){// 服务端并行/顺序拉取数据并转换constrowsawaitlistDepts();consttreebuildTree(rows);return(main classNamep-6h1 classNamemb-4 text-xl font-semibold text-gray-800部门管理/h1{/* 将准备好的树形数据打入 Workspace 交互容器 */}DeptWorkspace tree{tree}//main);}第五步实现页面客户端容器创建文件app/dept/dept-workspace.tsxDeptWorkspace负责管理页面级交互状态。它不需要知道每个树节点内部是如何渲染和展开的只需要统一记录当前用户高亮选中了哪个部门selectedId。use client;import{useState}fromreact;importtype{TreeNode}from./dept-types;import{TreeNodeItem}from./components/tree-node-item;exportfunctionDeptWorkspace({tree}:{tree:TreeNode[]}){// 页面级状态管理当前选中的部门 IDconst[selectedId,setSelectedId]useStatestring|null(null);return(div classNamew-64 rounded-md border border-gray-200 bg-white p-3 shadow-smdiv classNamemb-2 text-sm font-medium text-gray-700组织架构/divdiv classNamemax-h-[70vh] space-y-0.5 overflow-y-auto{tree.map((node)(TreeNodeItem key{node.id}node{node}depth{0}selectedId{selectedId}onSelect{setSelectedId}/))}/div/div);}第六步实现递归树节点组件创建文件app/dept/components/tree-node-item.tsx这是 UI 渲染的核心逻辑。利用React 递归组件特性只要当前节点拥有子节点children.length 0并且用户处于展开状态expanded true组件就会调用自身来渲染子节点同时将深度depth 1通过动态内边距实现树形视觉缩进。use client;import{useState}fromreact;importtype{TreeNode}from../dept-types;typeTreeNodeItemProps{node:TreeNode;depth:number;selectedId:string|null;onSelect:(id:string)void;};exportfunctionTreeNodeItem({node,depth,selectedId,onSelect,}:TreeNodeItemProps){// 节点级状态管理自身节点的展开/收起const[expanded,setExpanded]useStateboolean(true);consthasChildrennode.children.length0;constisSelectedselectedIdnode.id;return(div{/* 节点单行容器 */}div className{flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors${isSelected?bg-blue-600 text-white:text-gray-700 hover:bg-gray-100}}style{{// 根据层级 depth 动态计算缩进距离paddingLeft:8depth*16,}}{/* 展开/收起切换按钮 */}{hasChildren?(button typebuttonclassNamew-4 text-center text-xs opacity-70 hover:opacity-100onClick{()setExpanded((v)!v)}{expanded?▾:▸}/button):(span classNamew-4/)}{/* 部门名称与选中点击 */}button typebuttonclassNameflex-1 truncate text-leftonClick{()onSelect(node.id)}{node.name}/button/div{/* 递归渲染子节点仅当展开且存在子节点时渲染 */}{expandedhasChildrennode.children.map((child)(TreeNodeItem key{child.id}node{child}depth{depth1}// 深度递增实现视觉缩进selectedId{selectedId}onSelect{onSelect}/))}/div);}运行与闭环验证完成上述六个步骤的代码编写后我们在终端启动开发服务器pnpmdev打开浏览器访问http://localhost:3000/dept进行完整的需求与功能闭环验证[x] 视觉层级页面正常展示“总公司→ \rightarrow→技术部→ \rightarrow→前端组”三层缩进结构。[x] 局部状态交互点击“总公司”左侧的▾符号能够正常收起/展开下属部门。[x] 容器状态联动点击“技术部”文本该节点背景变蓝高亮选中selectedId状态成功被记录。[x] 链路数据响应在 Prisma Studio 里把“前端组”重命名为“前端开发组”刷新网页页面实时同步最新数据库变更证明前后端及数据库完整数据流运行正常。
返回列表