react-admin 树形管理实战:使用<TreeWithDetails>构建目录/分类同页编辑界面
【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin
<TreeWithDetails>是 react-admin Enterprise Edition 生态中@react-admin/ra-tree包提供的树形列表组件,用于替代标准<List>来管理目录、分类等天然具有树结构的记录的浏览、编辑与拖拽重排。本文以官方文档为基础,结合同仓库的Tree.md、TreeInput.md、Resource.md等文档,系统讲解其 Props、视图组合方式与常见实战配置,帮助你在 react-admin 中快速落地"树 + 详情/编辑同页"的管理界面。
一、组件定位与适用场景
在标准 react-admin 中,<List>以表格形式平铺展示记录。但当数据是树结构(如商品分类、目录、组织结构、权限树)时,扁平表格既难以表达层级,也不便于就地编辑。<TreeWithDetails>正是为这类场景设计的替代品:
- 在同一页面中同时渲染树形结构和选中节点的 show 视图 / 编辑表单;
- 允许用户浏览树、编辑节点、创建子节点,并通过拖拽重排整棵树。
从源码结构看,该组件属于 Enterprise Edition(企业版)扩展包@react-admin/ra-tree,底层基于 rc-tree 的<Tree>实现,并通过 react-admin 的dataProvider与后端交互。使用时需要先安装并配置支持树操作的数据提供器(dataProvider 需提供树查询与节点增删改能力,例如dataProvider.addChildNode())。
二、快速上手:从<List>切换到<TreeWithDetails>
<TreeWithDetails>的使用方式与<List>高度相似,核心差异在于:
- 列表视图组件改用
<TreeWithDetails>; - 创建视图使用
<CreateNode>替代标准<Create>; - 编辑视图使用
<EditNode>替代标准<Edit>,且表单必须搭配<EditNodeToolbar>工具栏。
官方文档给出一个完整的分类管理示例:
// in src/category.js import { Admin, Resource, Create, Edit, SimpleForm, TextInput, } from 'react-admin'; import { CreateNode, EditNode, EditNodeToolbar, TreeWithDetails, } from '@react-admin/ra-tree'; // a Create view for a tree uses <CreateNode> instead of the standard <Create> const CategoriesCreate = () => ( <CreateNode> <SimpleForm> <TextInput source="name" /> </SimpleForm> </CreateNode> ); // an Edit view for a tree uses <EditNode> instead of the standard <Edit> const CategoriesEdit = () => ( <EditNode> <SimpleForm toolbar={<EditNodeToolbar />}> <TextInput source="title" /> </SimpleForm> </EditNode> ); // a List view for a tree uses <TreeWithDetails> export const CategoriesList = () => ( <TreeWithDetails create={CategoriesCreate} edit={CategoriesEdit} /> ); // in src/App.js import { CategoriesList } from './category'; const App = () => ( <Admin dataProvider={dataProvider}> <Resource list={CategoriesList} /> </Admin> );最小可运行配置只需两条:在<Admin>中正常声明<Resource>,并把list指向CategoriesList即可。树的渲染数据来源于 dataProvider 返回的树数据格式——即"包含children字段的节点数组",格式详见Tree.md(例如{ id: 1, name: 'Clothing', children: [2, 6] })。
三、Props 总览
<TreeWithDetails>的完整 Props 如下表所示(另接受<Tree>的全部 Props):
| Prop | Required | Type | Default | Description |
|---|---|---|---|---|
addRootButton | Optional | ReactNode或false | - | 用于添加根节点的创建按钮 |
allowMultipleRoots | Optional | boolean | false | 是否允许树存在多个根节点 |
create | Optional | ReactNode | - | 资源的创建表单 |
draggable | Optional | boolean | false | 是否允许用户拖拽重排节点 |
edit | Optional | ReactNode | - | 资源的编辑表单 |
filter | Optional | object | - | 永久过滤条件 |
hideRootNodes | Optional | boolean | false | 是否隐藏所有根节点 |
lazy | Optional | boolean | false | 是否仅在节点展开时才加载其子节点 |
motion | Optional | boolean | false | 是否启用 rc-tree<Tree>的展开/折叠过渡动画 |
mutationMode | Optional | string | undoable | 拖拽操作使用的mutationMode(undoable、optimistic或pessimistic) |
nodeActions | Optional | ReactNode | - | 自定义每个节点悬停时的下拉操作菜单 |
show | Optional | ReactNode | - | 资源的 show 视图 |
showLine | Optional | boolean | false | 是否显示节点连接线 |
sx | Optional | SxProps | - | Material UI 的sx快捷样式 |
title | Optional | string | - | 显示在<AppBar>中的页面标题 |
titleField | Optional | string | - | 指定树中节点展示所用记录字段 |
下文将按功能类别逐一深入说明每个 Props 的实战用法。
四、create/edit/show:三类视图的组合
如果你希望同时开放创建、编辑和详情查看能力,可以将三者同时传入:
import { EditButton, Labeled, SimpleForm, TextField, TextInput, TopToolbar, } from 'react-admin'; import { AddChildButton, CreateNode, EditNode, EditNodeToolbar, ShowNode, TreeWithDetails, } from '@react-admin/ra-tree'; const NodeShowAction = () => ( <TopToolbar> <EditButton /> <AddChildButton /> </TopToolbar> ); const CategoriesShow = () => ( <ShowNode actions={<NodeShowAction />}> <SimpleForm> <Labeled label="Id"> <TextField source="id" /> </Labeled> <Labeled label="Title"> <TextField source="title" /> </Labeled> </SimpleForm> </ShowNode> ); const CategoriesEdit = () => ( <EditNode> <SimpleForm toolbar={<EditNodeToolbar />}> <TextField source="id" label="id" /> <TextInput source="title" /> </SimpleForm> </EditNode> ); const CategoriesCreate = () => ( <CreateNode> <SimpleForm> <TextInput source="title" /> </SimpleForm> </CreateNode> ); export const CategoriesList = () => ( <TreeWithDetails linkTo="show" show={CategoriesShow} edit={CategoriesEdit} create={CategoriesCreate} /> );必须使用<EditNodeToolbar>的注意事项
IMPORTANT:在编辑视图中,
<SimpleForm>必须搭配<EditNodeToolbar>。该工具栏将 react-admin 默认的<DeleteButton>替换为 ra-tree 版本——后者删除的是整棵分支(branch)而不是单条记录。
因此,当你自定义Toolbar并需要包含删除按钮时,必须从@react-admin/ra-tree导入替代按钮:
import { Toolbar, ToolbarProps } from 'react-admin'; import { DeleteBranchButton } from '@react-admin/ra-tree'; import MyCustomButton from './MyCustomButton'; export const MyToolbar = (props: ToolbarProps) => ( <Toolbar> <MyCustomButton /> <DeleteBranchButton /> </Toolbar> );覆盖节点创建/编辑的 mutationOptions
CreateNode与EditNode都接受mutationOptionsprop,可用于覆写主 mutation 查询的配置(例如onSuccess/onError回调,或随请求透传给 dataProvider 的meta对象):
const CategoriesCreate = () => ( <CreateNode mutationOptions={{ onSuccess: () => { console.log('Success!'); }, onError: () => { console.log('Error'); }, meta: { foo: 'bar' }, // The 'meta' object will be passed to the dataProvider methods }} > <SimpleForm> <TextInput source="name" /> </SimpleForm> </CreateNode> );五、多根树支持:allowMultipleRoots与addRootButton
默认情况下,ra-tree 一棵树只允许一个根节点。当业务上需要多个根(如多级分类并列展示)时,设置allowMultipleRoots:
export const CategoriesList = (props: ListProps) => ( <TreeWithDetails create={CategoriesCreate} edit={CategoriesEdit} allowMultipleRoots {...props} /> );当allowMultipleRoots为true、或当前树中没有任何根节点时,组件会显示一个"添加根节点"按钮。你可以用addRootButton传入自定义按钮:
// in src/posts.js import { CreateButton } from 'react-admin'; export const CategoriesList = () => ( <TreeWithDetails allowMultipleRoots addRootButton={<CreateButton label="Add Categories!" />}> ... </TreeWithDetails> );Tip:向
addRootButton传入false可以完全隐藏该按钮。
六、拖拽重排:draggable与mutationMode
允许用户通过拖拽重排节点,只需添加draggableprop:
export const CategoriesList = () => <TreeWithDetails draggable />;mutationMode决定拖拽操作采用哪种提交策略(默认undoable),可选值与标准 react-admin<Edit>的三种 mutation 模式一致(详见Edit.md):
pessimistic:先调用 dataProvider,成功后才在本地应用变更并执行副作用;optimistic:立即在本地应用变更并执行副作用,再调用 dataProvider;失败则刷新页面并报错;undoable(默认):立即在本地应用变更,弹出含撤销按钮的通知;用户撤销则不发请求,否则约 5 秒后提交给 dataProvider。
<TreeWithDetails mutationMode="pessimistic" />Note:使用
undoable(默认)或pessimistic模式时,拖拽操作后、mutation 真正完成(即 dataProvider 被调用并返回)之前,节点数据可能是过期的。原因在于:react-admin 虽然可以乐观地重排节点顺序,但无法根据你的具体实现,对节点数据本身应用所需的变更。因此依赖节点数据内容渲染的 UI 可能出现短暂不一致。
拖拽产生的变更最终会通过 dataProvider 的节点移动相关方法(例如移动节点、调整子级归属等)持久化,请确保你的数据提供器实现了对应的树操作。
七、永久过滤子集:filter
如果只想展示整棵树的某个子树,可以使用filterprop 进行永久过滤。例如employees资源带有department字段,只想展示 Finance 部门的树:
const EmployeeList = () => <TreeWithDetails filter={{ department: 'finance' }} />;Note:
filter仅在过滤字段能提取出带独立根节点的子树时才有效。如果用它过滤出零散的节点子集(例如只显示male员工),树中的拖拽行为将不会按预期工作。
八、隐藏根节点:hideRootNodes
有些树出于技术原因只有一个根节点,用户不应该看到它。此时可用hideRootNodes隐藏所有根节点:
export const CategoriesList = () => <TreeWithDetails hideRootNodes />;九、懒加载:lazy
当树的节点数量很大时,可只在初始阶段加载根节点、展开某个节点时再加载其子节点。启用方式为设置lazy:
export const CategoriesList = () => <TreeWithDetails lazy />;Important:使用
lazy模式时,不能使用undoablemutation 模式。必须在<EditNode>上将mutationMode设置为'pessimistic'或'optimistic'。
一个完整的懒加载示例(注意编辑视图的 mutationMode 配置):
import React from 'react'; import { Admin, Resource, SimpleForm, TextField, TextInput } from 'react-admin'; import { EditNode, EditNodeToolbar, TreeWithDetails } from '@react-admin/ra-tree'; import CategoriesCreate from '../CategoriesCreate'; import i18nProvider from '../i18nProvider'; import dataProvider from './dataProvider'; const CategoriesEdit = () => ( <EditNode mutationMode="pessimistic"> <SimpleForm toolbar={<EditNodeToolbar />}> <TextField source="id" /> <TextInput source="name" /> </SimpleForm> </EditNode> ); const CategoriesList = () => ( <TreeWithDetails titleField="name" edit={CategoriesEdit} create={CategoriesCreate} lazy /> ); export const App = () => ( <Admin dataProvider={dataProvider} i18nProvider={i18nProvider}> <Resource name="categories" list={CategoriesList} /> </Admin> );官方文档中的懒加载演示视频(docs/img/ra-tree-lazy.webm/ra-tree-lazy.mp4)直观展示了"展开时按需加载子节点"的效果:首屏只请求根节点,点击展开某节点时才发起对应子节点的请求。
十、展开/折叠过渡动画:motion
rc-tree 的<Tree>本身支持自定义节点展开/折叠的过渡动画,但 ra-tree默认禁用了这些动画——它们已知会与"点击展开"(expand on click)功能产生冲突。需要时可通过motionprop 启用:
export const CategoriesList = () => <TreeWithDetails motion />;motion也可以传入一个过渡配置对象,完全自定义动画细节:
import { TreeWithDetails } from '@react-admin/ra-tree'; import { CSSProperties } from 'react'; const myMotion = { motionName: 'node-motion', motionAppear: false, onAppearStart: (): CSSProperties => ({ height: 0, width: 0 }), onAppearActive: (node: HTMLElement): CSSProperties => ({ height: node.scrollHeight, width: node.scrollWidth, }), onLeaveStart: (node: HTMLElement): CSSProperties => ({ height: node.offsetHeight, width: node.scrollWidth, }), onLeaveActive: (): CSSProperties => ({ height: 0, width: 0 }), }; export const CategoriesList = () => ( <TreeWithDetails motion={myMotion} sx={{ '& .node-motion': { transition: 'all .7s', overflowX: 'hidden', overflowY: 'hidden', }, }} /> );其中motionName用于指定 CSS 类名,配合sx中的transition、overflow样式即可实现节点高度的平滑伸缩效果。
十一、自定义节点操作菜单:nodeActions
默认情况下,每个节点在悬停时会显示一个操作下拉菜单,菜单默认只包含一个"删除"动作。通过nodeActions可以定制该菜单:
import { NodeActions, DeleteMenuItem, TreeWithDetails, } from '@react-admin/ra-tree'; const MyCustomActionMenuItem = forwardRef( ({ record, resource, parentId }, ref) => { const handleClick = () => { // Do something with dataProvider ? }; return ( <MenuItem ref={ref} onClick={handleClick}> Do something </MenuItem> ); } ); const MyActions = (props: NodeActionsProps) => ( <NodeActions {...props}> <MyCustomActionMenuItem /> <DeleteMenuItem /> </NodeActions> ); const CategoriesList = () => ( <TreeWithDetails titleField="name" edit={CategoriesEdit} draggable showLine nodeActions={<MyActions />} /> );自定义菜单项(如上面的MyCustomActionMenuItem)会接收到当前记录record、资源名resource与父节点 idparentId,方便你基于这些上下文实现针对当前节点的操作。
十二、节点连接线:showLine
ra-tree 默认采用 react-admin 的样式(通过缩进与箭头表达层级)。设置showLine为true后,将保留 rc-tree 原生的层级图标,并以连接线勾勒父子关系:
export const CategoriesList = () => <TreeWithDetails showLine />;十三、页面标题与节点标题:title与titleField
title
树视图默认标题为"[资源名] list"(例如 "Posts list")。用title自定义:
// in src/posts.js export const CategoriesList = () => ( <TreeWithDetails title="List of categories">...</TreeWithDetails> );title既可以是字符串,也可以是你自定义的 React 元素。
titleField
树中每个节点默认使用资源的recordRepresentation作为标题。recordRepresentation未设置时,react-admin 会按name→title→label→reference→id的顺序选取可用字段。如需显式指定节点标题字段,使用titleField:
// in src/posts.js export const CategoriesList = () => ( <TreeWithDetails titleField="name">...</TreeWithDetails> );十四、在表单中选择节点:搭配<TreeInput>
如果你的业务需要在表单中让用户选择树中的节点(例如给商品设置所属分类),请使用 ra-tree 的<TreeInput>组件,而不是<TreeWithDetails>:
import { Edit, SimpleForm, TextInput } from 'react-admin'; import { TreeInput } from '@react-admin/ra-tree'; export const ProductEdit = () => ( <Edit> <SimpleForm> <TextInput source="id" disabled /> <TextInput source="name" /> <TreeInput source="category" data={[ { id: 1, title: 'Clothing', isRoot: true, children: [2, 6] }, { id: 2, title: 'Men', children: [3] }, { id: 3, title: 'Suits', children: [4, 5] }, { id: 4, title: 'Slacks', children: [] }, { id: 5, title: 'Jackets', children: [] }, { id: 6, title: 'Women', children: [7, 10, 11] }, { id: 7, title: 'Dresses', children: [8, 9] }, { id: 8, title: 'Evening Gowns', children: [] }, { id: 9, title: 'Sun Dresses', children: [] }, { id: 10, title: 'Skirts', children: [] }, { id: 11, title: 'Blouses', children: [] }, ]} /> </SimpleForm> </Edit> );<TreeInput>底层同样基于 rc-tree,数据格式与dataProvider.getTree()的返回格式一致(含children字段的节点数组)。它支持multiple(多选)、hideRootNodes、titleField、checkStrictly等配置;若需要通过引用资源自动拉取树数据,可搭配<ReferenceNodeInput>使用。两者分工明确:<TreeWithDetails>负责管理整棵树,<TreeInput>负责在表单中挑选节点。
十五、控制子节点插入位置:insertAsFirstChild
默认情况下,用户为某节点新增子节点时,新节点会被插入为父节点的最后一个子节点。如需强制插入为第一个子节点,在<AddChildButton>上设置insertAsFirstChild:
// in src/posts.js import { TopToolbar } from 'react-admin'; import { AddChildButton, EditNode, TreeWithDetails, } from '@react-admin/ra-tree'; const NodeEditActions = () => ( <TopToolbar> <AddChildButton label="Add child at top" insertAsFirstChild /> </TopToolbar> ); const CategoriesEdit = () => ( <EditNode actions={<NodeEditActions />}>...</EditNode> ); export const CategoriesList = () => ( <TreeWithDetails edit={CategoriesEdit}>...</TreeWithDetails> );Note:该特性要求
dataProvider.addChildNode()支持position参数,请确认你的数据提供器实现已涵盖此能力。
十六、小结
<TreeWithDetails>将 react-admin 的列表、创建、编辑、show 视图整合进一棵可交互的树中,覆盖了树形资源管理的完整闭环:浏览(展开/折叠、懒加载、子树过滤)→ 编辑(同页表单、分支删除)→ 组织(拖拽重排、多根、插入位置控制)→ 引用(表单内<TreeInput>选节点)。核心配置要点可归纳为:
- 创建/编辑视图分别用
<CreateNode>、<EditNode>,编辑表单务必搭配<EditNodeToolbar>(删除分支而非单条记录); - 拖拽重排开启
draggable,并根据数据一致性要求选择mutationMode; - 大节点量场景开启
lazy懒加载,同时将<EditNode>的mutationMode调整为optimistic或pessimistic; - 节点标题由
titleField控制,页面标题由title控制,样式统一走sx; - 涉及表单选节点时切换到
<TreeInput>(或<ReferenceNodeInput>)。
如需进一步了解树数据格式、<Tree>的展开/选中/拖拽事件回调(onExpand、onSelect、onDrop等)以及dataprop 的详细说明,可继续阅读仓库中的Tree.md与TreeInput.md。
【免费下载链接】react-adminA frontend Framework for single-page applications on top of REST/GraphQL APIs, using TypeScript, React and Material Design项目地址: https://gitcode.com/gh_mirrors/re/react-admin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考