一、项目需求分析
在实际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模型关联与数据验证
- 前端动态级联交互实现
- 缓存机制提升系统性能
这种设计方案具有很好的扩展性,可以轻松应用于各种需要层级管理的业务场景,为后续的功能扩展奠定了坚实的基础。