Files
MrleeAdmin/backend/core/plugin/traits/MenuTrait.php
T

511 lines
17 KiB
PHP
Raw Normal View History

2026-08-31 11:33:59 +08:00
<?php
namespace core\plugin\traits;
use core\uuid\Snowflake;
use support\Db;
/**
* 菜单操作 Trait
*/
trait MenuTrait
{
/**
* 预加载系统菜单的 code 映射(用于智能关联父级)
*
* @var array
*/
protected array $_admin_menu_codes = [];
protected array $_web_menu_codes = [];
/**
* 加载菜单文件
*/
protected function loadMenus(): void
{
$menuResourceDir = $this->getConfig('resource.menu', 'menu');
$menuDir = $this->pluginPath . '/resource/' . $menuResourceDir;
$this->output("📂 Loading menus from: {$menuDir}");
if (!is_dir($menuDir)) {
$this->output("📂 No menu directory found, skipping...");
return;
}
$files = scandir($menuDir);
$this->output("📂 Menu files found: " . implode(', ', array_diff($files, ['.', '..'])));
// 预加载系统菜单的 code 映射(用于智能关联父级)
$this->preloadSystemMenuCodes('admin');
$this->preloadSystemMenuCodes('web');
// 只加载 admin 和 web 两种类型的菜单
$this->loadMenuFile($menuDir . '/admin.php', 'admin');
$this->loadMenuFile($menuDir . '/web.php', 'web');
}
/**
* 预加载系统菜单的 code 映射
* 用于插件菜单关联到系统已有菜单
*
* @param string $type 菜单类型 (admin/web)
*/
protected function preloadSystemMenuCodes(string $type): void
{
$tableName = $type === 'admin' ? 'sys_menu' : 'web_menu';
$connection = $this->connection ?? null;
try {
// 获取系统中非插件来源的菜单 code -> id 映射
$menus = Db::connection($connection)
->table($tableName)
->where('source', 'system')
->whereNotNull('code')
->where('code', '!=', '')
->select(['id', 'code', 'pid', 'type'])
->get()
->toArray();
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
$this->$key = [];
foreach ($menus as $menu) {
$code = $menu->code;
$this->$key[$code] = [
'id' => $menu->id,
'pid' => $menu->pid,
'type' => $menu->type,
];
}
$this->output(" 📋 Preloaded {$type} system menus: " . count($this->$key) . " codes");
} catch (\Throwable $e) {
$this->output(" ⚠️ Failed to preload {$type} system menus: {$e->getMessage()}");
}
}
/**
* 根据 pid_code 查找父级 ID
*
* @param string|null $pidCode 父级菜单的 code
* @param string $type 菜单类型
* @return int 父级 ID,0 表示顶级菜单
*/
protected function findParentIdByCode(?string $pidCode, string $type): int
{
if (empty($pidCode)) {
return 0;
}
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
$codes = $this->$key ?? [];
if (!isset($codes[$pidCode])) {
return 0;
}
$parentInfo = $codes[$pidCode];
// 如果父级是目录类型(type=1),则返回其 ID
// 否则作为顶级菜单
if ($type === 'admin') {
// admin: 1=目录 2=菜单 3=按钮
return in_array($parentInfo['type'], [1]) ? $parentInfo['id'] : 0;
} else {
// web 菜单类型判断
return $parentInfo['id'];
}
}
/**
* 加载单个菜单文件
*/
protected function loadMenuFile(string $filePath, string $type): void
{
if (!file_exists($filePath)) {
$this->output(" ⚠️ {$type}: Menu file not found");
return;
}
$menu = include $filePath;
if (empty($menu)) {
$this->output(" ⚠️ {$type}: Empty menu file");
return;
}
if (!is_array($menu)) {
$this->output(" ❌ {$type}: Invalid menu format");
return;
}
$this->saveMenuToSystem($type, $menu);
$this->output(" ✅ {$type}: Menu loaded (" . count($menu) . " items)");
}
/**
* 保存菜单到系统
*/
protected function saveMenuToSystem(string $type, array $menu): void
{
if ($type === 'admin') {
$this->saveAdminMenu($menu);
} else {
$this->saveWebMenu($menu);
}
}
/**
* 保存后台菜单
*/
protected function saveAdminMenu(array $menu): void
{
$tableName = 'sys_menu';
$this->output(" 📝 Saving admin menu to {$tableName}...");
try {
$connection = $this->connection ?? null;
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
if (!$hasTable) {
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
return;
}
$this->insertAdminMenuItems($tableName, $menu, $this->pluginName);
$this->output(" ✅ Admin menu saved to {$tableName}");
} catch (\Throwable $e) {
$this->output(" ❌ Failed to save admin menu: {$e->getMessage()}");
}
}
/**
* 插入后台菜单项
*
* @param string $tableName 表名
* @param array $items 菜单项
* @param string $plugin 插件标识
* @param int $parentId 父级ID
* @param int $level 当前层级
*/
protected function insertAdminMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
{
$connection = $this->connection ?? null;
foreach ($items as $index => $item) {
$menuId = (int)Snowflake::generate();
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
// 子菜单通过 parentId(children 嵌套)确定父级
if ($parentId === 0 && isset($item['pid_code'])) {
$parentId = $this->findParentIdByCode($item['pid_code'], 'admin');
}
// 规范化菜单数据:自动生成缺失的 code
$normalizedItem = $this->normalizeMenuItem($item, $plugin, $level, $index);
$data = [
'id' => $menuId,
'pid' => $parentId,
'app' => $item['app'] ?? $plugin,
'source' => 'plugin',
'title' => $normalizedItem['title'],
'code' => $normalizedItem['code'],
'level' => $level,
'type' => $normalizedItem['type'],
'sort' => $normalizedItem['sort'],
'path' => $normalizedItem['path'],
'component' => $normalizedItem['component'],
'icon' => $normalizedItem['icon'],
'is_show' => $normalizedItem['is_show'],
'is_link' => $normalizedItem['is_link'],
'is_cache' => $normalizedItem['is_cache'],
'is_sync' => $normalizedItem['is_sync'],
'created_at' => time(),
'updated_at' => time(),
];
Db::connection($connection)->table($tableName)->insert($data);
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
if ($normalizedItem['code']) {
$this->_admin_menu_codes[$normalizedItem['code']] = [
'id' => $menuId,
'pid' => $parentId,
'type' => $data['type'],
];
}
// 递归处理子菜单,层级 +1
if (!empty($item['children'])) {
$this->insertAdminMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
}
}
}
/**
* 规范化菜单项数据
* 确保必要字段存在,自动生成缺失的 code
*
* @param array $item 菜单项
* @param string $plugin 插件标识
* @param int $level 当前层级
* @param int $index 当前索引
* @return array 规范化后的菜单项
*/
protected function normalizeMenuItem(array $item, string $plugin, int $level, int $index): array
{
// 获取菜单名称
$title = $item['name'] ?? $item['title'] ?? '';
// 获取路径
$path = $item['path'] ?? '';
// 自动生成 code:如果没有 code 字段,根据插件名和路径生成
$code = $item['code'] ?? '';
if (empty($code)) {
// 从 path 生成 code: /test-plugin/index -> test-plugin:index
if (!empty($path)) {
$code = trim($path, '/');
$code = str_replace('/', ':', $code);
}
// 如果路径也为空,使用插件名+层级+索引生成唯一 code
if (empty($code)) {
$code = $plugin . ':level' . $level . '_' . $index;
}
}
return [
'title' => $title,
'code' => $code,
'type' => $item['type'] ?? ($level === 1 ? 1 : 2), // 1=目录 2=菜单
'sort' => $item['sort'] ?? 0,
'path' => $path,
'component' => $item['component'] ?? ($level === 1 ? '/layout' : ''),
'icon' => $item['icon'] ?? '',
'is_show' => $item['is_show'] ?? 1,
'is_link' => $item['is_link'] ?? 0,
'is_cache' => $item['is_cache'] ?? 0,
'is_sync' => $item['is_sync'] ?? 0,
];
}
/**
* 保存前台菜单
*/
protected function saveWebMenu(array $menu): void
{
$tableName = 'web_menu';
$this->output(" 📝 Saving web menu to {$tableName}...");
try {
$connection = $this->connection ?? null;
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
if (!$hasTable) {
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
return;
}
$this->insertWebMenuItems($tableName, $menu, $this->pluginName);
$this->output(" ✅ Web menu saved to {$tableName}");
} catch (\Throwable $e) {
$this->output(" ❌ Failed to save web menu: {$e->getMessage()}");
}
}
/**
* 插入前台菜单项
*
* @param string $tableName 表名
* @param array $items 菜单项
* @param string $plugin 插件标识
* @param int $parentId 父级ID
* @param int $level 当前层级
*/
protected function insertWebMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
{
$connection = $this->connection ?? null;
foreach ($items as $index => $item) {
$menuId = (int)Snowflake::generate();
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
// 子菜单通过 parentId(children 嵌套)确定父级
if ($parentId === 0 && isset($item['pid_code'])) {
$parentId = $this->findParentIdByCode($item['pid_code'], 'web');
}
// 规范化菜单数据:自动生成缺失的 code
$normalizedItem = $this->normalizeWebMenuItem($item, $plugin, $level, $index);
// target: 1=当前窗口 2=新窗口
$targetValue = 1; // 默认当前窗口
if (isset($item['target'])) {
$targetValue = $item['target'] === '_self' || $item['target'] === 1 || $item['target'] === '1' ? 1 : 2;
}
$data = [
'id' => $menuId,
'app' => $item['app'] ?? $plugin,
'category' => $item['category'] ?? '1',
'source' => 'plugin',
'code' => $normalizedItem['code'],
'name' => $normalizedItem['name'],
'url' => $normalizedItem['url'],
'pid' => $parentId,
'level' => $level,
'type' => $normalizedItem['type'],
'sort' => $normalizedItem['sort'],
'target' => $targetValue,
'icon' => $normalizedItem['icon'],
'is_show' => $normalizedItem['is_show'],
'enabled' => $normalizedItem['enabled'],
'created_at' => time(),
'updated_at' => time(),
];
Db::connection($connection)->table($tableName)->insert($data);
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
if ($normalizedItem['code']) {
$this->_web_menu_codes[$normalizedItem['code']] = [
'id' => $menuId,
'pid' => $parentId,
'type' => $data['type'],
];
}
// 递归处理子菜单,层级 +1
if (!empty($item['children'])) {
$this->insertWebMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
}
}
}
/**
* 规范化前台菜单项数据
* 确保必要字段存在,自动生成缺失的 code
*
* @param array $item 菜单项
* @param string $plugin 插件标识
* @param int $level 当前层级
* @param int $index 当前索引
* @return array 规范化后的菜单项
*/
protected function normalizeWebMenuItem(array $item, string $plugin, int $level, int $index): array
{
// 获取菜单名称
$name = $item['name'] ?? $item['title'] ?? '';
// 获取 URL
$url = $item['path'] ?? $item['url'] ?? '';
// 自动生成 code:如果没有 code 字段,根据插件名和 URL 生成
$code = $item['code'] ?? '';
if (empty($code)) {
// 从 url 生成 code
if (!empty($url)) {
$code = trim($url, '/');
$code = str_replace('/', ':', $code);
}
// 如果 URL 也为空,使用插件名+层级+索引生成唯一 code
if (empty($code)) {
$code = $plugin . ':level' . $level . '_' . $index;
}
}
return [
'name' => $name,
'code' => $code,
'url' => $url,
'type' => $item['type'] ?? 1,
'sort' => $item['sort'] ?? 0,
'icon' => $item['icon'] ?? '',
'is_show' => $item['is_show'] ?? 1,
'enabled' => $item['enabled'] ?? 1,
];
}
/**
* 清除插件菜单
*/
protected function clearPluginMenus(string $plugin): void
{
$this->clearMenusByTable('sys_menu', $plugin);
$this->clearMenusByTable('web_menu', $plugin);
}
/**
* 根据表清除菜单
*/
protected function clearMenusByTable(string $tableName, string $plugin): void
{
try {
$connection = $this->connection ?? null;
if (!Db::connection($connection)->getSchemaBuilder()->hasTable($tableName)) {
return;
}
$query = Db::connection($connection)
->table($tableName)
->where('source', 'plugin')
->where(function ($query) use ($tableName, $plugin) {
$query->where('app', $plugin)
->orWhere('code', 'like', $plugin . ':%');
if ($tableName === 'sys_menu') {
$query->orWhere('path', 'like', '/' . $plugin . '%')
->orWhere('component', 'like', '/' . $plugin . '/%');
} else {
$query->orWhere('url', 'like', '/' . $plugin . '%');
}
});
$deleted = $query->delete();
if ($deleted > 0) {
$this->output(" 🗑️ Cleared {$deleted} menus from {$tableName}");
}
} catch (\Throwable $e) {
$this->output(" ⚠️ Failed to clear {$tableName}: {$e->getMessage()}");
}
}
/**
* 导入插件菜单(快捷方法)
*/
public function importPluginMenus(): void
{
$this->importMenus();
}
/**
* 卸载插件菜单(快捷方法)
*/
public function uninstallPluginMenus(): void
{
$this->clearMenus();
}
/**
* 导入菜单(子类可重写)
*/
protected function importMenus(): void
{
$this->loadMenus();
}
/**
* 清除菜单(子类可重写)
*/
protected function clearMenus(): void
{
$this->clearPluginMenus($this->pluginName);
}
}