filament v4 中 树形表格(tree table)的实现
树表格
filament 数据表格自身是不带tree结构的, 要自己去改造table原有组件支持tree 难度太大,通过一番研究,最终通过外部控制的方式实现了树表格。
先看效果


逻辑
当点击 设置的 tree-title 后 会展开他的下级子类,展示后,再次点击会收缩子类。
实现步骤
1. 让model支持tree 结构
必须有这三个字段,parent_id, title,order,
AdminMenuModel.php
# 加上以下函数
class AdminMenu extends Model{
// 获取父菜单
public function parent(): BelongsTo
{
return $this->belongsTo(AdminMenu::class, 'parent_id');
}
// 获取子菜单
public function children(): HasMany
{
return $this->hasMany(AdminMenu::class, 'parent_id')->ordered()->visible();
}
public function childrenRecursive(): HasMany
{
return $this->children()->with('childrenRecursive');
}
// 获取菜单深度
public function depth(): int
{
$depth = 0;
$parent = $this->parent;
while ($parent) {
$depth++;
$parent = $parent->parent;
}
return $depth;
}
// 排序
public function scopeOrdered(Builder $query): Builder
{
return $query->orderBy('order');
}
// 显示
public function scopeVisible(Builder $query): Builder
{
return $query->where('show', true);
}
}
2.定义树形表格宏,让表格支持 treeView() 方法
app/Providers/AppServiceProvider.php
use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Filament\Tables\Table;
use Filament\Actions\Action;
use Filament\Support\Enums\IconSize;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
// 树形表格宏定义
Table::macro('treeView', function (array $options = []) {
/** @var Table $this */
$options = array_merge([
'parentColumn' => 'parent_id',
'rootValues' => [0, '0', null, -1, '-1'],
'orderColumn' => 'order',
'keyColumn' => null,
'reorderable' => true,
'hiddenClass' => 'hidden',
'recordVisibleUsing' => null,
'isRootUsing' => null,
'reorderLabels' => [
'enable' => '启用重新排序',
'disable' => '禁用重新排序',
],
], $options);
$isRecordVisible = function ($record) use ($options): bool {
if ($options['recordVisibleUsing'] instanceof Closure) {
return (bool) value($options['recordVisibleUsing'], $record, $options);
}
if ($options['isRootUsing'] instanceof Closure) {
return (bool) value($options['isRootUsing'], $record, $options);
}
$parentValue = data_get($record, $options['parentColumn']);
return in_array($parentValue, Arr::wrap($options['rootValues']), true);
};
$this->paginated(false)
->recordUrl(false)
->recordClasses(function ($record) use ($options, $isRecordVisible) {
$rid = 'pr-'.$record->parent_id;
return $isRecordVisible($record) ? '' : $rid.' '.$options['hiddenClass'];
});
if ($options['reorderable']) {
$this->reorderable($options['orderColumn'])
->reorderRecordsTriggerAction(
fn (Action $action, bool $isReordering) => $action
->button()
->label(
$isReordering
? $options['reorderLabels']['disable']
: $options['reorderLabels']['enable'],
),
);
}
$this->records(function () use ($options) {
/** @var Table $this */
$query = $this->getLivewire()->getFilteredSortedTableQuery();
if (! $query) {
return collect();
}
$records = $query->get();
if ($records->isEmpty()) {
return $records;
}
$keyColumn = $options['keyColumn'] ?? $query->getModel()->getKeyName();
$parentColumn = $options['parentColumn'];
$orderColumn = $options['orderColumn'];
$rootValues = Arr::wrap($options['rootValues']);
$grouped = $records->groupBy(fn ($record) => data_get($record, $parentColumn));
$visited = [];
$flattened = collect();
$traverse = function ($parentValue) use (&$traverse, $grouped, &$flattened, &$visited, $keyColumn, $orderColumn): void {
$children = $grouped->get($parentValue);
if (! $children) {
return;
}
$children = $children
->sortBy([[$orderColumn, 'asc'], [$keyColumn, 'asc']])
->values();
foreach ($children as $child) {
$recordKey = data_get($child, $keyColumn);
if ($recordKey === null) {
continue;
}
$flattened->push($child);
$visited[$recordKey] = true;
$traverse($recordKey);
}
};
foreach ($rootValues as $rootValue) {
$traverse($rootValue);
}
if ($records->count() !== count($visited)) {
$records
->filter(fn ($record) => ! array_key_exists(data_get($record, $keyColumn), $visited))
->sortBy([[$parentColumn, 'asc'], [$orderColumn, 'asc'], [$keyColumn, 'asc']])
->each(function ($record) use (&$traverse, &$flattened, $keyColumn, &$visited): void {
$recordKey = data_get($record, $keyColumn);
if ($recordKey === null || array_key_exists($recordKey, $visited)) {
return;
}
$flattened->push($record);
$visited[$recordKey] = true;
$traverse($recordKey);
});
}
return $flattened;
});
return $this;
});
说明:这一步最为关键。
'rootValues' => [0, '0', null, -1, '-1'],可以指定你的顶级 parent_id 对应的值。
3 加上外部js
resources/js/custom.js
const resolveRecordId = (row) => {
if (!row) {
return null;
}
const dataId = row.dataset?.recordId ?? row.dataset?.key ?? row.dataset?.id;
if (dataId) {
return dataId;
}
const wireKey = row.getAttribute('wire:key');
if (!wireKey) {
return null;
}
const segments = wireKey.split('.');
return segments.pop() || null;
};
document.addEventListener('click', (event) => {
const treeCell = event.target.closest('.tree-title');
if (!treeCell) {
return;
}
const treeRow = treeCell.closest('tr');
if (!treeRow) {
return;
}
const recordId = resolveRecordId(treeRow);
if (!recordId) {
return;
}
const childRows = document.querySelectorAll(`.pr-${recordId}`);
if (!childRows.length) {
return;
}
const isExpanded = Array.from(childRows).every((element) => ! element.classList.contains('hidden'));
childRows.forEach((element) => {
element.classList.toggle('hidden', isExpanded);
});
});
把自定义js注册 在
app/Providers/AppServiceProvider.php的boot中添加如下代码, 使用php artisan filament:assets发布资源FilamentAsset::register([ Js::make('custom-script', __DIR__ . '/../../resources/js/custom.js'), ]);
4 在表格中使用
<?php
namespace App\Filament\Resources\AdminMenus\Tables;
use App\Models\AdminMenu;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\HtmlString;
use Livewire\Component;
use Filament\Tables\Enums\FiltersLayout;
class AdminMenusTable
{
public static function configure(Table $table): Table
{
return $table
->treeView() // 加上 treeView 宏定义
->modifyQueryUsing(fn (Builder $query) => $query->with('parent')->withCount(['children as children_count']))
->columns([
TextColumn::make('id')
->label('ID')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('title')
->extraAttributes(['class' => 'tree-title']) // 这个是必须要设置的,否则无法点击展开
->label('标题')
->icon(fn (AdminMenu $record) => $record->children_count ? 'heroicon-o-chevron-right' : null)
->searchable()
->html()
->formatStateUsing(
fn (string $state, AdminMenu $record): HtmlString => new HtmlString(
str_repeat(' ', $record->depth()) . e($state)
)
),
]);
}
}
存在不完美的地方
- 图标不会因展开,收缩而变化。
- 当点击[编辑]等,其它行动作时,会自动收缩。
总结
虽然过程有点绕,但是一次搞好后,其它地方数据表格都可以使用。
暂时还没有想到更好的解决办法。本人在此抛砖引玉。filament v3,是否也可以使用,我想应该也是可以的。但本人精力有限,没有去做测试。
如果您也在用filament ,遇到什么难题,可以进群交流

本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: