🍔 插件菜单

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 方法
  • 配置返回: 返回包含菜单项信息的数组
  • 结构描述: 描述插件的菜单结构、权限要求等
  • 读取时机: 插件中心展示插件专属空间时读取;写入全局菜单需要迁移脚本主动调用

菜单配置选项

常用字段包括 nametitlenodeurliconparamstargetsortsubs。插件中心专属菜单主要使用 name/title、一级 urlnodeiconsubs;全局菜单写入会把这些字段转换为 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 中已经存在 urlnodewechat/fans/index 的记录时,不再重复写入。

菜单配置参数说明

参数类型必填说明示例
namestring菜单名称,写入全局菜单时会转为 title'微信管理'
titlestring菜单标题,存在时优先作为显示标题'微信管理'
nodestring权限节点;有 node 的插件菜单会生成插件内跳转地址'wechat/config/index'"{$code}/config/index"
urlstring一级插件菜单可直接使用;二级插件菜单会忽略该字段;全局菜单未配置时会回退到 node#'admin/config/index'
iconstring菜单图标(LayUI 图标类)'layui-icon layui-icon-set'
paramsstring全局菜单链接参数'type=base'
targetstring全局菜单打开方式,默认 _self'_blank'
sortint全局菜单排序权重100
subsarray子菜单数组[['name' => '...', 'node' => '...']]

菜单结构说明

插件中心专属菜单当前按两级结构处理:

  • 一级菜单:包含 name/titlesubs,或直接包含 node/url;有 url 时直接使用该地址。
  • 二级菜单:包含 name/titlenode 和可选的 icon;地址只由 node 生成,url 字段不会参与插件中心二级菜单跳转。

全局菜单写入使用 PhinxExtend::write2menu(),当前最多处理三层 subs,字段会写入 system_menupidtitleurlnodeiconparamstargetsort。写入时 title 来自 name,没有 name 时读取 titleurl 为空时优先回退到 node,再回退到 #node 为空时优先回退到 urltarget 默认 _selfsort 默认 0system_menu.status 使用数据表默认值,不能通过 write2menu() 参数直接设置。

注意事项:

  • menu() 方法必须是静态方法(public static function menu()
  • 使用 app(static::class)->appCode 动态获取插件编码,避免硬编码
  • 权限节点格式:{插件编码}/{控制器}/{方法},如:plugin-account/master/index
  • 插件专属菜单不需要写入 system_menu;只有需要出现在后台全局菜单时才调用 write2menu()

参考链接

最近更新:
Contributors: 邹景立, Anyon