Diamond_ore 3 месяцев назад
Родитель
Сommit
0b9c57a3e8

+ 1 - 0
src/api/user.ts

@@ -16,6 +16,7 @@ export interface UserProfile {
   created_at: string
   updated_at: string
   last_login_at?: string
+  roles?: string[]
 }
 
 export interface UpdateProfileRequest {

+ 648 - 0
src/components/MenuPermissionTree.vue

@@ -0,0 +1,648 @@
+<template>
+  <div class="menu-permission-tree">
+    <div class="tree-header">
+      <div class="tree-actions">
+        <el-button size="small" @click="expandAll">
+          <el-icon><Expand /></el-icon>
+          展开全部
+        </el-button>
+        <el-button size="small" @click="collapseAll">
+          <el-icon><Fold /></el-icon>
+          收起全部
+        </el-button>
+        <el-button size="small" @click="checkAll">
+          <el-icon><Check /></el-icon>
+          全选
+        </el-button>
+        <el-button size="small" @click="uncheckAll">
+          <el-icon><Close /></el-icon>
+          全不选
+        </el-button>
+      </div>
+      <div class="tree-stats">
+        <span class="stats-text">
+          已选择: <strong>{{ checkedCount }}</strong> / {{ totalCount }} 项
+        </span>
+      </div>
+    </div>
+    
+    <div class="tree-container">
+      <el-tree
+        ref="treeRef"
+        :data="treeData"
+        :props="treeProps"
+        show-checkbox
+        node-key="id"
+        :default-expand-all="false"
+        :default-checked-keys="defaultCheckedKeys"
+        :check-strictly="true"
+        @check="handleCheck"
+        @check-change="handleCheckChange"
+        class="permission-tree"
+      >
+        <template #default="{ node, data }">
+          <div class="tree-node" :class="getNodeClass(data)">
+            <div class="node-content">
+              <!-- 层级指示器 -->
+              <div class="level-indicator" :class="getLevelClass(data)">
+                <div class="level-bar" :class="getLevelBarClass(data)"></div>
+              </div>
+              
+              <!-- 节点图标 -->
+              <el-icon class="node-icon" :class="getIconClass(data)">
+                <component :is="getNodeIcon(data)" />
+              </el-icon>
+              
+              <!-- 节点标题 -->
+              <span class="node-title" :class="getTitleClass(data)">
+                {{ data.title }}
+              </span>
+              
+              <!-- 节点类型标签 -->
+              <el-tag 
+                v-if="data.menu_type === 'button'" 
+                type="warning" 
+                size="small"
+                class="node-type-tag"
+              >
+                按钮权限
+              </el-tag>
+              <el-tag 
+                v-else-if="data.menu_type === 'menu' && data.parent_id" 
+                type="success" 
+                size="small"
+                class="node-type-tag"
+              >
+                功能菜单
+              </el-tag>
+              <el-tag 
+                v-else-if="data.menu_type === 'menu' && !data.parent_id" 
+                type="primary" 
+                size="small"
+                class="node-type-tag"
+              >
+                主菜单
+              </el-tag>
+              
+              <!-- 节点描述 -->
+              <span v-if="data.description" class="node-description">
+                {{ data.description }}
+              </span>
+            </div>
+          </div>
+        </template>
+      </el-tree>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, nextTick } from 'vue'
+import { Expand, Fold, Check, Close } from '@element-plus/icons-vue'
+import * as ElementPlusIcons from '@element-plus/icons-vue'
+
+// Props
+interface Props {
+  menuData: any[]
+  checkedKeys?: string[]
+  disabled?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  menuData: () => [],
+  checkedKeys: () => [],
+  disabled: false
+})
+
+// Emits
+const emit = defineEmits<{
+  'update:checkedKeys': [keys: string[]]
+  'check': [data: any, checked: boolean, indeterminate: boolean]
+  'check-change': [data: any, checkedKeys: string[], checkedNodes: any[]]
+}>()
+
+// Refs
+const treeRef = ref()
+
+// Tree props
+const treeProps = {
+  children: 'children',
+  label: 'title',
+  disabled: () => props.disabled
+}
+
+// 默认选中的keys
+const defaultCheckedKeys = ref<string[]>([...props.checkedKeys])
+
+// 监听外部传入的checkedKeys变化
+watch(() => props.checkedKeys, (newKeys) => {
+  console.log('🔍 MenuPermissionTree: checkedKeys changed to:', newKeys)
+  console.log('📊 MenuPermissionTree: checkedKeys count:', newKeys.length)
+  
+  defaultCheckedKeys.value = [...newKeys]
+  if (treeRef.value) {
+    console.log('🎯 MenuPermissionTree: Setting tree checked keys to:', newKeys)
+    treeRef.value.setCheckedKeys(newKeys)
+  }
+}, { deep: true })
+
+// 构建树形数据
+const treeData = computed(() => {
+  const tree = buildMenuTree(props.menuData)
+  console.log('🌳 MenuPermissionTree: Built tree data with', tree.length, 'root nodes')
+  console.log('📋 MenuPermissionTree: Total menu data items:', props.menuData.length)
+  return tree
+})
+
+// 统计信息
+const totalCount = computed(() => {
+  return countAllNodes(treeData.value)
+})
+
+const checkedCount = computed(() => {
+  if (!treeRef.value) return 0
+  return treeRef.value.getCheckedKeys().length
+})
+
+// 构建菜单树
+const buildMenuTree = (menuList: any[]) => {
+  if (!menuList || menuList.length === 0) {
+    return []
+  }
+  
+  // 创建菜单映射
+  const menuMap = new Map()
+  const tree: any[] = []
+  
+  // 先创建所有菜单节点
+  menuList.forEach(menu => {
+    menuMap.set(menu.id, { 
+      ...menu, 
+      children: [],
+      hasChildren: false
+    })
+  })
+  
+  // 按层级和sort_order排序
+  const sortedMenus = [...menuList].sort((a, b) => {
+    // 先按层级排序(主菜单优先)
+    const aLevel = a.parent_id ? 1 : 0
+    const bLevel = b.parent_id ? 1 : 0
+    if (aLevel !== bLevel) {
+      return aLevel - bLevel
+    }
+    // 同层级按sort_order排序
+    return (a.sort_order || 0) - (b.sort_order || 0)
+  })
+  
+  // 构建树结构
+  sortedMenus.forEach(menu => {
+    const menuNode = menuMap.get(menu.id)
+    if (menu.parent_id && menuMap.has(menu.parent_id)) {
+      const parentNode = menuMap.get(menu.parent_id)
+      parentNode.children.push(menuNode)
+      parentNode.hasChildren = true
+    } else {
+      tree.push(menuNode)
+    }
+  })
+  
+  // 对每个节点的children进行排序
+  const sortChildren = (node: any) => {
+    if (node.children && node.children.length > 0) {
+      // 先按菜单类型排序(menu在前,button在后),再按sort_order排序
+      node.children.sort((a: any, b: any) => {
+        if (a.menu_type !== b.menu_type) {
+          return a.menu_type === 'menu' ? -1 : 1
+        }
+        return (a.sort_order || 0) - (b.sort_order || 0)
+      })
+      
+      // 递归排序子节点
+      node.children.forEach(child => sortChildren(child))
+    }
+  }
+  
+  tree.forEach(node => sortChildren(node))
+  
+  return tree
+}
+
+// 统计所有节点数量
+const countAllNodes = (nodes: any[]): number => {
+  let count = 0
+  nodes.forEach(node => {
+    count++
+    if (node.children && node.children.length > 0) {
+      count += countAllNodes(node.children)
+    }
+  })
+  return count
+}
+
+// 获取节点层级
+const getNodeLevel = (data: any) => {
+  if (!data.parent_id) {
+    return 0  // 主菜单
+  } else {
+    // 查找父菜单是否也有父菜单
+    const parentMenu = props.menuData.find(m => m.id === data.parent_id)
+    if (parentMenu && parentMenu.parent_id) {
+      return 2  // 第三级(按钮权限)
+    } else {
+      return 1  // 第二级(子菜单)
+    }
+  }
+}
+
+// 获取节点样式类
+const getNodeClass = (data: any) => {
+  const level = getNodeLevel(data)
+  return `tree-node-level-${level}`
+}
+
+const getLevelClass = (data: any) => {
+  const level = getNodeLevel(data)
+  return `level-${level}`
+}
+
+const getLevelBarClass = (data: any) => {
+  const level = getNodeLevel(data)
+  switch (level) {
+    case 0: return 'level-bar-main'
+    case 1: return 'level-bar-sub'
+    case 2: return 'level-bar-button'
+    default: return 'level-bar-default'
+  }
+}
+
+const getIconClass = (data: any) => {
+  const level = getNodeLevel(data)
+  switch (level) {
+    case 0: return 'icon-main'
+    case 1: return 'icon-sub'
+    case 2: return 'icon-button'
+    default: return 'icon-default'
+  }
+}
+
+const getTitleClass = (data: any) => {
+  const level = getNodeLevel(data)
+  switch (level) {
+    case 0: return 'title-main'
+    case 1: return 'title-sub'
+    case 2: return 'title-button'
+    default: return 'title-default'
+  }
+}
+
+// 获取节点图标
+const getNodeIcon = (data: any) => {
+  if (data.icon && (ElementPlusIcons as any)[data.icon]) {
+    return (ElementPlusIcons as any)[data.icon]
+  }
+  
+  if (data.menu_type === 'button') {
+    return 'Operation'
+  } else if (data.parent_id) {
+    return 'Document'
+  } else {
+    return 'Folder'
+  }
+}
+
+// 展开/收起操作
+const expandAll = () => {
+  if (treeRef.value) {
+    const expandNode = (nodes: any[]) => {
+      nodes.forEach(node => {
+        if (node.children && node.children.length > 0) {
+          treeRef.value.store.nodesMap[node.id].expanded = true
+          expandNode(node.children)
+        }
+      })
+    }
+    expandNode(treeData.value)
+  }
+}
+
+const collapseAll = () => {
+  if (treeRef.value) {
+    const collapseNode = (nodes: any[]) => {
+      nodes.forEach(node => {
+        if (node.children && node.children.length > 0) {
+          treeRef.value.store.nodesMap[node.id].expanded = false
+          collapseNode(node.children)
+        }
+      })
+    }
+    collapseNode(treeData.value)
+  }
+}
+
+// 全选/全不选操作
+const checkAll = () => {
+  if (treeRef.value) {
+    const allKeys = getAllNodeKeys(treeData.value)
+    treeRef.value.setCheckedKeys(allKeys)
+  }
+}
+
+const uncheckAll = () => {
+  if (treeRef.value) {
+    treeRef.value.setCheckedKeys([])
+  }
+}
+
+// 获取所有节点的key
+const getAllNodeKeys = (nodes: any[]): string[] => {
+  const keys: string[] = []
+  nodes.forEach(node => {
+    keys.push(node.id)
+    if (node.children && node.children.length > 0) {
+      keys.push(...getAllNodeKeys(node.children))
+    }
+  })
+  return keys
+}
+
+// 复选框事件处理
+const handleCheck = (data: any, checked: any) => {
+  const checkedKeys = treeRef.value.getCheckedKeys()
+  emit('update:checkedKeys', checkedKeys)
+  emit('check', data, checked.checkedKeys.includes(data.id), checked.halfCheckedKeys.includes(data.id))
+}
+
+const handleCheckChange = (data: any, checked: boolean, indeterminate: boolean) => {
+  const checkedKeys = treeRef.value.getCheckedKeys()
+  const checkedNodes = treeRef.value.getCheckedNodes()
+  emit('update:checkedKeys', checkedKeys)
+  emit('check-change', data, checkedKeys, checkedNodes)
+}
+
+// 暴露方法给父组件
+defineExpose({
+  getCheckedKeys: () => treeRef.value?.getCheckedKeys() || [],
+  getCheckedNodes: () => treeRef.value?.getCheckedNodes() || [],
+  getHalfCheckedKeys: () => treeRef.value?.getHalfCheckedKeys() || [],
+  getHalfCheckedNodes: () => treeRef.value?.getHalfCheckedNodes() || [],
+  setCheckedKeys: (keys: string[]) => treeRef.value?.setCheckedKeys(keys),
+  setCheckedNodes: (nodes: any[]) => treeRef.value?.setCheckedNodes(nodes),
+  expandAll,
+  collapseAll,
+  checkAll,
+  uncheckAll
+})
+</script>
+
+<style scoped>
+.menu-permission-tree {
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  background: #fff;
+}
+
+.tree-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  border-bottom: 1px solid #e4e7ed;
+  background: #f8f9fa;
+  border-radius: 8px 8px 0 0;
+}
+
+.tree-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.tree-stats {
+  display: flex;
+  align-items: center;
+}
+
+.stats-text {
+  font-size: 14px;
+  color: #606266;
+}
+
+.tree-container {
+  padding: 16px;
+  max-height: 500px;
+  overflow-y: auto;
+}
+
+.permission-tree {
+  background: transparent;
+}
+
+.tree-node {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  padding: 4px 0;
+}
+
+.node-content {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex: 1;
+  min-height: 28px;
+}
+
+/* 层级指示器 */
+.level-indicator {
+  display: flex;
+  align-items: center;
+  margin-right: 4px;
+}
+
+.level-bar {
+  width: 3px;
+  height: 16px;
+  border-radius: 2px;
+}
+
+.level-bar-main {
+  background: linear-gradient(135deg, #409eff, #66b1ff);
+  box-shadow: 0 1px 3px rgba(64, 158, 255, 0.3);
+}
+
+.level-bar-sub {
+  background: linear-gradient(135deg, #67c23a, #85ce61);
+  box-shadow: 0 1px 3px rgba(103, 194, 58, 0.3);
+}
+
+.level-bar-button {
+  background: linear-gradient(135deg, #e6a23c, #ebb563);
+  box-shadow: 0 1px 3px rgba(230, 162, 60, 0.3);
+}
+
+.level-bar-default {
+  background: #909399;
+}
+
+/* 节点图标 */
+.node-icon {
+  flex-shrink: 0;
+  transition: all 0.3s ease;
+}
+
+.icon-main {
+  color: #409eff;
+  font-size: 18px;
+  font-weight: bold;
+}
+
+.icon-sub {
+  color: #67c23a;
+  font-size: 16px;
+}
+
+.icon-button {
+  color: #e6a23c;
+  font-size: 14px;
+}
+
+.icon-default {
+  color: #909399;
+  font-size: 14px;
+}
+
+/* 节点标题 */
+.node-title {
+  font-weight: 500;
+  flex: 1;
+  transition: all 0.3s ease;
+}
+
+.title-main {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.title-sub {
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.title-button {
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+
+.title-default {
+  font-size: 14px;
+  color: #606266;
+}
+
+/* 节点类型标签 */
+.node-type-tag {
+  margin-left: 8px;
+  flex-shrink: 0;
+  font-size: 11px;
+  padding: 1px 5px;
+}
+
+/* 节点描述 */
+.node-description {
+  font-size: 12px;
+  color: #909399;
+  margin-left: 8px;
+  flex-shrink: 0;
+}
+
+/* 层级样式 */
+.tree-node-level-0 {
+  background: linear-gradient(90deg, rgba(64, 158, 255, 0.03), transparent);
+  border-radius: 4px;
+  margin: 2px 0;
+}
+
+.tree-node-level-1 {
+  background: linear-gradient(90deg, rgba(103, 194, 58, 0.03), transparent);
+  border-radius: 4px;
+  margin: 1px 0;
+}
+
+.tree-node-level-2 {
+  background: linear-gradient(90deg, rgba(230, 162, 60, 0.03), transparent);
+  border-radius: 4px;
+  margin: 1px 0;
+}
+
+/* 悬停效果 */
+.tree-node:hover {
+  background: rgba(64, 158, 255, 0.05);
+  border-radius: 4px;
+}
+
+.tree-node:hover .node-title {
+  color: #409eff;
+}
+
+.tree-node:hover .level-bar {
+  transform: scale(1.1);
+}
+
+/* Element Plus Tree 样式覆盖 */
+:deep(.el-tree-node__content) {
+  padding: 4px 0;
+  height: auto;
+  min-height: 32px;
+}
+
+:deep(.el-tree-node__expand-icon) {
+  color: #409eff;
+  font-size: 14px;
+  transition: all 0.3s ease;
+}
+
+:deep(.el-tree-node__expand-icon:hover) {
+  color: #66b1ff;
+  transform: scale(1.1);
+}
+
+:deep(.el-tree-node__expand-icon.expanded) {
+  color: #67c23a;
+  transform: rotate(90deg);
+}
+
+:deep(.el-checkbox) {
+  margin-right: 8px;
+}
+
+:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+
+:deep(.el-checkbox__input.is-indeterminate .el-checkbox__inner) {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+
+/* 滚动条样式 */
+.tree-container::-webkit-scrollbar {
+  width: 6px;
+}
+
+.tree-container::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.tree-container::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.tree-container::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+</style>

+ 164 - 38
src/layouts/MainLayout.vue

@@ -42,45 +42,46 @@
             :default-active="activeMenu"
             class="sidebar-menu"
             router
+            v-loading="menuLoading"
           >
-            <el-menu-item index="/dashboard">
-              <el-icon><House /></el-icon>
-              <span>仪表盘</span>
-            </el-menu-item>
-            <el-menu-item index="/profile">
-              <el-icon><User /></el-icon>
-              <span>个人资料</span>
-            </el-menu-item>
-            <el-menu-item index="/apps">
-              <el-icon><Grid /></el-icon>
-              <span>我的应用</span>
-            </el-menu-item>
-            <el-sub-menu v-if="authStore.isAdmin" index="/admin">
-              <template #title>
-                <el-icon><Setting /></el-icon>
-                <span>系统管理</span>
+            <!-- 动态渲染菜单 -->
+            <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 #title>
+                    <el-icon v-if="menu.icon">
+                      <component :is="getIconComponent(menu.icon)" />
+                    </el-icon>
+                    <span>{{ menu.title }}</span>
+                  </template>
+                  <el-menu-item 
+                    v-for="child in getMenuChildren(menu)" 
+                    :key="child.id"
+                    :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>
+                </el-sub-menu>
+                
+                <!-- 没有子菜单的情况(具体菜单页面) -->
+                <el-menu-item 
+                  v-else 
+                  :index="menu.path || menu.name"
+                  v-show="!menu.is_hidden"
+                >
+                  <el-icon v-if="menu.icon">
+                    <component :is="getIconComponent(menu.icon)" />
+                  </el-icon>
+                  <span>{{ menu.title }}</span>
+                </el-menu-item>
               </template>
-              <el-menu-item index="/admin/dashboard">
-                <el-icon><Monitor /></el-icon>
-                <span>管理概览</span>
-              </el-menu-item>
-              <el-menu-item index="/admin/users">
-                <el-icon><UserFilled /></el-icon>
-                <span>用户管理</span>
-              </el-menu-item>
-              <el-menu-item index="/admin/apps">
-                <el-icon><Grid /></el-icon>
-                <span>应用管理</span>
-              </el-menu-item>
-              <el-menu-item index="/admin/logs">
-                <el-icon><Document /></el-icon>
-                <span>系统日志</span>
-              </el-menu-item>
-              <el-menu-item index="/admin/settings">
-                <el-icon><Tools /></el-icon>
-                <span>系统设置</span>
-              </el-menu-item>
-            </el-sub-menu>
+            </template>
           </el-menu>
         </el-aside>
 
@@ -94,15 +95,21 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
+import { computed, ref, onMounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useAuthStore } from '@/stores/auth'
+import request from '@/api/request'
+import * as ElementPlusIcons from '@element-plus/icons-vue'
 
 const router = useRouter()
 const route = useRoute()
 const authStore = useAuthStore()
 
+// 响应式数据
+const userMenus = ref<any[]>([])
+const menuLoading = ref(false)
+
 const activeMenu = computed(() => {
   // 处理子路由的菜单激活状态
   const path = route.path
@@ -115,6 +122,120 @@ const activeMenu = computed(() => {
   return path
 })
 
+// 获取图标组件
+const getIconComponent = (iconName: string) => {
+  return (ElementPlusIcons as any)[iconName] || ElementPlusIcons.Menu
+}
+
+// 检查是否有菜单类型的子项
+const hasMenuChildren = (menu: any) => {
+  return menu.children && menu.children.some((child: any) => child.menu_type === 'menu')
+}
+
+// 获取菜单类型的子项
+const getMenuChildren = (menu: any) => {
+  return menu.children ? menu.children.filter((child: any) => child.menu_type === 'menu') : []
+}
+
+// 获取用户菜单
+const loadUserMenus = async () => {
+  menuLoading.value = true
+  try {
+    const result = await request.get('/api/v1/user/menus')
+    if (result.code === 0) {
+      userMenus.value = result.data
+      console.log('用户菜单加载成功:', result.data)
+    } else {
+      throw new Error(result.message || '获取菜单失败')
+    }
+  } catch (error) {
+    console.error('获取用户菜单失败:', error)
+    ElMessage.error('获取菜单失败,请刷新页面重试')
+    
+    // 如果获取菜单失败,使用默认菜单
+    userMenus.value = getDefaultMenus()
+  } finally {
+    menuLoading.value = false
+  }
+}
+
+// 获取默认菜单(兜底方案)
+const getDefaultMenus = () => {
+  const defaultMenus = [
+    {
+      id: 'dashboard',
+      name: 'dashboard',
+      title: '仪表盘',
+      path: '/dashboard',
+      icon: 'House',
+      children: []
+    },
+    {
+      id: 'profile',
+      name: 'profile', 
+      title: '个人资料',
+      path: '/profile',
+      icon: 'User',
+      children: []
+    },
+    {
+      id: 'apps',
+      name: 'apps',
+      title: '我的应用',
+      path: '/apps',
+      icon: 'Grid',
+      children: []
+    }
+  ]
+  
+  // 如果是管理员,添加系统管理菜单
+  if (authStore.isAdmin) {
+    defaultMenus.push({
+      id: 'admin',
+      name: 'admin',
+      title: '系统管理',
+      path: '/admin',
+      icon: 'Setting',
+      children: [
+        {
+          id: 'admin-dashboard',
+          name: 'admin-dashboard',
+          title: '管理概览',
+          path: '/admin/dashboard',
+          icon: 'Monitor',
+          is_hidden: false
+        },
+        {
+          id: 'admin-users',
+          name: 'admin-users',
+          title: '用户管理',
+          path: '/admin/users',
+          icon: 'UserFilled',
+          is_hidden: false
+        },
+        {
+          id: 'admin-roles',
+          name: 'admin-roles',
+          title: '角色管理',
+          path: '/admin/roles',
+          icon: 'Avatar',
+          is_hidden: false
+        },
+        {
+          id: 'admin-menus',
+          name: 'admin-menus',
+          title: '菜单管理',
+          path: '/admin/menus',
+          icon: 'Menu',
+          is_hidden: false
+        }
+      ]
+    })
+  }
+  
+  return defaultMenus
+}
+
 // 处理下拉菜单命令
 const handleCommand = async (command: string) => {
   switch (command) {
@@ -141,6 +262,11 @@ const handleCommand = async (command: string) => {
       break
   }
 }
+
+onMounted(() => {
+  // 加载用户菜单
+  loadUserMenus()
+})
 </script>
 
 <style scoped>

+ 24 - 2
src/main.ts

@@ -7,6 +7,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
 
 import App from './App.vue'
 import router from './router'
+import { useAuthStore } from '@/stores/auth'
 
 const app = createApp(App)
 
@@ -15,8 +16,29 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
   app.component(key, component)
 }
 
-app.use(createPinia())
+const pinia = createPinia()
+app.use(pinia)
 app.use(router)
 app.use(ElementPlus)
 
-app.mount('#app')
+// 初始化认证状态
+const initAuth = async () => {
+  const authStore = useAuthStore()
+  
+  // 如果有token但没有用户信息,尝试获取用户信息
+  if (authStore.token && !authStore.user) {
+    try {
+      await authStore.checkAuth()
+    } catch (error) {
+      console.error('初始化认证状态失败:', error)
+    }
+  }
+}
+
+// 在应用挂载前初始化认证状态
+initAuth().then(() => {
+  app.mount('#app')
+}).catch((error) => {
+  console.error('应用初始化失败:', error)
+  app.mount('#app')
+})

+ 43 - 13
src/router/index.ts

@@ -40,16 +40,6 @@ const routes: RouteRecordRaw[] = [
         name: 'Profile',
         component: () => import('@/views/user/Profile.vue')
       },
-      {
-        path: 'apps',
-        name: 'Apps',
-        component: () => import('@/views/apps/Index.vue')
-      },
-      {
-        path: 'apps/create',
-        name: 'CreateApp',
-        component: () => import('@/views/apps/Create.vue')
-      },
       {
         path: 'admin',
         redirect: '/admin/dashboard'
@@ -66,6 +56,24 @@ const routes: RouteRecordRaw[] = [
         component: () => import('@/views/admin/Users.vue'),
         meta: { requiresAdmin: true }
       },
+      {
+        path: 'admin/roles',
+        name: 'AdminRoles',
+        component: () => import('@/views/admin/Roles.vue'),
+        meta: { requiresAdmin: true }
+      },
+      {
+        path: 'admin/menus',
+        name: 'AdminMenus',
+        component: () => import('@/views/admin/Menus.vue'),
+        meta: { requiresAdmin: true }
+      },
+      {
+        path: 'admin/permissions',
+        name: 'AdminPermissions',
+        component: () => import('@/views/admin/Permissions.vue'),
+        meta: { requiresAdmin: true }
+      },
       {
         path: 'admin/apps',
         name: 'AdminApps',
@@ -107,6 +115,15 @@ const router = createRouter({
 router.beforeEach(async (to, from, next) => {
   const authStore = useAuthStore()
   
+  // 如果有token但没有用户信息,先尝试获取用户信息
+  if (authStore.token && !authStore.user && !authStore.loading) {
+    try {
+      await authStore.checkAuth()
+    } catch (error) {
+      console.error('路由守卫检查认证状态失败:', error)
+    }
+  }
+  
   // 检查是否需要认证
   if (to.meta.requiresAuth) {
     if (!authStore.isAuthenticated) {
@@ -119,9 +136,22 @@ router.beforeEach(async (to, from, next) => {
     }
     
     // 检查是否需要管理员权限
-    if (to.meta.requiresAdmin && !authStore.isAdmin) {
-      next({ name: 'Unauthorized' })
-      return
+    if (to.meta.requiresAdmin) {
+      // For admin routes, check permissions
+      if (authStore.isAdmin) {
+        // Super admin or admin users can access all admin routes
+        next()
+        return
+      } else {
+        // For non-admin users, check if they have menu access to this specific path
+        if (authStore.hasPathAccess(to.path)) {
+          next()
+          return
+        } else {
+          next({ name: 'Unauthorized' })
+          return
+        }
+      }
     }
   }
   

+ 55 - 2
src/stores/auth.ts

@@ -4,15 +4,43 @@ import type { User, LoginForm, TokenResponse } from '@/types/auth'
 import { authApi } from '@/api/auth'
 import { userApi, type UserProfile } from '@/api/user'
 import { getToken, setToken, removeToken } from '@/utils/auth'
+import request from '@/api/request'
 
 export const useAuthStore = defineStore('auth', () => {
   const user = ref<UserProfile | null>(null)
   const token = ref<string | null>(getToken())
   const loading = ref(false)
+  const userMenus = ref<any[]>([])
 
   // 计算属性
   const isAuthenticated = computed(() => !!token.value && !!user.value)
-  const isAdmin = computed(() => user.value?.is_superuser || false)
+  const isAdmin = computed(() => {
+    if (!user.value) return false
+    // Check if user is superuser OR has admin/super_admin roles
+    return user.value.is_superuser || 
+           (user.value.roles && user.value.roles.some((role: string) => 
+             role === 'admin' || role === 'super_admin'
+           ))
+  })
+
+  // 检查用户是否有访问特定路径的权限
+  const hasPathAccess = (targetPath: string): boolean => {
+    if (isAdmin.value) return true // Admin users have access to all paths
+    
+    const checkMenuAccess = (menus: any[], path: string): boolean => {
+      for (const menu of menus) {
+        if (menu.path === path && menu.menu_type === 'menu') {
+          return true
+        }
+        if (menu.children && checkMenuAccess(menu.children, path)) {
+          return true
+        }
+      }
+      return false
+    }
+    
+    return checkMenuAccess(userMenus.value, targetPath)
+  }
 
   // 登录
   const login = async (loginForm: LoginForm): Promise<void> => {
@@ -44,6 +72,7 @@ export const useAuthStore = defineStore('auth', () => {
       // 清除本地数据
       user.value = null
       token.value = null
+      userMenus.value = []
       removeToken()
     }
   }
@@ -55,6 +84,9 @@ export const useAuthStore = defineStore('auth', () => {
     try {
       const response = await userApi.getProfile()
       user.value = response.data
+      
+      // 同时获取用户菜单权限
+      await fetchUserMenus()
     } catch (error) {
       console.error('获取用户信息失败:', error)
       // 如果获取用户信息失败,可能token已过期,清除登录状态
@@ -63,14 +95,32 @@ export const useAuthStore = defineStore('auth', () => {
     }
   }
 
+  // 获取用户菜单权限
+  const fetchUserMenus = async (): Promise<void> => {
+    if (!token.value) return
+    
+    try {
+      const response = await request.get('/api/v1/user/menus')
+      if (response.code === 0) {
+        userMenus.value = response.data
+      }
+    } catch (error) {
+      console.error('获取用户菜单失败:', error)
+      userMenus.value = []
+    }
+  }
+
   // 检查认证状态
   const checkAuth = async (): Promise<void> => {
-    if (token.value && !user.value) {
+    const storedToken = getToken()
+    if (storedToken && !user.value) {
+      token.value = storedToken // 确保token被设置
       try {
         await fetchUserInfo()
       } catch (error) {
         // 认证失败,清除token
         await logout()
+        throw error
       }
     }
   }
@@ -108,11 +158,14 @@ export const useAuthStore = defineStore('auth', () => {
     user,
     token,
     loading,
+    userMenus,
     isAuthenticated,
     isAdmin,
+    hasPathAccess,
     login,
     logout,
     fetchUserInfo,
+    fetchUserMenus,
     checkAuth,
     refreshToken,
     updateProfile

Разница между файлами не показана из-за своего большого размера
+ 1003 - 10
src/views/admin/Apps.vue


+ 519 - 21
src/views/admin/Logs.vue

@@ -5,19 +5,453 @@
       <p>查看系统操作日志和安全事件</p>
     </div>
 
-    <el-card>
-      <div class="coming-soon">
-        <el-icon size="64" color="#E6A23C"><Document /></el-icon>
-        <h3>系统日志功能</h3>
-        <p>此功能正在开发中,敬请期待...</p>
-        <el-button type="primary" @click="$router.go(-1)">返回</el-button>
+    <!-- 操作栏 -->
+    <div class="toolbar">
+      <div class="toolbar-left">
+        <el-button @click="refreshLogs">
+          <el-icon><Refresh /></el-icon>
+          刷新
+        </el-button>
+        <el-button @click="exportLogs">
+          <el-icon><Download /></el-icon>
+          导出日志
+        </el-button>
       </div>
-    </el-card>
+      <div class="toolbar-right">
+        <el-select
+          v-model="logTypeFilter"
+          placeholder="日志类型"
+          style="width: 120px; margin-right: 12px;"
+          @change="handleTypeFilter"
+          clearable
+        >
+          <el-option label="全部类型" value="" />
+          <el-option label="登录" value="login" />
+          <el-option label="登出" value="logout" />
+          <el-option label="授权" value="oauth" />
+          <el-option label="应用操作" value="app" />
+          <el-option label="用户管理" value="user" />
+          <el-option label="系统配置" value="system" />
+        </el-select>
+        <el-select
+          v-model="levelFilter"
+          placeholder="日志级别"
+          style="width: 100px; margin-right: 12px;"
+          @change="handleLevelFilter"
+          clearable
+        >
+          <el-option label="全部级别" value="" />
+          <el-option label="信息" value="info" />
+          <el-option label="警告" value="warning" />
+          <el-option label="错误" value="error" />
+        </el-select>
+        <el-date-picker
+          v-model="dateRange"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          format="YYYY-MM-DD HH:mm:ss"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          @change="handleDateFilter"
+          style="width: 350px; margin-right: 12px;"
+        />
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索用户或操作..."
+          style="width: 250px"
+          @input="handleSearch"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </div>
+
+    <!-- 日志列表 -->
+    <el-table
+      v-loading="loading"
+      :data="logs"
+      style="width: 100%"
+      :default-sort="{ prop: 'created_at', order: 'descending' }"
+    >
+      <el-table-column prop="created_at" label="时间" width="180" sortable>
+        <template #default="{ row }">
+          <div class="time-info">
+            {{ formatDateTime(row.created_at) }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="level" label="级别" width="80" align="center">
+        <template #default="{ row }">
+          <el-tag :type="getLevelTagType(row.level)" size="small">
+            {{ getLevelLabel(row.level) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="type" label="类型" width="100">
+        <template #default="{ row }">
+          <el-tag :type="getTypeTagType(row.type)" size="small">
+            {{ getTypeLabel(row.type) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="username" label="用户" width="120">
+        <template #default="{ row }">
+          <span v-if="row.username">{{ row.username }}</span>
+          <span v-else class="text-muted">系统</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="action" label="操作" min-width="200">
+        <template #default="{ row }">
+          <div class="action-info">
+            <span class="action-text">{{ row.action }}</span>
+            <el-tag v-if="row.resource" type="info" size="small" style="margin-left: 8px;">
+              {{ row.resource }}
+            </el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="ip_address" label="IP地址" width="140">
+        <template #default="{ row }">
+          <code class="ip-address">{{ row.ip_address || '-' }}</code>
+        </template>
+      </el-table-column>
+      <el-table-column prop="user_agent" label="用户代理" min-width="200" show-overflow-tooltip>
+        <template #default="{ row }">
+          <span class="user-agent">{{ row.user_agent || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="status" label="状态" width="80" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.status === 'success' ? 'success' : 'danger'" size="small">
+            {{ row.status === 'success' ? '成功' : '失败' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right">
+        <template #default="{ row }">
+          <el-button type="primary" size="small" @click="viewLogDetail(row)">
+            详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="pagination">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 日志详情对话框 -->
+    <el-dialog
+      v-model="showDetailDialog"
+      title="日志详情"
+      width="800px"
+    >
+      <div v-if="currentLog">
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="时间">
+            {{ formatDateTime(currentLog.created_at) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="级别">
+            <el-tag :type="getLevelTagType(currentLog.level)" size="small">
+              {{ getLevelLabel(currentLog.level) }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="类型">
+            <el-tag :type="getTypeTagType(currentLog.type)" size="small">
+              {{ getTypeLabel(currentLog.type) }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="用户">
+            {{ currentLog.username || '系统' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="操作" :span="2">
+            {{ currentLog.action }}
+          </el-descriptions-item>
+          <el-descriptions-item label="资源">
+            {{ currentLog.resource || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag :type="currentLog.status === 'success' ? 'success' : 'danger'" size="small">
+              {{ currentLog.status === 'success' ? '成功' : '失败' }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="IP地址">
+            <code>{{ currentLog.ip_address || '-' }}</code>
+          </el-descriptions-item>
+          <el-descriptions-item label="会话ID">
+            <code>{{ currentLog.session_id || '-' }}</code>
+          </el-descriptions-item>
+          <el-descriptions-item label="用户代理" :span="2">
+            <div class="user-agent-detail">
+              {{ currentLog.user_agent || '-' }}
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="详细信息" :span="2" v-if="currentLog.details">
+            <el-input
+              :value="formatLogDetails(currentLog.details)"
+              type="textarea"
+              :rows="6"
+              readonly
+            />
+          </el-descriptions-item>
+        </el-descriptions>
+      </div>
+      <template #footer>
+        <el-button @click="showDetailDialog = false">关闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-// 系统日志页面
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Search, Refresh, Download } from '@element-plus/icons-vue'
+import request from '@/api/request'
+
+// 响应式数据
+const loading = ref(false)
+const logs = ref<any[]>([])
+const searchKeyword = ref('')
+const logTypeFilter = ref('')
+const levelFilter = ref('')
+const dateRange = ref<[string, string] | null>(null)
+const currentPage = ref(1)
+const pageSize = ref(20)
+const total = ref(0)
+
+// 对话框状态
+const showDetailDialog = ref(false)
+const currentLog = ref<any>(null)
+
+// 获取级别标签类型
+const getLevelTagType = (level: string) => {
+  const types: Record<string, string> = {
+    info: 'primary',
+    warning: 'warning',
+    error: 'danger'
+  }
+  return types[level] || 'default'
+}
+
+// 获取级别标签文本
+const getLevelLabel = (level: string) => {
+  const labels: Record<string, string> = {
+    info: '信息',
+    warning: '警告',
+    error: '错误'
+  }
+  return labels[level] || level
+}
+
+// 获取类型标签类型
+const getTypeTagType = (type: string) => {
+  const types: Record<string, string> = {
+    login: 'success',
+    logout: 'info',
+    oauth: 'primary',
+    app: 'warning',
+    user: 'success',
+    system: 'danger'
+  }
+  return types[type] || 'default'
+}
+
+// 获取类型标签文本
+const getTypeLabel = (type: string) => {
+  const labels: Record<string, string> = {
+    login: '登录',
+    logout: '登出',
+    oauth: '授权',
+    app: '应用',
+    user: '用户',
+    system: '系统'
+  }
+  return labels[type] || type
+}
+
+// 加载日志列表
+const loadLogs = async () => {
+  loading.value = true
+  try {
+    // 模拟日志数据,实际应该调用API
+    const mockLogs = [
+      {
+        id: '1',
+        created_at: new Date().toISOString(),
+        level: 'info',
+        type: 'login',
+        username: 'admin',
+        action: '用户登录',
+        resource: 'auth',
+        ip_address: '127.0.0.1',
+        user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+        status: 'success',
+        session_id: 'sess_123456',
+        details: { login_method: 'password', remember_me: false }
+      },
+      {
+        id: '2',
+        created_at: new Date(Date.now() - 300000).toISOString(),
+        level: 'info',
+        type: 'oauth',
+        username: 'admin',
+        action: 'OAuth2授权',
+        resource: 'oauth',
+        ip_address: '127.0.0.1',
+        user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+        status: 'success',
+        session_id: 'sess_123456',
+        details: { client_id: 'eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY', scope: 'read profile' }
+      },
+      {
+        id: '3',
+        created_at: new Date(Date.now() - 600000).toISOString(),
+        level: 'warning',
+        type: 'login',
+        username: 'test_user',
+        action: '登录失败',
+        resource: 'auth',
+        ip_address: '192.168.1.100',
+        user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+        status: 'failed',
+        session_id: null,
+        details: { error: 'invalid_credentials', attempts: 3 }
+      },
+      {
+        id: '4',
+        created_at: new Date(Date.now() - 900000).toISOString(),
+        level: 'info',
+        type: 'app',
+        username: 'admin',
+        action: '创建应用',
+        resource: 'application',
+        ip_address: '127.0.0.1',
+        user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+        status: 'success',
+        session_id: 'sess_123456',
+        details: { app_name: '测试应用', client_id: 'new_client_123' }
+      },
+      {
+        id: '5',
+        created_at: new Date(Date.now() - 1200000).toISOString(),
+        level: 'error',
+        type: 'system',
+        username: null,
+        action: '数据库连接失败',
+        resource: 'database',
+        ip_address: null,
+        user_agent: null,
+        status: 'failed',
+        session_id: null,
+        details: { error: 'connection_timeout', retry_count: 3 }
+      }
+    ]
+    
+    logs.value = mockLogs
+    total.value = mockLogs.length
+    
+    // TODO: 实际API调用
+    // const result = await request.get('/api/v1/admin/logs', {
+    //   params: {
+    //     page: currentPage.value,
+    //     page_size: pageSize.value,
+    //     keyword: searchKeyword.value || undefined,
+    //     type: logTypeFilter.value || undefined,
+    //     level: levelFilter.value || undefined,
+    //     start_time: dateRange.value?.[0],
+    //     end_time: dateRange.value?.[1]
+    //   }
+    // })
+    
+  } catch (error) {
+    console.error('加载日志列表失败:', error)
+    ElMessage.error('加载日志列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 刷新日志
+const refreshLogs = () => {
+  loadLogs()
+}
+
+// 导出日志
+const exportLogs = () => {
+  ElMessage.info('导出功能开发中...')
+}
+
+// 搜索处理
+const handleSearch = () => {
+  currentPage.value = 1
+  loadLogs()
+}
+
+// 类型过滤
+const handleTypeFilter = () => {
+  currentPage.value = 1
+  loadLogs()
+}
+
+// 级别过滤
+const handleLevelFilter = () => {
+  currentPage.value = 1
+  loadLogs()
+}
+
+// 日期过滤
+const handleDateFilter = () => {
+  currentPage.value = 1
+  loadLogs()
+}
+
+// 分页处理
+const handleSizeChange = (size: number) => {
+  pageSize.value = size
+  currentPage.value = 1
+  loadLogs()
+}
+
+const handleCurrentChange = (page: number) => {
+  currentPage.value = page
+  loadLogs()
+}
+
+// 查看日志详情
+const viewLogDetail = (log: any) => {
+  currentLog.value = log
+  showDetailDialog.value = true
+}
+
+// 格式化日志详情
+const formatLogDetails = (details: any) => {
+  if (!details) return ''
+  return JSON.stringify(details, null, 2)
+}
+
+// 格式化日期时间
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return '-'
+  return new Date(dateTime).toLocaleString('zh-CN')
+}
+
+// 组件挂载
+onMounted(() => {
+  loadLogs()
+})
 </script>
 
 <style scoped>
@@ -26,14 +460,12 @@
 }
 
 .page-header {
-  margin-bottom: 24px;
+  margin-bottom: 20px;
 }
 
 .page-header h2 {
   margin: 0 0 8px 0;
   color: #333;
-  font-size: 24px;
-  font-weight: 600;
 }
 
 .page-header p {
@@ -42,20 +474,86 @@
   font-size: 14px;
 }
 
-.coming-soon {
-  text-align: center;
-  padding: 60px 20px;
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 }
 
-.coming-soon h3 {
-  margin: 16px 0 8px 0;
-  color: #333;
-  font-size: 20px;
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
 }
 
-.coming-soon p {
-  margin: 0 0 24px 0;
+.time-info {
+  font-size: 12px;
   color: #666;
-  font-size: 14px;
+}
+
+.action-info {
+  display: flex;
+  align-items: center;
+}
+
+.action-text {
+  font-weight: 500;
+}
+
+.ip-address {
+  background: #f5f5f5;
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 12px;
+}
+
+.user-agent {
+  font-size: 12px;
+  color: #666;
+}
+
+.user-agent-detail {
+  word-break: break-all;
+  font-size: 12px;
+  color: #666;
+}
+
+.text-muted {
+  color: #999;
+  font-style: italic;
+}
+
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-table) {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-table .el-table__cell) {
+  padding: 8px 0;
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+:deep(.el-descriptions__body .el-descriptions__table .el-descriptions__cell) {
+  padding: 8px 12px;
 }
 </style>

+ 1115 - 0
src/views/admin/Menus.vue

@@ -0,0 +1,1115 @@
+<template>
+  <div class="menus-management">
+    <div class="page-header">
+      <h2>菜单管理</h2>
+      <p>管理系统菜单结构和权限</p>
+    </div>
+
+    <!-- 操作栏 -->
+    <div class="toolbar">
+      <div class="toolbar-left">
+        <el-button type="primary" @click="showCreateDialog = true">
+          <el-icon><Plus /></el-icon>
+          创建菜单
+        </el-button>
+        <el-button @click="expandAll">
+          <el-icon><Expand /></el-icon>
+          展开全部
+        </el-button>
+        <el-button @click="collapseAll">
+          <el-icon><Fold /></el-icon>
+          收起全部
+        </el-button>
+      </div>
+      <div class="toolbar-right">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索菜单名称..."
+          style="width: 300px"
+          @input="handleSearch"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </div>
+
+    <!-- 菜单树表格 -->
+    <el-table
+      ref="menuTableRef"
+      v-loading="loading"
+      :data="menuTree"
+      style="width: 100%"
+      row-key="id"
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+      :default-expand-all="false"
+      :indent="60"
+      border
+      class="menu-tree-table"
+    >
+      <el-table-column prop="title" label="菜单名称" min-width="400">
+        <template #default="{ row }">
+          <div class="menu-info" :class="getMenuLevelClass(row)">
+            <div class="menu-content">
+              <!-- 强化层级指示器 -->
+              <div class="level-indicators">
+                <!-- 主菜单层级指示 -->
+                <div v-if="!row.parent_id" class="level-indicator level-0">
+                  <div class="level-bar main-level"></div>
+                </div>
+                
+                <!-- 子菜单层级指示 -->
+                <div v-else-if="getMenuLevel(row) === 1" class="level-indicator level-1">
+                  <div class="level-bar sub-level"></div>
+                  <div class="level-connector"></div>
+                </div>
+                
+                <!-- 按钮权限层级指示 -->
+                <div v-else-if="getMenuLevel(row) === 2" class="level-indicator level-2">
+                  <div class="level-bar button-level"></div>
+                  <div class="level-connector"></div>
+                  <div class="level-connector-deep"></div>
+                </div>
+              </div>
+              
+              <!-- 菜单图标 -->
+              <el-icon v-if="row.icon" class="menu-icon" :class="getMenuIconClass(row)">
+                <component :is="getIconComponent(row.icon)" />
+              </el-icon>
+              <el-icon v-else class="menu-icon default-icon" :class="getMenuIconClass(row)">
+                <component :is="getDefaultIcon(row)" />
+              </el-icon>
+              
+              <!-- 菜单标题 -->
+              <span class="menu-title" :class="getMenuTitleClass(row)">{{ row.title }}</span>
+              
+              <!-- 层级标识 -->
+              <span class="level-badge" :class="getLevelBadgeClass(row)">
+                {{ getLevelText(row) }}
+              </span>
+              
+              <!-- 菜单类型标签 -->
+              <el-tag 
+                v-if="row.menu_type === 'button'" 
+                type="warning" 
+                size="small"
+                class="menu-type-tag"
+              >
+                按钮权限
+              </el-tag>
+              <el-tag 
+                v-else-if="row.menu_type === 'menu' && row.parent_id" 
+                type="success" 
+                size="small"
+                class="menu-type-tag"
+              >
+                功能菜单
+              </el-tag>
+              <el-tag 
+                v-else-if="row.menu_type === 'menu' && !row.parent_id" 
+                type="primary" 
+                size="small"
+                class="menu-type-tag"
+              >
+                主菜单
+              </el-tag>
+              
+              <!-- 隐藏状态 -->
+              <el-tag 
+                v-if="row.is_hidden" 
+                type="info" 
+                size="small"
+                class="menu-type-tag"
+              >
+                隐藏
+              </el-tag>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="name" label="菜单标识" width="150" />
+      <el-table-column prop="path" label="路由路径" width="200" show-overflow-tooltip />
+      <el-table-column prop="component" label="组件路径" width="200" show-overflow-tooltip />
+      <el-table-column prop="sort_order" label="排序" width="80" align="center" />
+      <el-table-column prop="is_hidden" label="隐藏" width="80" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.is_hidden ? 'danger' : 'success'" size="small">
+            {{ row.is_hidden ? '隐藏' : '显示' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="is_active" label="状态" width="100" align="center">
+        <template #default="{ row }">
+          <el-switch
+            v-model="row.is_active"
+            @change="handleStatusChange(row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column prop="created_at" label="创建时间" width="180">
+        <template #default="{ row }">
+          {{ formatDateTime(row.created_at) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="200" fixed="right">
+        <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>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="pagination">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 创建/编辑菜单对话框 -->
+    <el-dialog
+      v-model="showCreateDialog"
+      :title="editingMenu ? '编辑菜单' : '创建菜单'"
+      width="700px"
+      @close="resetForm"
+    >
+      <el-form
+        ref="menuFormRef"
+        :model="menuForm"
+        :rules="menuRules"
+        label-width="120px"
+      >
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="菜单名称" prop="title">
+              <el-input v-model="menuForm.title" placeholder="请输入菜单显示名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="菜单标识" prop="name">
+              <el-input v-model="menuForm.name" placeholder="请输入菜单标识(英文)" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="父级菜单" prop="parent_id">
+              <el-tree-select
+                v-model="menuForm.parent_id"
+                :data="parentMenuOptions"
+                :props="{ value: 'id', label: 'title', children: 'children' }"
+                placeholder="请选择父级菜单(可选)"
+                clearable
+                check-strictly
+              />
+            </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>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="路由路径" prop="path">
+              <el-input v-model="menuForm.path" placeholder="请输入路由路径" />
+            </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>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="菜单图标" prop="icon">
+              <el-select v-model="menuForm.icon" placeholder="请选择图标" filterable>
+                <el-option
+                  v-for="icon in iconOptions"
+                  :key="icon.value"
+                  :label="icon.label"
+                  :value="icon.value"
+                >
+                  <div class="icon-option">
+                    <el-icon><component :is="getIconComponent(icon.value)" /></el-icon>
+                    <span>{{ icon.label }}</span>
+                  </div>
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="排序顺序" prop="sort_order">
+              <el-input-number v-model="menuForm.sort_order" :min="0" :max="999" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="是否隐藏">
+              <el-switch v-model="menuForm.is_hidden" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="是否启用">
+              <el-switch v-model="menuForm.is_active" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="菜单描述" prop="description">
+          <el-input
+            v-model="menuForm.description"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入菜单描述"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="saveMenu" :loading="saving">
+          {{ editingMenu ? '更新' : '创建' }}
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<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 * as ElementPlusIcons from '@element-plus/icons-vue'
+import request from '@/api/request'
+
+// 响应式数据
+const loading = ref(false)
+const saving = ref(false)
+const menus = ref<any[]>([])
+const searchKeyword = ref('')
+const currentPage = ref(1)
+const pageSize = ref(1000)  // 增大页面大小确保加载所有菜单
+const total = ref(0)
+
+// 对话框状态
+const showCreateDialog = ref(false)
+const editingMenu = ref<any>(null)
+const parentMenuId = ref<string | null>(null)
+
+// 表单数据
+const menuForm = reactive({
+  title: '',
+  name: '',
+  parent_id: null as string | null,
+  path: '',
+  component: '',
+  icon: '',
+  sort_order: 0,
+  menu_type: 'menu',
+  is_hidden: false,
+  is_active: true,
+  description: ''
+})
+
+// 表单验证规则
+const menuRules = {
+  title: [
+    { required: true, message: '请输入菜单名称', trigger: 'blur' }
+  ],
+  name: [
+    { required: true, message: '请输入菜单标识', trigger: 'blur' },
+    { pattern: /^[a-zA-Z_][a-zA-Z0-9_-]*$/, message: '菜单标识只能包含字母、数字、下划线和连字符,且以字母或下划线开头', trigger: 'blur' }
+  ],
+  menu_type: [
+    { required: true, message: '请选择菜单类型', trigger: 'change' }
+  ]
+}
+
+// 图标选项
+const iconOptions = [
+  { label: '首页', value: 'House' },
+  { label: '用户', value: 'User' },
+  { label: '设置', value: 'Setting' },
+  { label: '网格', value: 'Grid' },
+  { label: '监控', value: 'Monitor' },
+  { label: '用户填充', value: 'UserFilled' },
+  { label: '头像', value: 'Avatar' },
+  { label: '菜单', value: 'Menu' },
+  { label: '钥匙', value: 'Key' },
+  { label: '文档', value: 'Document' },
+  { label: '工具', value: 'Tools' },
+  { label: '加号', value: 'Plus' },
+  { label: '编辑', value: 'Edit' },
+  { label: '删除', value: 'Delete' },
+  { label: '搜索', value: 'Search' }
+]
+
+// 引用
+const menuFormRef = ref()
+const menuTableRef = ref()
+
+// 计算属性
+const menuTree = computed(() => {
+  return buildMenuTree(menus.value)
+})
+
+const parentMenuOptions = computed(() => {
+  return buildParentOptions(menus.value)
+})
+
+// 获取图标组件
+const getIconComponent = (iconName: string) => {
+  return (ElementPlusIcons as any)[iconName] || ElementPlusIcons.Menu
+}
+
+// 获取默认图标
+const getDefaultIcon = (menu: any) => {
+  if (menu.menu_type === 'button') {
+    return 'Operation'
+  } else if (menu.parent_id) {
+    return 'Document'
+  } else {
+    return 'Folder'
+  }
+}
+
+// 获取菜单层级
+const getMenuLevel = (menu: any) => {
+  if (!menu.parent_id) {
+    return 0  // 主菜单
+  } else {
+    // 查找父菜单是否也有父菜单
+    const parentMenu = menus.value.find(m => m.id === menu.parent_id)
+    if (parentMenu && parentMenu.parent_id) {
+      return 2  // 第三级(按钮权限)
+    } else {
+      return 1  // 第二级(子菜单)
+    }
+  }
+}
+
+// 获取层级文本
+const getLevelText = (menu: any) => {
+  const level = getMenuLevel(menu)
+  switch (level) {
+    case 0: return 'L1'
+    case 1: return 'L2'
+    case 2: return 'L3'
+    default: return 'L?'
+  }
+}
+
+// 获取层级徽章样式类
+const getLevelBadgeClass = (menu: any) => {
+  const level = getMenuLevel(menu)
+  switch (level) {
+    case 0: return 'level-badge-main'
+    case 1: return 'level-badge-sub'
+    case 2: return 'level-badge-button'
+    default: return 'level-badge-default'
+  }
+}
+
+// 获取菜单层级样式类
+const getMenuLevelClass = (menu: any) => {
+  const level = getMenuLevel(menu)
+  switch (level) {
+    case 0: return 'menu-level-0'  // 主菜单
+    case 1: return 'menu-level-1'  // 子菜单
+    case 2: return 'menu-level-2'  // 按钮权限
+    default: return 'menu-level-default'
+  }
+}
+
+// 获取菜单图标样式类
+const getMenuIconClass = (menu: any) => {
+  if (!menu.parent_id) {
+    return 'main-menu-icon'
+  } else if (menu.menu_type === 'button') {
+    return 'button-icon'
+  } else {
+    return 'sub-menu-icon'
+  }
+}
+
+// 获取菜单标题样式类
+const getMenuTitleClass = (menu: any) => {
+  if (!menu.parent_id) {
+    return 'main-menu-title'
+  } else if (menu.menu_type === 'button') {
+    return 'button-title'
+  } else {
+    return 'sub-menu-title'
+  }
+}
+
+// 构建菜单树
+const buildMenuTree = (menuList: any[]) => {
+  console.log('🌳 构建菜单树,原始数据:', menuList.length, '个菜单')
+  
+  if (!menuList || menuList.length === 0) {
+    return []
+  }
+  
+  // 创建菜单映射
+  const menuMap = new Map()
+  const tree: any[] = []
+  
+  // 先创建所有菜单节点
+  menuList.forEach(menu => {
+    menuMap.set(menu.id, { 
+      ...menu, 
+      children: [],
+      hasChildren: false
+    })
+  })
+  
+  console.log('📋 菜单映射创建完成,共', menuMap.size, '个节点')
+  
+  // 按层级和sort_order排序
+  const sortedMenus = [...menuList].sort((a, b) => {
+    // 先按层级排序(主菜单优先)
+    const aLevel = a.parent_id ? 1 : 0
+    const bLevel = b.parent_id ? 1 : 0
+    if (aLevel !== bLevel) {
+      return aLevel - bLevel
+    }
+    // 同层级按sort_order排序
+    return (a.sort_order || 0) - (b.sort_order || 0)
+  })
+  
+  // 构建树结构
+  let rootCount = 0
+  let childCount = 0
+  
+  sortedMenus.forEach(menu => {
+    const menuNode = menuMap.get(menu.id)
+    if (menu.parent_id && menuMap.has(menu.parent_id)) {
+      const parentNode = menuMap.get(menu.parent_id)
+      parentNode.children.push(menuNode)
+      parentNode.hasChildren = true  // 确保设置hasChildren为true
+      childCount++
+    } else {
+      tree.push(menuNode)
+      rootCount++
+    }
+  })
+  
+  console.log('🌲 树结构构建完成:', rootCount, '个根节点,', childCount, '个子节点')
+  
+  // 对每个节点的children进行排序,并确保hasChildren正确设置
+  const sortChildren = (node: any, level = 0) => {
+    if (node.children && node.children.length > 0) {
+      // 确保hasChildren为true
+      node.hasChildren = true
+      
+      // 先按菜单类型排序(menu在前,button在后),再按sort_order排序
+      node.children.sort((a: any, b: any) => {
+        if (a.menu_type !== b.menu_type) {
+          return a.menu_type === 'menu' ? -1 : 1
+        }
+        return (a.sort_order || 0) - (b.sort_order || 0)
+      })
+      
+      console.log(`📂 层级 ${level}: ${node.title} 有 ${node.children.length} 个子节点`)
+      
+      // 递归排序子节点
+      node.children.forEach(child => sortChildren(child, level + 1))
+    } else {
+      // 确保没有子节点的节点hasChildren为false
+      node.hasChildren = false
+    }
+  }
+  
+  tree.forEach(node => sortChildren(node, 0))
+  
+  console.log('✅ 菜单树构建完成,返回', tree.length, '个根节点')
+  console.log('🔍 根节点详情:', tree.map(t => ({ 
+    title: t.title, 
+    children: t.children.length, 
+    hasChildren: t.hasChildren 
+  })))
+  
+  return tree
+}
+
+// 构建父级菜单选项
+const buildParentOptions = (menuList: any[]) => {
+  return menuList
+    .filter(menu => menu.menu_type === 'menu')
+    .map(menu => ({
+      id: menu.id,
+      title: menu.title,
+      children: []
+    }))
+}
+
+// 加载菜单列表
+const loadMenus = async () => {
+  loading.value = true
+  try {
+    const result = await request.get('/api/v1/admin/menus', {
+      params: {
+        page: currentPage.value,
+        page_size: pageSize.value,
+        keyword: searchKeyword.value || undefined
+      }
+    })
+    
+    if (result.code === 0) {
+      menus.value = result.data.items
+      total.value = result.data.total
+      
+      // 数据加载完成后,等待DOM更新,然后展开主要节点
+      await nextTick()
+      setTimeout(() => {
+        expandMainNodes()
+      }, 100)
+    } else {
+      throw new Error(result.message)
+    }
+  } catch (error) {
+    console.error('加载菜单列表失败:', error)
+    ElMessage.error('加载菜单列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 展开主要节点(只展开有子菜单的主菜单)
+const expandMainNodes = () => {
+  if (menuTableRef.value && menuTree.value) {
+    menuTree.value.forEach(node => {
+      if (node.hasChildren && node.children && node.children.length > 0) {
+        // 只展开主菜单,不展开子菜单
+        menuTableRef.value.toggleRowExpansion(node, true)
+      }
+    })
+  }
+}
+
+// 搜索处理
+const handleSearch = () => {
+  currentPage.value = 1
+  loadMenus()
+}
+
+// 分页处理
+const handleSizeChange = (size: number) => {
+  pageSize.value = size
+  currentPage.value = 1
+  loadMenus()
+}
+
+const handleCurrentChange = (page: number) => {
+  currentPage.value = page
+  loadMenus()
+}
+
+// 展开/收起
+const expandAll = () => {
+  if (menuTableRef.value) {
+    // 递归展开所有有子节点的节点
+    const expandNode = (nodes: any[]) => {
+      nodes.forEach(node => {
+        if (node.hasChildren && node.children && node.children.length > 0) {
+          menuTableRef.value.toggleRowExpansion(node, true)
+          expandNode(node.children)
+        }
+      })
+    }
+    expandNode(menuTree.value)
+  }
+}
+
+const collapseAll = () => {
+  if (menuTableRef.value) {
+    // 递归收起所有有子节点的节点
+    const collapseNode = (nodes: any[]) => {
+      nodes.forEach(node => {
+        if (node.hasChildren && node.children && node.children.length > 0) {
+          menuTableRef.value.toggleRowExpansion(node, false)
+          collapseNode(node.children)
+        }
+      })
+    }
+    collapseNode(menuTree.value)
+  }
+}
+
+// 状态切换
+const handleStatusChange = async (menu: any) => {
+  try {
+    await request.put(`/api/v1/admin/menus/${menu.id}`, {
+      is_active: menu.is_active
+    })
+    ElMessage.success('菜单状态更新成功')
+  } catch (error) {
+    console.error('更新菜单状态失败:', error)
+    ElMessage.error('更新菜单状态失败')
+    // 恢复原状态
+    menu.is_active = !menu.is_active
+  }
+}
+
+// 编辑菜单
+const editMenu = (menu: any) => {
+  editingMenu.value = menu
+  Object.assign(menuForm, {
+    title: menu.title,
+    name: menu.name,
+    parent_id: menu.parent_id,
+    path: menu.path,
+    component: menu.component,
+    icon: menu.icon,
+    sort_order: menu.sort_order,
+    menu_type: menu.menu_type,
+    is_hidden: menu.is_hidden,
+    is_active: menu.is_active,
+    description: menu.description
+  })
+  showCreateDialog.value = true
+}
+
+// 添加子菜单
+const addChildMenu = (parentMenu: any) => {
+  parentMenuId.value = parentMenu.id
+  menuForm.parent_id = parentMenu.id
+  showCreateDialog.value = true
+}
+
+// 删除菜单
+const deleteMenu = async (menu: any) => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除菜单 "${menu.title}" 吗?此操作不可恢复。`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    
+    await request.delete(`/api/v1/admin/menus/${menu.id}`)
+    ElMessage.success('菜单删除成功')
+    loadMenus()
+  } catch (error) {
+    // 用户取消删除
+  }
+}
+
+// 保存菜单
+const saveMenu = async () => {
+  try {
+    await menuFormRef.value.validate()
+    
+    saving.value = true
+    
+    if (editingMenu.value) {
+      // 更新菜单
+      await request.put(`/api/v1/admin/menus/${editingMenu.value.id}`, menuForm)
+      ElMessage.success('菜单更新成功')
+    } else {
+      // 创建菜单
+      await request.post('/api/v1/admin/menus', menuForm)
+      ElMessage.success('菜单创建成功')
+    }
+    
+    showCreateDialog.value = false
+    loadMenus()
+  } catch (error) {
+    console.error('保存菜单失败:', error)
+    ElMessage.error('保存菜单失败')
+  } finally {
+    saving.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  editingMenu.value = null
+  parentMenuId.value = null
+  Object.assign(menuForm, {
+    title: '',
+    name: '',
+    parent_id: null,
+    path: '',
+    component: '',
+    icon: '',
+    sort_order: 0,
+    menu_type: 'menu',
+    is_hidden: false,
+    is_active: true,
+    description: ''
+  })
+  if (menuFormRef.value) {
+    menuFormRef.value.clearValidate()
+  }
+}
+
+// 格式化日期时间
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return '-'
+  return new Date(dateTime).toLocaleString('zh-CN')
+}
+
+// 组件挂载
+onMounted(() => {
+  loadMenus()
+})
+</script>
+
+<style scoped>
+.menus-management {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+/* 菜单信息容器 */
+.menu-info {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+
+.menu-content {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex: 1;
+  min-height: 32px;
+}
+
+/* 强化层级指示器 */
+.level-indicators {
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  min-width: 60px;
+}
+
+.level-indicator {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.level-bar {
+  width: 4px;
+  height: 20px;
+  border-radius: 2px;
+}
+
+.main-level {
+  background: linear-gradient(135deg, #409eff, #66b1ff);
+  box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
+}
+
+.sub-level {
+  background: linear-gradient(135deg, #67c23a, #85ce61);
+  box-shadow: 0 2px 4px rgba(103, 194, 58, 0.3);
+}
+
+.button-level {
+  background: linear-gradient(135deg, #e6a23c, #ebb563);
+  box-shadow: 0 2px 4px rgba(230, 162, 60, 0.3);
+}
+
+.level-connector {
+  width: 12px;
+  height: 2px;
+  background: #dcdfe6;
+  border-radius: 1px;
+}
+
+.level-connector-deep {
+  width: 8px;
+  height: 2px;
+  background: #f0f0f0;
+  border-radius: 1px;
+}
+
+/* 层级样式类 */
+.menu-level-0 {
+  background: linear-gradient(90deg, rgba(64, 158, 255, 0.05), transparent);
+  border-left: 4px solid #409eff;
+  padding-left: 12px;
+  margin-left: -16px;
+}
+
+.menu-level-1 {
+  background: linear-gradient(90deg, rgba(103, 194, 58, 0.05), transparent);
+  border-left: 4px solid #67c23a;
+  padding-left: 12px;
+  margin-left: -16px;
+}
+
+.menu-level-2 {
+  background: linear-gradient(90deg, rgba(230, 162, 60, 0.05), transparent);
+  border-left: 4px solid #e6a23c;
+  padding-left: 12px;
+  margin-left: -16px;
+}
+
+/* 菜单图标样式 */
+.menu-icon {
+  flex-shrink: 0;
+  transition: all 0.3s ease;
+}
+
+.main-menu-icon {
+  color: #409eff;
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.sub-menu-icon {
+  color: #67c23a;
+  font-size: 18px;
+}
+
+.button-icon {
+  color: #e6a23c;
+  font-size: 16px;
+}
+
+.default-icon {
+  color: #909399;
+}
+
+/* 菜单标题样式 */
+.menu-title {
+  font-weight: 500;
+  flex: 1;
+  transition: all 0.3s ease;
+}
+
+.main-menu-title {
+  font-size: 16px;
+  font-weight: 700;
+  color: #303133;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.sub-menu-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #606266;
+}
+
+.button-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #909399;
+}
+
+/* 层级徽章 */
+.level-badge {
+  display: inline-block;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-size: 10px;
+  font-weight: bold;
+  text-align: center;
+  min-width: 24px;
+  margin-right: 8px;
+  color: white;
+  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+}
+
+.level-badge-main {
+  background: linear-gradient(135deg, #409eff, #66b1ff);
+  border: 1px solid #409eff;
+}
+
+.level-badge-sub {
+  background: linear-gradient(135deg, #67c23a, #85ce61);
+  border: 1px solid #67c23a;
+}
+
+.level-badge-button {
+  background: linear-gradient(135deg, #e6a23c, #ebb563);
+  border: 1px solid #e6a23c;
+}
+
+.level-badge-default {
+  background: #909399;
+  border: 1px solid #909399;
+}
+
+/* 菜单类型标签 */
+.menu-type-tag {
+  margin-left: 4px;
+  flex-shrink: 0;
+  font-size: 11px;
+  padding: 2px 6px;
+}
+
+/* 图标选项 */
+.icon-option {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+/* 分页 */
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+/* 表格样式增强 */
+:deep(.el-table) {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-table__row) {
+  transition: all 0.3s ease;
+}
+
+:deep(.el-table__row:hover) {
+  background-color: #f8f9fa !important;
+}
+
+/* 树形表格特殊样式 */
+.menu-tree-table :deep(.el-table__expand-icon) {
+  color: #409eff;
+  font-size: 16px;
+  transition: all 0.3s ease;
+}
+
+.menu-tree-table :deep(.el-table__expand-icon:hover) {
+  color: #66b1ff;
+  transform: scale(1.1);
+}
+
+.menu-tree-table :deep(.el-table__expand-icon.el-table__expand-icon--expanded) {
+  color: #67c23a;
+  transform: rotate(90deg);
+}
+
+/* 表格行层级样式 */
+.menu-tree-table :deep(.el-table__row[data-level="0"]) {
+  background: linear-gradient(90deg, rgba(64, 158, 255, 0.02), transparent);
+  font-weight: 600;
+}
+
+.menu-tree-table :deep(.el-table__row[data-level="1"]) {
+  background: linear-gradient(90deg, rgba(103, 194, 58, 0.02), transparent);
+}
+
+.menu-tree-table :deep(.el-table__row[data-level="2"]) {
+  background: linear-gradient(90deg, rgba(230, 162, 60, 0.02), transparent);
+}
+
+/* 对话框样式 */
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .toolbar {
+    flex-direction: column;
+    gap: 16px;
+  }
+  
+  .toolbar-left,
+  .toolbar-right {
+    width: 100%;
+    justify-content: center;
+  }
+  
+  .level-indicators {
+    min-width: 40px;
+  }
+  
+  .menu-content {
+    gap: 6px;
+  }
+}
+
+/* 动画效果 */
+@keyframes levelHighlight {
+  0% { transform: scale(1); }
+  50% { transform: scale(1.05); }
+  100% { transform: scale(1); }
+}
+
+.menu-info:hover .level-bar {
+  animation: levelHighlight 0.6s ease-in-out;
+}
+
+.menu-info:hover .menu-title {
+  color: #409eff;
+}
+
+.menu-info:hover .level-badge {
+  transform: scale(1.1);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+</style>

+ 598 - 0
src/views/admin/Permissions.vue

@@ -0,0 +1,598 @@
+<template>
+  <div class="permissions-management">
+    <div class="page-header">
+      <h2>权限管理</h2>
+      <p>管理系统权限和资源访问控制</p>
+    </div>
+
+    <!-- 操作栏 -->
+    <div class="toolbar">
+      <div class="toolbar-left">
+        <el-button type="primary" @click="showCreateDialog = true">
+          <el-icon><Plus /></el-icon>
+          创建权限
+        </el-button>
+        <el-button @click="refreshPermissions">
+          <el-icon><Refresh /></el-icon>
+          刷新
+        </el-button>
+      </div>
+      <div class="toolbar-right">
+        <el-select
+          v-model="selectedResource"
+          placeholder="选择资源类型"
+          style="width: 150px; margin-right: 12px;"
+          @change="handleResourceFilter"
+          clearable
+        >
+          <el-option label="全部资源" value="" />
+          <el-option label="用户管理" value="user" />
+          <el-option label="角色管理" value="role" />
+          <el-option label="菜单管理" value="menu" />
+          <el-option label="应用管理" value="app" />
+          <el-option label="系统管理" value="system" />
+        </el-select>
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索权限名称..."
+          style="width: 300px"
+          @input="handleSearch"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </div>
+
+    <!-- 权限列表 -->
+    <el-table
+      v-loading="loading"
+      :data="permissions"
+      style="width: 100%"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
+      <el-table-column prop="display_name" label="权限名称" min-width="150">
+        <template #default="{ row }">
+          <div class="permission-info">
+            <span class="permission-name">{{ row.display_name }}</span>
+            <el-tag size="small" style="margin-left: 8px;">{{ row.name }}</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="resource" label="资源" width="120">
+        <template #default="{ row }">
+          <el-tag :type="getResourceTagType(row.resource)" size="small">
+            {{ getResourceLabel(row.resource) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="action" label="操作" width="100">
+        <template #default="{ row }">
+          <el-tag :type="getActionTagType(row.action)" size="small">
+            {{ getActionLabel(row.action) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
+      <el-table-column prop="is_active" label="状态" width="100" align="center">
+        <template #default="{ row }">
+          <el-switch
+            v-model="row.is_active"
+            @change="handleStatusChange(row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column prop="created_at" label="创建时间" width="180">
+        <template #default="{ row }">
+          {{ formatDateTime(row.created_at) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="200" fixed="right">
+        <template #default="{ row }">
+          <el-button type="primary" size="small" @click="editPermission(row)">
+            编辑
+          </el-button>
+          <el-button type="info" size="small" @click="viewRoles(row)">
+            查看角色
+          </el-button>
+          <el-button type="danger" size="small" @click="deletePermission(row)">
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="pagination">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 创建/编辑权限对话框 -->
+    <el-dialog
+      v-model="showCreateDialog"
+      :title="editingPermission ? '编辑权限' : '创建权限'"
+      width="600px"
+      @close="resetForm"
+    >
+      <el-form
+        ref="permissionFormRef"
+        :model="permissionForm"
+        :rules="permissionRules"
+        label-width="100px"
+      >
+        <el-form-item label="权限名称" prop="display_name">
+          <el-input v-model="permissionForm.display_name" placeholder="请输入权限显示名称" />
+        </el-form-item>
+        <el-form-item label="权限标识" prop="name">
+          <el-input 
+            v-model="permissionForm.name" 
+            placeholder="请输入权限标识(如:user.create)"
+            :disabled="!!editingPermission"
+          />
+        </el-form-item>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="资源类型" prop="resource">
+              <el-select v-model="permissionForm.resource" placeholder="请选择资源类型">
+                <el-option label="用户管理" value="user" />
+                <el-option label="角色管理" value="role" />
+                <el-option label="菜单管理" value="menu" />
+                <el-option label="应用管理" value="app" />
+                <el-option label="系统管理" value="system" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="操作类型" prop="action">
+              <el-select v-model="permissionForm.action" placeholder="请选择操作类型">
+                <el-option label="查看" value="view" />
+                <el-option label="创建" value="create" />
+                <el-option label="编辑" value="edit" />
+                <el-option label="删除" value="delete" />
+                <el-option label="分配" value="assign" />
+                <el-option label="配置" value="config" />
+                <el-option label="审计" value="audit" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="权限描述" prop="description">
+          <el-input
+            v-model="permissionForm.description"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入权限描述"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="savePermission" :loading="saving">
+          {{ editingPermission ? '更新' : '创建' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 查看权限角色对话框 -->
+    <el-dialog
+      v-model="showRolesDialog"
+      title="权限关联角色"
+      width="500px"
+    >
+      <div v-if="currentPermission">
+        <h4>权限 "{{ currentPermission.display_name }}" 关联的角色</h4>
+        <el-empty v-if="permissionRoles.length === 0" description="暂无关联角色" />
+        <div v-else class="roles-list">
+          <el-tag 
+            v-for="role in permissionRoles" 
+            :key="role.id"
+            size="large"
+            style="margin: 4px;"
+          >
+            {{ role.display_name }}
+            <el-tag v-if="role.is_system" type="info" size="small" style="margin-left: 4px;">
+              系统角色
+            </el-tag>
+          </el-tag>
+        </div>
+      </div>
+      <template #footer>
+        <el-button @click="showRolesDialog = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search, Refresh } from '@element-plus/icons-vue'
+import request from '@/api/request'
+
+// 响应式数据
+const loading = ref(false)
+const saving = ref(false)
+const permissions = ref<any[]>([])
+const selectedPermissions = ref<any[]>([])
+const searchKeyword = ref('')
+const selectedResource = ref('')
+const currentPage = ref(1)
+const pageSize = ref(20)
+const total = ref(0)
+
+// 对话框状态
+const showCreateDialog = ref(false)
+const showRolesDialog = ref(false)
+const editingPermission = ref<any>(null)
+const currentPermission = ref<any>(null)
+const permissionRoles = ref<any[]>([])
+
+// 表单数据
+const permissionForm = reactive({
+  display_name: '',
+  name: '',
+  resource: '',
+  action: '',
+  description: ''
+})
+
+// 表单验证规则
+const permissionRules = {
+  display_name: [
+    { required: true, message: '请输入权限名称', trigger: 'blur' }
+  ],
+  name: [
+    { required: true, message: '请输入权限标识', trigger: 'blur' },
+    { pattern: /^[a-zA-Z_][a-zA-Z0-9_.]*$/, message: '权限标识只能包含字母、数字、下划线和点号,且以字母或下划线开头', trigger: 'blur' }
+  ],
+  resource: [
+    { required: true, message: '请选择资源类型', trigger: 'change' }
+  ],
+  action: [
+    { required: true, message: '请选择操作类型', trigger: 'change' }
+  ]
+}
+
+// 引用
+const permissionFormRef = ref()
+
+// 获取资源标签类型
+const getResourceTagType = (resource: string) => {
+  const types: Record<string, string> = {
+    user: 'primary',
+    role: 'success',
+    menu: 'warning',
+    app: 'info',
+    system: 'danger'
+  }
+  return types[resource] || 'default'
+}
+
+// 获取资源标签文本
+const getResourceLabel = (resource: string) => {
+  const labels: Record<string, string> = {
+    user: '用户',
+    role: '角色',
+    menu: '菜单',
+    app: '应用',
+    system: '系统'
+  }
+  return labels[resource] || resource
+}
+
+// 获取操作标签类型
+const getActionTagType = (action: string) => {
+  const types: Record<string, string> = {
+    view: 'info',
+    create: 'success',
+    edit: 'warning',
+    delete: 'danger',
+    assign: 'primary',
+    config: 'warning',
+    audit: 'info'
+  }
+  return types[action] || 'default'
+}
+
+// 获取操作标签文本
+const getActionLabel = (action: string) => {
+  const labels: Record<string, string> = {
+    view: '查看',
+    create: '创建',
+    edit: '编辑',
+    delete: '删除',
+    assign: '分配',
+    config: '配置',
+    audit: '审计'
+  }
+  return labels[action] || action
+}
+
+// 加载权限列表
+const loadPermissions = async () => {
+  loading.value = true
+  try {
+    // TODO: 实现权限列表API
+    // 模拟数据
+    const mockData = {
+      code: 0,
+      data: {
+        items: [
+          {
+            id: '1',
+            name: 'user.view',
+            display_name: '查看用户',
+            resource: 'user',
+            action: 'view',
+            description: '查看用户列表和详情',
+            is_active: true,
+            created_at: new Date().toISOString()
+          },
+          {
+            id: '2',
+            name: 'user.create',
+            display_name: '创建用户',
+            resource: 'user',
+            action: 'create',
+            description: '创建新用户',
+            is_active: true,
+            created_at: new Date().toISOString()
+          },
+          {
+            id: '3',
+            name: 'role.view',
+            display_name: '查看角色',
+            resource: 'role',
+            action: 'view',
+            description: '查看角色列表和详情',
+            is_active: true,
+            created_at: new Date().toISOString()
+          }
+        ],
+        total: 3,
+        page: currentPage.value,
+        page_size: pageSize.value
+      }
+    }
+    
+    permissions.value = mockData.data.items
+    total.value = mockData.data.total
+  } catch (error) {
+    console.error('加载权限列表失败:', error)
+    ElMessage.error('加载权限列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 刷新权限
+const refreshPermissions = () => {
+  loadPermissions()
+}
+
+// 搜索处理
+const handleSearch = () => {
+  currentPage.value = 1
+  loadPermissions()
+}
+
+// 资源过滤
+const handleResourceFilter = () => {
+  currentPage.value = 1
+  loadPermissions()
+}
+
+// 分页处理
+const handleSizeChange = (size: number) => {
+  pageSize.value = size
+  currentPage.value = 1
+  loadPermissions()
+}
+
+const handleCurrentChange = (page: number) => {
+  currentPage.value = page
+  loadPermissions()
+}
+
+// 选择处理
+const handleSelectionChange = (selection: any[]) => {
+  selectedPermissions.value = selection
+}
+
+// 状态切换
+const handleStatusChange = async (permission: any) => {
+  try {
+    // TODO: 实现权限状态切换API
+    ElMessage.success('权限状态更新成功')
+  } catch (error) {
+    console.error('更新权限状态失败:', error)
+    ElMessage.error('更新权限状态失败')
+    // 恢复原状态
+    permission.is_active = !permission.is_active
+  }
+}
+
+// 编辑权限
+const editPermission = (permission: any) => {
+  editingPermission.value = permission
+  Object.assign(permissionForm, {
+    display_name: permission.display_name,
+    name: permission.name,
+    resource: permission.resource,
+    action: permission.action,
+    description: permission.description
+  })
+  showCreateDialog.value = true
+}
+
+// 查看权限关联角色
+const viewRoles = async (permission: any) => {
+  currentPermission.value = permission
+  try {
+    // TODO: 实现获取权限关联角色API
+    // 模拟数据
+    permissionRoles.value = [
+      { id: '1', display_name: '超级管理员', is_system: true },
+      { id: '2', display_name: '管理员', is_system: true }
+    ]
+    showRolesDialog.value = true
+  } catch (error) {
+    console.error('获取权限角色失败:', error)
+    ElMessage.error('获取权限角色失败')
+  }
+}
+
+// 删除权限
+const deletePermission = async (permission: any) => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除权限 "${permission.display_name}" 吗?此操作不可恢复。`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    
+    // TODO: 实现删除权限API
+    ElMessage.success('权限删除成功')
+    loadPermissions()
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('删除权限失败')
+    }
+  }
+}
+
+// 保存权限
+const savePermission = async () => {
+  try {
+    await permissionFormRef.value.validate()
+    
+    saving.value = true
+    
+    if (editingPermission.value) {
+      // TODO: 实现更新权限API
+      ElMessage.success('权限更新成功')
+    } else {
+      // TODO: 实现创建权限API
+      ElMessage.success('权限创建成功')
+    }
+    
+    showCreateDialog.value = false
+    loadPermissions()
+  } catch (error) {
+    console.error('保存权限失败:', error)
+    ElMessage.error('保存权限失败')
+  } finally {
+    saving.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  editingPermission.value = null
+  Object.assign(permissionForm, {
+    display_name: '',
+    name: '',
+    resource: '',
+    action: '',
+    description: ''
+  })
+  permissionFormRef.value?.clearValidate()
+}
+
+// 格式化日期时间
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return '-'
+  return new Date(dateTime).toLocaleString('zh-CN')
+}
+
+// 组件挂载
+onMounted(() => {
+  loadPermissions()
+})
+</script>
+
+<style scoped>
+.permissions-management {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.permission-info {
+  display: flex;
+  align-items: center;
+}
+
+.permission-name {
+  font-weight: 500;
+}
+
+.roles-list {
+  margin-top: 16px;
+}
+
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-table) {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+</style>

+ 750 - 0
src/views/admin/Roles.vue

@@ -0,0 +1,750 @@
+<template>
+  <div class="roles-management">
+    <div class="page-header">
+      <h2>角色管理</h2>
+      <p>管理系统角色和权限分配</p>
+    </div>
+
+    <!-- 操作栏 -->
+    <div class="toolbar">
+      <div class="toolbar-left">
+        <el-button type="primary" @click="showCreateDialog = true">
+          <el-icon><Plus /></el-icon>
+          创建角色
+        </el-button>
+      </div>
+      <div class="toolbar-right">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索角色名称..."
+          style="width: 300px"
+          @input="handleSearch"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </div>
+
+    <!-- 角色列表 -->
+    <div class="table-container">
+      <el-table
+        v-loading="loading"
+        :data="roles"
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column type="selection" width="55" />
+        <el-table-column prop="display_name" label="角色名称" min-width="150" fixed="left">
+          <template #default="{ row }">
+            <div class="role-info">
+              <span class="role-name">{{ row.display_name }}</span>
+              <el-tag v-if="row.is_system" type="info" size="small" style="margin-left: 8px;">
+                系统角色
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="角色标识" width="120" />
+        <el-table-column prop="description" label="描述" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="user_count" label="用户数" width="80" align="center">
+          <template #default="{ row }">
+            <el-tag type="success" size="small">{{ row.user_count }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="is_active" label="状态" width="80" align="center">
+          <template #default="{ row }">
+            <el-switch
+              v-model="row.is_active"
+              @change="handleStatusChange(row)"
+              :disabled="row.is_system"
+              size="small"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="created_at" label="创建时间" width="140">
+          <template #default="{ row }">
+            <div class="time-info">
+              {{ formatDateTime(row.created_at) }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="180" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" size="small" @click="editRole(row)">
+              编辑
+            </el-button>
+            <el-dropdown @command="(command) => handleRoleDropdown(command, row)">
+              <el-button size="small">
+                更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="permissions">权限管理</el-dropdown-item>
+                  <el-dropdown-item 
+                    command="delete" 
+                    :disabled="row.is_system"
+                    divided
+                  >
+                    删除角色
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 创建/编辑角色对话框 -->
+    <el-dialog
+      v-model="showCreateDialog"
+      :title="editingRole ? '编辑角色' : '创建角色'"
+      width="600px"
+      @close="resetForm"
+    >
+      <el-form
+        ref="roleFormRef"
+        :model="roleForm"
+        :rules="roleRules"
+        label-width="100px"
+      >
+        <el-form-item label="角色名称" prop="display_name">
+          <el-input v-model="roleForm.display_name" placeholder="请输入角色显示名称" />
+        </el-form-item>
+        <el-form-item label="角色标识" prop="name">
+          <el-input 
+            v-model="roleForm.name" 
+            placeholder="请输入角色标识(英文)"
+            :disabled="editingRole && editingRole.is_system"
+          />
+        </el-form-item>
+        <el-form-item label="角色描述" prop="description">
+          <el-input
+            v-model="roleForm.description"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入角色描述"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="saveRole" :loading="saving">
+          {{ editingRole ? '更新' : '创建' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 权限管理对话框 -->
+    <el-dialog
+      v-model="showPermissionDialog"
+      title="权限管理"
+      width="900px"
+      :close-on-click-modal="false"
+    >
+      <div v-if="currentRole" class="permission-dialog-content">
+        <div class="dialog-header">
+          <h4>为角色 "{{ currentRole.display_name }}" 分配菜单权限</h4>
+          <p v-if="isSuperAdminRole" class="dialog-description super-admin-notice">
+            <el-icon><InfoFilled /></el-icon>
+            超级管理员角色默认拥有所有菜单权限,无需手动分配。
+          </p>
+          <p v-else class="dialog-description">
+            选择该角色可以访问的菜单和功能。勾选父菜单将自动选中所有子菜单,取消勾选父菜单将自动取消所有子菜单。
+          </p>
+        </div>
+        
+        <div class="permission-content">
+          <MenuPermissionTree
+            ref="permissionTreeRef"
+            :menu-data="allMenus"
+            :checked-keys="rolePermissions"
+            :disabled="isSuperAdminRole"
+            @update:checked-keys="handlePermissionChange"
+            @check-change="handlePermissionCheckChange"
+          />
+        </div>
+        
+        <div class="permission-summary">
+          <el-alert
+            v-if="isSuperAdminRole"
+            title="超级管理员拥有全部菜单权限"
+            type="success"
+            :closable="false"
+            show-icon
+          >
+            <template #default>
+              <div class="summary-details">
+                <span>主菜单: {{ selectedMainMenus }} 个</span>
+                <span>功能菜单: {{ selectedSubMenus }} 个</span>
+                <span>按钮权限: {{ selectedButtons }} 个</span>
+                <span class="total-permissions">总计: {{ selectedPermissionCount }} 个权限</span>
+              </div>
+            </template>
+          </el-alert>
+          <el-alert
+            v-else
+            :title="`已选择 ${selectedPermissionCount} 个菜单权限`"
+            type="info"
+            :closable="false"
+            show-icon
+          >
+            <template #default>
+              <div class="summary-details">
+                <span>主菜单: {{ selectedMainMenus }} 个</span>
+                <span>功能菜单: {{ selectedSubMenus }} 个</span>
+                <span>按钮权限: {{ selectedButtons }} 个</span>
+              </div>
+            </template>
+          </el-alert>
+        </div>
+      </div>
+      
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="showPermissionDialog = false">
+            {{ isSuperAdminRole ? '关闭' : '取消' }}
+          </el-button>
+          <el-button 
+            v-if="!isSuperAdminRole"
+            type="primary" 
+            @click="savePermissions" 
+            :loading="savingPermissions"
+          >
+            <el-icon><Check /></el-icon>
+            保存权限
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search, ArrowDown, Check, InfoFilled } from '@element-plus/icons-vue'
+import request from '@/api/request'
+import MenuPermissionTree from '@/components/MenuPermissionTree.vue'
+
+// 响应式数据
+const loading = ref(false)
+const saving = ref(false)
+const savingPermissions = ref(false)
+const roles = ref<any[]>([])
+const selectedRoles = ref<any[]>([])
+const searchKeyword = ref('')
+const currentPage = ref(1)
+const pageSize = ref(20)
+const total = ref(0)
+
+// 对话框状态
+const showCreateDialog = ref(false)
+const showPermissionDialog = ref(false)
+const editingRole = ref<any>(null)
+const currentRole = ref<any>(null)
+
+// 表单数据
+const roleForm = reactive({
+  display_name: '',
+  name: '',
+  description: ''
+})
+
+// 权限相关
+const allMenus = ref<any[]>([])
+const rolePermissions = ref<string[]>([])
+const selectedPermissions = ref<string[]>([])
+
+// 计算属性
+const selectedPermissionCount = computed(() => selectedPermissions.value.length)
+
+const selectedMainMenus = computed(() => {
+  return selectedPermissions.value.filter(id => {
+    const menu = allMenus.value.find(m => m.id === id)
+    return menu && menu.menu_type === 'menu' && !menu.parent_id
+  }).length
+})
+
+const selectedSubMenus = computed(() => {
+  return selectedPermissions.value.filter(id => {
+    const menu = allMenus.value.find(m => m.id === id)
+    return menu && menu.menu_type === 'menu' && menu.parent_id
+  }).length
+})
+
+const selectedButtons = computed(() => {
+  return selectedPermissions.value.filter(id => {
+    const menu = allMenus.value.find(m => m.id === id)
+    return menu && menu.menu_type === 'button'
+  }).length
+})
+
+// 判断是否为超级管理员角色
+const isSuperAdminRole = computed(() => {
+  return currentRole.value && currentRole.value.name === 'super_admin'
+})
+
+// 表单验证规则
+const roleRules = {
+  display_name: [
+    { required: true, message: '请输入角色名称', trigger: 'blur' }
+  ],
+  name: [
+    { required: true, message: '请输入角色标识', trigger: 'blur' },
+    { pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: '角色标识只能包含字母、数字和下划线,且以字母或下划线开头', trigger: 'blur' }
+  ]
+}
+
+// 引用
+const roleFormRef = ref()
+const permissionTreeRef = ref()
+
+// 加载角色列表
+const loadRoles = async () => {
+  loading.value = true
+  try {
+    const result = await request.get('/api/v1/admin/roles', {
+      params: {
+        page: currentPage.value,
+        page_size: pageSize.value,
+        keyword: searchKeyword.value || undefined
+      }
+    })
+    
+    if (result.code === 0) {
+      roles.value = result.data.items
+      total.value = result.data.total
+    } else {
+      throw new Error(result.message)
+    }
+  } catch (error) {
+    console.error('加载角色列表失败:', error)
+    ElMessage.error('加载角色列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 搜索处理
+const handleSearch = () => {
+  currentPage.value = 1
+  loadRoles()
+}
+
+// 分页处理
+const handleSizeChange = (size: number) => {
+  pageSize.value = size
+  currentPage.value = 1
+  loadRoles()
+}
+
+const handleCurrentChange = (page: number) => {
+  currentPage.value = page
+  loadRoles()
+}
+
+// 选择处理
+const handleSelectionChange = (selection: any[]) => {
+  selectedRoles.value = selection
+}
+
+// 状态切换
+const handleStatusChange = async (role: any) => {
+  try {
+    await request.put(`/api/v1/admin/roles/${role.id}`, {
+      is_active: role.is_active
+    })
+    ElMessage.success('角色状态更新成功')
+  } catch (error) {
+    console.error('更新角色状态失败:', error)
+    ElMessage.error('更新角色状态失败')
+    // 恢复原状态
+    role.is_active = !role.is_active
+  }
+}
+
+// 编辑角色
+const editRole = (role: any) => {
+  editingRole.value = role
+  roleForm.display_name = role.display_name
+  roleForm.name = role.name
+  roleForm.description = role.description
+  showCreateDialog.value = true
+}
+
+// 删除角色
+const deleteRole = async (role: any) => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除角色 "${role.display_name}" 吗?此操作不可恢复。`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    
+    await request.delete(`/api/v1/admin/roles/${role.id}`)
+    ElMessage.success('角色删除成功')
+    loadRoles()
+  } catch (error: any) {
+    if (error.response?.data?.message) {
+      ElMessage.error(error.response.data.message)
+    } else if (error !== 'cancel') {
+      ElMessage.error('删除角色失败')
+    }
+  }
+}
+
+// 下拉菜单处理
+const handleRoleDropdown = (command: string, role: any) => {
+  switch (command) {
+    case 'permissions':
+      managePermissions(role)
+      break
+    case 'delete':
+      deleteRole(role)
+      break
+  }
+}
+
+// 权限管理
+const managePermissions = async (role: any) => {
+  currentRole.value = role
+  
+  try {
+    // 加载所有菜单
+    await loadAllMenus()
+    
+    // 加载角色已分配的权限
+    await loadRolePermissions(role.id)
+    
+    showPermissionDialog.value = true
+  } catch (error) {
+    console.error('加载权限数据失败:', error)
+    ElMessage.error('加载权限数据失败')
+  }
+}
+
+// 加载所有菜单
+const loadAllMenus = async () => {
+  try {
+    const result = await request.get('/api/v1/admin/menus', {
+      params: {
+        page_size: 1000  // 获取所有菜单
+      }
+    })
+    
+    if (result.code === 0) {
+      allMenus.value = result.data.items
+    } else {
+      throw new Error(result.message)
+    }
+  } catch (error) {
+    console.error('加载菜单列表失败:', error)
+    throw error
+  }
+}
+
+// 加载角色权限
+const loadRolePermissions = async (roleId: string) => {
+  try {
+    console.log('🔍 Loading role permissions for roleId:', roleId)
+    const result = await request.get(`/api/v1/admin/roles/${roleId}/menus`)
+    
+    console.log('📡 Role permissions API response:', result)
+    
+    if (result.code === 0) {
+      const menuIds = result.data.menu_ids || []
+      console.log('✅ Received menu_ids:', menuIds)
+      console.log('📊 Menu IDs count:', menuIds.length)
+      
+      rolePermissions.value = menuIds
+      selectedPermissions.value = [...menuIds]
+      
+      console.log('🎯 Set rolePermissions.value to:', rolePermissions.value)
+      console.log('🎯 Set selectedPermissions.value to:', selectedPermissions.value)
+    } else {
+      console.warn('⚠️ API returned error:', result.message)
+      // 如果API不存在,使用空数组
+      rolePermissions.value = []
+      selectedPermissions.value = []
+    }
+  } catch (error) {
+    console.error('❌ 加载角色权限失败:', error)
+    // 如果API不存在,使用空数组
+    rolePermissions.value = []
+    selectedPermissions.value = []
+  }
+}
+
+// 权限变化处理
+const handlePermissionChange = (checkedKeys: string[]) => {
+  selectedPermissions.value = checkedKeys
+}
+
+const handlePermissionCheckChange = (data: any, checkedKeys: string[], checkedNodes: any[]) => {
+  selectedPermissions.value = checkedKeys
+}
+
+// 保存角色
+const saveRole = async () => {
+  try {
+    await roleFormRef.value.validate()
+    
+    saving.value = true
+    
+    if (editingRole.value) {
+      // 更新角色
+      await request.put(`/api/v1/admin/roles/${editingRole.value.id}`, {
+        display_name: roleForm.display_name,
+        description: roleForm.description
+      })
+      ElMessage.success('角色更新成功')
+    } else {
+      // 创建角色
+      await request.post('/api/v1/admin/roles', roleForm)
+      ElMessage.success('角色创建成功')
+    }
+    
+    showCreateDialog.value = false
+    loadRoles()
+  } catch (error: any) {
+    console.error('保存角色失败:', error)
+    if (error.response?.data?.message) {
+      ElMessage.error(error.response.data.message)
+    } else {
+      ElMessage.error('保存角色失败')
+    }
+  } finally {
+    saving.value = false
+  }
+}
+
+// 权限选择处理
+const handlePermissionCheck = (data: any, checked: any) => {
+  // 这个方法保留用于兼容性,实际逻辑在MenuPermissionTree组件中处理
+}
+
+// 保存权限
+const savePermissions = async () => {
+  if (!currentRole.value) return
+  
+  try {
+    savingPermissions.value = true
+    
+    // 获取选中的权限
+    const checkedKeys = permissionTreeRef.value?.getCheckedKeys() || []
+    
+    // 保存角色菜单权限
+    await request.put(`/api/v1/admin/roles/${currentRole.value.id}/menus`, {
+      menu_ids: checkedKeys
+    })
+    
+    ElMessage.success('权限保存成功')
+    showPermissionDialog.value = false
+    
+    // 刷新角色列表
+    loadRoles()
+  } catch (error) {
+    console.error('保存权限失败:', error)
+    ElMessage.error('保存权限失败')
+  } finally {
+    savingPermissions.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  editingRole.value = null
+  roleForm.display_name = ''
+  roleForm.name = ''
+  roleForm.description = ''
+  roleFormRef.value?.clearValidate()
+}
+
+// 格式化日期时间
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return '-'
+  return new Date(dateTime).toLocaleString('zh-CN')
+}
+
+// 组件挂载
+onMounted(() => {
+  loadRoles()
+})
+</script>
+
+<style scoped>
+.roles-management {
+  padding: 20px;
+}
+
+.page-header {
+  margin-bottom: 20px;
+}
+
+.page-header h2 {
+  margin: 0 0 8px 0;
+  color: #333;
+}
+
+.page-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.table-container {
+  overflow-x: auto;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.role-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.role-name {
+  font-weight: 500;
+}
+
+.time-info {
+  font-size: 12px;
+  color: #666;
+}
+
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-table) {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-table .el-table__cell) {
+  padding: 8px 0;
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+/* 权限对话框样式 */
+.permission-dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.dialog-header h4 {
+  margin: 0 0 8px 0;
+  color: #303133;
+  font-size: 16px;
+}
+
+.dialog-description {
+  margin: 0;
+  color: #606266;
+  font-size: 14px;
+  line-height: 1.5;
+}
+
+.super-admin-notice {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 16px;
+  background: linear-gradient(90deg, rgba(103, 194, 58, 0.1), rgba(103, 194, 58, 0.05));
+  border: 1px solid rgba(103, 194, 58, 0.3);
+  border-radius: 6px;
+  color: #67c23a;
+  font-weight: 500;
+}
+
+.super-admin-notice .el-icon {
+  color: #67c23a;
+  font-size: 16px;
+}
+
+.permission-content {
+  flex: 1;
+  min-height: 400px;
+}
+
+.permission-summary {
+  margin-top: 16px;
+}
+
+.summary-details {
+  display: flex;
+  gap: 16px;
+  margin-top: 8px;
+  font-size: 13px;
+}
+
+.summary-details span {
+  padding: 2px 8px;
+  background: rgba(64, 158, 255, 0.1);
+  border-radius: 4px;
+  color: #409eff;
+}
+
+.summary-details .total-permissions {
+  background: rgba(103, 194, 58, 0.1);
+  color: #67c23a;
+  font-weight: 600;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 731 - 23
src/views/admin/Users.vue

@@ -1,39 +1,656 @@
 <template>
-  <div class="admin-users">
+  <div class="users-management">
     <div class="page-header">
       <h2>用户管理</h2>
-      <p>管理系统用户账户、权限和状态</p>
+      <p>管理系统用户和角色分配</p>
     </div>
 
-    <el-card>
-      <div class="coming-soon">
-        <el-icon size="64" color="#409EFF"><UserFilled /></el-icon>
-        <h3>用户管理功能</h3>
-        <p>此功能正在开发中,敬请期待...</p>
-        <el-button type="primary" @click="$router.go(-1)">返回</el-button>
+    <!-- 操作栏 -->
+    <div class="toolbar">
+      <div class="toolbar-left">
+        <el-button type="primary" @click="showCreateDialog = true">
+          <el-icon><Plus /></el-icon>
+          创建用户
+        </el-button>
+        <el-button 
+          type="danger" 
+          :disabled="selectedUsers.length === 0"
+          @click="batchDelete"
+        >
+          <el-icon><Delete /></el-icon>
+          批量删除
+        </el-button>
       </div>
-    </el-card>
+      <div class="toolbar-right">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索用户名、邮箱、姓名..."
+          style="width: 300px"
+          @input="handleSearch"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+      </div>
+    </div>
+
+    <!-- 用户列表 -->
+    <div class="table-container">
+      <el-table
+        v-loading="loading"
+        :data="users"
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+        :scroll-x="true"
+      >
+      <el-table-column type="selection" width="55" />
+      <el-table-column prop="username" label="用户信息" width="140" fixed="left">
+        <template #default="{ row }">
+          <div class="user-info">
+            <el-avatar :size="28" :src="row.avatar_url">
+              {{ row.username.charAt(0).toUpperCase() }}
+            </el-avatar>
+            <div class="user-details">
+              <div class="username">{{ row.username }}</div>
+              <div class="real-name" v-if="row.real_name">{{ row.real_name }}</div>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="email" label="邮箱" width="180" show-overflow-tooltip />
+      <el-table-column prop="phone" label="手机号" width="120" />
+      <el-table-column prop="roles" label="角色" width="130">
+        <template #default="{ row }">
+          <div class="roles-container">
+            <el-tag 
+              v-for="(role, index) in row.roles.slice(0, 1)" 
+              :key="role" 
+              size="small" 
+              style="margin-right: 4px; margin-bottom: 2px;"
+            >
+              {{ role }}
+            </el-tag>
+            <el-tooltip 
+              v-if="row.roles.length > 1"
+              :content="row.roles.slice(1).join(', ')"
+              placement="top"
+            >
+              <el-tag size="small" type="info">+{{ row.roles.length - 1 }}</el-tag>
+            </el-tooltip>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column prop="is_superuser" label="管理员" width="70" align="center">
+        <template #default="{ row }">
+          <el-tag :type="row.is_superuser ? 'danger' : 'info'" size="small">
+            {{ row.is_superuser ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="is_active" label="状态" width="70" align="center">
+        <template #default="{ row }">
+          <el-switch
+            v-model="row.is_active"
+            @change="handleStatusChange(row)"
+            size="small"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column prop="last_login_at" label="最后登录" width="130">
+        <template #default="{ row }">
+          <div class="time-info">
+            {{ formatDateTime(row.last_login_at) }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="160" fixed="right">
+        <template #default="{ row }">
+          <div class="action-buttons">
+            <el-button type="primary" size="small" @click="editUser(row)">
+              编辑
+            </el-button>
+            <el-dropdown @command="(command) => handleDropdownCommand(command, row)">
+              <el-button size="small">
+                更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="roles">分配角色</el-dropdown-item>
+                  <el-dropdown-item 
+                    command="delete" 
+                    :disabled="row.is_superuser || isCurrentUser(row.id)"
+                    divided
+                  >
+                    删除用户
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
+        </template>
+      </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 分页 -->
+    <div class="pagination">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 50, 100]"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+
+    <!-- 创建/编辑用户对话框 -->
+    <el-dialog
+      v-model="showCreateDialog"
+      :title="editingUser ? '编辑用户' : '创建用户'"
+      width="600px"
+      @close="resetForm"
+    >
+      <el-form
+        ref="userFormRef"
+        :model="userForm"
+        :rules="userRules"
+        label-width="80px"
+      >
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="用户名" prop="username">
+              <el-input 
+                v-model="userForm.username" 
+                placeholder="请输入用户名"
+                :disabled="!!editingUser"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="邮箱" prop="email">
+              <el-input v-model="userForm.email" placeholder="请输入邮箱" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="密码" :prop="editingUser ? '' : 'password'">
+              <el-input 
+                v-model="userForm.password" 
+                type="password" 
+                :placeholder="editingUser ? '留空则不修改密码' : '请输入密码'"
+                show-password
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="手机号" prop="phone">
+              <el-input v-model="userForm.phone" placeholder="请输入手机号" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="真实姓名" prop="real_name">
+              <el-input v-model="userForm.real_name" placeholder="请输入真实姓名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="公司" prop="company">
+              <el-input v-model="userForm.company" placeholder="请输入公司名称" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="部门" prop="department">
+              <el-input v-model="userForm.department" placeholder="请输入部门" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="角色" prop="role_ids">
+              <el-select 
+                v-model="userForm.role_ids" 
+                multiple 
+                placeholder="请选择角色"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="role in allRoles"
+                  :key="role.id"
+                  :label="role.display_name"
+                  :value="role.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="管理员">
+              <el-switch v-model="userForm.is_superuser" />
+              <span style="margin-left: 8px; font-size: 12px; color: #666;">
+                拥有系统所有权限
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="启用状态">
+              <el-switch v-model="userForm.is_active" />
+              <span style="margin-left: 8px; font-size: 12px; color: #666;">
+                用户是否可以登录
+              </span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="saveUser" :loading="saving">
+          {{ editingUser ? '更新' : '创建' }}
+        </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 角色分配对话框 -->
+    <el-dialog
+      v-model="showRoleDialog"
+      title="分配角色"
+      width="500px"
+    >
+      <div v-if="currentUser">
+        <h4>为用户 "{{ currentUser.username }}" 分配角色</h4>
+        <el-checkbox-group v-model="selectedRoleIds">
+          <el-checkbox 
+            v-for="role in allRoles" 
+            :key="role.id" 
+            :label="role.id"
+            :value="role.id"
+          >
+            {{ role.display_name }}
+            <el-tag v-if="role.is_system" type="info" size="small" style="margin-left: 8px;">
+              系统角色
+            </el-tag>
+          </el-checkbox>
+        </el-checkbox-group>
+      </div>
+      <template #footer>
+        <el-button @click="showRoleDialog = false">取消</el-button>
+        <el-button type="primary" @click="saveUserRoles" :loading="savingRoles">
+          保存
+        </el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-// 用户管理页面
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search, Delete, ArrowDown } from '@element-plus/icons-vue'
+import { useAuthStore } from '@/stores/auth'
+import request from '@/api/request'
+
+const authStore = useAuthStore()
+
+// 响应式数据
+const loading = ref(false)
+const saving = ref(false)
+const savingRoles = ref(false)
+const users = ref<any[]>([])
+const allRoles = ref<any[]>([])
+const selectedUsers = ref<any[]>([])
+const searchKeyword = ref('')
+const currentPage = ref(1)
+const pageSize = ref(20)
+const total = ref(0)
+
+// 对话框状态
+const showCreateDialog = ref(false)
+const showRoleDialog = ref(false)
+const editingUser = ref<any>(null)
+const currentUser = ref<any>(null)
+const selectedRoleIds = ref<string[]>([])
+
+// 表单数据
+const userForm = reactive({
+  username: '',
+  email: '',
+  password: '',
+  phone: '',
+  real_name: '',
+  company: '',
+  department: '',
+  role_ids: [] as string[],
+  is_superuser: false,
+  is_active: true
+})
+
+// 表单验证规则
+const userRules = {
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
+  ],
+  email: [
+    { required: true, message: '请输入邮箱', trigger: 'blur' },
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+  ]
+}
+
+// 引用
+const userFormRef = ref()
+
+// 判断是否为当前用户
+const isCurrentUser = (userId: string) => {
+  return userId === authStore.user?.id
+}
+
+// 加载用户列表
+const loadUsers = async () => {
+  loading.value = true
+  try {
+    const result = await request.get('/api/v1/admin/users', {
+      params: {
+        page: currentPage.value,
+        page_size: pageSize.value,
+        keyword: searchKeyword.value || undefined
+      }
+    })
+    
+    if (result.code === 0) {
+      users.value = result.data.items
+      total.value = result.data.total
+    } else {
+      throw new Error(result.message)
+    }
+  } catch (error) {
+    console.error('加载用户列表失败:', error)
+    ElMessage.error('加载用户列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 加载所有角色
+const loadRoles = async () => {
+  try {
+    const result = await request.get('/api/v1/roles/all')
+    
+    if (result.code === 0) {
+      allRoles.value = result.data
+    }
+  } catch (error) {
+    console.error('加载角色列表失败:', error)
+  }
+}
+
+// 搜索处理
+const handleSearch = () => {
+  currentPage.value = 1
+  loadUsers()
+}
+
+// 分页处理
+const handleSizeChange = (size: number) => {
+  pageSize.value = size
+  currentPage.value = 1
+  loadUsers()
+}
+
+const handleCurrentChange = (page: number) => {
+  currentPage.value = page
+  loadUsers()
+}
+
+// 选择处理
+const handleSelectionChange = (selection: any[]) => {
+  selectedUsers.value = selection
+}
+
+// 状态切换
+const handleStatusChange = async (user: any) => {
+  try {
+    await request.put(`/api/v1/admin/users/${user.id}`, {
+      is_active: user.is_active
+    })
+    ElMessage.success('用户状态更新成功')
+  } catch (error) {
+    console.error('更新用户状态失败:', error)
+    ElMessage.error('更新用户状态失败')
+    // 恢复原状态
+    user.is_active = !user.is_active
+  }
+}
+
+// 编辑用户
+const editUser = (user: any) => {
+  editingUser.value = user
+  Object.assign(userForm, {
+    username: user.username,
+    email: user.email,
+    password: '',
+    phone: user.phone || '',
+    real_name: user.real_name || '',
+    company: user.company || '',
+    department: user.department || '',
+    role_ids: [], // 需要从用户角色关联中获取
+    is_superuser: user.is_superuser,
+    is_active: user.is_active
+  })
+  showCreateDialog.value = true
+}
+
+// 删除用户
+const deleteUser = async (user: any) => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除用户 "${user.username}" 吗?此操作不可恢复。`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    
+    await request.delete(`/api/v1/admin/users/${user.id}`)
+    ElMessage.success('用户删除成功')
+    loadUsers()
+  } catch (error: any) {
+    if (error.response?.data?.message) {
+      ElMessage.error(error.response.data.message)
+    } else if (error !== 'cancel') {
+      ElMessage.error('删除用户失败')
+    }
+  }
+}
+
+// 批量删除
+const batchDelete = async () => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除选中的 ${selectedUsers.value.length} 个用户吗?此操作不可恢复。`,
+      '确认批量删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    
+    // 逐个删除(可以优化为批量API)
+    for (const user of selectedUsers.value) {
+      await request.delete(`/api/v1/admin/users/${user.id}`)
+    }
+    
+    ElMessage.success('批量删除成功')
+    loadUsers()
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('批量删除失败')
+    }
+  }
+}
+
+// 下拉菜单处理
+const handleDropdownCommand = (command: string, user: any) => {
+  switch (command) {
+    case 'roles':
+      assignRoles(user)
+      break
+    case 'delete':
+      deleteUser(user)
+      break
+  }
+}
+
+// 分配角色
+const assignRoles = async (user: any) => {
+  currentUser.value = user
+  // TODO: 获取用户当前角色
+  selectedRoleIds.value = []
+  showRoleDialog.value = true
+}
+
+// 保存用户
+const saveUser = async () => {
+  try {
+    await userFormRef.value.validate()
+    
+    saving.value = true
+    
+    if (editingUser.value) {
+      // 更新用户
+      const updateData: any = {
+        email: userForm.email,
+        phone: userForm.phone,
+        real_name: userForm.real_name,
+        company: userForm.company,
+        department: userForm.department,
+        role_ids: userForm.role_ids,
+        is_superuser: userForm.is_superuser,
+        is_active: userForm.is_active
+      }
+      
+      // 如果填写了密码,则更新密码
+      if (userForm.password) {
+        updateData.password = userForm.password
+      }
+      
+      await request.put(`/api/v1/admin/users/${editingUser.value.id}`, updateData)
+      ElMessage.success('用户更新成功')
+    } else {
+      // 创建用户
+      await request.post('/api/v1/admin/users', userForm)
+      ElMessage.success('用户创建成功')
+    }
+    
+    showCreateDialog.value = false
+    loadUsers()
+  } catch (error: any) {
+    console.error('保存用户失败:', error)
+    if (error.response?.data?.message) {
+      ElMessage.error(error.response.data.message)
+    } else {
+      ElMessage.error('保存用户失败')
+    }
+  } finally {
+    saving.value = false
+  }
+}
+
+// 保存用户角色
+const saveUserRoles = async () => {
+  try {
+    savingRoles.value = true
+    
+    await request.put(`/api/v1/admin/users/${currentUser.value.id}`, {
+      role_ids: selectedRoleIds.value
+    })
+    
+    ElMessage.success('角色分配成功')
+    showRoleDialog.value = false
+    loadUsers()
+  } catch (error) {
+    console.error('保存用户角色失败:', error)
+    ElMessage.error('保存用户角色失败')
+  } finally {
+    savingRoles.value = false
+  }
+}
+
+// 重置表单
+const resetForm = () => {
+  editingUser.value = null
+  Object.assign(userForm, {
+    username: '',
+    email: '',
+    password: '',
+    phone: '',
+    real_name: '',
+    company: '',
+    department: '',
+    role_ids: [],
+    is_superuser: false,
+    is_active: true
+  })
+  userFormRef.value?.clearValidate()
+}
+
+// 格式化日期时间
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return '-'
+  const date = new Date(dateTime)
+  const now = new Date()
+  const diffTime = now.getTime() - date.getTime()
+  const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
+  
+  if (diffDays === 0) {
+    return date.toLocaleTimeString('zh-CN', { 
+      hour: '2-digit', 
+      minute: '2-digit' 
+    })
+  } else if (diffDays < 7) {
+    return `${diffDays}天前`
+  } else {
+    return date.toLocaleDateString('zh-CN', { 
+      month: '2-digit', 
+      day: '2-digit' 
+    })
+  }
+}
+
+// 组件挂载
+onMounted(() => {
+  loadUsers()
+  loadRoles()
+})
 </script>
 
 <style scoped>
-.admin-users {
+.users-management {
   padding: 20px;
 }
 
 .page-header {
-  margin-bottom: 24px;
+  margin-bottom: 20px;
 }
 
 .page-header h2 {
   margin: 0 0 8px 0;
   color: #333;
-  font-size: 24px;
-  font-weight: 600;
 }
 
 .page-header p {
@@ -42,20 +659,111 @@
   font-size: 14px;
 }
 
-.coming-soon {
-  text-align: center;
-  padding: 60px 20px;
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.toolbar-left {
+  display: flex;
+  gap: 12px;
+}
+
+.toolbar-right {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.table-container {
+  overflow-x: auto;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.user-details {
+  display: flex;
+  flex-direction: column;
+  min-width: 0; /* 允许文本截断 */
+  flex: 1;
 }
 
-.coming-soon h3 {
-  margin: 16px 0 8px 0;
+.username {
+  font-weight: 500;
   color: #333;
-  font-size: 20px;
+  font-size: 13px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
-.coming-soon p {
-  margin: 0 0 24px 0;
+.real-name {
+  font-size: 11px;
   color: #666;
-  font-size: 14px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.roles-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 2px;
+  align-items: center;
+}
+
+.time-info {
+  font-size: 12px;
+  color: #666;
+  white-space: nowrap;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 6px;
+  align-items: center;
+}
+
+.pagination {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-table) {
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-table .el-table__cell) {
+  padding: 8px 0;
+}
+
+:deep(.el-dialog__body) {
+  padding: 20px;
+}
+
+:deep(.el-checkbox-group) {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+:deep(.el-checkbox) {
+  margin-right: 0;
 }
 </style>

Некоторые файлы не были показаны из-за большого количества измененных файлов