zgy-dcat / master-detail
Master-Detail (tree + grid) layout extension for zgy-dcat/laravel-admin. 左树右表(树表联动)布局扩展。
Requires
- php: >=8.1
- zgy-dcat/laravel-admin: dev-master
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-08 05:34:16 UTC
README
支持三种模式:
- 树表联动:左树右表布局,左侧树形导航 + 右侧异步表格,点击树节点联动刷新右侧表格数据。
- 纯树模式:不传
->table()即返回纯树组件,树占满整行,点击节点仅选中、不联动表格。- 面板模式:右侧放任意自定义异步内容(卡片/详情/统计等,非表格),同样点击树节点联动刷新。 适配 zgy-dcat/laravel-admin(dcat-admin 分支)。
特性
- 三模式:传
->table(YourTable::class)为树表联动;传->panel(YourPanel::class)为面板模式(右侧任意自定义异步内容);都不传则自动进入纯树模式(树占满整行、无右侧联动)。 - 默认折叠 + 双击区域分流:含子级的节点初始全部折叠;点节点标题选中 + 联动右侧,点展开箭头仅展开/收起、不触发联动(三者模式一致)。
- 表格完全归业务:继承
MasterDetailTable自定义grid(),扩展只负责布局 + 联动 + 传参。 - 参数名可配:默认
parent_id,可通过->param('dept_id')修改。 - 节点自定义参数(多参数):每个节点可携带一组键值参数(默认字段
params),点击该节点时全部传入右侧,右侧用getParam('name')按名读取;老的单参数用法零改动。
- 多参数的忽略规则:节点
params为空数组/非数组时整体忽略(老数据零影响);自定义参数名与主联动参数名(param())同名时该参数被跳过,避免覆盖节点 id。
- 含不含子部门由业务决定:默认只传当前节点 id;开启
->withChildren(true)后把当前节点+全部子孙 id 一并传入,表格拿到parentIds()自行决定过滤。 - 左右栅格布局:使用 Bootstrap row + col,列宽可配(默认 4/8),移动端自动堆叠。
- 内联 CSS/JS:开箱即用,无需
admin:ext-update发布资源。 - 主题跟随:树节点选中态、图标颜色自动跟随 dcat 当前主题色(
admin.layout.color),亮/暗模式都有适配。 - 内置搜索定位(可开关):默认隐藏,
->searchable(true)显示树顶搜索框;整体匹配搜索框中的全部内容,命中节点自动高亮、展开祖先链并滚动定位到第一个命中项;Enter跳到下一个命中,无匹配时给出提示。大数据量下用 id/child 索引避免 O(n²) 遍历。清空搜索框 = 重建整棵树:回到初始默认折叠态,搜索期间自动展开的祖先链/选中态会重置(用户手动展开的也一起还原,属于预期行为)。 - XSS 防护:树标题/图标在 JSON 输出与前端渲染双层转义,恶意内容不会注入页面。
- 多实例安全:同一页面可放多个 TreeTable,"每页数量"拦截器按表格归属处理,互不干扰。
- 友好报错:树数据为空时抛明确异常,不白屏。
- 修复 jQuery
.data()缓存坑:AsyncTable 重载读的是data('url'),联动脚本同时写attr与data,避免"URL 不更新"问题。
安装
composer require zgy-dcat/master-detail
然后在后台「扩展」里启用即可(无需发布资源)。
用法
1. 自定义右侧表格(树表联动必写)
纯树模式(不传
->table())跳过本节,直接看 3. 纯树模式。
<?php namespace App\Admin\Tables; use App\Models\DepartmentMember; use Dcat\Admin\Grid; use Zgy\MasterDetail\MasterDetailTable; class DepartmentMemberTable extends MasterDetailTable { public function grid(): Grid { return Grid::make(DepartmentMember::class, function (Grid $grid) { $ids = $this->parentIds(); // 当前节点 + 子孙 id(withChildren 开启时) $grid->model()->whereIn('dept_id', $ids); $grid->column('name')->label(); $grid->column('created_at'); }); } }
可用的联动参数读取方法:
| 方法 | 说明 |
|---|---|
parentId() |
当前点击的节点 id(根节点为 0) |
parentIds() |
当前节点 + 全部子孙节点 id 数组 |
param() |
联动参数名 |
paramValue($default) |
主联动参数的原始值(payload[param()] ?? $default,parentId()/parentIds() 的底层读取) |
getParam('name') |
节点自定义参数(点击节点携带的任意键值)按名读取 |
params() |
读取当前节点携带的全部自定义参数(自动排除主联动参数、master_detail_param 元信息与 renderable/_trans_ 等框架参数) |
两个易踩的点:
- 首屏右侧收到
parentId() == 0:页面打开时(未点击任何节点)右侧就会加载「根节点」数据(URL 上parent_id=0)。业务应在grid()/render()里决定根节点显示什么——例如只显示顶级分类、或提示「请点击左侧节点」。withChildren(true)时避免用parentId()过滤:开启后 URL 传的是逗号拼接的 ids(parent_id=2,3,4),parentId()只会返回第一个2;此时应始终用parentIds()(数组)做过滤。
2. 页面控制器
<?php namespace App\Admin\Controllers; use App\Admin\Tables\DepartmentMemberTable; use App\Models\Department; use Dcat\Admin\Layout\Content; use Dcat\Admin\Layout\Row; use Dcat\Admin\Controllers\AdminController; use Zgy\MasterDetail\TreeTable; class OrgTreeController extends AdminController { public function index(Content $content) { // 树数据只吃数组:模型→转数组(可按需过滤),或直接从接口/静态数据来 $nodes = Department::query() ->orderBy('order') ->get(['id', 'parent_id', 'name']) ->toArray(); $content->header('组织架构'); $content->description('左树右表'); $content->row(function (Row $row) use ($nodes) { $row->column(12, TreeTable::make() ->treeData($nodes) ->table(DepartmentMemberTable::class) ->param('dept_id') // 联动参数名可自定;右侧 parentId()/parentIds() 自动跟随这个名 ->titleField('name') // 字段名可配(默认 title) ->iconField('icon') // 数据项自带的图标 class 字段(可选,优先) ->iconStyle('color: #e74c3c; font-size: 16px') // 图标内联样式(可选) ->folderIcon('fa fa-folder') // 含子级节点默认 class(完整 class,任意图标库) ->leafIcon('fa fa-file') // 叶子节点默认 class(完整 class,任意图标库) ->withChildren(true) // 含子部门(业务自定义) ->leftCol(4) // 左树占 4 列(默认) ->rightCol(8) // 右表占 8 列(默认) ->render()); }); return $content; } }
3. 纯树模式
只要树、不要表格时,不调用 ->table() 即可。组件自动切换为纯树模式:
树占满整行宽度(不再左右分栏),高度默认 500px(可用 ->treeHeight() 覆盖),
点击节点仅切换选中态,不发生任何表格联动。搜索/图标/主题跟随等树的能力全部照常可用。
$content->row(function (Row $row) use ($nodes) { $row->column(12, TreeTable::make() ->treeData($nodes) ->titleField('name') ->searchable(true) // 树照常支持搜索 ->treeHeight('60vh') // 树可以自定义高度 ->render()); });
3b. 面板模式(右侧自定义异步内容)
右侧不想放表格、想放任意异步内容(卡片/详情/统计等)时,用 ->panel() 替代 ->table()。
内容类继承 MasterDetailPanel(与表格同款联动参数:parentId()/parentIds()/param()):
<?php namespace App\Admin\Panels; use Dcat\Admin\Widgets\Card; use Zgy\MasterDetail\MasterDetailPanel; class MenuDetailPanel extends MasterDetailPanel { public function render() { $menu = QbtMenu::find($this->parentId()); if (! $menu) { return Card::make('菜单详情', '未找到该菜单'); } return Card::make('菜单详情', $menu->title.' · '.$menu->uri) ->tool('ID: '.$menu->id); } }
控制器用法与树表联动一致,只是把 ->table() 换成 ->panel():
TreeTable::make() ->treeData($nodes) ->panel(MenuDetailPanel::class) ->param('menu_id') // 与表格模式一样,参数名可自定,右侧 parentId() 自动跟随 ->render();
说明:官方
LazyTable的 URL 放data-url可联动,但通用Lazy组件把 URL 写死在 JS 里无法联动。 扩展自带LazyPanel(机制与 LazyTable 一致),面板模式因此同样支持点击树节点联动刷新。
4. 节点自定义参数(多参数联动)
每个树节点可以携带一组键值参数,点击该节点时全部作为 query 参数传给右侧内容(表格或面板)。
字段名默认 params,可用 ->paramField('xxx') 换名;节点没有该字段/不是数组时忽略,老用法完全兼容。
$nodes = [ ['id' => 1, 'parent_id' => 0, 'title' => '系统', 'params' => ['type' => 'system', 'scene' => 'admin']], ['id' => 2, 'parent_id' => 1, 'title' => '用户', 'params' => ['type' => 'user', 'scene' => 'admin']], ['id' => 3, 'parent_id' => 0, 'title' => '报表', 'params' => ['type' => 'report']], ]; TreeTable::make() ->treeData($nodes) ->table(YourTable::class) // 点击节点 2 时,右侧请求带 parent_id=2&type=user&scene=admin ->render();
右侧表格/面板里按名读取:
class YourTable extends MasterDetailTable { public function grid(): Grid { $type = $this->getParam('type'); // 'system' | 'user' | 'report' $scene = $this->getParam('scene', 'default'); // 带默认值 return Grid::make(YourModel::class, function (Grid $grid) use ($type, $scene) { $grid->model()->where('type', $type)->where('scene', $scene); // ... }); } }
注意:参数名不要与主联动参数名(
param()默认parent_id)重复,重复时以主参数(节点 id)为准。
5. 注册路由
正常注册到六端路由即可(例如):
$router->get('examples/org-tree', '\App\Admin\Controllers\OrgTreeController@index');
注意:如使用带 namespace 的 route group,请用带前导
\的字符串字面量引用控制器,不要用use + ::class。
配置项
TreeTable 链式方法:
| 方法 | 默认 | 说明 |
|---|---|---|
treeData(array) |
[] |
树数据数组:关联数组/对象均可,字段由 idField/parentField/titleField 指定 |
table(string $class) |
- | 右侧表格 Renderable 类(继承 MasterDetailTable)。可选:不传则进入纯树模式(树占满整行,点击仅选中、不联动) |
panel(string $class) |
- | 右侧自定义异步内容 Renderable 类(继承 MasterDetailPanel)。可选:与 table() 二选一,后调用者覆盖;不传也不传 table 则进入纯树模式 |
param(string) |
parent_id |
联动参数名 |
paramField(string) |
params |
节点自定义参数数组所在的字段名(节点数据里该字段为关联数组时,点击时全部传入右侧) |
withChildren(bool) |
false |
点击节点时把当前节点+全部子节点 id 传给表格 |
idField(string) |
id |
节点 id 字段 |
parentField(string) |
parent_id |
父级字段 |
titleField(string) |
title |
标题字段 |
iconField(string) |
- | 数据项自带图标 class 字段名(可选,优先于默认图标) |
iconStyle(string) |
'' |
图标内联样式(如 'color: #e74c3c; font-size: 16px',留空用 CSS 默认) |
folderIcon(string) |
fa fa-folder-open |
含子级节点的默认图标 class(完整 class,支持任意图标库) |
leafIcon(string) |
fa fa-file-o |
叶子节点默认图标 class(完整 class,支持任意图标库) |
leftCol(int) |
4 |
左侧树栅格列数(1-12) |
rightCol(int) |
8 |
右侧表格栅格列数(1-12) |
treeHeight(int|string) |
500px |
树容器高度:传数字按 px(如 600),传字符串原样透传(如 '60vh'、'calc(100vh - 220px)');超出自动滚动 |
searchable(bool) |
false |
是否显示树顶搜索框;开启后整体匹配搜索框中的全部内容 |
工作原理
TreeTable::make() 布局容器
├─ treeData([...]) 数组树 → 扁平节点 JSON(id/parent/title,data_get 取值)
├─ table(MemberTable::class) 首屏 URL 由 payload(getUrl) 生成
│ └─ panel(SomePanel::class) 面板模式:LazyPanel 同机制渲染任意异步内容
└─ render() 输出布局 HTML + 内联 CSS + 联动 JS
点击树节点
└─ loadTable(id)
├─ 计算 ids(withChildren ? 子孙 : 单节点)
├─ 写入节点自定义参数(node.params → type=xxx&scene=xxx,全部作为 query)
├─ 拼 data-url + 双写 attr/data
└─ trigger('table:load') AsyncTable 异步渲染 → 后端渲染 grid()(payload 注入 parent_id + 自定义参数)
└─ trigger('panel:load') LazyPanel 异步渲染 → 后端渲染内容类 render()
RenderableController 会把 query 参数(parent_id/dept_id 等)自动注入 renderable 的 payload,
因此业务类的 parentId()/parentIds() 直接可用。
参数名一致性:使用
->param('dept_id')自定义参数名时,TreeTable 会把实际参数名写入右侧 renderable 的master_detail_param元信息,右侧parentId()/parentIds()/paramValue()自动跟随, 无需在业务类里手动改defaultParam。