Jelajahi Sumber

自定义菜单

bianjunhui 1 bulan lalu
induk
melakukan
6d31723075
25 mengubah file dengan 1545 tambahan dan 0 penghapusan
  1. 3 0
      .gitignore
  2. 1 0
      bootstrap/providers.php
  3. 1 0
      composer.json
  4. 30 0
      packages/Longyi/DynamicMenu/composer.json
  5. 19 0
      packages/Longyi/DynamicMenu/src/Config/menu.php
  6. 38 0
      packages/Longyi/DynamicMenu/src/Database/Migrations/2026_01_01_000001_create_dynamic_menu_items_table.php
  7. 39 0
      packages/Longyi/DynamicMenu/src/Database/Seeders/MenuItemSeeder.php
  8. 48 0
      packages/Longyi/DynamicMenu/src/DynamicMenu.php
  9. 63 0
      packages/Longyi/DynamicMenu/src/DynamicMenuServiceProvider.php
  10. 155 0
      packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php
  11. 40 0
      packages/Longyi/DynamicMenu/src/Http/Middleware/CheckMenuPermission.php
  12. 59 0
      packages/Longyi/DynamicMenu/src/Models/MenuItem.php
  13. 17 0
      packages/Longyi/DynamicMenu/src/Models/MenuItemRole.php
  14. 108 0
      packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php
  15. 126 0
      packages/Longyi/DynamicMenu/src/Providers/MenuProvider.php
  16. 44 0
      packages/Longyi/DynamicMenu/src/Repositories/MenuItemRepository.php
  17. 43 0
      packages/Longyi/DynamicMenu/src/Resources/lang/en/app.php
  18. 37 0
      packages/Longyi/DynamicMenu/src/Resources/lang/zh_CN/app.php
  19. 224 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php
  20. 179 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php
  21. 99 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/index.blade.php
  22. 44 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/partials/tree.blade.php
  23. 22 0
      packages/Longyi/DynamicMenu/src/Resources/views/admin/permission/index.blade.php
  24. 57 0
      packages/Longyi/DynamicMenu/src/Routes/admin-routes.php
  25. 49 0
      packages/Longyi/DynamicMenu/src/Services/MenuService.php

+ 3 - 0
.gitignore

@@ -8,6 +8,9 @@
 *.hot
 /stubs
 /data
+/tmp
+/config/flexible_variant.php
+/config/octane.php
 /docker-compose-collection
 Homestead.json
 Homestead.yaml

+ 1 - 0
bootstrap/providers.php

@@ -11,6 +11,7 @@ return [
      */
     Webkul\Admin\Providers\AdminServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
+    Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
     Webkul\Attribute\Providers\AttributeServiceProvider::class,
     Webkul\BookingProduct\Providers\BookingProductServiceProvider::class,
     Webkul\CMS\Providers\CMSServiceProvider::class,

+ 1 - 0
composer.json

@@ -64,6 +64,7 @@
             "Database\\Factories\\": "database/factories/",
             "Database\\Seeders\\": "database/seeders/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
+            "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
             "Webkul\\Admin\\": "packages/Webkul/Admin/src",
             "Webkul\\Attribute\\": "packages/Webkul/Attribute/src",
             "Webkul\\BookingProduct\\": "packages/Webkul/BookingProduct/src",

+ 30 - 0
packages/Longyi/DynamicMenu/composer.json

@@ -0,0 +1,30 @@
+{
+    "name": "longyi/dynamicMenu",
+    "description": "Longyi Core Package for Bagisto - Flexible Variant Product Type with Option-Based Variants",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Longyi Team",
+            "email": "dev@longyi.com"
+        }
+    ],
+    "require": {
+        "php": "^8.1|^8.2",
+        "illuminate/support": "^10.0|^11.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Longyi\\DynamicMenu\\": "src/"
+        }
+    },
+    "extra": {
+        "laravel": {
+            "providers": [
+                "Longyi\\DynamicMenu\\Providers\\DynamicMenuServiceProvider"
+            ]
+        }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true
+}

+ 19 - 0
packages/Longyi/DynamicMenu/src/Config/menu.php

@@ -0,0 +1,19 @@
+<?php
+
+return [
+    [
+        'key'   => 'settings.dynamicmenu',
+        'name'  => '测试菜单',
+        'route' => 'admin.dynamicmenu.index',
+        'sort' => 12,
+        'icon' => 'icon-menu',
+    ],
+    [
+        'key' => 'settings.dynamicmenu.menu',
+        'name' => '菜单项管理',
+        'route' => 'admin.dynamicmenu.index',
+        'sort' => 1,
+        'icon' => 'icon-list',
+        'parent' => 'settings.dynamicmenu'
+    ],
+];

+ 38 - 0
packages/Longyi/DynamicMenu/src/Database/Migrations/2026_01_01_000001_create_dynamic_menu_items_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('dynamic_menu_items', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->string('key')->unique();
+            $table->string('route');
+            $table->string('icon')->nullable();
+            $table->unsignedBigInteger('parent_id')->nullable();
+            $table->integer('sort_order')->default(0);
+            $table->boolean('status')->default(true);
+            $table->unsignedBigInteger('created_by')->nullable();
+            $table->timestamps();
+            // Indexes
+            $table->index('sort_order', 'idx_order');
+           
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('product_options');
+    }
+};

+ 39 - 0
packages/Longyi/DynamicMenu/src/Database/Seeders/MenuItemSeeder.php

@@ -0,0 +1,39 @@
+<?php
+namespace Longyi\DynamicMenu\Database\Seeders;  // 修改这里
+use Illuminate\Database\Seeder;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class MenuItemSeeder extends Seeder
+{
+    public function run()
+    {
+        $dynamicMenu = MenuItem::create([
+            'name' => 'Dynamic Menu',
+            'key' => 'dynamicmenu',
+            'route' => 'admin.dynamicmenu.index',
+            'icon' => 'icon-menu',
+            'sort_order' => 13,
+            'status' => true
+        ]);
+
+        MenuItem::create([
+            'name' => 'Menu Items',
+            'key' => 'dynamicmenu.menu',
+            'route' => 'admin.dynamicmenu.index',
+            'icon' => 'icon-list',
+            'parent_id' => $dynamicMenu->id,
+            'sort_order' => 1,
+            'status' => true
+        ]);
+
+        MenuItem::create([
+            'name' => 'Menu Permissions',
+            'key' => 'dynamicmenu.permission',
+            'route' => 'admin.dynamicmenu.permission',
+            'icon' => 'icon-lock',
+            'parent_id' => $dynamicMenu->id,
+            'sort_order' => 2,
+            'status' => true
+        ]);
+    }
+}

+ 48 - 0
packages/Longyi/DynamicMenu/src/DynamicMenu.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace Longyi\DynamicMenu;
+
+use Longyi\DynamicMenu\Models\MenuItem as MenuItemModel;
+use Illuminate\Support\Facades\Route;
+
+class DynamicMenu
+{
+    public function registerMenuItems()
+    {
+        try {
+            $items = MenuItemModel::where('status', 1)
+                ->orderBy('parent_id')
+                ->orderBy('sort_order')
+                ->get();
+                
+            foreach ($items as $item) {
+                $this->addToBagistoMenu($item);
+            }
+        } catch (\Exception $e) {
+            report($e);
+        }
+    }
+    
+    private function addToBagistoMenu($item)
+    {
+        $parent = $item->parent ? $item->parent->key : null;
+        
+        // 方法1:使用路由名称(推荐)
+        // Bagisto 会在内部使用 route() 辅助函数生成URL
+        menu()->addItem(
+            $item->key,
+            $item->name,
+            $parent,
+            $item->route,  // 这里应该是路由名称,如 'admin.dynamicmenu.index'
+            $item->icon,
+            (int) $item->sort_order
+        );
+        
+        // 调试日志
+        \Log::info('注册菜单项', [
+            'key' => $item->key,
+            'route_name' => $item->route,
+            'generated_url' => route($item->route, [], false)
+        ]);
+    }
+}

+ 63 - 0
packages/Longyi/DynamicMenu/src/DynamicMenuServiceProvider.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Longyi\DynamicMenu;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Routing\Router;
+use Longyi\DynamicMenu\Http\Middleware\CheckMenuPermission;
+
+class DynamicMenuServiceProvider extends ServiceProvider
+{
+    public function boot(Router $router)
+    {
+        // Load routes
+        $this->loadRoutesFrom(__DIR__ . '/Routes/admin-routes.php');
+
+        // Load views
+        $this->loadViewsFrom(__DIR__ . '/Resources/views', 'dynamicmenu');
+
+        // Load translations
+        $this->loadTranslationsFrom(__DIR__ . '/Resources/lang', 'dynamicmenu');
+
+        // Load migrations
+        $this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
+
+        // Register middleware
+        $router->aliasMiddleware('menu.permission', CheckMenuPermission::class);
+
+        // Publish assets
+        $this->publishes([
+            __DIR__ . '/Resources/views' => resource_path('views/vendor/longyi/dynamicmenu'),
+        ], 'dynamicmenu-views');
+
+        $this->publishes([
+            __DIR__ . '/Database/Seeders' => database_path('seeders'),
+        ], 'dynamicmenu-seeds');
+
+        $this->publishes([
+            __DIR__ . '/Config' => config_path('longyi'),
+        ], 'dynamicmenu-config');
+
+        // Register menu items
+        $this->app->booted(function () {
+            $this->registerMenuItems();
+        });
+    }
+
+    public function register()
+    {
+        $this->mergeConfigFrom(__DIR__ . '/Config/menu.php', 'dynamicmenu');
+
+        $this->app->singleton('dynamicmenu', function () {
+            return new DynamicMenu();
+        });
+    }
+
+    private function registerMenuItems()
+    {
+        if (app('request')->is('admin*')) {
+            $dynamicMenu = app('dynamicmenu');
+            $dynamicMenu->registerMenuItems();
+        }
+    }
+}

+ 155 - 0
packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace Longyi\DynamicMenu\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller;
+use Longyi\DynamicMenu\Models\MenuItem;
+use Webkul\User\Repositories\RoleRepository;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\View;
+
+class MenuController extends Controller
+{
+    protected $roleRepository;
+    
+    public function __construct(RoleRepository $roleRepository)
+    {
+        $this->roleRepository = $roleRepository;
+        $this->_config = request('_config');
+    }
+    
+    public function index()
+    {
+        
+        
+    try {
+        
+        $allItems = MenuItem::with('children')
+            ->orderBy('parent_id')
+            ->orderBy('sort_order')
+            ->get();
+        $menuItems = $allItems->whereNull('parent_id');
+        return view('dynamicmenu::admin.menu.index', compact('menuItems', 'allItems'));
+        
+    } catch (\Exception $e) {
+        \Log::error('MenuController 错误: ' . $e->getMessage());
+        \Log::error($e->getTraceAsString());
+        throw $e;
+    }
+        //\Log::info('===== MenuController::index 结束 =====');
+    }
+    
+    public function create()
+    {
+        $menuItems = MenuItem::orderBy('parent_id')->orderBy('sort_order')->get();
+        return view($this->_config['view'], compact('menuItems'));
+    }
+    
+    public function store(Request $request)
+    {
+        
+        $request->validate([
+            'name' => 'required|string|max:255',
+            'key' => 'required|string|unique:dynamic_menu_items,key',
+            'route' => 'required|string',
+            'icon' => 'nullable|string',
+            'parent_id' => 'nullable|exists:dynamic_menu_items,id',
+            'sort_order' => 'integer',
+            'status' => 'boolean'
+        ]);
+        
+        $data = $request->all();
+        $data['parent_id'] = $data['parent_id'] === '' ? null : $data['parent_id'];
+        $data['created_by'] = auth()->guard('admin')->user()->id;
+        
+        MenuItem::create($data);
+        
+        session()->flash('success', '菜单项创建成功');
+        
+        return redirect()->route($this->_config['redirect']);
+    }
+    
+    public function edit($id)
+    {
+        $menuItem = MenuItem::with('parent')->findOrFail($id);
+        $menuItems = MenuItem::orderBy('parent_id')->orderBy('sort_order')->get();
+        
+        return view($this->_config['view'], compact('menuItem', 'menuItems'));
+    }
+    
+    public function update(Request $request, $id)
+{
+    $request->validate([
+        'name' => 'required|string|max:255',
+        'key' => 'required|string|unique:dynamic_menu_items,key,' . $id,  // 排除当前记录
+        'route' => 'required|string',
+        'icon' => 'nullable|string',
+        'parent_id' => 'nullable|exists:dynamic_menu_items,id',
+        'sort_order' => 'integer',
+        'status' => 'boolean'
+    ]);
+    
+    $menuItem = MenuItem::findOrFail($id);
+    $menuItem->update($request->all());
+    
+    session()->flash('success', '菜单项更新成功');
+    
+    return redirect()->route($this->_config['redirect']);
+}
+    
+    public function destroy($id)
+    {
+        $menuItem = MenuItem::with('children')->findOrFail($id);
+        
+        if ($menuItem->children->count() > 0) {
+            session()->flash('error', '请先删除子菜单项');
+            return redirect()->back();
+        }
+        
+        $menuItem->delete();
+        
+        session()->flash('success', '菜单项删除成功');
+        
+        return redirect()->route($this->_config['redirect']);
+    }
+    
+    public function permission()
+    {
+        $roles = $this->roleRepository->all();
+        $menuItems = MenuItem::with('children')->orderBy('parent_id')->orderBy('sort_order')->get();
+        
+        return view($this->_config['view'], compact('roles', 'menuItems'));
+    }
+    
+    public function updatePermission(Request $request)
+    {
+        $roleId = $request->input('role_id');
+        $menuItemIds = $request->input('menu_items', []);
+        
+        DB::table('menu_item_role')->where('role_id', $roleId)->delete();
+        
+        foreach ($menuItemIds as $menuItemId) {
+            DB::table('menu_item_role')->insert([
+                'menu_item_id' => $menuItemId,
+                'role_id' => $roleId
+            ]);
+        }
+        
+        session()->flash('success', '权限更新成功');
+        
+        return redirect()->back();
+    }
+    
+    public function getRolePermissions(Request $request)
+    {
+        $roleId = $request->input('role_id');
+        
+        $permissions = DB::table('menu_item_role')
+            ->where('role_id', $roleId)
+            ->pluck('menu_item_id')
+            ->toArray();
+            
+        return response()->json($permissions);
+    }
+}

+ 40 - 0
packages/Longyi/DynamicMenu/src/Http/Middleware/CheckMenuPermission.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Longyi\DynamicMenu\Http\Middleware;
+
+use Closure;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class CheckMenuPermission
+{
+    public function handle($request, Closure $next)
+    {
+        if (! auth()->guard('admin')->check()) {
+            return $next($request);
+        }
+
+        $admin = auth()->guard('admin')->user();
+        $routeName = $request->route()->getName();
+
+        // Skip check for super admin
+        if ($admin->role->permission_type === 'all') {
+            return $next($request);
+        }
+
+        // Find menu item by route
+        $menuItem = MenuItem::where('route', $routeName)->first();
+
+        if ($menuItem) {
+            $hasPermission = $admin->role->menuItems()
+                ->where('menu_item_id', $menuItem->id)
+                ->exists();
+
+            if (! $hasPermission) {
+                session()->flash('warning', trans('dynamicmenu::app.permission.denied'));
+                return redirect()->route('admin.dashboard.index');
+            }
+        }
+
+        return $next($request);
+    }
+}

+ 59 - 0
packages/Longyi/DynamicMenu/src/Models/MenuItem.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Longyi\DynamicMenu\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Cache;
+class MenuItem extends Model
+{
+    protected $table = 'dynamic_menu_items';
+    
+    protected $fillable = [
+        'name',
+        'key',
+        'route',
+        'icon',
+        'parent_id',
+        'sort_order',
+        'status',
+        'created_by'
+    ];
+    
+    protected $casts = [
+        'status' => 'boolean',
+        'sort_order' => 'integer'
+    ];
+    protected static function boot()
+    {
+        parent::boot();
+        
+        // 当菜单项被创建、更新或删除时,清除缓存
+        static::saved(function () {
+            Cache::forget('dynamic_menu_config');
+            Cache::forget('dynamic_menu_items');
+            \Log::info('菜单缓存已清除 (saved)');
+        });
+        
+        static::deleted(function () {
+            Cache::forget('dynamic_menu_config');
+            Cache::forget('dynamic_menu_items');
+            \Log::info('菜单缓存已清除 (deleted)');
+        });
+    }
+
+    /**
+     * 获取父菜单
+     */
+    public function parent()
+    {
+        return $this->belongsTo(MenuItem::class, 'parent_id');
+    }
+    
+    /**
+     * 获取子菜单
+     */
+    public function children()
+    {
+        return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('sort_order');
+    }
+}

+ 17 - 0
packages/Longyi/DynamicMenu/src/Models/MenuItemRole.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Longyi\DynamicMenu\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class MenuItemRole extends Model
+{
+    protected $table = 'menu_item_role';
+
+    protected $fillable = [
+        'menu_item_id',
+        'role_id'
+    ];
+
+    public $timestamps = false;
+}

+ 108 - 0
packages/Longyi/DynamicMenu/src/Providers/DynamicMenuServiceProvider.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace Longyi\DynamicMenu\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Cache;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class DynamicMenuServiceProvider extends ServiceProvider
+{
+    public function boot()
+    {
+        // 加载迁移
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+        
+        // 加载路由
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+        
+        // 加载视图
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'dynamicmenu');
+         // 加载语言文件
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'dynamicmenu');
+        // 发布资源
+        $this->publishes([
+            __DIR__ . '/../../publishable/assets' => public_path('vendor/dynamicmenu'),
+        ], 'public');
+    }
+
+    public function register()
+    {
+        // 延迟合并配置,等到服务提供者注册完成后
+        $this->app->booted(function () {
+            $this->mergeDynamicMenuConfig();
+        });
+    }
+
+    /**
+     * 合并动态菜单配置
+     */
+    protected function mergeDynamicMenuConfig()
+    {
+        try {
+            // 从数据库获取菜单配置
+            $menuConfig = $this->getMenuConfigFromDatabase();
+            // 获取现有配置
+            $existingConfig = $this->app['config']->get('menu.admin', []);
+            // 合并配置
+            $mergedConfig = array_merge($existingConfig, $menuConfig);
+            // 设置配置
+            $this->app['config']->set('menu.admin', $mergedConfig);
+            
+        } catch (\Exception $e) {
+            \Log::error('合并动态菜单配置失败: ' . $e->getMessage());
+        }
+    }
+
+    /**
+     * 从数据库获取菜单配置
+     */
+    protected function getMenuConfigFromDatabase()
+    {
+        $menuItems = MenuItem::with('children')
+                    ->where('status', 1)
+                    ->orderBy('sort_order')
+                    ->get();
+                
+        return $this->buildMenuConfig($menuItems);
+        // 使用缓存
+        /*
+        return Cache::remember('dynamic_menu_config', 3600, function () {
+            try {
+                $menuItems = MenuItem::with('children')
+                    ->whereNull('parent_id')
+                    ->where('status', 1)
+                    ->orderBy('sort_order')
+                    ->get();
+                
+                return $this->buildMenuConfig($menuItems);
+                
+            } catch (\Exception $e) {
+                \Log::warning('无法从数据库获取菜单: ' . $e->getMessage());
+                return [];
+            }
+        });
+        */
+    }
+
+    /**
+     * 构建菜单配置数组
+     */
+    protected function buildMenuConfig($menuItems)
+    {
+        $config = [];
+        
+        foreach ($menuItems as $item) {
+            $menuItem = [
+                'key'   => $item->key,
+                'name'  => $item->name,
+                'route' => $item->route ?: 'admin.dynamicmenu.index',
+                'sort'  => (int) $item->sort_order,
+                'icon'  => $item->icon ?: 'icon-menu',
+            ];
+            $config[] = $menuItem;
+        }
+        
+        return $config;
+    }
+}

+ 126 - 0
packages/Longyi/DynamicMenu/src/Providers/MenuProvider.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace Longyi\DynamicMenu\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Longyi\DynamicMenu\Services\MenuService;
+
+class MenuProvider extends ServiceProvider
+{
+    public function boot(MenuService $menuService)
+    {
+        // 在 Bagisto 菜单构建时添加动态菜单
+        $this->app['events']->listen('bagisto.admin.menu.build', function ($menu) use ($menuService) {
+            $this->registerDynamicMenus($menu, $menuService);
+        });
+    }
+    
+    /**
+     * 注册动态菜单到 Bagisto
+     */
+    protected function registerDynamicMenus($menu, MenuService $menuService)
+    {
+        // 获取所有顶级菜单
+        $topLevelMenus = $menuService->buildMenuTree();
+        
+        foreach ($topLevelMenus as $menuItem) {
+            // 检查菜单是否应该显示在 Settings 下
+            if (strpos($menuItem['key'], 'settings.') === 0) {
+                // 如果菜单key以settings.开头,添加到Settings菜单下
+                $this->addToSettingsMenu($menu, $menuItem);
+            } else {
+                // 否则作为顶级菜单添加
+                $this->addMenuItem($menu, null, $menuItem);
+            }
+        }
+    }
+    
+    /**
+     * 添加菜单项到指定父菜单
+     */
+    protected function addMenuItem($menu, $parentKey = null, $item)
+    {
+        if (isset($item['children']) && count($item['children']) > 0) {
+            // 有子菜单
+            $parent = $menu->add(
+                $item['key'],
+                $item['name'],
+                $item['route'],
+                $item['sort'],
+                $item['icon']
+            );
+            
+            // 添加子菜单
+            foreach ($item['children'] as $child) {
+                $this->addMenuItem($parent, $item['key'], $child);
+            }
+        } else {
+            // 无子菜单
+            if ($parentKey) {
+                // 添加到指定父菜单
+                $menu->add(
+                    $item['key'],
+                    $item['name'],
+                    $item['route'],
+                    $item['sort'],
+                    $item['icon'],
+                    $parentKey
+                );
+            } else {
+                // 作为顶级菜单
+                $menu->add(
+                    $item['key'],
+                    $item['name'],
+                    $item['route'],
+                    $item['sort'],
+                    $item['icon']
+                );
+            }
+        }
+    }
+    
+    /**
+     * 添加到 Settings 菜单
+     */
+    protected function addToSettingsMenu($menu, $item)
+    {
+        // 确保 Settings 菜单存在
+        $settings = $menu->get('settings');
+        
+        if (!$settings) {
+            // 如果 Settings 菜单不存在,先创建
+            $settings = $menu->add('settings', 'Settings', 'admin.settings.index', 1, 'icon-settings');
+        }
+        
+        if (isset($item['children']) && count($item['children']) > 0) {
+            // 有子菜单
+            $parent = $settings->addChild(
+                $item['key'],
+                $item['name'],
+                $item['route'],
+                $item['sort'],
+                $item['icon']
+            );
+            
+            // 添加子菜单
+            foreach ($item['children'] as $child) {
+                $parent->addChild(
+                    $child['key'],
+                    $child['name'],
+                    $child['route'],
+                    $child['sort'],
+                    $child['icon']
+                );
+            }
+        } else {
+            // 无子菜单,直接添加到 Settings
+            $settings->addChild(
+                $item['key'],
+                $item['name'],
+                $item['route'],
+                $item['sort'],
+                $item['icon']
+            );
+        }
+    }
+}

+ 44 - 0
packages/Longyi/DynamicMenu/src/Repositories/MenuItemRepository.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Longyi\DynamicMenu\Repositories;
+
+use Webkul\Core\Eloquent\Repository;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class MenuItemRepository extends Repository
+{
+    public function model()
+    {
+        return MenuItem::class;
+    }
+
+    public function getMenuTree()
+    {
+        $items = $this->all();
+        return $this->buildTree($items);
+    }
+
+    private function buildTree($items, $parentId = null)
+    {
+        $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;
+    }
+
+    public function getMenuItemsForRole($roleId)
+    {
+        return $this->model->whereHas('roles', function($query) use ($roleId) {
+            $query->where('role_id', $roleId);
+        })->where('status', 1)->get();
+    }
+}

+ 43 - 0
packages/Longyi/DynamicMenu/src/Resources/lang/en/app.php

@@ -0,0 +1,43 @@
+<?php
+
+return [
+    'admin' => [
+        'menu' => [
+            'title' => 'Dynamic Menu',
+            'items' => 'Menu Items',
+            'permissions' => 'Menu Permissions',
+            'add-title' => 'Add Menu Item',
+            'edit-title' => 'Edit Menu Item',
+            'add-btn' => 'Add Menu Item',
+            'back-btn' => 'Back',
+            'save-btn' => 'Save',
+            'cancel-btn' => 'Cancel',
+            'basic-info' => 'Basic Information',
+            'name' => 'Name',
+            'name-placeholder' => 'e.g. Dashboard',
+            'key' => 'Key',
+            'key-placeholder' => 'e.g. admin.dashboard',
+            'key-info' => 'Used for permission validation and menu activation',
+            'route' => 'URL/Route',
+            'route-placeholder' => 'e.g. admin.dashboard',
+            'route-info' => 'Can be relative path, full URL or route name',
+            'icon' => 'Icon',
+            'icon-placeholder' => 'e.g. icon-menu',
+            'icon-info' => 'Webkul admin panel icon class',
+            'parent' => 'Parent Menu',
+            'parent-none' => 'As Top Level Menu',
+            'has-children' => 'Has Children',
+            'sort-order' => 'Sort Order',
+            'sort-order-info' => 'Lower number appears first',
+            'status' => 'Status',
+            'status-active' => 'Active',
+            'status-inactive' => 'Inactive',
+            'edit' => 'Edit',
+            'delete' => 'Delete',
+            'delete-confirm' => 'Are you sure?',
+            'delete-disabled' => 'Please delete child menus first',
+            'disabled' => 'Disabled',
+            'no-identifier' => 'No identifier',
+        ],
+    ],
+];

+ 37 - 0
packages/Longyi/DynamicMenu/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,37 @@
+<?php
+
+return [
+    'admin' => [
+        'menu' => [
+            'title' => '动态菜单',
+            'items' => '菜单项',
+            'permissions' => '菜单权限',
+            'add-title' => '添加菜单项',
+            'edit-title' => '编辑菜单项',
+            'add-btn' => '添加菜单项',
+            'back-btn' => '返回列表',
+            'save-btn' => '保存菜单项',
+            'cancel-btn' => '取消',
+            'basic-info' => '基本信息',
+            'name' => '名称',
+            'name-placeholder' => '例如:仪表盘',
+            'key' => 'Key',
+            'key-placeholder' => '例如:admin.dashboard',
+            'key-info' => '用于权限验证和菜单激活状态判断',
+            'route' => 'URL/路由',
+            'route-placeholder' => '例如:admin.dashboard',
+            'route-info' => '可以是相对路径、完整URL或路由名称',
+            'icon' => '图标',
+            'icon-placeholder' => '例如:icon-menu',
+            'icon-info' => 'Webkul admin面板的图标类名',
+            'parent' => '父级菜单',
+            'parent-none' => '作为顶级菜单',
+            'has-children' => '有子菜单',
+            'sort-order' => '排序',
+            'sort-order-info' => '数字越小越靠前',
+            'status' => '状态',
+            'status-active' => '启用',
+            'status-inactive' => '禁用',
+        ],
+    ],
+];

+ 224 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php

@@ -0,0 +1,224 @@
+<x-admin::layouts>
+    {{-- 页面标题 --}}
+    <x-slot:title>添加菜单项</x-slot:title>
+
+    {{-- 页面内容 --}}
+    <div class="content" style="padding: 20px;">
+        {{-- 页头部分 --}}
+        <div class="page-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #dee2e6;">
+            <div class="page-title">
+                <h1><i class="fas fa-plus-circle"></i> 添加菜单项</h1>
+            </div>
+            <div class="page-action">
+                <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 8px 16px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;">
+                    <i class="fas fa-arrow-left"></i> 返回列表
+                </a>
+            </div>
+        </div>
+
+        {{-- 显示错误信息 --}}
+        @if($errors->any())
+            <div class="alert alert-danger" style="background: #f8d7da; color: #721c24; padding: 12px; border-radius: 4px; margin-bottom: 20px; border: 1px solid #f5c6cb;">
+                <ul class="mb-0" style="margin: 0; padding-left: 20px;">
+                    @foreach($errors->all() as $error)
+                        <li>{{ $error }}</li>
+                    @endforeach
+                </ul>
+            </div>
+        @endif
+
+        {{-- 表单面板 --}}
+        <div class="panel" style="background: white; border: 1px solid #dee2e6; border-radius: 4px; margin-bottom: 20px;">
+            <div class="panel-header" style="padding: 12px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6;">
+                <h3 style="margin: 0; font-size: 18px;">基本信息</h3>
+            </div>
+
+            <div class="panel-body" style="padding: 20px;">
+                <form method="POST" action="{{ route('admin.dynamicmenu.store') }}">
+                    @csrf
+
+                    {{-- 名称 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            名称 <span style="color: red;">*</span>
+                        </label>
+                        <input type="text"
+                               name="name"
+                               class="control"
+                               value="{{ old('name') }}"
+                               required
+                               placeholder="例如:仪表盘"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                    </div>
+
+                    {{-- Key --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            Key <span style="color: red;">*</span>
+                        </label>
+                        <input type="text"
+                            name="key"
+                            class="control"
+                            value="{{ old('key') }}"
+                            required
+                            placeholder="例如:settings.dynamic-menu"
+                            style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            <strong style="color: #dc3545;">重要提示:</strong>
+                            如果想让菜单显示在 Settings 下,Key 必须以 <strong style="background: #f0f0f0; padding: 2px 5px; border-radius: 3px;">settings.</strong> 开头<br>
+                            例如:<code>settings.dynamic-menu</code>、<code>settings.my-menu</code>、<code>settings.custom</code>
+                        </small>
+                    </div>
+
+                    {{-- URL/路由 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">URL/路由</label>
+                        <input type="text"
+                               name="route"
+                               class="control"
+                               value="{{ old('route') }}"
+                               placeholder="例如:admin.dashboard"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            可以是相对路径、完整URL或路由名称
+                        </small>
+                    </div>
+
+                    {{-- 图标 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">图标</label>
+                        <input type="text"
+                               name="icon"
+                               class="control"
+                               value="{{ old('icon', 'fas fa-file') }}"
+                               placeholder="例如:fas fa-dashboard、fas fa-users等"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            FontAwesome图标类名,如:fas fa-home
+                        </small>
+                    </div>
+
+                    {{-- 父级菜单 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">父级菜单</label>
+                        <select name="parent_id" class="control" style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                            <option value="">作为顶级菜单</option>
+                            @foreach($menuItems as $menuItem)
+                                <option value="{{ $menuItem->id }}" {{ old('parent_id') == $menuItem->id ? 'selected' : '' }}>
+                                    {{ $menuItem->name }}
+                                    @if($menuItem->children && $menuItem->children->count() > 0)
+                                        (有子菜单)
+                                    @endif
+                                </option>
+                            @endforeach
+                        </select>
+                    </div>
+
+                    {{-- 排序 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">排序</label>
+                        <input type="number"
+                               name="sort_order"
+                               class="control"
+                               value="{{ old('sort_order', 0) }}"
+                               min="0"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            数字越小越靠前
+                        </small>
+                    </div>
+
+                    {{-- 状态 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">状态</label>
+                        <div>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="1" {{ old('status', 1) == 1 ? 'checked' : '' }} style="margin-right: 5px;">
+                                启用
+                            </label>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="0" {{ old('status') == 0 ? 'checked' : '' }} style="margin-right: 5px;">
+                                禁用
+                            </label>
+                        </div>
+                    </div>
+
+                    {{-- 提交按钮 --}}
+                    <div class="form-group" style="margin-top: 30px;">
+                        <button type="submit" class="btn btn-primary" style="padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 18px; cursor: pointer; margin-right: 10px;">
+                            <i class="fas fa-save"></i> 保存菜单项
+                        </button>
+                        <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; font-size: 18px;">
+                            <i class="fas fa-times"></i> 取消
+                        </a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    {{-- 引入必要的JavaScript --}}
+@push('scripts')
+<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+<script>
+   /* $(document).ready(function() {
+        // 自动生成key
+        $('input[name="name"]').on('blur', function() {
+            var name = $(this).val();
+            var keyInput = $('input[name="key"]');
+
+            if (keyInput.val() === '') {
+                // 将名称转换为小写,空格替换为横线,移除特殊字符
+                var key = name.toLowerCase()
+                             .replace(/\s+/g, '-')
+                             .replace(/[^a-z0-9-]/g, '');
+                // 默认添加到 settings 下
+                keyInput.val('settings.' + key);
+
+                // 添加一个视觉提示
+                keyInput.css('border-color', '#28a745');
+                setTimeout(function() {
+                    keyInput.css('border-color', '');
+                }, 1000);
+
+                // 显示成功提示
+                var successMsg = $('<small class="key-success" style="display: block; margin-top: 5px; font-size: 12px; color: #28a745;">' +
+                                 '✓ 已自动生成Key: settings.' + key + '</small>');
+                keyInput.after(successMsg);
+                setTimeout(function() {
+                    $('.key-success').remove();
+                }, 3000);
+            }
+        });
+
+        // 为所有表单元素添加Bagisto样式类
+        $('.control').addClass('form-control');
+
+        // 检查key格式
+        $('form').on('submit', function(e) {
+            var keyValue = $('input[name="key"]').val();
+            if (keyValue && keyValue.indexOf('settings.') !== 0) {
+                if (!confirm('当前Key不以"settings."开头,菜单可能不会显示在Settings下。确定要继续吗?')) {
+                    e.preventDefault();
+                }
+            }
+        });
+
+        // 添加提示
+        $('input[name="key"]').on('focus', function() {
+            var currentVal = $(this).val();
+            if (currentVal && currentVal.indexOf('settings.') !== 0) {
+                $(this).after(
+                    '<small class="key-warning" style="display: block; margin-top: 5px; font-size: 12px; color: #dc3545;">' +
+                    '⚠️ 警告:当前Key不以"settings."开头,菜单可能不会显示在Settings下</small>'
+                );
+
+                setTimeout(function() {
+                    $('.key-warning').remove();
+                }, 3000);
+            }
+        });
+    });*/
+</script>
+@endpush
+</x-admin::layouts>

+ 179 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/edit.blade.php

@@ -0,0 +1,179 @@
+<x-admin::layouts>
+    {{-- 页面标题 --}}
+    <x-slot:title>编辑菜单项 - {{ $menuItem->name }}</x-slot:title>
+
+    {{-- 页面内容 --}}
+    <div class="content" style="padding: 20px;">
+        {{-- 页头部分 --}}
+        <div class="page-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #dee2e6;">
+            <div class="page-title">
+                <h1><i class="fas fa-edit"></i> 编辑菜单项: {{ $menuItem->name }}</h1>
+            </div>
+            <div class="page-action">
+                <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 8px 16px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;">
+                    <i class="fas fa-arrow-left"></i> 返回列表
+                </a>
+            </div>
+        </div>
+
+        {{-- 显示错误信息 --}}
+        @if($errors->any())
+            <div class="alert alert-danger" style="background: #f8d7da; color: #721c24; padding: 12px; border-radius: 4px; margin-bottom: 20px; border: 1px solid #f5c6cb;">
+                <ul class="mb-0" style="margin: 0; padding-left: 20px;">
+                    @foreach($errors->all() as $error)
+                        <li>{{ $error }}</li>
+                    @endforeach
+                </ul>
+            </div>
+        @endif
+
+        {{-- 表单面板 --}}
+        <div class="panel" style="background: white; border: 1px solid #dee2e6; border-radius: 4px; margin-bottom: 20px;">
+            <div class="panel-header" style="padding: 12px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6;">
+                <h3 style="margin: 0; font-size: 18px;">编辑基本信息</h3>
+            </div>
+
+            <div class="panel-body" style="padding: 20px;">
+                <form method="POST" action="{{ route('admin.dynamicmenu.update', $menuItem->id) }}">
+                    @csrf
+                    @method('PUT')
+
+                    {{-- 名称 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label class="required" style="display: block; margin-bottom: 5px; font-weight: 500;">
+                            名称 <span style="color: red;">*</span>
+                        </label>
+                        <input type="text"
+                               name="name"
+                               class="control"
+                               value="{{ old('name', $menuItem->name) }}"
+                               required
+                               placeholder="例如:仪表盘"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                    </div>
+
+                    {{-- Key 字段 --}}
+                    <div class="form-group">
+                        <label class="required">Key</label>
+                        <input type="text"
+                            name="key"
+                            class="control"
+                            value="{{ old('key', $menuItem->key ?? '') }}"
+                            required
+                            placeholder="例如:settings.my-menu">
+                        <small class="control-info">
+                            建议以 'settings.' 开头,这样菜单会显示在 Settings 下。例如:settings.dynamic-menu
+                        </small>
+                    </div>
+
+                    {{-- URL/路由 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">URL/路由</label>
+                        <input type="text"
+                               name="route"
+                               class="control"
+                               value="{{ old('route', $menuItem->route) }}"
+                               placeholder="例如:admin.dashboard"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            可以是相对路径、完整URL或路由名称
+                        </small>
+                    </div>
+
+                    {{-- 图标 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">图标</label>
+                        <input type="text"
+                               name="icon"
+                               class="control"
+                               value="{{ old('icon', $menuItem->icon) }}"
+                               placeholder="例如:fas fa-dashboard、fas fa-users等"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            FontAwesome图标类名,如:fas fa-home
+                        </small>
+                    </div>
+
+                    {{-- 父级菜单 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">父级菜单</label>
+                        <select name="parent_id" class="control" style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                            <option value="">作为顶级菜单</option>
+                            @foreach($menuItems as $menuItemOption)
+                                @if($menuItemOption->id != $menuItem->id) {{-- 不能选择自己作为父级 --}}
+                                <option value="{{ $menuItemOption->id }}" {{ old('parent_id', $menuItem->parent_id) == $menuItemOption->id ? 'selected' : '' }}>
+                                    {{ $menuItemOption->name }}
+                                    @if($menuItemOption->children && $menuItemOption->children->count() > 0)
+                                        (有子菜单)
+                                    @endif
+                                </option>
+                                @endif
+                            @endforeach
+                        </select>
+                    </div>
+
+                    {{-- 排序 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">排序</label>
+                        <input type="number"
+                               name="sort_order"
+                               class="control"
+                               value="{{ old('sort_order', $menuItem->sort_order) }}"
+                               min="0"
+                               style="width: 100%; padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; font-size: 14px;">
+                        <small class="control-info" style="display: block; margin-top: 5px; font-size: 12px; color: #6c757d;">
+                            数字越小越靠前
+                        </small>
+                    </div>
+
+                    {{-- 状态 --}}
+                    <div class="form-group" style="margin-bottom: 20px;">
+                        <label style="display: block; margin-bottom: 5px; font-weight: 500;">状态</label>
+                        <div>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="1" {{ old('status', $menuItem->status) == 1 ? 'checked' : '' }} style="margin-right: 5px;">
+                                启用
+                            </label>
+                            <label class="radio-inline" style="margin-right: 20px; display: inline-flex; align-items: center;">
+                                <input type="radio" name="status" value="0" {{ old('status', $menuItem->status) == 0 ? 'checked' : '' }} style="margin-right: 5px;">
+                                禁用
+                            </label>
+                        </div>
+                    </div>
+
+                    {{-- 提交按钮 --}}
+                    <div class="form-group" style="margin-top: 30px;">
+                        <button type="submit" class="btn btn-primary" style="padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 18px; cursor: pointer; margin-right: 10px;">
+                            <i class="fas fa-save"></i> 更新菜单项
+                        </button>
+                        <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary" style="padding: 12px 24px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; font-size: 18px;">
+                            <i class="fas fa-times"></i> 取消
+                        </a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    {{-- 引入必要的JavaScript --}}
+    @push('scripts')
+    <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
+    <script>
+        /*$(document).ready(function() {
+            // 自动生成key
+            $('input[name="name"]').on('blur', function() {
+                var name = $(this).val();
+                var keyInput = $('input[name="key"]');
+
+                if (keyInput.val() === '') {
+                    var key = name.toLowerCase()
+                                .replace(/\s+/g, '-')
+                                .replace(/[^a-z0-9-]/g, '');
+                    // 默认添加到 settings 下
+                    keyInput.val('settings.' + key);
+                }
+            });
+        });*/
+    </script>
+    @endpush
+</x-admin::layouts>

+ 99 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/index.blade.php

@@ -0,0 +1,99 @@
+<x-admin::layouts>
+    {{-- 页面标题 --}}
+    <x-slot:title>
+        @lang('dynamicmenu::app.admin.menu.title')
+    </x-slot>
+
+    <div class="content">
+        {{-- 页头部分 --}}
+        <div class="flex gap-4 justify-between items-center max-sm:flex-wrap">
+            <p class="py-[11px] text-xl text-gray-800 dark:text-white font-bold">
+                @lang('dynamicmenu::app.admin.menu.title')
+            </p>
+
+            <div class="flex gap-x-2.5 items-center">
+                <a href="{{ route('admin.dynamicmenu.create') }}" class="primary-button">
+                    @lang('dynamicmenu::app.admin.menu.add-btn')
+                </a>
+            </div>
+        </div>
+
+        {{-- 页面内容 --}}
+        <div class="mt-8">
+            @if(session('success'))
+                <div class="alert alert-success">
+                    {{ session('success') }}
+                </div>
+            @endif
+
+            @if(session('error'))
+                <div class="alert alert-danger">
+                    {{ session('error') }}
+                </div>
+            @endif
+
+            <div class="accordion">
+                @foreach($menuItems as $item)
+                    @include('dynamicmenu::admin.menu.partials.tree', ['item' => $item])
+                @endforeach
+            </div>
+        </div>
+    </div>
+
+    {{-- 自定义样式 --}}
+    @push('styles')
+    <style>
+        /* 将您原有的 <style> 标签内容移到这里 */
+        .accordion { margin: 10px 0; }
+        .accordion-item { margin: 5px 0; border: 1px solid #dee2e6; border-radius: 4px; overflow: hidden; }
+        .accordion-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: #f8f9fa; cursor: pointer; transition: background 0.3s; }
+        .accordion-header:hover { background: #e9ecef; }
+        .accordion-title { display: flex; align-items: center; gap: 8px; }
+        .accordion-title i { font-size: 16px; color: #6c757d; }
+        .accordion-title .text-muted { color: #6c757d; font-size: 0.9em; }
+        .accordion-actions { display: flex; gap: 8px; }
+        .btn-edit, .btn-delete { padding: 4px 8px; border: none; background: none; cursor: pointer; border-radius: 4px; transition: background 0.3s; }
+        .btn-edit { color: #0d6efd; }
+        .btn-edit:hover { background: #e7f1ff; }
+        .btn-delete { color: #dc3545; }
+        .btn-delete:hover:not(.disabled) { background: #f8d7da; }
+        .btn-delete.disabled { color: #6c757d; cursor: not-allowed; opacity: 0.5; }
+        .accordion-content { display: none; padding: 15px 15px 15px 30px; background: white; border-top: 1px solid #dee2e6; }
+        .accordion-content.expanded { display: block; }
+        .accordion-header::after { content: '▶'; font-size: 12px; transition: transform 0.3s; margin-left: 10px; }
+        .accordion-header.expanded::after { transform: rotate(90deg); }
+    </style>
+    @endpush
+
+    {{-- 自定义脚本 --}}
+    @push('scripts')
+    <script>
+        function toggleAccordion(header) {
+            if (event.target.closest('.accordion-actions')) {
+                return;
+            }
+
+            header.classList.toggle('expanded');
+            var content = header.nextElementSibling;
+
+            if (content && content.classList.contains('accordion-content')) {
+                content.classList.toggle('expanded');
+            }
+        }
+
+        // 页面加载完成后,可以默认展开所有有子菜单的项(可选)
+        document.addEventListener('DOMContentLoaded', function() {
+            // 如果你想默认展开所有菜单,取消下面的注释
+            /*
+            document.querySelectorAll('.accordion-header').forEach(function(header) {
+                var content = header.nextElementSibling;
+                if (content && content.classList.contains('accordion-content')) {
+                    header.classList.add('expanded');
+                    content.classList.add('expanded');
+                }
+            });
+            */
+        });
+    </script>
+    @endpush
+</x-admin::layouts>

+ 44 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/partials/tree.blade.php

@@ -0,0 +1,44 @@
+<div class="accordion-item">
+    <div class="accordion-header {{ $item->children && $item->children->count() > 0 ? 'has-children' : '' }}" 
+         onclick="toggleAccordion(this)">
+        <div class="accordion-title">
+            <i class="{{ $item->icon ?? 'icon-file' }}"></i>
+            <span>{{ $item->name }}</span>
+            <span class="text-muted">({{ $item->key ?? $item->url ?? '无标识' }})</span>
+            @if(!$item->status)
+                <span class="badge badge-warning" style="background: #ffc107; color: #000; padding: 2px 6px; border-radius: 4px; font-size: 0.8em;">已禁用</span>
+            @endif
+        </div>
+        <div class="accordion-actions" onclick="event.stopPropagation()">
+            {{-- 编辑按钮 --}}
+            <a href="{{ route('admin.dynamicmenu.edit', $item->id) }}" class="btn-edit" title="编辑">
+                <i class="fas fa-edit"></i> 编辑
+            </a>
+            
+            {{-- 删除按钮 --}}
+            @if(!$item->children || $item->children->count() == 0)
+                <form action="{{ route('admin.dynamicmenu.delete', $item->id) }}" method="POST" style="display: inline;" onsubmit="return confirm('确定删除这个菜单项吗?');">
+                    @csrf
+                    @method('DELETE')
+                    <button type="submit" class="btn-delete" title="删除">
+                        <i class="fas fa-trash"></i> 删除
+                    </button>
+                </form>
+            @else
+                <span class="btn-delete disabled" title="请先删除子菜单">
+                    <i class="fas fa-trash"></i> 删除
+                </span>
+            @endif
+        </div>
+    </div>
+    
+    @if($item->children && $item->children->count() > 0)
+        <div class="accordion-content">
+            <div class="accordion">
+                @foreach($item->children as $child)
+                    @include('dynamicmenu::admin.menu.partials.tree', ['item' => $child])
+                @endforeach
+            </div>
+        </div>
+    @endif
+</div>

+ 22 - 0
packages/Longyi/DynamicMenu/src/Resources/views/admin/permission/index.blade.php

@@ -0,0 +1,22 @@
+@extends('admin::layouts.master')
+
+@section('page_title')
+    菜单权限管理
+@stop
+
+@section('content-wrapper')
+    <div class="content full-page">
+        <div class="page-header">
+            <div class="page-title">
+                <h1>菜单权限管理</h1>
+            </div>
+        </div>
+        <div class="page-content">
+            <div class="panel">
+                <div class="panel-body">
+                    <p>权限管理页面</p>
+                </div>
+            </div>
+        </div>
+    </div>
+@stop

+ 57 - 0
packages/Longyi/DynamicMenu/src/Routes/admin-routes.php

@@ -0,0 +1,57 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\DynamicMenu\Http\Controllers\MenuController;
+
+// 关键:使用正确的路由组
+Route::group(['middleware' => ['web', 'admin'], 'prefix' => 'admin'], function () {
+    
+    
+    
+    // 菜单管理路由 - 注意这里不要重复加 'admin' 前缀
+    Route::get('dynamicmenu', [MenuController::class, 'index'])
+        ->name('admin.dynamicmenu.index')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.menu.index'
+        ]);
+    
+    Route::get('dynamicmenu/create', [MenuController::class, 'create'])
+        ->name('admin.dynamicmenu.create')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.menu.create'
+        ]);
+    
+    Route::post('dynamicmenu', [MenuController::class, 'store'])
+        ->name('admin.dynamicmenu.store')
+        ->defaults('_config', [
+            'redirect' => 'admin.dynamicmenu.index'
+        ]);
+    
+    Route::get('dynamicmenu/{id}/edit', [MenuController::class, 'edit'])
+        ->name('admin.dynamicmenu.edit')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.menu.edit'
+        ]);
+    
+    Route::put('dynamicmenu/{id}', [MenuController::class, 'update'])
+        ->name('admin.dynamicmenu.update')
+        ->defaults('_config', [
+            'redirect' => 'admin.dynamicmenu.index'
+        ]);
+    
+    Route::delete('dynamicmenu/{id}', [MenuController::class, 'destroy'])
+        ->name('admin.dynamicmenu.delete');
+    
+    // 权限管理路由
+    Route::get('dynamicmenu-permissions', [MenuController::class, 'permission'])
+        ->name('admin.dynamicmenu.permission')
+        ->defaults('_config', [
+            'view' => 'dynamicmenu::admin.permission.index'
+        ]);
+    
+    Route::post('dynamicmenu-permissions', [MenuController::class, 'updatePermission'])
+        ->name('admin.dynamicmenu.permission.update');
+    
+    Route::post('dynamicmenu-permissions/get', [MenuController::class, 'getRolePermissions'])
+        ->name('admin.dynamicmenu.permission.get');
+});

+ 49 - 0
packages/Longyi/DynamicMenu/src/Services/MenuService.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Longyi\DynamicMenu\Services;
+
+use Longyi\DynamicMenu\Models\MenuItem;
+use Illuminate\Support\Facades\Cache;
+
+class MenuService
+{
+
+    public function boot()
+    {
+        // ... 其他代码
+
+        Event::listen('bagisto.admin.menu.build', function ($menu) {
+            $settings = $menu->get('settings');
+            
+            if ($settings) {
+                // 使用缓存,避免每次请求都查询数据库
+                $menuItems = Cache::remember('dynamic_menu_items', 3600, function () {
+                    return app(MenuService::class)->getEnabledMenuItems();
+                });
+                
+                foreach ($menuItems as $item) {
+                    $this->addMenuItemToMenu($settings, $item);
+                }
+            }
+        });
+    }
+    /**
+     * 获取所有启用的菜单项
+     */
+    public function getEnabledMenuItems()
+    {
+        return MenuItem::with('children')
+            ->whereNull('parent_id')
+            ->where('status', 1)
+            ->orderBy('sort_order')
+            ->get();
+    }
+    
+    /**
+     * 清除菜单缓存
+     */
+    public function clearCache()
+    {
+        Cache::forget('dynamic_menu_items');
+    }
+}