🍔 插件菜单
ThinkAdmin 的插件菜单主要有两种使用方式:通过插件 Service::menu() 给插件中心展示专属菜单,或在数据库迁移脚本中调用 PhinxExtend::write2menu() 写入全局后台菜单。
📚 基础概念
🤔 基本介绍
插件菜单用于声明插件的后台功能入口。当前实现中,Service::menu() 主要供插件中心展示插件专属菜单;需要进入后台全局菜单时,应在迁移脚本中调用 PhinxExtend::write2menu() 写入 system_menu。
实际例子:
// 插件菜单定义
public static function menu(): array
{
return [
[
'name' => '账号管理',
'subs' => [
['name' => '账号列表', 'node' => 'plugin-account/index'],
['name' => '账号配置', 'node' => 'plugin-account/config'],
],
],
];
}🤔 使用优势
问题1:插件功能无法访问
不使用插件菜单时,用户无法找到插件的功能入口:
// ❌ 不使用插件菜单:用户不知道插件有哪些功能
// 需要手动输入 URL 访问:/admin/plugin-account/index
// 用户体验差,容易忘记功能入口问题2:菜单管理困难
不使用插件菜单时,需要手动在后台添加菜单:
// ❌ 需要手动在后台添加菜单
// 系统管理 → 菜单管理 → 添加菜单
// 每个插件都要手动添加,工作量大使用插件菜单的优势:
// ✅ 使用插件菜单:统一定义菜单
// 1. 在 Service 类中定义菜单
// 2. 插件中心读取 menu() 展示专属菜单
// 3. 如需写入全局菜单,在迁移脚本中显式调用 write2menu()🎯 核心功能
功能1:定义插件菜单
继承 think\admin\Plugin 的插件服务类必须实现 menu(),插件中心会读取该方法返回的菜单:
// 插件中心进入插件专属空间时读取
$menus = $plugin['service']::menu();功能2:权限控制
菜单项可通过 node 关联权限节点。插件中心会过滤无权限的二级入口,并在一级菜单自身配置 node 时检查一级权限;如果一级菜单既没有可打开地址也没有可见子菜单,会被隐藏。后台全局菜单会由 MenuService 按菜单树和权限节点过滤:
// 菜单项关联权限节点
['name' => '账号列表', 'node' => 'plugin-account/index']
// 只有拥有该节点权限的用户才能看到入口功能3:动态生成
菜单由插件服务类动态生成:
// 使用 appCode 动态获取插件编码
$code = app(static::class)->appCode;
['node' => "{$code}/config/index"]
// 避免硬编码,提高灵活性🚀 主要功能
- 专属菜单: 插件中心可读取
Service::menu()展示插件专属菜单 - 标准定义: 继承
think\admin\Plugin的服务类通过静态menu()声明菜单 - 全局菜单: 迁移脚本可调用
PhinxExtend::write2menu()写入系统菜单表 - 权限控制: 通过
node与后台权限节点关联 - 灵活配置: 支持菜单名称、图标、节点、链接、参数、排序和打开方式等字段
- 统一入口: 插件中心可作为已注册模块插件的入口
📋 菜单类型
ThinkAdmin 支持两种菜单类型,各有特点:
1. 全局菜单
写入 system_menu 数据表的菜单,显示在系统主菜单中。
特点:
- 存储方式: 写入
system_menu数据表。 - 写入方式: 通常在迁移脚本中调用
PhinxExtend::write2menu()。 - 结构能力:
write2menu()当前按三层结构写入菜单。 - 数据持久: 菜单数据持久化后可在后台菜单管理中维护。
使用场景:
- 系统核心功能菜单
- 需要持久化存储的菜单
- 需要手动管理的菜单
2. 插件专属菜单
由插件服务注册类动态生成的菜单,显示在插件专属空间。
特点:
- 显示位置: 插件中心打开插件后的专属空间。
- 动态生成: 由插件服务类
menu()返回。 - 结构能力: 当前插件中心按一级分组和二级入口处理
subs。 - 链接生成: 一级菜单会优先使用自身
url,未配置url时才根据node通过plguri()生成插件空间内地址;二级菜单只根据node生成地址,没有node时为#,不会读取二级菜单自己的url字段。
使用场景:
- 插件功能菜单
- 需要动态生成的菜单
- 插件专属功能入口
⚙️ 配置方式
Service 类配置
在 Service 类中实现 menu() 方法,返回菜单配置数组。
方法要求:
- 方法实现: 在 Service 类中实现
menu方法 - 配置返回: 返回包含菜单项信息的数组
- 结构描述: 描述插件的菜单结构、权限要求等
- 读取时机: 插件中心展示插件专属空间时读取;写入全局菜单需要迁移脚本主动调用
菜单配置选项
常用字段包括 name、title、node、url、icon、params、target、sort 和 subs。插件中心专属菜单主要使用 name/title、一级 url、node、icon、subs;全局菜单写入会把这些字段转换为 system_menu 记录。
菜单分为两种类型
为了提高插件的集成性和易用性,系统优化了全局菜单的写入方式,并改进了插件专属空间的菜单管理。
全局菜单适合系统主导航入口,写入后会成为后台菜单表的一部分。插件专属菜单适合放在插件中心内部,由插件服务类动态声明,不需要提前写入菜单表。
插件中心统一入口
在安装 插件中心 后,系统进一步提升了插件管理的便捷性和统一性。用户可以通过插件中心的统一入口,直接访问各个应用插件的独立管理菜单。 具体来说,每个已通过 Composer/ThinkPHP 服务机制注册的应用插件,可在 Service 类中定义 menu 方法生成专属管理菜单。当插件中心被安装并启用后,它会读取已注册插件的菜单定义并展示对应入口。这样,用户只需通过插件中心的统一入口,即可管理和操作已注册的插件。
写入全局菜单数据
在数据库脚本的执行过程中,可以通过调用应用插件 Service 中的 menu 方法获取菜单配置,再利用 think\admin\extend\PhinxExtend::write2menu() 写入 system_menu 数据表。系统不会仅因为插件安装就自动把 menu() 写入全局菜单,是否写入取决于迁移脚本是否显式调用 write2menu()。
温馨提示: 当卸载应用插件时,并不会自动删除与之相关的菜单项。这可能导致在卸载插件后,系统中仍然残留有无效或不再需要的菜单项,需要管理员手动去系统菜单管理界面进行删除操作。
📝 菜单配置案例
Service 类菜单定义
插件的 Service 类必须继承 think\admin\Plugin 并实现静态方法 menu():
<?php
declare(strict_types=1);
namespace app\wechat;
use think\admin\Plugin;
/**
* 组件注册服务
* @class Service
* @package app\wechat
*/
class Service extends Plugin
{
/**
* 定义插件名称
* @var string
*/
protected $appName = '微信管理';
/**
* 定义安装包名
* @var string
*/
protected $package = 'zoujingli/think-plugs-wechat';
/**
* 定义插件菜单(静态方法)
* @return array
*/
public static function menu(): array
{
$code = app(static::class)->appCode; // 获取插件编码
return [
[
'name' => '微信管理',
'subs' => [
['name' => '微信接口配置', 'icon' => 'layui-icon layui-icon-set', 'node' => "{$code}/config/options"],
['name' => '微信支付配置', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "{$code}/config/payment"],
],
],
[
'name' => '微信定制',
'subs' => [
['name' => '微信粉丝管理', 'icon' => 'layui-icon layui-icon-username', 'node' => "{$code}/fans/index"],
['name' => '微信图文管理', 'icon' => 'layui-icon layui-icon-template-1', 'node' => "{$code}/news/index"],
['name' => '微信菜单配置', 'icon' => 'layui-icon layui-icon-cellphone', 'node' => "{$code}/menu/index"],
['name' => '回复规则管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "{$code}/keys/index"],
['name' => '关注自动回复', 'icon' => 'layui-icon layui-icon-release', 'node' => "{$code}/auto/index"],
],
],
[
'name' => '微信支付',
'subs' => [
['name' => '微信支付行为', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "{$code}/payment.record/index"],
['name' => '微信退款管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "{$code}/payment.refund/index"],
],
],
];
}
}数据库迁移脚本
<?php
use think\migration\Migrator;
use think\admin\extend\PhinxExtend;
use app\wechat\Service;
class InstallWechat20241011 extends Migrator
{
/**
* 安装微信插件数据
*/
public function change()
{
// 写入菜单数据
PhinxExtend::write2menu([
[
'name' => '微信管理',
'sort' => '200',
'subs' => Service::menu(),
],
], [
'url|node' => 'wechat/fans/index',
]);
}
}write2menu() 的第二个参数是写入前的存在性检查条件;示例表示当 system_menu 中已经存在 url 或 node 为 wechat/fans/index 的记录时,不再重复写入。
菜单配置参数说明
| 参数 | 类型 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| name | string | 是 | 菜单名称,写入全局菜单时会转为 title | '微信管理' |
| title | string | 否 | 菜单标题,存在时优先作为显示标题 | '微信管理' |
| node | string | 否 | 权限节点;有 node 的插件菜单会生成插件内跳转地址 | 'wechat/config/index' 或 "{$code}/config/index" |
| url | string | 否 | 一级插件菜单可直接使用;二级插件菜单会忽略该字段;全局菜单未配置时会回退到 node 或 # | 'admin/config/index' |
| icon | string | 否 | 菜单图标(LayUI 图标类) | 'layui-icon layui-icon-set' |
| params | string | 否 | 全局菜单链接参数 | 'type=base' |
| target | string | 否 | 全局菜单打开方式,默认 _self | '_blank' |
| sort | int | 否 | 全局菜单排序权重 | 100 |
| subs | array | 否 | 子菜单数组 | [['name' => '...', 'node' => '...']] |
菜单结构说明
插件中心专属菜单当前按两级结构处理:
- 一级菜单:包含
name/title和subs,或直接包含node/url;有url时直接使用该地址。 - 二级菜单:包含
name/title、node和可选的icon;地址只由node生成,url字段不会参与插件中心二级菜单跳转。
全局菜单写入使用 PhinxExtend::write2menu(),当前最多处理三层 subs,字段会写入 system_menu 的 pid、title、url、node、icon、params、target 和 sort。写入时 title 来自 name,没有 name 时读取 title;url 为空时优先回退到 node,再回退到 #;node 为空时优先回退到 url;target 默认 _self,sort 默认 0。system_menu.status 使用数据表默认值,不能通过 write2menu() 参数直接设置。
注意事项:
menu()方法必须是静态方法(public static function menu())- 使用
app(static::class)->appCode动态获取插件编码,避免硬编码 - 权限节点格式:
{插件编码}/{控制器}/{方法},如:plugin-account/master/index - 插件专属菜单不需要写入
system_menu;只有需要出现在后台全局菜单时才调用write2menu()
参考链接
- Service 类菜单定义:think-plugs-wechat Service.php
- 数据库迁移脚本:install_wechat20241011.php
