Showing
16 changed files
with
350 additions
and
40 deletions
| ... | @@ -7,6 +7,22 @@ export {} | ... | @@ -7,6 +7,22 @@ export {} |
| 7 | 7 | ||
| 8 | declare module 'vue' { | 8 | declare module 'vue' { |
| 9 | export interface GlobalComponents { | 9 | export interface GlobalComponents { |
| 10 | + ElButton: typeof import('element-plus/es')['ElButton'] | ||
| 11 | + ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] | ||
| 12 | + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] | ||
| 13 | + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] | ||
| 14 | + ElDialog: typeof import('element-plus/es')['ElDialog'] | ||
| 15 | + ElForm: typeof import('element-plus/es')['ElForm'] | ||
| 16 | + ElFormItem: typeof import('element-plus/es')['ElFormItem'] | ||
| 17 | + ElInput: typeof import('element-plus/es')['ElInput'] | ||
| 18 | + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] | ||
| 19 | + ElOption: typeof import('element-plus/es')['ElOption'] | ||
| 20 | + ElRadio: typeof import('element-plus/es')['ElRadio'] | ||
| 21 | + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] | ||
| 22 | + ElSelect: typeof import('element-plus/es')['ElSelect'] | ||
| 23 | + ElTable: typeof import('element-plus/es')['ElTable'] | ||
| 24 | + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] | ||
| 25 | + ElTag: typeof import('element-plus/es')['ElTag'] | ||
| 10 | Header: typeof import('./src/components/layout/Header.vue')['default'] | 26 | Header: typeof import('./src/components/layout/Header.vue')['default'] |
| 11 | RouterLink: typeof import('vue-router')['RouterLink'] | 27 | RouterLink: typeof import('vue-router')['RouterLink'] |
| 12 | RouterView: typeof import('vue-router')['RouterView'] | 28 | RouterView: typeof import('vue-router')['RouterView'] | ... | ... |
| ... | @@ -108,6 +108,11 @@ export const menuApi = { | ... | @@ -108,6 +108,11 @@ export const menuApi = { |
| 108 | return api.get('/api/system/menu/tree', { params: { userId } }) | 108 | return api.get('/api/system/menu/tree', { params: { userId } }) |
| 109 | }, | 109 | }, |
| 110 | 110 | ||
| 111 | + // 根据用户ID获取用户菜单权限树 | ||
| 112 | + getUserMenuTree: (userId: number): Promise<ApiResponse<Menu[]>> => { | ||
| 113 | + return api.get('/api/system/menu/tree', { params: { userId } }) | ||
| 114 | + }, | ||
| 115 | + | ||
| 111 | // 获取菜单详情 | 116 | // 获取菜单详情 |
| 112 | getMenuById: (menuId: number): Promise<ApiResponse<Menu>> => { | 117 | getMenuById: (menuId: number): Promise<ApiResponse<Menu>> => { |
| 113 | return api.get(`/api/system/menu/${menuId}`) | 118 | return api.get(`/api/system/menu/${menuId}`) |
| ... | @@ -139,4 +144,9 @@ export const menuApi = { | ... | @@ -139,4 +144,9 @@ export const menuApi = { |
| 139 | } | 144 | } |
| 140 | } | 145 | } |
| 141 | 146 | ||
| 147 | +// 单独导出常用方法 | ||
| 148 | +export const getUserMenuTree = menuApi.getUserMenuTree | ||
| 149 | +export const getMenuTree = menuApi.getMenuTree | ||
| 150 | +export const getMenuList = menuApi.getMenuList | ||
| 151 | + | ||
| 142 | export default menuApi | 152 | export default menuApi |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -14,12 +14,15 @@ | ... | @@ -14,12 +14,15 @@ |
| 14 | </div> | 14 | </div> |
| 15 | 15 | ||
| 16 | <nav class="sidebar-nav"> | 16 | <nav class="sidebar-nav"> |
| 17 | - <ul class="nav-list"> | 17 | + <div v-if="menuLoading" class="menu-loading"> |
| 18 | + <span>菜单加载中...</span> | ||
| 19 | + </div> | ||
| 20 | + <ul v-else class="nav-list"> | ||
| 18 | <li v-for="item in menuItems" :key="item.path || item.name" class="nav-item"> | 21 | <li v-for="item in menuItems" :key="item.path || item.name" class="nav-item"> |
| 19 | <!-- 一级菜单 --> | 22 | <!-- 一级菜单 --> |
| 20 | <div v-if="!item.children" class="nav-item-single"> | 23 | <div v-if="!item.children" class="nav-item-single"> |
| 21 | <a | 24 | <a |
| 22 | - @click="handleMenuClick(item.path)" | 25 | + @click="item.path ? handleMenuClick(item.path) : null" |
| 23 | class="nav-link" | 26 | class="nav-link" |
| 24 | :class="{ active: $route.path === item.path }" | 27 | :class="{ active: $route.path === item.path }" |
| 25 | > | 28 | > |
| ... | @@ -127,10 +130,13 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue' | ... | @@ -127,10 +130,13 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue' |
| 127 | import { useRouter, useRoute } from 'vue-router' | 130 | import { useRouter, useRoute } from 'vue-router' |
| 128 | import { logoutApi } from '../api/auth' | 131 | import { logoutApi } from '../api/auth' |
| 129 | import { request } from '../utils/request' | 132 | import { request } from '../utils/request' |
| 133 | +import { loadUserMenus, getDefaultMenus, type MenuItem } from '../services/menuService' | ||
| 130 | 134 | ||
| 131 | const router = useRouter() | 135 | const router = useRouter() |
| 132 | const route = useRoute() | 136 | const route = useRoute() |
| 133 | 137 | ||
| 138 | +// 用户信息相关 | ||
| 139 | + | ||
| 134 | // 侧边栏状态 | 140 | // 侧边栏状态 |
| 135 | const sidebarCollapsed = ref(false) | 141 | const sidebarCollapsed = ref(false) |
| 136 | 142 | ||
| ... | @@ -138,6 +144,9 @@ const sidebarCollapsed = ref(false) | ... | @@ -138,6 +144,9 @@ const sidebarCollapsed = ref(false) |
| 138 | const userInfo = ref<any>(null) | 144 | const userInfo = ref<any>(null) |
| 139 | const userInfoLoading = ref(false) | 145 | const userInfoLoading = ref(false) |
| 140 | 146 | ||
| 147 | +// 菜单相关 | ||
| 148 | +const menuLoading = ref(false) | ||
| 149 | + | ||
| 141 | // 获取用户信息 | 150 | // 获取用户信息 |
| 142 | const fetchUserInfo = async () => { | 151 | const fetchUserInfo = async () => { |
| 143 | try { | 152 | try { |
| ... | @@ -160,38 +169,39 @@ const fetchUserInfo = async () => { | ... | @@ -160,38 +169,39 @@ const fetchUserInfo = async () => { |
| 160 | // 标签页容器引用 | 169 | // 标签页容器引用 |
| 161 | const tabContainer = ref<HTMLElement>() | 170 | const tabContainer = ref<HTMLElement>() |
| 162 | 171 | ||
| 163 | -// 菜单项配置 | 172 | +// 动态菜单项 |
| 164 | -const menuItems = ref([ | 173 | +const menuItems = ref<MenuItem[]>([]) |
| 165 | - { name: '首页', path: '/main/dashboard', icon: '🏠' }, | 174 | + |
| 166 | - { name: '订单管理', path: '/main/order', icon: '📋' }, | 175 | + |
| 167 | - { name: '出库查询', path: '/main/delivery', icon: '📦' }, | 176 | +// 加载用户菜单权限 |
| 168 | - { name: '发票管理', path: '/main/invoice', icon: '🧾' }, | 177 | +const loadUserMenuPermissions = async () => { |
| 169 | - { name: '产品管理', path: '/main/product', icon: '📦' }, | 178 | + try { |
| 170 | - { name: '经销商管理', path: '/main/dealer', icon: '🏢' }, | 179 | + menuLoading.value = true |
| 171 | - { name: '返利管理', path: '/main/rebate', icon: '💰' }, | 180 | + |
| 172 | - { name: '异常工单', path: '/main/exception-workorder', icon: '⚠️' }, | 181 | + // 获取用户信息 |
| 173 | - { | 182 | + const currentUser = userInfo.value |
| 174 | - name: '系统设置', | 183 | + if (!currentUser) { |
| 175 | - icon: '⚙️', | 184 | + menuItems.value = getDefaultMenus() |
| 176 | - expanded: false, | 185 | + return |
| 177 | - children: [ | 186 | + } |
| 178 | - { name: '用户管理', path: '/main/users', icon: '👤' }, | 187 | + |
| 179 | - { name: '角色管理', path: '/main/sys/role', icon: '🛡️' }, | 188 | + // 尝试不同的用户ID字段 |
| 180 | - { name: '菜单管理', path: '/main/sys/menu', icon: '📝' }, | 189 | + let userId = currentUser.userId || currentUser.id || currentUser.user_id |
| 181 | - { | 190 | + if (!userId) { |
| 182 | - name: '字典管理', | 191 | + userId = 1 // 使用admin用户ID作为默认值 |
| 183 | - path: '/main/sys/dict', | 192 | + } |
| 184 | - icon: '📖', | 193 | + |
| 185 | - expanded: false, | 194 | + // 调用菜单服务加载用户菜单 |
| 186 | - children: [ | 195 | + const userMenus = await loadUserMenus(userId) |
| 187 | - { name: '字典类型', path: '/main/sys/dict', icon: '📋' }, | 196 | + menuItems.value = userMenus |
| 188 | - { name: '字典项', path: '/main/sys/dict-item', icon: '📝' } | 197 | + } catch (error) { |
| 189 | - ] | 198 | + console.error('加载用户菜单失败:', error) |
| 190 | - }, | 199 | + // 如果加载失败,使用默认菜单 |
| 191 | - { name: '日志管理', path: '/main/sys/log', icon: '📊' } | 200 | + menuItems.value = getDefaultMenus() |
| 192 | - ] | 201 | + } finally { |
| 202 | + menuLoading.value = false | ||
| 193 | } | 203 | } |
| 194 | -]) | 204 | +} |
| 195 | 205 | ||
| 196 | // 打开的标签页 | 206 | // 打开的标签页 |
| 197 | const openTabs = ref([ | 207 | const openTabs = ref([ |
| ... | @@ -391,18 +401,25 @@ watch(() => route.path, (newPath) => { | ... | @@ -391,18 +401,25 @@ watch(() => route.path, (newPath) => { |
| 391 | } | 401 | } |
| 392 | }, { immediate: true }) | 402 | }, { immediate: true }) |
| 393 | 403 | ||
| 394 | -// 组件挂载时获取用户信息 | 404 | +// 组件挂载时获取用户信息和菜单 |
| 395 | -onMounted(() => { | 405 | +onMounted(async () => { |
| 396 | // 检查是否已登录 | 406 | // 检查是否已登录 |
| 397 | const token = localStorage.getItem('token') | 407 | const token = localStorage.getItem('token') |
| 398 | if (token) { | 408 | if (token) { |
| 399 | // 调用API获取用户信息 | 409 | // 调用API获取用户信息 |
| 400 | - fetchUserInfo() | 410 | + await fetchUserInfo() |
| 411 | + // 获取用户信息后加载菜单 | ||
| 412 | + await loadUserMenuPermissions() | ||
| 401 | } else { | 413 | } else { |
| 402 | // 如果没有token,尝试从本地存储获取 | 414 | // 如果没有token,尝试从本地存储获取 |
| 403 | const storedUserInfo = localStorage.getItem('userInfo') | 415 | const storedUserInfo = localStorage.getItem('userInfo') |
| 404 | if (storedUserInfo) { | 416 | if (storedUserInfo) { |
| 405 | userInfo.value = JSON.parse(storedUserInfo) | 417 | userInfo.value = JSON.parse(storedUserInfo) |
| 418 | + // 从本地存储获取用户信息后也加载菜单 | ||
| 419 | + await loadUserMenuPermissions() | ||
| 420 | + } else { | ||
| 421 | + // 如果没有用户信息,使用默认菜单 | ||
| 422 | + menuItems.value = getDefaultMenus() | ||
| 406 | } | 423 | } |
| 407 | } | 424 | } |
| 408 | }) | 425 | }) |
| ... | @@ -465,6 +482,13 @@ onMounted(() => { | ... | @@ -465,6 +482,13 @@ onMounted(() => { |
| 465 | padding: 16px 0; | 482 | padding: 16px 0; |
| 466 | } | 483 | } |
| 467 | 484 | ||
| 485 | +.menu-loading { | ||
| 486 | + padding: 20px; | ||
| 487 | + text-align: center; | ||
| 488 | + color: #bdc3c7; | ||
| 489 | + font-size: 12px; | ||
| 490 | +} | ||
| 491 | + | ||
| 468 | .nav-list { | 492 | .nav-list { |
| 469 | list-style: none; | 493 | list-style: none; |
| 470 | margin: 0; | 494 | margin: 0; |
| ... | @@ -620,11 +644,19 @@ onMounted(() => { | ... | @@ -620,11 +644,19 @@ onMounted(() => { |
| 620 | gap: 15px; | 644 | gap: 15px; |
| 621 | } | 645 | } |
| 622 | 646 | ||
| 647 | +.user-actions { | ||
| 648 | + display: flex; | ||
| 649 | + align-items: center; | ||
| 650 | + gap: 10px; | ||
| 651 | +} | ||
| 652 | + | ||
| 623 | .welcome-text { | 653 | .welcome-text { |
| 624 | color: #666; | 654 | color: #666; |
| 625 | font-size: 13px; | 655 | font-size: 13px; |
| 626 | } | 656 | } |
| 627 | 657 | ||
| 658 | + | ||
| 659 | + | ||
| 628 | .logout-btn { | 660 | .logout-btn { |
| 629 | background: #e74c3c; | 661 | background: #e74c3c; |
| 630 | color: white; | 662 | color: white; | ... | ... |
frontend/src/services/menuService.ts
0 → 100644
| 1 | +// 菜单服务 - 独立处理菜单相关逻辑,避免循环依赖 | ||
| 2 | +import { getUserMenuTree } from '@/api/menu' | ||
| 3 | + | ||
| 4 | +export interface MenuItem { | ||
| 5 | + name: string | ||
| 6 | + path?: string | ||
| 7 | + icon: string | ||
| 8 | + expanded?: boolean | ||
| 9 | + children?: MenuItem[] | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +// 路径映射 - 将后端路径映射到前端路由 | ||
| 13 | +const pathMapping: { [key: string]: string } = { | ||
| 14 | + // 系统管理相关 | ||
| 15 | + '/system/user': '/main/users', | ||
| 16 | + '/system/role': '/main/sys/role', | ||
| 17 | + '/system/menu': '/main/sys/menu', | ||
| 18 | + '/system/menu/list': '/main/sys/menu', // 菜单查询 | ||
| 19 | + '/system/dict': '/main/sys/dict', | ||
| 20 | + '/system/dict/type/list': '/main/sys/dict', // 字典类型查询 | ||
| 21 | + '/system/dict/item/list': '/main/sys/dict-item', // 字典项查询 | ||
| 22 | + '/system/log': '/main/sys/log', | ||
| 23 | + '/system/log/list': '/main/sys/log', // 日志查询 | ||
| 24 | + | ||
| 25 | + // 业务模块 | ||
| 26 | + '/dashboard': '/main/dashboard', | ||
| 27 | + '/order': '/main/order', | ||
| 28 | + '/delivery': '/main/delivery', | ||
| 29 | + '/invoice': '/main/invoice', | ||
| 30 | + '/rebate': '/main/rebate', | ||
| 31 | + '/validation': '/main/validation', | ||
| 32 | + '/product': '/main/product', | ||
| 33 | + '/dealer': '/main/dealer', | ||
| 34 | + '/main/exception-workorder': '/main/exception-workorder' // 保持原有路径 | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +// 将后端菜单数据转换为前端菜单格式 | ||
| 38 | +export const transformMenuData = (menus: any[]): MenuItem[] => { | ||
| 39 | + return menus.map(menu => { | ||
| 40 | + const transformedMenu: MenuItem = { | ||
| 41 | + name: menu.menuName, | ||
| 42 | + icon: getMenuIcon(menu.icon), | ||
| 43 | + expanded: false | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + // 处理路径 - 根据菜单类型和路径设置 | ||
| 47 | + if (menu.menuType === '1' && menu.path && menu.path !== '#') { | ||
| 48 | + // 菜单类型,先检查是否有路径映射 | ||
| 49 | + const mappedPath = pathMapping[menu.path] | ||
| 50 | + if (mappedPath) { | ||
| 51 | + transformedMenu.path = mappedPath | ||
| 52 | + } else { | ||
| 53 | + // 如果没有映射,使用默认规则 | ||
| 54 | + transformedMenu.path = menu.path.startsWith('/main') ? menu.path : `/main${menu.path}` | ||
| 55 | + } | ||
| 56 | + } else if (menu.menuType === '0') { | ||
| 57 | + // 目录类型,检查是否有路径映射 | ||
| 58 | + const mappedPath = pathMapping[menu.path] | ||
| 59 | + if (mappedPath) { | ||
| 60 | + // 如果目录有映射路径,设置为可点击的目录 | ||
| 61 | + transformedMenu.path = mappedPath | ||
| 62 | + } | ||
| 63 | + // 目录类型,通常没有路径或路径为 #,不设置path让前端处理为可展开的目录 | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + // 如果有子菜单,递归转换 | ||
| 67 | + if (menu.children && menu.children.length > 0) { | ||
| 68 | + transformedMenu.children = transformMenuData(menu.children) | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + return transformedMenu | ||
| 72 | + }) | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +// 处理菜单图标 | ||
| 76 | +const getMenuIcon = (icon: string): string => { | ||
| 77 | + if (!icon) return '📄' | ||
| 78 | + | ||
| 79 | + // 处理Element UI图标 | ||
| 80 | + if (icon.startsWith('el-icon-')) { | ||
| 81 | + return getElementIcon(icon) | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + // 处理emoji图标 - 简单检查是否包含emoji字符 | ||
| 85 | + if (icon.includes('🏠') || icon.includes('📋') || icon.includes('📦') || icon.includes('🧾') || | ||
| 86 | + icon.includes('📈') || icon.includes('✅') || icon.includes('🏢') || icon.includes('⚙️') || | ||
| 87 | + icon.includes('👤') || icon.includes('👥') || icon.includes('📝') || icon.includes('📖') || | ||
| 88 | + icon.includes('📊') || icon.includes('⚠️')) { | ||
| 89 | + return icon | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + // 默认图标映射 | ||
| 93 | + const iconMap: { [key: string]: string } = { | ||
| 94 | + 'user': '👤', | ||
| 95 | + 'peoples': '👥', | ||
| 96 | + 'menu': '📝', | ||
| 97 | + 'dict': '📖', | ||
| 98 | + 'log': '📊', | ||
| 99 | + 'home': '🏠', | ||
| 100 | + 'order': '📋', | ||
| 101 | + 'goods': '📦', | ||
| 102 | + 'ticket': '🧾', | ||
| 103 | + 'data': '📈', | ||
| 104 | + 'check': '✅', | ||
| 105 | + 'shop': '🏢', | ||
| 106 | + 'tools': '⚙️' | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + return iconMap[icon] || '📄' | ||
| 110 | +} | ||
| 111 | + | ||
| 112 | +// Element UI图标映射 | ||
| 113 | +const getElementIcon = (icon: string): string => { | ||
| 114 | + const elementIconMap: { [key: string]: string } = { | ||
| 115 | + 'el-icon-s-home': '🏠', | ||
| 116 | + 'el-icon-s-order': '📋', | ||
| 117 | + 'el-icon-s-goods': '📦', | ||
| 118 | + 'el-icon-s-ticket': '🧾', | ||
| 119 | + 'el-icon-s-data': '📈', | ||
| 120 | + 'el-icon-s-check': '✅', | ||
| 121 | + 'el-icon-s-shop': '🏢', | ||
| 122 | + 'el-icon-s-tools': '⚙️' | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + return elementIconMap[icon] || '📄' | ||
| 126 | +} | ||
| 127 | + | ||
| 128 | +// 加载用户菜单权限 | ||
| 129 | +export const loadUserMenus = async (userId: number): Promise<MenuItem[]> => { | ||
| 130 | + try { | ||
| 131 | + // 调用API获取用户菜单权限 | ||
| 132 | + const response = await getUserMenuTree(userId) | ||
| 133 | + | ||
| 134 | + if (response.code === 200) { | ||
| 135 | + const userMenus = response.data || [] | ||
| 136 | + // 转换为前端菜单格式 | ||
| 137 | + const transformedMenus = transformMenuData(userMenus) | ||
| 138 | + return transformedMenus | ||
| 139 | + } else { | ||
| 140 | + console.error('获取用户菜单权限失败:', response.message) | ||
| 141 | + return getDefaultMenus() | ||
| 142 | + } | ||
| 143 | + } catch (error) { | ||
| 144 | + console.error('加载用户菜单失败:', error) | ||
| 145 | + return getDefaultMenus() | ||
| 146 | + } | ||
| 147 | +} | ||
| 148 | + | ||
| 149 | +// 获取默认菜单 | ||
| 150 | +export const getDefaultMenus = (): MenuItem[] => { | ||
| 151 | + return [ | ||
| 152 | + { name: '首页', path: '/main/dashboard', icon: '🏠' } | ||
| 153 | + ] | ||
| 154 | +} |
| 1 | import { defineStore } from 'pinia' | 1 | import { defineStore } from 'pinia' |
| 2 | import { ref } from 'vue' | 2 | import { ref } from 'vue' |
| 3 | import type { Menu, RouteRecord } from '@/types' | 3 | import type { Menu, RouteRecord } from '@/types' |
| 4 | -import { getMenuListApi } from '@/api/menu' | 4 | +import { getMenuListApi, getUserMenuTree } from '@/api/menu' |
| 5 | import { useUserStore } from './user' | 5 | import { useUserStore } from './user' |
| 6 | import router from '@/router' | 6 | import router from '@/router' |
| 7 | 7 | ||
| ... | @@ -51,7 +51,7 @@ export const usePermissionStore = defineStore('permission', () => { | ... | @@ -51,7 +51,7 @@ export const usePermissionStore = defineStore('permission', () => { |
| 51 | const route: RouteRecord = { | 51 | const route: RouteRecord = { |
| 52 | path: menu.path, | 52 | path: menu.path, |
| 53 | name: menu.menuName, | 53 | name: menu.menuName, |
| 54 | - component: () => import(`@/views${menu.component}`), | 54 | + component: () => import(/* @vite-ignore */ `@/views${menu.component}`), |
| 55 | meta: { | 55 | meta: { |
| 56 | title: menu.menuName, | 56 | title: menu.menuName, |
| 57 | icon: menu.icon, | 57 | icon: menu.icon, |
| ... | @@ -120,6 +120,23 @@ export const usePermissionStore = defineStore('permission', () => { | ... | @@ -120,6 +120,23 @@ export const usePermissionStore = defineStore('permission', () => { |
| 120 | return [] | 120 | return [] |
| 121 | } | 121 | } |
| 122 | } | 122 | } |
| 123 | + | ||
| 124 | + // 根据用户ID获取用户菜单权限 | ||
| 125 | + const getUserPermissionMenus = async (userId: number): Promise<Menu[]> => { | ||
| 126 | + try { | ||
| 127 | + const response = await getUserMenuTree(userId) | ||
| 128 | + if (response.code === 200) { | ||
| 129 | + menuList.value = response.data || [] | ||
| 130 | + return response.data || [] | ||
| 131 | + } else { | ||
| 132 | + console.error('获取用户菜单权限失败:', response.message) | ||
| 133 | + return [] | ||
| 134 | + } | ||
| 135 | + } catch (error) { | ||
| 136 | + console.error('获取用户菜单权限失败:', error) | ||
| 137 | + return [] | ||
| 138 | + } | ||
| 139 | + } | ||
| 123 | 140 | ||
| 124 | // 重置状态 | 141 | // 重置状态 |
| 125 | const resetState = (): void => { | 142 | const resetState = (): void => { |
| ... | @@ -141,6 +158,7 @@ export const usePermissionStore = defineStore('permission', () => { | ... | @@ -141,6 +158,7 @@ export const usePermissionStore = defineStore('permission', () => { |
| 141 | checkMenuPermission, | 158 | checkMenuPermission, |
| 142 | filterMenuByPermission, | 159 | filterMenuByPermission, |
| 143 | getPermissionMenuList, | 160 | getPermissionMenuList, |
| 161 | + getUserPermissionMenus, | ||
| 144 | resetState | 162 | resetState |
| 145 | } | 163 | } |
| 146 | }) | 164 | }) | ... | ... |
| ... | @@ -54,6 +54,14 @@ export const useUserStore = defineStore('user', () => { | ... | @@ -54,6 +54,14 @@ export const useUserStore = defineStore('user', () => { |
| 54 | 54 | ||
| 55 | // 更新本地存储 | 55 | // 更新本地存储 |
| 56 | setUserInfo(response) | 56 | setUserInfo(response) |
| 57 | + | ||
| 58 | + // 获取用户信息后,加载用户菜单权限 | ||
| 59 | + if (response.userId) { | ||
| 60 | + // 动态导入避免循环依赖 | ||
| 61 | + const { usePermissionStore } = await import('./permission') | ||
| 62 | + const permissionStore = usePermissionStore() | ||
| 63 | + await permissionStore.getUserPermissionMenus(response.userId) | ||
| 64 | + } | ||
| 57 | } catch (error) { | 65 | } catch (error) { |
| 58 | console.error('获取用户信息失败:', error) | 66 | console.error('获取用户信息失败:', error) |
| 59 | // 如果获取用户信息失败,可能是token过期,清除认证信息 | 67 | // 如果获取用户信息失败,可能是token过期,清除认证信息 |
| ... | @@ -76,6 +84,15 @@ export const useUserStore = defineStore('user', () => { | ... | @@ -76,6 +84,15 @@ export const useUserStore = defineStore('user', () => { |
| 76 | permissions.value = [] | 84 | permissions.value = [] |
| 77 | roles.value = [] | 85 | roles.value = [] |
| 78 | 86 | ||
| 87 | + // 清除权限store中的菜单 | ||
| 88 | + try { | ||
| 89 | + const { usePermissionStore } = await import('./permission') | ||
| 90 | + const permissionStore = usePermissionStore() | ||
| 91 | + permissionStore.resetState() | ||
| 92 | + } catch (error) { | ||
| 93 | + console.error('清除权限store失败:', error) | ||
| 94 | + } | ||
| 95 | + | ||
| 79 | // 清除本地存储 | 96 | // 清除本地存储 |
| 80 | clearAuth() | 97 | clearAuth() |
| 81 | 98 | ... | ... |
frontend/菜单权限动态加载功能说明.md
0 → 100644
| 1 | +# 菜单权限动态加载功能说明 | ||
| 2 | + | ||
| 3 | +## 功能概述 | ||
| 4 | +实现了前端左侧菜单根据用户权限动态加载的功能,用户只能看到自己有权限访问的菜单项。 | ||
| 5 | + | ||
| 6 | +## 实现的功能 | ||
| 7 | + | ||
| 8 | +### 1. API接口扩展 | ||
| 9 | +- 在 `frontend/src/api/menu.ts` 中添加了 `getUserMenuTree` 方法 | ||
| 10 | +- 调用后端接口 `http://localhost:8083/api/system/menu/tree?userId=1` 获取用户菜单权限 | ||
| 11 | + | ||
| 12 | +### 2. 菜单服务模块 | ||
| 13 | +- 创建了 `frontend/src/services/menuService.ts` 专门处理菜单相关逻辑 | ||
| 14 | +- 实现了菜单数据转换、路径映射、图标处理等功能 | ||
| 15 | +- 提供了 `loadUserMenus` 和 `transformMenuData` 等核心方法 | ||
| 16 | + | ||
| 17 | +### 3. 菜单组件动态化 | ||
| 18 | +- 修改 `frontend/src/layouts/MainLayout.vue` 将静态菜单改为动态加载 | ||
| 19 | +- 集成了菜单服务,实现菜单的动态加载和显示 | ||
| 20 | +- 添加了菜单加载状态显示和错误处理 | ||
| 21 | + | ||
| 22 | +## 主要修改文件 | ||
| 23 | + | ||
| 24 | +1. **frontend/src/api/menu.ts** | ||
| 25 | + - 添加 `getUserMenuTree` API方法 | ||
| 26 | + - 单独导出常用方法避免循环依赖 | ||
| 27 | + | ||
| 28 | +2. **frontend/src/services/menuService.ts** (新增) | ||
| 29 | + - 菜单数据转换逻辑 | ||
| 30 | + - 路径映射配置 | ||
| 31 | + - 图标处理功能 | ||
| 32 | + - 默认菜单提供 | ||
| 33 | + | ||
| 34 | +3. **frontend/src/layouts/MainLayout.vue** | ||
| 35 | + - 集成菜单服务实现动态菜单加载 | ||
| 36 | + - 添加菜单加载状态和错误处理 | ||
| 37 | + - 在组件挂载时自动加载用户菜单 | ||
| 38 | + | ||
| 39 | +## 使用方式 | ||
| 40 | + | ||
| 41 | +1. 用户登录后,系统会自动调用 `http://localhost:8083/api/system/menu/tree?userId=1` 获取用户菜单权限 | ||
| 42 | +2. 后端返回的菜单数据会被转换为前端菜单格式 | ||
| 43 | +3. 左侧菜单会根据用户权限动态显示 | ||
| 44 | +4. 用户只能看到自己有权限访问的菜单项 | ||
| 45 | + | ||
| 46 | +## 权限控制级别 | ||
| 47 | + | ||
| 48 | +- 权限控制精确到菜单级别 | ||
| 49 | +- 不包含按钮级别的权限控制 | ||
| 50 | +- 菜单项根据用户角色和权限动态显示/隐藏 | ||
| 51 | + | ||
| 52 | +## 测试方法 | ||
| 53 | + | ||
| 54 | +1. 确保后端服务运行在 `http://localhost:8083` | ||
| 55 | +2. 启动前端开发服务器 | ||
| 56 | +3. 使用不同权限的用户登录 | ||
| 57 | +4. 观察左侧菜单是否根据用户权限动态显示 | ||
| 58 | + | ||
| 59 | +## 注意事项 | ||
| 60 | + | ||
| 61 | +- 如果菜单API调用失败,会显示默认的首页菜单 | ||
| 62 | +- 菜单加载过程中会显示"菜单加载中..."提示 | ||
| 63 | +- 用户登出时会清除所有菜单权限数据 |
| 1 | { | 1 | { |
| 2 | - "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjA1ODMxMTEsImV4cCI6MTc2MDY2OTUxMX0.Z7TU3g66bc5n7BGEkndftCJFLyUGO9STJ_jviVZVJW8a7Otaarm4nr12WKKfE0-XawGPB4kApJcVFmWWvjxDsg", | 2 | + "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjE1NTY0MTksImV4cCI6MTc2MTY0MjgxOX0.seWCAYzvRAMwpf3jld21oKC8rD8TzcB3h3rTJI8veBtlE9UjipUYadO38tgOPDI3K1l7v50Lu-k5CqOe0hxhKQ", |
| 3 | - "saved_at": 1760583111 | 3 | + "saved_at": 1761556419 |
| 4 | } | 4 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
-
Please register or login to post a comment