MainLayout.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. <template>
  2. <div class="main-layout">
  3. <el-container>
  4. <!-- 头部 -->
  5. <el-header class="header">
  6. <div class="header-left">
  7. <h1>SSO 认证中心</h1>
  8. </div>
  9. <div class="header-right">
  10. <el-dropdown @command="handleCommand">
  11. <span class="user-info">
  12. <el-avatar :src="authStore.user?.avatar_url" :size="32">
  13. {{ authStore.user?.username?.charAt(0).toUpperCase() }}
  14. </el-avatar>
  15. <span class="username">{{ authStore.user?.username }}</span>
  16. <el-icon><ArrowDown /></el-icon>
  17. </span>
  18. <template #dropdown>
  19. <el-dropdown-menu>
  20. <el-dropdown-item command="profile">
  21. <el-icon><User /></el-icon>
  22. 个人资料
  23. </el-dropdown-item>
  24. <el-dropdown-item command="settings">
  25. <el-icon><Setting /></el-icon>
  26. 设置
  27. </el-dropdown-item>
  28. <el-dropdown-item divided command="logout">
  29. <el-icon><SwitchButton /></el-icon>
  30. 退出登录
  31. </el-dropdown-item>
  32. </el-dropdown-menu>
  33. </template>
  34. </el-dropdown>
  35. </div>
  36. </el-header>
  37. <el-container>
  38. <!-- 侧边栏 -->
  39. <el-aside width="200px" class="sidebar">
  40. <el-menu
  41. :default-active="activeMenu"
  42. class="sidebar-menu"
  43. router
  44. v-loading="menuLoading"
  45. >
  46. <!-- 动态渲染菜单 -->
  47. <template v-for="menu in userMenus" :key="menu.id">
  48. <!-- 只显示菜单类型,过滤掉按钮类型 -->
  49. <template v-if="menu.menu_type === 'menu'">
  50. <!-- 有子菜单的情况(父级目录) -->
  51. <el-sub-menu v-if="hasMenuChildren(menu)" :index="menu.path || menu.name">
  52. <template #title>
  53. <el-icon v-if="menu.icon">
  54. <component :is="getIconComponent(menu.icon)" />
  55. </el-icon>
  56. <span>{{ menu.title }}</span>
  57. </template>
  58. <el-menu-item
  59. v-for="child in getMenuChildren(menu)"
  60. :key="child.id"
  61. :index="child.path || child.name"
  62. v-show="!child.is_hidden"
  63. >
  64. <el-icon v-if="child.icon">
  65. <component :is="getIconComponent(child.icon)" />
  66. </el-icon>
  67. <span>{{ child.title }}</span>
  68. </el-menu-item>
  69. </el-sub-menu>
  70. <!-- 没有子菜单的情况(具体菜单页面) -->
  71. <el-menu-item
  72. v-else
  73. :index="menu.path || menu.name"
  74. v-show="!menu.is_hidden"
  75. >
  76. <el-icon v-if="menu.icon">
  77. <component :is="getIconComponent(menu.icon)" />
  78. </el-icon>
  79. <span>{{ menu.title }}</span>
  80. </el-menu-item>
  81. </template>
  82. </template>
  83. </el-menu>
  84. </el-aside>
  85. <!-- 主内容区 -->
  86. <el-main class="main-content">
  87. <router-view />
  88. </el-main>
  89. </el-container>
  90. </el-container>
  91. </div>
  92. </template>
  93. <script setup lang="ts">
  94. import { computed, ref, onMounted } from 'vue'
  95. import { useRouter, useRoute } from 'vue-router'
  96. import { ElMessage, ElMessageBox } from 'element-plus'
  97. import { useAuthStore } from '@/stores/auth'
  98. import request from '@/api/request'
  99. import * as ElementPlusIcons from '@element-plus/icons-vue'
  100. const router = useRouter()
  101. const route = useRoute()
  102. const authStore = useAuthStore()
  103. // 接口定义
  104. interface MenuItem {
  105. id: string | number
  106. name: string
  107. title: string
  108. path: string
  109. icon?: string
  110. menu_type?: string
  111. is_hidden?: boolean
  112. is_active?: boolean
  113. children?: MenuItem[]
  114. parent_id?: string | number | null
  115. }
  116. interface ApiResponse<T = any> {
  117. code: number
  118. message: string
  119. data: T
  120. timestamp: string
  121. }
  122. // 响应式数据
  123. const userMenus = ref<MenuItem[]>([])
  124. const menuLoading = ref(false)
  125. const activeMenu = computed(() => {
  126. // 处理子路由的菜单激活状态
  127. const path = route.path
  128. if (path.startsWith('/admin')) {
  129. return path
  130. }
  131. if (path.startsWith('/apps')) {
  132. return '/apps'
  133. }
  134. return path
  135. })
  136. // 获取图标组件
  137. const getIconComponent = (iconName: string) => {
  138. return (ElementPlusIcons as any)[iconName] || ElementPlusIcons.Menu
  139. }
  140. // 检查是否有菜单类型的子项
  141. const hasMenuChildren = (menu: MenuItem) => {
  142. return menu.children && menu.children.some((child: MenuItem) => child.menu_type === 'menu')
  143. }
  144. // 获取菜单类型的子项
  145. const getMenuChildren = (menu: MenuItem) => {
  146. return menu.children ? menu.children.filter((child: MenuItem) => child.menu_type === 'menu') : []
  147. }
  148. // 获取用户菜单
  149. const loadUserMenus = async () => {
  150. menuLoading.value = true
  151. try {
  152. const result = await request.get<any, ApiResponse<MenuItem[]>>('/api/v1/system/user/menus')
  153. if (result.code === 0) {
  154. userMenus.value = result.data
  155. console.log('用户菜单加载成功:', result.data)
  156. } else {
  157. throw new Error(result.message || '获取菜单失败')
  158. }
  159. } catch (error) {
  160. console.error('获取用户菜单失败:', error)
  161. ElMessage.error('获取菜单失败,请刷新页面重试')
  162. // 如果获取菜单失败,使用默认菜单
  163. userMenus.value = getDefaultMenus()
  164. } finally {
  165. menuLoading.value = false
  166. }
  167. }
  168. // 获取默认菜单(兜底方案)
  169. const getDefaultMenus = (): MenuItem[] => {
  170. const defaultMenus: MenuItem[] = [
  171. {
  172. id: 'dashboard',
  173. name: 'dashboard',
  174. title: '仪表盘',
  175. path: '/dashboard',
  176. icon: 'House',
  177. children: []
  178. },
  179. {
  180. id: 'profile',
  181. name: 'profile',
  182. title: '个人资料',
  183. path: '/profile',
  184. icon: 'User',
  185. children: []
  186. },
  187. {
  188. id: 'apps',
  189. name: 'apps',
  190. title: '我的应用',
  191. path: '/apps',
  192. icon: 'Grid',
  193. children: []
  194. }
  195. ]
  196. // 如果是管理员,添加系统管理菜单
  197. if (authStore.isAdmin) {
  198. // 管理员默认菜单中包含文档中心
  199. defaultMenus.push({
  200. id: 'documents',
  201. name: 'documents',
  202. title: '文档管理中心',
  203. path: '/admin/documents',
  204. icon: 'Document',
  205. children: [
  206. {
  207. id: 'kb-list',
  208. name: 'kb-list',
  209. title: '知识库管理',
  210. path: '/admin/documents/kb',
  211. icon: 'Collection',
  212. menu_type: 'menu',
  213. is_hidden: false
  214. },
  215. {
  216. id: 'kb-snippet',
  217. name: 'kb-snippet',
  218. title: '知识片段',
  219. path: '/admin/documents/snippet',
  220. icon: 'Tickets',
  221. menu_type: 'menu',
  222. is_hidden: false
  223. },
  224. {
  225. id: 'search-engine',
  226. name: 'search-engine',
  227. title: '检索引擎',
  228. path: '/admin/documents/search-engine',
  229. icon: 'Search',
  230. menu_type: 'menu',
  231. is_hidden: false
  232. }
  233. ]
  234. })
  235. defaultMenus.push({
  236. id: 'admin',
  237. name: 'admin',
  238. title: '系统管理',
  239. path: '/admin',
  240. icon: 'Setting',
  241. children: [
  242. {
  243. id: 'admin-dashboard',
  244. name: 'admin-dashboard',
  245. title: '管理概览',
  246. path: '/admin/dashboard',
  247. icon: 'Monitor',
  248. is_hidden: false
  249. },
  250. {
  251. id: 'admin-users',
  252. name: 'admin-users',
  253. title: '用户管理',
  254. path: '/admin/users',
  255. icon: 'UserFilled',
  256. is_hidden: false
  257. },
  258. {
  259. id: 'admin-roles',
  260. name: 'admin-roles',
  261. title: '角色管理',
  262. path: '/admin/roles',
  263. icon: 'Avatar',
  264. is_hidden: false
  265. },
  266. {
  267. id: 'admin-menus',
  268. name: 'admin-menus',
  269. title: '菜单管理',
  270. path: '/admin/menus',
  271. icon: 'Menu',
  272. is_hidden: false
  273. }
  274. ]
  275. })
  276. }
  277. return defaultMenus
  278. }
  279. // 处理下拉菜单命令
  280. const handleCommand = async (command: string) => {
  281. switch (command) {
  282. case 'profile':
  283. router.push('/profile')
  284. break
  285. case 'settings':
  286. router.push('/settings')
  287. break
  288. case 'logout':
  289. try {
  290. await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
  291. confirmButtonText: '确定',
  292. cancelButtonText: '取消',
  293. type: 'warning'
  294. })
  295. await authStore.logout()
  296. ElMessage.success('已退出登录')
  297. router.push('/login')
  298. } catch (error) {
  299. // 用户取消
  300. }
  301. break
  302. }
  303. }
  304. onMounted(() => {
  305. // 加载用户菜单
  306. loadUserMenus()
  307. })
  308. </script>
  309. <style scoped>
  310. .main-layout {
  311. height: 100vh;
  312. }
  313. .header {
  314. display: flex;
  315. justify-content: space-between;
  316. align-items: center;
  317. background: #fff;
  318. border-bottom: 1px solid #e6e6e6;
  319. padding: 0 20px;
  320. }
  321. .header-left h1 {
  322. margin: 0;
  323. color: #333;
  324. font-size: 20px;
  325. font-weight: 600;
  326. }
  327. .header-right {
  328. display: flex;
  329. align-items: center;
  330. }
  331. .user-info {
  332. display: flex;
  333. align-items: center;
  334. gap: 8px;
  335. cursor: pointer;
  336. padding: 8px;
  337. border-radius: 4px;
  338. transition: background-color 0.3s;
  339. }
  340. .user-info:hover {
  341. background-color: #f5f5f5;
  342. }
  343. .username {
  344. font-size: 14px;
  345. color: #333;
  346. }
  347. .sidebar {
  348. background: #fff;
  349. border-right: 1px solid #e6e6e6;
  350. }
  351. .sidebar-menu {
  352. border-right: none;
  353. }
  354. .main-content {
  355. background: #f5f5f5;
  356. padding: 0;
  357. }
  358. </style>