Laravel 8.x 中无限级分类下拉框需实现层级缩进、保留选中值及避免 N+1 查询:通过模型静态方法 getFlatTree 生成带前缀扁平数组,视图中用 old() 回填选项;大数据量时改用 Select2 + AJAX 懒加载;验证用 exists 规则即可。

在 Laravel 8.x 后台管理中,把无限级分类(基于 parent_id 的树形结构)适配到 <select></select> 下拉框,核心目标是:**清晰展示层级关系、支持根/子级区分、验证失败后能保留选中值、避免 N+1 查询拖慢渲染**。不是简单列出所有分类,而是让树形结构“可读、可选、可回填”。
一、数据库与模型准备(确保基础正确)
确保 categories 表含 id、name、parent_id 字段,其中 parent_id 允许为 NULL(根节点),迁移中写法必须是:
$table->foreignId('parent_id')->nullable()->constrained()->nullOnDelete();- 不要用
unsignedBigInteger加非空约束,否则根节点无法插入
Category 模型中定义关系:
public function children() { return $this->hasMany(Category::class, 'parent_id'); }public function parent() { return $this->belongsTo(Category::class, 'parent_id'); }
二、后台下拉框渲染:带缩进的扁平化列表
直接递归嵌套 <optgroup></optgroup> 不现实(HTML 不支持动态多层分组),通用做法是将树转为带前缀的扁平数组,例如:
- 电子产品
- └ 手机
- └ iPhone
- └ Android
- └ 笔记本
- 图书
控制器中生成该数组(推荐用 Eloquent 集合 + 递归方法,不查 N+1):
// 在 Category 模型里加一个静态方法
public static function getFlatTree($parentId = null, $level = 0)
{
$items = self::where('parent_id', $parentId)->orderBy('name')->get();
$result = [];
<pre class="brush:php;toolbar:false;">foreach ($items as $item) {
$prefix = str_repeat(' ', $level) . '└ ';
$result[] = ['id' => $item->id, 'name' => $prefix . $item->name];
// 递归追加子项
$result = array_merge($result, self::getFlatTree($item->id, $level + 1));
}
return $result;}
视图中渲染 select:
<select name="category_id" class="form-control">
<option value="">-- 请选择分类 --</option>
@foreach(App\Models\Category::getFlatTree() as $opt)
<option value="{{ $opt['id'] }}" {{ old('category_id') == $opt['id'] ? 'selected' : '' }}>
{{ $opt['name'] }}
</option>
@endforeach
</select>✅ 关键点:old('category_id') 必须写在每个 <option></option> 内做条件判断,不能写在 <select></select> 上。
三、优化:大数量时避免全量加载
如果分类超 200 条,一次性加载全部会影响页面速度。可改用 Select2 + AJAX 懒加载:
- 前端初始化 Select2,启用搜索
- 监听
query事件,向后端传关键词和可选的parent_id - 后端接口只返回匹配的顶层或指定父级下的子类(加
limit 20) - 返回格式为
[{"id":"1","text":"手机"},{"id":"2","text":"笔记本"}]
这样既保持体验,又规避了深树渲染瓶颈。
四、保存与验证注意事项
提交后,category_id 是一个整数 ID,验证规则用 'category_id' => 'required|exists:categories,id' 即可。
- 若允许选择根分类(即
parent_id IS NULL),无需额外限制 - 若业务上禁止选择叶子节点(只许选中间分类),可在验证时加自定义规则,查
$category->children->count() - 表单重定向失败后,
old('category_id')自动生效,无需额外逻辑


















