导航菜单多级分类搭建|下拉栏目后台配置完整流程

发布时间:2026-07-18 11:55

在网站或应用的后台管理中,导航菜单的多级分类与下拉栏目配置是提升用户体验和内容组织效率的核心功能。无论是企业官网、电商平台还是内容管理系统,合理的菜单结构能让用户快速定位信息,同时减轻后台维护压力。本教程将带你从零开始,完整搭建一套支持无限级下拉的导航菜单系统,涵盖数据库设计、后台配置接口、前端渲染逻辑及常见问题处理。

### 前置准备

在开始操作前,请确保已具备以下环境和工具:

1. **运行环境**:已安装 PHP 7.4+ 和 MySQL 5.7+ 的服务器(本地或线上均可),推荐使用集成环境如 XAMPP、WAMP 或宝塔面板。

2. **开发工具**:任意代码编辑器(如 VS Code、Sublime Text)、数据库管理工具(如 phpMyAdmin、Navicat)。

3. **基础框架**:本教程基于 ThinkPHP 6.0 框架演示,但核心逻辑适用于 Laravel、Yii2 等主流框架。若使用其他框架,请自行调整数据库查询和路由部分。

4. **前端资源**:jQuery 库(用于异步操作)、Bootstrap 4+ 样式库(用于后台界面美化,非必需但推荐)。

5. **权限准备**:确保你有服务器文件写入权限和数据库创建/修改权限。

### 分步操作步骤

#### 1. 设计并创建导航菜单数据表

导航菜单的核心是“无限级分类”结构,通常采用“邻接表”模型,通过 `parent_id` 字段关联父级菜单。打开数据库管理工具,执行以下 SQL 语句创建菜单表:

```sql

CREATE TABLE `nav_menu` (

`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,

`name` varchar(100) NOT NULL COMMENT '菜单名称',

`parent_id` int(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '父级ID,0表示顶级',

`url` varchar(255) DEFAULT NULL COMMENT '链接地址',

`sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序值,越小越靠前',

`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用',

`created_at` datetime DEFAULT NULL COMMENT '创建时间',

`updated_at` datetime DEFAULT NULL COMMENT '更新时间',

PRIMARY KEY (`id`),

KEY `parent_id` (`parent_id`),

KEY `sort` (`sort`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导航菜单表';

```

- **关键字段说明**:`parent_id` 为 0 表示顶级菜单,非 0 则表示该菜单的父级 ID。`sort` 字段用于同级菜单的排序控制。`status` 可快速启用/禁用菜单项而不删除数据。

- **插入测试数据**:执行以下 SQL 插入几组示例数据,便于后续验证:

```sql

INSERT INTO `nav_menu` (`id`, `name`, `parent_id`, `url`, `sort`, `status`) VALUES

(1, '产品中心', 0, '/product', 1, 1),

(2, '解决方案', 0, '/solution', 2, 1),

(3, '企业版', 1, '/product/enterprise', 1, 1),

(4, '个人版', 1, '/product/personal', 2, 1),

(5, '行业方案', 2, '/solution/industry', 1, 1),

(6, '技术方案', 2, '/solution/tech', 2, 1),

(7, '金融行业', 5, '/solution/industry/finance', 1, 1),

(8, '教育行业', 5, '/solution/industry/education', 2, 1);

```

此时,你已拥有一个包含两级和三级菜单的测试数据集。

#### 2. 创建后台菜单管理控制器与模型

在 ThinkPHP 6 中,使用命令行快速生成控制器和模型(或手动创建文件)。打开终端,进入项目根目录,执行:

```bash

php think make:controller admin/NavMenu

php think make:model NavMenu

```

- **模型文件** `app/model/NavMenu.php`:定义与数据库的交互逻辑。编辑该文件,添加以下方法用于获取树形菜单:

```php

namespace app\model;

use think\Model;

class NavMenu extends Model

{

// 定义与父级菜单的关联关系(自关联)

public function parent()

{

return $this->belongsTo(NavMenu::class, 'parent_id');

}

// 定义与子菜单的关联关系

public function children()

{

return $this->hasMany(NavMenu::class, 'parent_id');

}

// 递归获取所有子菜单(用于前端渲染)

public static function getTree($parentId = 0)

{

$menus = self::where('parent_id', $parentId)

->where('status', 1)

->order('sort', 'asc')

->select();

foreach ($menus as &$menu) {

$menu['children'] = self::getTree($menu['id']);

}

return $menus;

}

}

```

- **控制器文件** `app/controller/admin/NavMenu.php`:编写增删改查接口。编辑该文件,添加以下核心方法:

```php

namespace app\controller\admin;

use app\BaseController;

use app\model\NavMenu as NavMenuModel;

use think\Request;

class NavMenu extends BaseController

{

// 获取菜单列表(树形结构)

public function index()

{

$tree = NavMenuModel::getTree(0);

return json(['code' => 200, 'data' => $tree]);

}

// 添加菜单

public function add(Request $request)

{

$data = $request->post();

// 基础验证

if (empty($data['name'])) {

return json(['code' => 400, 'msg' => '菜单名称不能为空']);

}

$menu = new NavMenuModel();

$menu->name = $data['name'];

$menu->parent_id = $data['parent_id'] ?? 0;

$menu->url = $data['url'] ?? '';

$menu->sort = $data['sort'] ?? 0;

$menu->status = $data['status'] ?? 1;

$menu->created_at = date('Y-m-d H:i:s');

$menu->save();

return json(['code' => 200, 'msg' => '添加成功']);

}

// 编辑菜单

public function edit(Request $request, $id)

{

$menu = NavMenuModel::find($id);

if (!$menu) {

return json(['code' => 404, 'msg' => '菜单不存在']);

}

$data = $request->post();

$menu->name = $data['name'] ?? $menu->name;

$menu->parent_id = $data['parent_id'] ?? $menu->parent_id;

$menu->url = $data['url'] ?? $menu->url;

$menu->sort = $data['sort'] ?? $menu->sort;

$menu->status = $data['status'] ?? $menu->status;

$menu->updated_at = date('Y-m-d H:i:s');

$menu->save();

return json(['code' => 200, 'msg' => '更新成功']);

}

// 删除菜单(同时删除所有子菜单)

public function delete($id)

{

$menu = NavMenuModel::find($id);

if (!$menu) {

return json(['code' => 404, 'msg' => '菜单不存在']);

}

// 递归删除子菜单

$this->deleteChildren($id);

$menu->delete();

return json(['code' => 200, 'msg' => '删除成功']);

}

private function deleteChildren($parentId)

{

$children = NavMenuModel::where('parent_id', $parentId)->select();

foreach ($children as $child) {

$this->deleteChildren($child['id']);

$child->delete();

}

}

}

```

- **配置路由**:在 `route/app.php` 中添加对应路由规则:

```php

use think\facade\Route;

Route::group('admin', function () {

Route::get('nav_menu', 'admin.NavMenu/index');

Route::post('nav_menu/add', 'admin.NavMenu/add');

Route::post('nav_menu/edit/:id', 'admin.NavMenu/edit');

Route::delete('nav_menu/delete/:id', 'admin.NavMenu/delete');

});

```

此时,后台 API 已就绪,可通过 Postman 或浏览器测试接口。

#### 3. 开发后台菜单管理页面(HTML + jQuery)

在 `app/view/admin/nav_menu.html` 创建管理界面,实现菜单的增删改查和树形展示。

- **基础页面结构**:引入 Bootstrap 和 jQuery,构建布局:

```html

导航菜单管理

导航菜单管理

```

- **JavaScript 逻辑**(`public/static/js/nav_menu.js`):

```javascript

// 加载菜单树

function loadMenuTree() {

$.get('/admin/nav_menu', function(res) {

if (res.code === 200) {

renderTree(res.data, 0);

}

});

}

// 递归渲染树形菜单

function renderTree(menus, level) {

var html = '

';

return html;

}

// 显示添加模态框

function showAddModal(parentId) {

$('#modalTitle').text('添加菜单');

$('#menuId').val('');

$('#parentId').val(parentId);

$('#menuName').val('');

$('#menuUrl').val('');

$('#menuSort').val(0);

$('#menuStatus').val(1);

$('#menuModal').modal('show');

}

// 显示编辑模态框(通过 AJAX 获取菜单数据)

function showEditModal(id) {

$.get('/admin/nav_menu', function(res) {

if (res.code === 200) {

// 由于接口返回树形,需要遍历找到指定菜单(实际应用可单独提供详情接口)

var menu = findMenuById(res.data, id);

if (menu) {

$('#modalTitle').text('编辑菜单');

$('#menuId').val(menu.id);

$('#parentId').val(menu.parent_id);

$('#menuName').val(menu.name);

$('#menuUrl').val(menu.url);

$('#menuSort').val(menu.sort);

$('#menuStatus').val(menu.status);

$('#menuModal').modal('show');

}

}

});

}

// 查找菜单(递归)

function findMenuById(menus, id) {

for (var i = 0; i < menus.length; i++) {

if (menus[i].id === id) return menus[i];

if (menus[i].children) {

var found = findMenuById(menus[i].children, id);

if (found) return found;

}

}

return null;

}

// 保存菜单(新增或更新)

function saveMenu() {

var id = $('#menuId').val();

var url = id ? '/admin/nav_menu/edit/' + id : '/admin/nav_menu/add';

var data = {

name: $('#menuName').val(),

parent_id: $('#parentId').val(),

url: $('#menuUrl').val(),

sort: $('#menuSort').val(),

status: $('#menuStatus').val()

};

$.post(url, data, function(res) {

if (res.code === 200) {

$('#menuModal').modal('hide');

loadMenuTree();

} else {

alert(res.msg);

}

});

}

// 删除菜单

function deleteMenu(id) {

if (confirm('确认删除该菜单及其所有子菜单?')) {

$.ajax({

url: '/admin/nav_menu/delete/' + id,

type: 'DELETE',

success: function(res) {

if (res.code === 200) {

loadMenuTree();

} else {

alert(res.msg);

}

}

});

}

}

// 初始化加载

$(function() {

loadMenuTree();

});

```

此时,访问 `http://你的域名/admin/nav_menu.html` 即可看到带添加、编辑、删除按钮的树形菜单管理界面。

#### 4. 前端渲染多级下拉菜单

后台配置完成后,需要在前端页面(如网站首页)展示多级下拉导航。在控制器中调用模型方法获取树形数据,然后传递给视图。

- **前端控制器示例**(`app/controller/Index.php`):

```php

namespace app\controller;

use app\BaseController;

use app\model\NavMenu;

class Index extends BaseController

{

public function index()

{

$navTree = NavMenu::getTree(0); // 获取顶级菜单及其子菜单

return view('index', ['navTree' => $navTree]);

}

}

```

- **前端视图模板**(`app/view/index/index.html`),使用 Bootstrap 下拉菜单结构:

```html

首页

```

**关键点**:Bootstrap 4 原生不支持三级下拉菜单,通过 `.dropdown-submenu` 配合 CSS 定位实现。若需更复杂的多级交互,可引入 `bootstrap-submenu` 插件或自行编写 JavaScript 控制 hover/click 事件。

### 常见问题

**问题1:添加子菜单时,父级菜单选择列表显示不全或出现循环引用。**

解决方案:在添加/编辑表单中,父级选择下拉框应只显示顶级和当前菜单以外的菜单项。可通过递归生成 `