bianjunhui 1 week ago
parent
commit
72ec60c565

+ 7 - 5
packages/Longyi/DynamicMenu/src/Http/Controllers/MenuController.php

@@ -55,17 +55,19 @@ class MenuController extends Controller
     
     public function store(Request $request)
     {
-        $this->validate($request, [
+        
+        $request->validate([
             'name' => 'required|string|max:255',
-            'key' => 'required|string|unique:ly_dynamic_menu_items,key',
+            'key' => 'required|string|unique:dynamic_menu_items,key',
             'route' => 'required|string',
             'icon' => 'nullable|string',
-            'parent_id' => 'nullable|exists:ly_dynamic_menu_items,id',
+            '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);
@@ -87,10 +89,10 @@ class MenuController extends Controller
     {
         $this->validate($request, [
             'name' => 'required|string|max:255',
-            'key' => 'required|string|unique:ly_dynamic_menu_items,key,' . $id,
+            'key' => 'required|string|unique:dynamic_menu_items,key,' . $id,
             'route' => 'required|string',
             'icon' => 'nullable|string',
-            'parent_id' => 'nullable|exists:ly_dynamic_menu_items,id',
+            'parent_id' => 'nullable|exists:dynamic_menu_items,id',
             'sort_order' => 'integer',
             'status' => 'boolean'
         ]);

+ 52 - 3
packages/Longyi/DynamicMenu/src/Models/MenuItem.php

@@ -3,12 +3,11 @@
 namespace Longyi\DynamicMenu\Models;
 
 use Illuminate\Database\Eloquent\Model;
-use Webkul\User\Models\Admin;
 
 class MenuItem extends Model
 {
     protected $table = 'dynamic_menu_items';
-
+    
     protected $fillable = [
         'name',
         'key',
@@ -19,11 +18,61 @@ class MenuItem extends Model
         'status',
         'created_by'
     ];
-
+    
     protected $casts = [
         'status' => 'boolean',
         'sort_order' => 'integer'
     ];
+    
+    /**
+     * 获取菜单链接
+     */
+    public function getUrlAttribute()
+    {
+        if (empty($this->route)) {
+            return '#';
+        }
+        
+        // 如果是完整的URL(以http://或https://开头)
+        if (str_starts_with($this->route, 'http://') || str_starts_with($this->route, 'https://')) {
+            return $this->route;
+        }
+        
+        // 如果是路由名称
+        try {
+            if (route_exists($this->route)) {
+                return route($this->route);
+            }
+        } catch (\Exception $e) {
+            // 路由不存在,返回原始值
+        }
+        
+        // 如果是相对路径
+        return url($this->route);
+    }
+    
+    /**
+     * 检查菜单是否当前活跃
+     */
+    public function getIsActiveAttribute()
+    {
+        $currentRoute = request()->route() ? request()->route()->getName() : '';
+        $currentPath = request()->path();
+        
+        // 检查路由名称匹配
+        if ($this->route == $currentRoute) {
+            return true;
+        }
+        
+        // 检查URL路径匹配
+        $menuPath = trim($this->route, '/');
+        $currentPath = trim($currentPath, '/');
+        
+        return $menuPath == $currentPath || str_starts_with($currentPath, $menuPath);
+    }
+    
+    // ... 其他关系方法
+
 
     public function parent()
     {

+ 270 - 29
packages/Longyi/DynamicMenu/src/Resources/views/admin/menu/create.blade.php

@@ -1,35 +1,276 @@
-@extends('admin::layouts.master')
-
-@section('page_title')
-    添加菜单项
-@stop
-
-@section('content-wrapper')
-    <div class="content full-page">
+{{-- 不继承任何布局 --}}
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="csrf-token" content="{{ csrf_token() }}">
+    <title>添加菜单项</title>
+    
+    {{-- 引入必要的CSS --}}
+    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
+    
+    <style>
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background-color: #f8f9fa;
+            padding: 20px;
+        }
+        .page-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 20px;
+            padding-bottom: 10px;
+            border-bottom: 1px solid #dee2e6;
+        }
+        .page-title h1 {
+            margin: 0;
+            font-size: 24px;
+            color: #333;
+        }
+        .page-action {
+            display: flex;
+            gap: 10px;
+        }
+        .btn {
+            display: inline-block;
+            padding: 8px 16px;
+            border-radius: 4px;
+            text-decoration: none;
+            cursor: pointer;
+            border: 1px solid transparent;
+        }
+        .btn-primary {
+            background-color: #007bff;
+            color: white;
+        }
+        .btn-primary:hover {
+            background-color: #0056b3;
+        }
+        .btn-secondary {
+            background-color: #6c757d;
+            color: white;
+        }
+        .btn-sm {
+            padding: 4px 8px;
+            font-size: 14px;
+        }
+        .btn-lg {
+            padding: 12px 24px;
+            font-size: 18px;
+        }
+        .panel {
+            background: white;
+            border: 1px solid #dee2e6;
+            border-radius: 4px;
+            margin-bottom: 20px;
+        }
+        .panel-header {
+            padding: 12px 20px;
+            background-color: #f8f9fa;
+            border-bottom: 1px solid #dee2e6;
+        }
+        .panel-header h3 {
+            margin: 0;
+            font-size: 18px;
+        }
+        .panel-body {
+            padding: 20px;
+        }
+        .form-group {
+            margin-bottom: 20px;
+        }
+        .form-group label {
+            display: block;
+            margin-bottom: 5px;
+            font-weight: 500;
+        }
+        .form-group label.required:after {
+            content: " *";
+            color: red;
+        }
+        .control {
+            width: 100%;
+            padding: 8px 12px;
+            border: 1px solid #dee2e6;
+            border-radius: 4px;
+            font-size: 14px;
+        }
+        .control:focus {
+            border-color: #80bdff;
+            outline: 0;
+            box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
+        }
+        .control-info {
+            display: block;
+            margin-top: 5px;
+            font-size: 12px;
+            color: #6c757d;
+        }
+        .radio-inline {
+            margin-right: 20px;
+            display: inline-flex;
+            align-items: center;
+        }
+        .radio-inline input {
+            margin-right: 5px;
+        }
+    </style>
+</head>
+<body>
+    <div class="container-fluid">
         <div class="page-header">
             <div class="page-title">
-                <h1>添加菜单项</h1>
+                <h1><i class="fas fa-plus-circle"></i> 添加菜单项</h1>
+            </div>
+            <div class="page-action">
+                <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary">
+                    <i class="fas fa-arrow-left"></i> 返回列表
+                </a>
             </div>
         </div>
-        <div class="page-content">
-            <form method="POST" action="{{ route('admin.dynamicmenu.store') }}">
-                @csrf
-                <div class="form-group">
-                    <label>名称</label>
-                    <input type="text" name="name" class="control" required>
-                </div>
-                <div class="form-group">
-                    <label>Key</label>
-                    <input type="text" name="key" class="control" required>
-                </div>
-                <div class="form-group">
-                    <label>路由</label>
-                    <input type="text" name="route" class="control" required>
-                </div>
-                <div class="form-group">
-                    <button type="submit" class="btn btn-lg btn-primary">保存</button>
-                </div>
-            </form>
+
+        {{-- 显示错误信息 --}}
+        @if($errors->any())
+            <div class="alert alert-danger">
+                <ul class="mb-0">
+                    @foreach($errors->all() as $error)
+                        <li>{{ $error }}</li>
+                    @endforeach
+                </ul>
+            </div>
+        @endif
+
+        <div class="panel">
+            <div class="panel-header">
+                <h3>基本信息</h3>
+            </div>
+            
+            <div class="panel-body">
+                <form method="POST" action="{{ route('admin.dynamicmenu.store') }}">
+                    @csrf
+                    
+                    {{-- 名称 --}}
+                    <div class="form-group">
+                        <label class="required">名称</label>
+                        <input type="text" 
+                               name="name" 
+                               class="control" 
+                               value="{{ old('name') }}" 
+                               required
+                               placeholder="例如:仪表盘">
+                    </div>
+
+                    {{-- Key --}}
+                    <div class="form-group">
+                        <label class="required">Key</label>
+                        <input type="text" 
+                               name="key" 
+                               class="control" 
+                               value="{{ old('key') }}" 
+                               required
+                               placeholder="例如:admin.dashboard">
+                        <small class="control-info">用于权限验证和菜单激活状态判断</small>
+                    </div>
+
+                    {{-- URL/路由 --}}
+                    <div class="form-group">
+                        <label>URL/路由</label>
+                        <input type="text" 
+                               name="route" 
+                               class="control" 
+                               value="{{ old('route') }}"
+                               placeholder="例如:admin.dashboard">
+                        <small class="control-info">可以是相对路径、完整URL或路由名称</small>
+                    </div>
+
+                    {{-- 图标 --}}
+                    <div class="form-group">
+                        <label>图标</label>
+                        <input type="text" 
+                               name="icon" 
+                               class="control" 
+                               value="{{ old('icon', 'fas fa-file') }}"
+                               placeholder="例如:fas fa-dashboard、fas fa-users等">
+                        <small class="control-info">FontAwesome图标类名,如:fas fa-home</small>
+                    </div>
+
+                    {{-- 父级菜单 --}}
+                    <div class="form-group">
+                        <label>父级菜单</label>
+                        <select name="parent_id" class="control">
+                            <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">
+                        <label>排序</label>
+                        <input type="number" 
+                               name="order" 
+                               class="control" 
+                               value="{{ old('order', 0) }}"
+                               min="0">
+                        <small class="control-info">数字越小越靠前</small>
+                    </div>
+
+                    {{-- 状态 --}}
+                    <div class="form-group">
+                        <label>状态</label>
+                        <div>
+                            <label class="radio-inline">
+                                <input type="radio" name="status" value="1" {{ old('is_active', 1) == 1 ? 'checked' : '' }}>
+                                启用
+                            </label>
+                            <label class="radio-inline">
+                                <input type="radio" name="status" value="0" {{ old('is_active') == 0 ? 'checked' : '' }}>
+                                禁用
+                            </label>
+                        </div>
+                    </div>
+
+                    {{-- 提交按钮 --}}
+                    <div class="form-group">
+                        <button type="submit" class="btn btn-primary btn-lg">
+                            <i class="fas fa-save"></i> 保存菜单项
+                        </button>
+                        <a href="{{ route('admin.dynamicmenu.index') }}" class="btn btn-secondary btn-lg">
+                            <i class="fas fa-times"></i> 取消
+                        </a>
+                    </div>
+                </form>
+            </div>
         </div>
     </div>
-@stop
+
+    {{-- 引入必要的JavaScript --}}
+    <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, '');
+                    keyInput.val('admin.' + key);
+                }
+            });
+        });
+    </script>
+</body>
+</html>

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

@@ -4,6 +4,109 @@
 <head>
     <title>动态菜单管理</title>
     <link rel="stylesheet" href="{{ asset('vendor/webkul/admin/assets/css/admin.css') }}">
+    <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);
+        }
+        .page-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 20px;
+            padding-bottom: 10px;
+            border-bottom: 1px solid #dee2e6;
+        }
+        .btn-primary {
+            display: inline-block;
+            padding: 8px 16px;
+            background: #0d6efd;
+            color: white;
+            text-decoration: none;
+            border-radius: 4px;
+            transition: background 0.3s;
+        }
+        .btn-primary:hover {
+            background: #0b5ed7;
+        }
+    </style>
 </head>
 <body>
     <div class="content" style="padding: 20px;">
@@ -12,6 +115,13 @@
             <a href="{{ route('admin.dynamicmenu.create') }}" class="btn btn-primary">添加菜单项</a>
         </div>
         <div class="page-content">
+            @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])
@@ -19,5 +129,41 @@
             </div>
         </div>
     </div>
+
+    <script>
+        // 定义切换函数
+        function toggleAccordion(header) {
+            // 如果点击的是操作按钮区域,不执行切换
+            if (event.target.closest('.accordion-actions')) {
+                return;
+            }
+            
+            // 切换header的expanded类
+            header.classList.toggle('expanded');
+            
+            // 获取对应的content
+            var content = header.nextElementSibling;
+            
+            // 检查是否是content元素
+            if (content && content.classList.contains('accordion-content')) {
+                // 切换content的expanded类
+                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>
 </body>
 </html>

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

@@ -1,29 +1,35 @@
 <div class="accordion-item">
-    <div class="accordion-header" onclick="toggleAccordion(this)">
+    <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 }})</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">
-                <i class="icon pencil"></i>
+            <a href="{{ route('admin.dynamicmenu.edit', $item->id) }}" class="btn-edit" title="编辑">
+                <i class="icon pencil">✏️</i>
             </a>
+            
             @if(!$item->children || $item->children->count() == 0)
                 <form action="{{ route('admin.dynamicmenu.delete', $item->id) }}" method="POST" style="display: inline;">
                     @csrf
                     @method('DELETE')
-                    <button type="submit" class="btn-delete" onclick="return confirm('确定删除?')">
-                        <i class="icon trash"></i>
+                    <button type="submit" class="btn-delete" onclick="return confirm('确定删除?')" title="删除">
+                        <i class="icon trash">🗑️</i>
                     </button>
                 </form>
             @else
-                <span class="btn-delete disabled">
-                    <i class="icon trash"></i>
+                <span class="btn-delete disabled" title="请先删除子菜单">
+                    <i class="icon trash">🗑️</i>
                 </span>
             @endif
         </div>
     </div>
+    
     @if($item->children && $item->children->count() > 0)
         <div class="accordion-content">
             <div class="accordion">
@@ -33,4 +39,4 @@
             </div>
         </div>
     @endif
-</div>
+</div>