ThinkPHP 6.0 实现多层级分类管理与无限级联下拉菜单实战教程

2025-09-25 0 251

一、项目需求分析

在实际Web开发中,我们经常需要处理具有层级结构的数据,如商品分类、地区选择、组织架构等。传统的固定层级设计往往无法满足灵活多变的需求。本文将带领大家实现一个基于ThinkPHP 6.0的无限层级分类管理系统。

核心功能需求:

  • 支持无限层级分类添加
  • 分类数据树状结构展示
  • 动态级联下拉菜单
  • 分类排序与状态管理

二、数据库设计与模型创建

1. 数据表结构设计


CREATE TABLE `category` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '分类名称',
  `parent_id` int(11) unsigned DEFAULT '0' COMMENT '父级ID',
  `level` tinyint(1) unsigned DEFAULT '1' COMMENT '层级',
  `sort` int(11) unsigned DEFAULT '0' COMMENT '排序',
  `status` tinyint(1) unsigned DEFAULT '1' COMMENT '状态:1正常 0禁用',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_parent_id` (`parent_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';
    

2. 创建Category模型


<?php
namespace appmodel;

use thinkModel;

class Category extends Model
{
    // 设置数据表名
    protected $name = 'category';
    
    // 自动时间戳
    protected $autoWriteTimestamp = true;
    protected $createTime = 'create_time';
    protected $updateTime = 'update_time';
    
    // 获取器 - 状态显示
    public function getStatusTextAttr($value, $data)
    {
        $status = [0 => '禁用', 1 => '正常'];
        return $status[$data['status']];
    }
    
    // 获取所有子分类ID(包含自身)
    public function getChildrenIds($categoryId)
    {
        $ids = [$categoryId];
        $children = $this->where('parent_id', $categoryId)->select();
        
        foreach ($children as $child) {
            $ids = array_merge($ids, $this->getChildrenIds($child['id']));
        }
        
        return $ids;
    }
}
?>
    

三、控制器逻辑实现

1. 分类控制器核心方法


<?php
namespace appcontroller;

use appBaseController;
use appmodelCategory;
use thinkfacadeView;

class CategoryController extends BaseController
{
    // 分类列表页
    public function index()
    {
        $categories = Category::where('status', 1)
            ->order('sort desc, id asc')
            ->select()
            ->toArray();
            
        $tree = $this->buildTree($categories);
        return View::fetch('index', ['tree' => $tree]);
    }
    
    // 构建树状结构
    private function buildTree($items, $parentId = 0)
    {
        $tree = [];
        foreach ($items as $item) {
            if ($item['parent_id'] == $parentId) {
                $children = $this->buildTree($items, $item['id']);
                if ($children) {
                    $item['children'] = $children;
                }
                $tree[] = $item;
            }
        }
        return $tree;
    }
    
    // 获取级联分类数据(API接口)
    public function getCascadeData()
    {
        $parentId = $this->request->param('parent_id', 0);
        $categories = Category::where('parent_id', $parentId)
            ->where('status', 1)
            ->order('sort desc, id asc')
            ->select();
            
        return json([
            'code' => 200,
            'data' => $categories,
            'msg' => 'success'
        ]);
    }
    
    // 添加分类
    public function add()
    {
        if ($this->request->isPost()) {
            $data = $this->request->post();
            
            // 验证数据
            $validate = new appvalidateCategory();
            if (!$validate->check($data)) {
                return json(['code' => 400, 'msg' => $validate->getError()]);
            }
            
            // 计算层级
            if ($data['parent_id'] > 0) {
                $parent = Category::find($data['parent_id']);
                $data['level'] = $parent->level + 1;
            } else {
                $data['level'] = 1;
            }
            
            $result = Category::create($data);
            if ($result) {
                return json(['code' => 200, 'msg' => '添加成功']);
            }
            
            return json(['code' => 500, 'msg' => '添加失败']);
        }
        
        // 获取所有分类用于选择父级
        $categories = Category::where('status', 1)->select();
        $tree = $this->buildTree($categories->toArray());
        
        return View::fetch('add', ['categories' => $tree]);
    }
}
?>
    

四、前端视图实现

1. 分类列表页模板


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>分类管理 - ThinkPHP实战</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="external nofollow"  rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <div class="row">
            <div class="col-12">
                <h2>分类管理</h2>
                <a href="{:url('category/add')}" rel="external nofollow"  class="btn btn-primary mb-3">添加分类</a>
                
                <div class="card">
                    <div class="card-body">
                        {if !empty($tree)}
                            <ul class="list-group">
                                {volist name="tree" id="category"}
                                    <li class="list-group-item">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <span>{$category.name} (ID: {$category.id})</span>
                                            <div>
                                                <a href="{:url('category/edit', ['id' => $category.id])}" rel="external nofollow"  class="btn btn-sm btn-warning">编辑</a>
                                                <a href="javascript:void(0)" rel="external nofollow"  rel="external nofollow"  onclick="deleteCategory({$category.id})" class="btn btn-sm btn-danger">删除</a>
                                            </div>
                                        </div>
                                        
                                        {if isset($category['children'])}
                                            <ul class="list-group mt-2">
                                                {volist name="category.children" id="child"}
                                                    <li class="list-group-item" style="margin-left: 30px;">
                                                        <div class="d-flex justify-content-between align-items-center">
                                                            <span>{$child.name} (ID: {$child.id})</span>
                                                            <div>
                                                                <a href="{:url('category/edit', ['id' => $child.id])}" rel="external nofollow"  class="btn btn-sm btn-warning">编辑</a>
                                                                <a href="javascript:void(0)" rel="external nofollow"  rel="external nofollow"  onclick="deleteCategory({$child.id})" class="btn btn-sm btn-danger">删除</a>
                                                            </div>
                                                        </div>
                                                    </li>
                                                {/volist}
                                            </ul>
                                        {/if}
                                    </li>
                                {/volist}
                            </ul>
                        {else /}
                            <p class="text-muted">暂无分类数据</p>
                        {/if}
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script>
    function deleteCategory(id) {
        if (confirm('确定要删除这个分类吗?')) {
            fetch('{:url('category/delete')}', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({id: id})
            })
            .then(response => response.json())
            .then(data => {
                if (data.code === 200) {
                    alert('删除成功');
                    location.reload();
                } else {
                    alert('删除失败:' + data.msg);
                }
            });
        }
    }
    </script>
</body>
</html>
    

2. 无限级联下拉菜单实现


<div class="mb-3">
    <label class="form-label">选择分类</label>
    <div class="row">
        <div class="col-md-4">
            <select class="form-select cascade-select" data-level="1">
                <option value="">请选择一级分类</option>
                {volist name="categories" id="category"}
                    {if $category.level == 1}
                    <option value="{$category.id}">{$category.name}</option>
                    {/if}
                {/volist}
            </select>
        </div>
        <div class="col-md-4">
            <select class="form-select cascade-select" data-level="2" disabled>
                <option value="">请选择二级分类</option>
            </select>
        </div>
        <div class="col-md-4">
            <select class="form-select cascade-select" data-level="3" disabled>
                <option value="">请选择三级分类</option>
            </select>
        </div>
    </div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const selects = document.querySelectorAll('.cascade-select');
    
    selects.forEach(select => {
        select.addEventListener('change', function() {
            const level = parseInt(this.dataset.level);
            const value = this.value;
            
            // 清空后续级别的选项
            for (let i = level; i < selects.length; i++) {
                selects[i].innerHTML = '<option value="">请选择' + (i+1) + '级分类</option>';
                selects[i].disabled = true;
            }
            
            if (value) {
                // 获取下一级数据
                fetch('{:url('category/getCascadeData')}?parent_id=' + value)
                    .then(response => response.json())
                    .then(data => {
                        if (data.code === 200 && data.data.length > 0) {
                            const nextSelect = selects[level];
                            nextSelect.disabled = false;
                            
                            data.data.forEach(item => {
                                const option = document.createElement('option');
                                option.value = item.id;
                                option.textContent = item.name;
                                nextSelect.appendChild(option);
                            });
                        }
                    });
            }
        });
    });
});
</script>
    

五、数据验证器


<?php
namespace appvalidate;

use thinkValidate;

class Category extends Validate
{
    protected $rule = [
        'name' => 'require|max:100',
        'parent_id' => 'number',
        'sort' => 'number',
        'status' => 'require|in:0,1'
    ];
    
    protected $message = [
        'name.require' => '分类名称不能为空',
        'name.max' => '分类名称不能超过100个字符',
        'parent_id.number' => '父级ID必须为数字',
        'sort.number' => '排序值必须为数字',
        'status.require' => '状态不能为空',
        'status.in' => '状态值不合法'
    ];
}
?>
    

六、性能优化建议

1. 使用缓存优化


// 在模型中添加缓存方法
public static function getCachedTree()
{
    return cache('category_tree') ?: self::buildAndCacheTree();
}

private static function buildAndCacheTree()
{
    $categories = self::where('status', 1)
        ->order('sort desc, id asc')
        ->select()
        ->toArray();
    
    $tree = self::buildTree($categories);
    cache('category_tree', $tree, 3600); // 缓存1小时
    return $tree;
}

// 在添加、编辑、删除分类时清除缓存
public static function clearCache()
{
    cache('category_tree', null);
}
    

2. 数据库索引优化

确保为parent_id、status、sort字段建立合适的索引,以提高查询效率。

七、总结

本文详细介绍了在ThinkPHP 6.0中实现多层级分类管理的完整方案,涵盖了从数据库设计到前后端实现的各个环节。通过递归算法构建树状结构,结合AJAX实现动态级联下拉菜单,为用户提供了良好的操作体验。

关键技术点总结:

  • 使用parent_id字段实现无限层级关系
  • 递归算法处理树状数据结构
  • ThinkPHP模型关联与数据验证
  • 前端动态级联交互实现
  • 缓存机制提升系统性能

这种设计方案具有很好的扩展性,可以轻松应用于各种需要层级管理的业务场景,为后续的功能扩展奠定了坚实的基础。

ThinkPHP 6.0 实现多层级分类管理与无限级联下拉菜单实战教程
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 thinkphp ThinkPHP 6.0 实现多层级分类管理与无限级联下拉菜单实战教程 https://www.taomawang.com/server/thinkphp/1113.html

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务