Search by

zgy-dcat / master-detail

gy23rm

Master-Detail (tree + grid) layout extension for zgy-dcat/laravel-admin. 左树右表(树表联动)布局扩展。

Package info

github.com/gy23rm/dcat-master-detail

pkg:composer/zgy-dcat/master-detail

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-master 2026-09-08 05:34 UTC

This package is auto-updated.

Last update: 2026-09-08 05:34:16 UTC


README

支持三种模式:

  1. 树表联动:左树右表布局,左侧树形导航 + 右侧异步表格,点击树节点联动刷新右侧表格数据。
  2. 纯树模式:不传 ->table() 即返回纯树组件,树占满整行,点击节点仅选中、不联动表格。
  3. 面板模式:右侧放任意自定义异步内容(卡片/详情/统计等,非表格),同样点击树节点联动刷新。 适配 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'),联动脚本同时写 attrdata,避免"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_ 等框架参数)

两个易踩的点

  1. 首屏右侧收到 parentId() == 0:页面打开时(未点击任何节点)右侧就会加载「根节点」数据(URL 上 parent_id=0)。业务应在 grid()/render() 里决定根节点显示什么——例如只显示顶级分类、或提示「请点击左侧节点」。
  2. 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