Pārlūkot izejas kodu

菜单优化调整

lingmin_package@163.com 3 nedēļas atpakaļ
vecāks
revīzija
2a5ebc6962
3 mainītis faili ar 557 papildinājumiem un 100 dzēšanām
  1. 64 10
      src/layouts/MainLayout.vue
  2. 493 36
      src/views/admin/Menus.vue
  3. 0 54
      test_datetime_format.html

+ 64 - 10
src/layouts/MainLayout.vue

@@ -46,10 +46,60 @@
           >
             <!-- 动态渲染菜单 -->
             <template v-for="menu in userMenus" :key="menu.id">
-              <!-- 只显示菜单类型,过滤掉按钮类型 -->
-              <template v-if="menu.menu_type === 'menu'">
-                <!-- 有子菜单的情况(父级目录) -->
-                <el-sub-menu v-if="hasMenuChildren(menu)" :index="menu.path || menu.name">
+              <!-- 父菜单类型:只做层级展示,点击展开子菜单 -->
+              <template v-if="menu.menu_type === 'parent'">
+                <el-sub-menu :index="menu.name" v-show="!menu.is_hidden">
+                  <template #title>
+                    <el-icon v-if="menu.icon">
+                      <component :is="getIconComponent(menu.icon)" />
+                    </el-icon>
+                    <span>{{ menu.title }}</span>
+                  </template>
+                  
+                  <!-- 渲染子菜单 -->
+                  <template v-for="child in getMenuChildren(menu)" :key="child.id">
+                    <!-- 子菜单也是父菜单类型 -->
+                    <el-sub-menu v-if="child.menu_type === 'parent'" :index="child.name" v-show="!child.is_hidden">
+                      <template #title>
+                        <el-icon v-if="child.icon">
+                          <component :is="getIconComponent(child.icon)" />
+                        </el-icon>
+                        <span>{{ child.title }}</span>
+                      </template>
+                      
+                      <!-- 渲染三级菜单项 -->
+                      <el-menu-item 
+                        v-for="grandChild in getMenuChildren(child)" 
+                        :key="grandChild.id"
+                        :index="grandChild.path || grandChild.name"
+                        v-show="!grandChild.is_hidden && grandChild.menu_type === 'menu'"
+                      >
+                        <el-icon v-if="grandChild.icon">
+                          <component :is="getIconComponent(grandChild.icon)" />
+                        </el-icon>
+                        <span>{{ grandChild.title }}</span>
+                      </el-menu-item>
+                    </el-sub-menu>
+                    
+                    <!-- 子菜单是菜单项类型 -->
+                    <el-menu-item 
+                      v-else-if="child.menu_type === 'menu'"
+                      :index="child.path || child.name"
+                      v-show="!child.is_hidden"
+                    >
+                      <el-icon v-if="child.icon">
+                        <component :is="getIconComponent(child.icon)" />
+                      </el-icon>
+                      <span>{{ child.title }}</span>
+                    </el-menu-item>
+                  </template>
+                </el-sub-menu>
+              </template>
+              
+              <!-- 菜单项类型:直接显示为菜单项 -->
+              <template v-else-if="menu.menu_type === 'menu'">
+                <!-- 有子菜单的菜单项 -->
+                <el-sub-menu v-if="hasMenuChildren(menu)" :index="menu.name" v-show="!menu.is_hidden">
                   <template #title>
                     <el-icon v-if="menu.icon">
                       <component :is="getIconComponent(menu.icon)" />
@@ -60,7 +110,7 @@
                     v-for="child in getMenuChildren(menu)" 
                     :key="child.id"
                     :index="child.path || child.name"
-                    v-show="!child.is_hidden"
+                    v-show="!child.is_hidden && child.menu_type === 'menu'"
                   >
                     <el-icon v-if="child.icon">
                       <component :is="getIconComponent(child.icon)" />
@@ -69,7 +119,7 @@
                   </el-menu-item>
                 </el-sub-menu>
                 
-                <!-- 没有子菜单的情况(具体菜单页面) -->
+                <!-- 没有子菜单的菜单项 -->
                 <el-menu-item 
                   v-else 
                   :index="menu.path || menu.name"
@@ -148,14 +198,18 @@ const getIconComponent = (iconName: string) => {
   return (ElementPlusIcons as any)[iconName] || ElementPlusIcons.Menu
 }
 
-// 检查是否有菜单类型的子项
+// 检查是否有菜单类型的子项(包括parent和menu类型)
 const hasMenuChildren = (menu: MenuItem) => {
-  return menu.children && menu.children.some((child: MenuItem) => child.menu_type === 'menu')
+  return menu.children && menu.children.some((child: MenuItem) => 
+    child.menu_type === 'menu' || child.menu_type === 'parent'
+  )
 }
 
-// 获取菜单类型的子项
+// 获取菜单类型的子项(包括parent和menu类型,排除button类型)
 const getMenuChildren = (menu: MenuItem) => {
-  return menu.children ? menu.children.filter((child: MenuItem) => child.menu_type === 'menu') : []
+  return menu.children ? menu.children.filter((child: MenuItem) => 
+    child.menu_type === 'menu' || child.menu_type === 'parent'
+  ) : []
 }
 
 // 获取用户菜单

+ 493 - 36
src/views/admin/Menus.vue

@@ -48,14 +48,27 @@
       border
       class="menu-tree-table"
     >
-      <el-table-column prop="title" label="菜单名称" min-width="400">
+      <el-table-column prop="title" label="菜单名称" min-width="450">
         <template #default="{ row }">
           <div class="menu-info" :class="getMenuLevelClass(row)">
             <div class="menu-content">
+              <!-- 展开/收起按钮前置 -->
+              <div class="expand-button-wrapper">
+                <el-button
+                  v-if="row.children && row.children.length > 0"
+                  :icon="row.expanded ? Fold : Expand"
+                  size="small"
+                  text
+                  @click="toggleExpand(row)"
+                  class="expand-btn"
+                />
+                <div v-else class="expand-placeholder"></div>
+              </div>
+              
               <!-- 强化层级指示器 -->
               <div class="level-indicators">
                 <!-- 主菜单层级指示 -->
-                <div v-if="!row.parent_id" class="level-indicator level-0">
+                <div v-if="getMenuLevel(row) === 0" class="level-indicator level-0">
                   <div class="level-bar main-level"></div>
                 </div>
                 
@@ -86,7 +99,7 @@
               
               <!-- 层级标识 -->
               <span class="level-badge" :class="getLevelBadgeClass(row)">
-                {{ getLevelText(row) }}
+                L{{ getMenuLevel(row) + 1 }}
               </span>
               
               <!-- 菜单类型标签 -->
@@ -96,23 +109,23 @@
                 size="small"
                 class="menu-type-tag"
               >
-                按钮权限
+                功能按钮
               </el-tag>
               <el-tag 
-                v-else-if="row.menu_type === 'menu' && row.parent_id" 
+                v-else-if="row.menu_type === 'menu'" 
                 type="success" 
                 size="small"
                 class="menu-type-tag"
               >
-                功能菜单
+                菜单
               </el-tag>
               <el-tag 
-                v-else-if="row.menu_type === 'menu' && !row.parent_id" 
+                v-else-if="row.menu_type === 'parent'" 
                 type="primary" 
                 size="small"
                 class="menu-type-tag"
               >
-                菜单
+                菜单
               </el-tag>
               
               <!-- 隐藏状态 -->
@@ -147,22 +160,44 @@
           />
         </template>
       </el-table-column>
-      <el-table-column prop="created_at" label="创建时间" width="180">
+      <el-table-column prop="created_time" label="创建人" width="120">
+        <template #default="{ row }">
+          {{ row.created_by_name || '系统' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="created_time" label="创建时间" width="160">
         <template #default="{ row }">
-          {{ formatDateTime(row.created_at) }}
+          {{ formatDateTime(row.created_time) }}
         </template>
       </el-table-column>
-      <el-table-column label="操作" width="200" fixed="right">
+      <el-table-column prop="updated_by" label="修改人" width="120">
         <template #default="{ row }">
-          <el-button type="primary" size="small" @click="editMenu(row)">
-            编辑
-          </el-button>
-          <el-button type="success" size="small" @click="addChildMenu(row)">
-            添加子菜单
-          </el-button>
-          <el-button type="danger" size="small" @click="deleteMenu(row)">
-            删除
-          </el-button>
+          {{ row.updated_by_name || '系统' }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="updated_time" label="修改时间" width="160">
+        <template #default="{ row }">
+          {{ formatDateTime(row.updated_time) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="240" fixed="right">
+        <template #default="{ row }">
+          <div class="action-buttons">
+            <el-button type="primary" size="small" @click="editMenu(row)">
+              编辑
+            </el-button>
+            <el-button 
+              v-if="row.menu_type !== 'button'" 
+              type="success" 
+              size="small" 
+              @click="addChildMenu(row)"
+            >
+              添加子项
+            </el-button>
+            <el-button type="danger" size="small" @click="deleteMenu(row)">
+              删除
+            </el-button>
+          </div>
         </template>
       </el-table-column>
     </el-table>
@@ -216,14 +251,45 @@
                 placeholder="请选择父级菜单(可选)"
                 clearable
                 check-strictly
-              />
+                :render-after-expand="false"
+                style="width: 100%"
+              >
+                <template #default="{ node, data }">
+                  <span class="parent-option">
+                    <el-icon v-if="data.children && data.children.length > 0">
+                      <Folder />
+                    </el-icon>
+                    <el-icon v-else>
+                      <Document />
+                    </el-icon>
+                    <span style="margin-left: 8px;">{{ data.title }}</span>
+                    <el-tag 
+                      v-if="getMenuTypeByTitle(data.title) === 'parent'" 
+                      type="primary" 
+                      size="small" 
+                      style="margin-left: 8px;"
+                    >
+                      父菜单
+                    </el-tag>
+                    <el-tag 
+                      v-else 
+                      type="success" 
+                      size="small" 
+                      style="margin-left: 8px;"
+                    >
+                      菜单项
+                    </el-tag>
+                  </span>
+                </template>
+              </el-tree-select>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="菜单类型" prop="menu_type">
-              <el-select v-model="menuForm.menu_type" placeholder="请选择菜单类型">
-                <el-option label="菜单" value="menu" />
-                <el-option label="按钮" value="button" />
+              <el-select v-model="menuForm.menu_type" placeholder="请选择菜单类型" @change="handleMenuTypeChange">
+                <el-option label="父菜单" value="parent" />
+                <el-option label="菜单项" value="menu" />
+                <el-option label="功能按钮" value="button" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -231,13 +297,33 @@
 
         <el-row :gutter="20">
           <el-col :span="12">
-            <el-form-item label="路由路径" prop="path">
-              <el-input v-model="menuForm.path" placeholder="请输入路由路径" />
+            <el-form-item label="路由路径" prop="path" :required="menuForm.menu_type === 'menu'">
+              <el-input 
+                v-model="menuForm.path" 
+                placeholder="请输入路由路径" 
+                :disabled="menuForm.menu_type === 'parent' || menuForm.menu_type === 'button'"
+              />
+              <div v-if="menuForm.menu_type === 'parent'" class="form-tip">
+                父菜单不需要配置路由路径
+              </div>
+              <div v-if="menuForm.menu_type === 'button'" class="form-tip">
+                功能按钮不需要配置路由路径
+              </div>
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="组件路径" prop="component">
-              <el-input v-model="menuForm.component" placeholder="请输入组件路径" />
+            <el-form-item label="组件路径" prop="component" :required="menuForm.menu_type === 'menu'">
+              <el-input 
+                v-model="menuForm.component" 
+                placeholder="请输入组件路径" 
+                :disabled="menuForm.menu_type === 'parent' || menuForm.menu_type === 'button'"
+              />
+              <div v-if="menuForm.menu_type === 'parent'" class="form-tip">
+                父菜单不需要配置组件路径
+              </div>
+              <div v-if="menuForm.menu_type === 'button'" class="form-tip">
+                功能按钮不需要配置组件路径
+              </div>
             </el-form-item>
           </el-col>
         </el-row>
@@ -302,7 +388,7 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, computed, nextTick } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus, Search, Expand, Fold } from '@element-plus/icons-vue'
+import { Plus, Search, Expand, Fold, Folder, Document } from '@element-plus/icons-vue'
 import * as ElementPlusIcons from '@element-plus/icons-vue'
 import request from '@/api/request'
 
@@ -559,13 +645,23 @@ const buildMenuTree = (menuList: any[]) => {
 
 // 构建父级菜单选项
 const buildParentOptions = (menuList: any[]) => {
-  return menuList
-    .filter(menu => menu.menu_type === 'menu')
-    .map(menu => ({
-      id: menu.id,
-      title: menu.title,
-      children: []
-    }))
+  // 过滤出可以作为父菜单的项:parent类型和menu类型都可以作为父菜单
+  const parentCandidates = menuList.filter(menu => 
+    menu.menu_type === 'parent' || menu.menu_type === 'menu'
+  )
+  
+  // 构建树形结构的父级选项
+  const buildTree = (items: any[], parentId: string | null = null): any[] => {
+    return items
+      .filter(item => item.parent_id === parentId)
+      .map(item => ({
+        id: item.id,
+        title: item.title,
+        children: buildTree(items, item.id)
+      }))
+  }
+  
+  return buildTree(parentCandidates)
 }
 
 // 加载菜单列表
@@ -723,6 +819,28 @@ const deleteMenu = async (menu: any) => {
   }
 }
 
+// 根据菜单标题获取菜单类型(用于父菜单选择器显示)
+const getMenuTypeByTitle = (title: string) => {
+  const menu = menus.value.find(m => m.title === title)
+  return menu ? menu.menu_type : 'menu'
+}
+
+// 菜单类型变化处理
+const handleMenuTypeChange = (type: string) => {
+  if (type === 'parent' || type === 'button') {
+    // 父菜单和功能按钮不需要路径和组件
+    menuForm.path = ''
+    menuForm.component = ''
+  }
+}
+
+// 切换展开状态
+const toggleExpand = (row: any) => {
+  if (menuTableRef.value) {
+    menuTableRef.value.toggleRowExpansion(row)
+  }
+}
+
 // 保存菜单
 const saveMenu = async () => {
   try {
@@ -1112,4 +1230,343 @@ onMounted(() => {
   transform: scale(1.1);
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
 }
+</style>
+
+<style scoped>
+.menus-management {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #303133;
+  font-size: 24px;
+  font-weight: 600;
+}
+
+.page-header p {
+  margin: 0;
+  color: #606266;
+  font-size: 14px;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #f8f9fa;
+  border-radius: 8px;
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+/* 菜单树表格样式 */
+.menu-tree-table {
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+/* 菜单信息容器 */
+.menu-info {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  padding: 4px 0;
+}
+
+.menu-content {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+}
+
+/* 展开按钮样式 */
+.expand-button-wrapper {
+  display: flex;
+  align-items: center;
+  width: 24px;
+  height: 24px;
+  justify-content: center;
+}
+
+.expand-btn {
+  padding: 0;
+  width: 20px;
+  height: 20px;
+  border: none;
+  background: transparent;
+}
+
+.expand-btn:hover {
+  background: #f0f0f0;
+  border-radius: 4px;
+}
+
+.expand-placeholder {
+  width: 20px;
+  height: 20px;
+}
+
+/* 层级指示器 */
+.level-indicators {
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+}
+
+.level-indicator {
+  display: flex;
+  align-items: center;
+  position: relative;
+}
+
+.level-bar {
+  width: 4px;
+  height: 20px;
+  border-radius: 2px;
+  margin-right: 6px;
+}
+
+.level-bar.main-level {
+  background: linear-gradient(135deg, #409eff, #67c23a);
+  box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
+}
+
+.level-bar.sub-level {
+  background: linear-gradient(135deg, #67c23a, #e6a23c);
+  box-shadow: 0 2px 4px rgba(103, 194, 58, 0.3);
+}
+
+.level-bar.button-level {
+  background: linear-gradient(135deg, #e6a23c, #f56c6c);
+  box-shadow: 0 2px 4px rgba(230, 162, 60, 0.3);
+}
+
+.level-connector {
+  width: 12px;
+  height: 1px;
+  background: #dcdfe6;
+  margin-right: 4px;
+}
+
+.level-connector-deep {
+  width: 8px;
+  height: 1px;
+  background: #e4e7ed;
+  margin-right: 4px;
+}
+
+/* 菜单图标样式 */
+.menu-icon {
+  font-size: 18px;
+  margin-right: 8px;
+}
+
+.main-menu-icon {
+  color: #409eff;
+  font-size: 20px;
+}
+
+.sub-menu-icon {
+  color: #67c23a;
+  font-size: 18px;
+}
+
+.button-icon {
+  color: #e6a23c;
+  font-size: 16px;
+}
+
+.default-icon {
+  color: #909399;
+}
+
+/* 菜单标题样式 */
+.menu-title {
+  font-weight: 500;
+  margin-right: 12px;
+}
+
+.main-menu-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.sub-menu-title {
+  font-size: 15px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.button-title {
+  font-size: 14px;
+  font-weight: 400;
+  color: #909399;
+}
+
+/* 层级徽章 */
+.level-badge {
+  font-size: 11px;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-weight: 600;
+  margin-right: 8px;
+  min-width: 24px;
+  text-align: center;
+}
+
+.level-badge-main {
+  background: #e1f3ff;
+  color: #409eff;
+  border: 1px solid #b3d8ff;
+}
+
+.level-badge-sub {
+  background: #f0f9ff;
+  color: #67c23a;
+  border: 1px solid #c2e7b0;
+}
+
+.level-badge-button {
+  background: #fdf6ec;
+  color: #e6a23c;
+  border: 1px solid #f5dab1;
+}
+
+.level-badge-default {
+  background: #f4f4f5;
+  color: #909399;
+  border: 1px solid #d3d4d6;
+}
+
+/* 菜单类型标签 */
+.menu-type-tag {
+  margin-right: 8px;
+  font-size: 11px;
+  padding: 2px 8px;
+  border-radius: 12px;
+}
+
+/* 菜单层级背景 */
+.menu-level-0 {
+  background: linear-gradient(90deg, rgba(64, 158, 255, 0.05), transparent);
+  border-left: 3px solid #409eff;
+  padding-left: 8px;
+}
+
+.menu-level-1 {
+  background: linear-gradient(90deg, rgba(103, 194, 58, 0.05), transparent);
+  border-left: 3px solid #67c23a;
+  padding-left: 12px;
+}
+
+.menu-level-2 {
+  background: linear-gradient(90deg, rgba(230, 162, 60, 0.05), transparent);
+  border-left: 3px solid #e6a23c;
+  padding-left: 16px;
+}
+
+/* 操作按钮 */
+.action-buttons {
+  display: flex;
+  gap: 8px;
+  flex-wrap: nowrap;
+}
+
+.action-buttons .el-button {
+  padding: 5px 12px;
+  font-size: 12px;
+}
+
+/* 分页样式 */
+.pagination {
+  display: flex;
+  justify-content: center;
+  margin-top: 20px;
+  padding: 20px 0;
+}
+
+/* 表单提示 */
+.form-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+  line-height: 1.4;
+}
+
+/* 图标选项 */
+.icon-option {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.icon-option .el-icon {
+  font-size: 16px;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .toolbar {
+    flex-direction: column;
+    gap: 16px;
+  }
+  
+  .toolbar-left,
+  .toolbar-right {
+    width: 100%;
+    justify-content: center;
+  }
+  
+  .action-buttons {
+    flex-direction: column;
+    gap: 4px;
+  }
+  
+  .menu-content {
+    flex-wrap: wrap;
+    gap: 4px;
+  }
+}
+
+/* 表格行悬停效果 */
+.menu-tree-table .el-table__row:hover {
+  background-color: #f5f7fa;
+}
+
+/* 表格边框优化 */
+.menu-tree-table .el-table__border-left-patch {
+  border-top: 1px solid #ebeef5;
+}
+
+.menu-tree-table .el-table__border-bottom-patch {
+  border-right: 1px solid #ebeef5;
+}
+
+/* 树形表格展开图标样式 */
+.menu-tree-table .el-table__expand-icon {
+  color: #409eff;
+  font-size: 14px;
+}
+
+.menu-tree-table .el-table__expand-icon.el-table__expand-icon--expanded {
+  transform: rotate(90deg);
+}
 </style>

+ 0 - 54
test_datetime_format.html

@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <title>DateTime Format Test</title>
-</head>
-<body>
-    <h1>DateTime Format Test</h1>
-    <div id="results"></div>
-
-    <script>
-        // 格式化日期时间函数
-        const formatDateTime = (dateTime) => {
-            if (!dateTime) return '-'
-            const date = new Date(dateTime)
-            
-            // 格式化为 YYYY-MM-DD HH:mm:ss
-            const year = date.getFullYear()
-            const month = String(date.getMonth() + 1).padStart(2, '0')
-            const day = String(date.getDate()).padStart(2, '0')
-            const hours = String(date.getHours()).padStart(2, '0')
-            const minutes = String(date.getMinutes()).padStart(2, '0')
-            const seconds = String(date.getSeconds()).padStart(2, '0')
-            
-            return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
-        }
-
-        // 测试数据
-        const testDates = [
-            '2026-01-06T17:46:31.000Z',
-            '2026-01-26T10:30:15.123Z',
-            '2025-12-25T23:59:59.999Z',
-            null,
-            undefined,
-            ''
-        ]
-
-        const resultsDiv = document.getElementById('results')
-        
-        testDates.forEach((date, index) => {
-            const formatted = formatDateTime(date)
-            const p = document.createElement('p')
-            p.innerHTML = `<strong>Test ${index + 1}:</strong> Input: ${date} → Output: ${formatted}`
-            resultsDiv.appendChild(p)
-        })
-
-        // 期望的输出格式示例
-        const expectedP = document.createElement('p')
-        expectedP.innerHTML = '<strong>Expected format:</strong> 2026-01-06 17:46:31'
-        expectedP.style.color = 'green'
-        expectedP.style.fontWeight = 'bold'
-        resultsDiv.appendChild(expectedP)
-    </script>
-</body>
-</html>