zhouhui.jiang

添加动态菜单栏

......@@ -7,6 +7,22 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
Header: typeof import('./src/components/layout/Header.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
......
......@@ -108,6 +108,11 @@ export const menuApi = {
return api.get('/api/system/menu/tree', { params: { userId } })
},
// 根据用户ID获取用户菜单权限树
getUserMenuTree: (userId: number): Promise<ApiResponse<Menu[]>> => {
return api.get('/api/system/menu/tree', { params: { userId } })
},
// 获取菜单详情
getMenuById: (menuId: number): Promise<ApiResponse<Menu>> => {
return api.get(`/api/system/menu/${menuId}`)
......@@ -139,4 +144,9 @@ export const menuApi = {
}
}
// 单独导出常用方法
export const getUserMenuTree = menuApi.getUserMenuTree
export const getMenuTree = menuApi.getMenuTree
export const getMenuList = menuApi.getMenuList
export default menuApi
\ No newline at end of file
......
......@@ -14,12 +14,15 @@
</div>
<nav class="sidebar-nav">
<ul class="nav-list">
<div v-if="menuLoading" class="menu-loading">
<span>菜单加载中...</span>
</div>
<ul v-else class="nav-list">
<li v-for="item in menuItems" :key="item.path || item.name" class="nav-item">
<!-- 一级菜单 -->
<div v-if="!item.children" class="nav-item-single">
<a
@click="handleMenuClick(item.path)"
@click="item.path ? handleMenuClick(item.path) : null"
class="nav-link"
:class="{ active: $route.path === item.path }"
>
......@@ -127,10 +130,13 @@ import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { logoutApi } from '../api/auth'
import { request } from '../utils/request'
import { loadUserMenus, getDefaultMenus, type MenuItem } from '../services/menuService'
const router = useRouter()
const route = useRoute()
// 用户信息相关
// 侧边栏状态
const sidebarCollapsed = ref(false)
......@@ -138,6 +144,9 @@ const sidebarCollapsed = ref(false)
const userInfo = ref<any>(null)
const userInfoLoading = ref(false)
// 菜单相关
const menuLoading = ref(false)
// 获取用户信息
const fetchUserInfo = async () => {
try {
......@@ -160,38 +169,39 @@ const fetchUserInfo = async () => {
// 标签页容器引用
const tabContainer = ref<HTMLElement>()
// 菜单项配置
const menuItems = ref([
{ name: '首页', path: '/main/dashboard', icon: '🏠' },
{ name: '订单管理', path: '/main/order', icon: '📋' },
{ name: '出库查询', path: '/main/delivery', icon: '📦' },
{ name: '发票管理', path: '/main/invoice', icon: '🧾' },
{ name: '产品管理', path: '/main/product', icon: '📦' },
{ name: '经销商管理', path: '/main/dealer', icon: '🏢' },
{ name: '返利管理', path: '/main/rebate', icon: '💰' },
{ name: '异常工单', path: '/main/exception-workorder', icon: '⚠️' },
{
name: '系统设置',
icon: '⚙️',
expanded: false,
children: [
{ name: '用户管理', path: '/main/users', icon: '👤' },
{ name: '角色管理', path: '/main/sys/role', icon: '🛡️' },
{ name: '菜单管理', path: '/main/sys/menu', icon: '📝' },
{
name: '字典管理',
path: '/main/sys/dict',
icon: '📖',
expanded: false,
children: [
{ name: '字典类型', path: '/main/sys/dict', icon: '📋' },
{ name: '字典项', path: '/main/sys/dict-item', icon: '📝' }
]
},
{ name: '日志管理', path: '/main/sys/log', icon: '📊' }
]
// 动态菜单项
const menuItems = ref<MenuItem[]>([])
// 加载用户菜单权限
const loadUserMenuPermissions = async () => {
try {
menuLoading.value = true
// 获取用户信息
const currentUser = userInfo.value
if (!currentUser) {
menuItems.value = getDefaultMenus()
return
}
// 尝试不同的用户ID字段
let userId = currentUser.userId || currentUser.id || currentUser.user_id
if (!userId) {
userId = 1 // 使用admin用户ID作为默认值
}
])
// 调用菜单服务加载用户菜单
const userMenus = await loadUserMenus(userId)
menuItems.value = userMenus
} catch (error) {
console.error('加载用户菜单失败:', error)
// 如果加载失败,使用默认菜单
menuItems.value = getDefaultMenus()
} finally {
menuLoading.value = false
}
}
// 打开的标签页
const openTabs = ref([
......@@ -391,18 +401,25 @@ watch(() => route.path, (newPath) => {
}
}, { immediate: true })
// 组件挂载时获取用户信息
onMounted(() => {
// 组件挂载时获取用户信息和菜单
onMounted(async () => {
// 检查是否已登录
const token = localStorage.getItem('token')
if (token) {
// 调用API获取用户信息
fetchUserInfo()
await fetchUserInfo()
// 获取用户信息后加载菜单
await loadUserMenuPermissions()
} else {
// 如果没有token,尝试从本地存储获取
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
// 从本地存储获取用户信息后也加载菜单
await loadUserMenuPermissions()
} else {
// 如果没有用户信息,使用默认菜单
menuItems.value = getDefaultMenus()
}
}
})
......@@ -465,6 +482,13 @@ onMounted(() => {
padding: 16px 0;
}
.menu-loading {
padding: 20px;
text-align: center;
color: #bdc3c7;
font-size: 12px;
}
.nav-list {
list-style: none;
margin: 0;
......@@ -620,11 +644,19 @@ onMounted(() => {
gap: 15px;
}
.user-actions {
display: flex;
align-items: center;
gap: 10px;
}
.welcome-text {
color: #666;
font-size: 13px;
}
.logout-btn {
background: #e74c3c;
color: white;
......
// 菜单服务 - 独立处理菜单相关逻辑,避免循环依赖
import { getUserMenuTree } from '@/api/menu'
export interface MenuItem {
name: string
path?: string
icon: string
expanded?: boolean
children?: MenuItem[]
}
// 路径映射 - 将后端路径映射到前端路由
const pathMapping: { [key: string]: string } = {
// 系统管理相关
'/system/user': '/main/users',
'/system/role': '/main/sys/role',
'/system/menu': '/main/sys/menu',
'/system/menu/list': '/main/sys/menu', // 菜单查询
'/system/dict': '/main/sys/dict',
'/system/dict/type/list': '/main/sys/dict', // 字典类型查询
'/system/dict/item/list': '/main/sys/dict-item', // 字典项查询
'/system/log': '/main/sys/log',
'/system/log/list': '/main/sys/log', // 日志查询
// 业务模块
'/dashboard': '/main/dashboard',
'/order': '/main/order',
'/delivery': '/main/delivery',
'/invoice': '/main/invoice',
'/rebate': '/main/rebate',
'/validation': '/main/validation',
'/product': '/main/product',
'/dealer': '/main/dealer',
'/main/exception-workorder': '/main/exception-workorder' // 保持原有路径
}
// 将后端菜单数据转换为前端菜单格式
export const transformMenuData = (menus: any[]): MenuItem[] => {
return menus.map(menu => {
const transformedMenu: MenuItem = {
name: menu.menuName,
icon: getMenuIcon(menu.icon),
expanded: false
}
// 处理路径 - 根据菜单类型和路径设置
if (menu.menuType === '1' && menu.path && menu.path !== '#') {
// 菜单类型,先检查是否有路径映射
const mappedPath = pathMapping[menu.path]
if (mappedPath) {
transformedMenu.path = mappedPath
} else {
// 如果没有映射,使用默认规则
transformedMenu.path = menu.path.startsWith('/main') ? menu.path : `/main${menu.path}`
}
} else if (menu.menuType === '0') {
// 目录类型,检查是否有路径映射
const mappedPath = pathMapping[menu.path]
if (mappedPath) {
// 如果目录有映射路径,设置为可点击的目录
transformedMenu.path = mappedPath
}
// 目录类型,通常没有路径或路径为 #,不设置path让前端处理为可展开的目录
}
// 如果有子菜单,递归转换
if (menu.children && menu.children.length > 0) {
transformedMenu.children = transformMenuData(menu.children)
}
return transformedMenu
})
}
// 处理菜单图标
const getMenuIcon = (icon: string): string => {
if (!icon) return '📄'
// 处理Element UI图标
if (icon.startsWith('el-icon-')) {
return getElementIcon(icon)
}
// 处理emoji图标 - 简单检查是否包含emoji字符
if (icon.includes('🏠') || icon.includes('📋') || icon.includes('📦') || icon.includes('🧾') ||
icon.includes('📈') || icon.includes('✅') || icon.includes('🏢') || icon.includes('⚙️') ||
icon.includes('👤') || icon.includes('👥') || icon.includes('📝') || icon.includes('📖') ||
icon.includes('📊') || icon.includes('⚠️')) {
return icon
}
// 默认图标映射
const iconMap: { [key: string]: string } = {
'user': '👤',
'peoples': '👥',
'menu': '📝',
'dict': '📖',
'log': '📊',
'home': '🏠',
'order': '📋',
'goods': '📦',
'ticket': '🧾',
'data': '📈',
'check': '✅',
'shop': '🏢',
'tools': '⚙️'
}
return iconMap[icon] || '📄'
}
// Element UI图标映射
const getElementIcon = (icon: string): string => {
const elementIconMap: { [key: string]: string } = {
'el-icon-s-home': '🏠',
'el-icon-s-order': '📋',
'el-icon-s-goods': '📦',
'el-icon-s-ticket': '🧾',
'el-icon-s-data': '📈',
'el-icon-s-check': '✅',
'el-icon-s-shop': '🏢',
'el-icon-s-tools': '⚙️'
}
return elementIconMap[icon] || '📄'
}
// 加载用户菜单权限
export const loadUserMenus = async (userId: number): Promise<MenuItem[]> => {
try {
// 调用API获取用户菜单权限
const response = await getUserMenuTree(userId)
if (response.code === 200) {
const userMenus = response.data || []
// 转换为前端菜单格式
const transformedMenus = transformMenuData(userMenus)
return transformedMenus
} else {
console.error('获取用户菜单权限失败:', response.message)
return getDefaultMenus()
}
} catch (error) {
console.error('加载用户菜单失败:', error)
return getDefaultMenus()
}
}
// 获取默认菜单
export const getDefaultMenus = (): MenuItem[] => {
return [
{ name: '首页', path: '/main/dashboard', icon: '🏠' }
]
}
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Menu, RouteRecord } from '@/types'
import { getMenuListApi } from '@/api/menu'
import { getMenuListApi, getUserMenuTree } from '@/api/menu'
import { useUserStore } from './user'
import router from '@/router'
......@@ -51,7 +51,7 @@ export const usePermissionStore = defineStore('permission', () => {
const route: RouteRecord = {
path: menu.path,
name: menu.menuName,
component: () => import(`@/views${menu.component}`),
component: () => import(/* @vite-ignore */ `@/views${menu.component}`),
meta: {
title: menu.menuName,
icon: menu.icon,
......@@ -121,6 +121,23 @@ export const usePermissionStore = defineStore('permission', () => {
}
}
// 根据用户ID获取用户菜单权限
const getUserPermissionMenus = async (userId: number): Promise<Menu[]> => {
try {
const response = await getUserMenuTree(userId)
if (response.code === 200) {
menuList.value = response.data || []
return response.data || []
} else {
console.error('获取用户菜单权限失败:', response.message)
return []
}
} catch (error) {
console.error('获取用户菜单权限失败:', error)
return []
}
}
// 重置状态
const resetState = (): void => {
menuList.value = []
......@@ -141,6 +158,7 @@ export const usePermissionStore = defineStore('permission', () => {
checkMenuPermission,
filterMenuByPermission,
getPermissionMenuList,
getUserPermissionMenus,
resetState
}
})
......
......@@ -54,6 +54,14 @@ export const useUserStore = defineStore('user', () => {
// 更新本地存储
setUserInfo(response)
// 获取用户信息后,加载用户菜单权限
if (response.userId) {
// 动态导入避免循环依赖
const { usePermissionStore } = await import('./permission')
const permissionStore = usePermissionStore()
await permissionStore.getUserPermissionMenus(response.userId)
}
} catch (error) {
console.error('获取用户信息失败:', error)
// 如果获取用户信息失败,可能是token过期,清除认证信息
......@@ -76,6 +84,15 @@ export const useUserStore = defineStore('user', () => {
permissions.value = []
roles.value = []
// 清除权限store中的菜单
try {
const { usePermissionStore } = await import('./permission')
const permissionStore = usePermissionStore()
permissionStore.resetState()
} catch (error) {
console.error('清除权限store失败:', error)
}
// 清除本地存储
clearAuth()
......
# 菜单权限动态加载功能说明
## 功能概述
实现了前端左侧菜单根据用户权限动态加载的功能,用户只能看到自己有权限访问的菜单项。
## 实现的功能
### 1. API接口扩展
-`frontend/src/api/menu.ts` 中添加了 `getUserMenuTree` 方法
- 调用后端接口 `http://localhost:8083/api/system/menu/tree?userId=1` 获取用户菜单权限
### 2. 菜单服务模块
- 创建了 `frontend/src/services/menuService.ts` 专门处理菜单相关逻辑
- 实现了菜单数据转换、路径映射、图标处理等功能
- 提供了 `loadUserMenus``transformMenuData` 等核心方法
### 3. 菜单组件动态化
- 修改 `frontend/src/layouts/MainLayout.vue` 将静态菜单改为动态加载
- 集成了菜单服务,实现菜单的动态加载和显示
- 添加了菜单加载状态显示和错误处理
## 主要修改文件
1. **frontend/src/api/menu.ts**
- 添加 `getUserMenuTree` API方法
- 单独导出常用方法避免循环依赖
2. **frontend/src/services/menuService.ts** (新增)
- 菜单数据转换逻辑
- 路径映射配置
- 图标处理功能
- 默认菜单提供
3. **frontend/src/layouts/MainLayout.vue**
- 集成菜单服务实现动态菜单加载
- 添加菜单加载状态和错误处理
- 在组件挂载时自动加载用户菜单
## 使用方式
1. 用户登录后,系统会自动调用 `http://localhost:8083/api/system/menu/tree?userId=1` 获取用户菜单权限
2. 后端返回的菜单数据会被转换为前端菜单格式
3. 左侧菜单会根据用户权限动态显示
4. 用户只能看到自己有权限访问的菜单项
## 权限控制级别
- 权限控制精确到菜单级别
- 不包含按钮级别的权限控制
- 菜单项根据用户角色和权限动态显示/隐藏
## 测试方法
1. 确保后端服务运行在 `http://localhost:8083`
2. 启动前端开发服务器
3. 使用不同权限的用户登录
4. 观察左侧菜单是否根据用户权限动态显示
## 注意事项
- 如果菜单API调用失败,会显示默认的首页菜单
- 菜单加载过程中会显示"菜单加载中..."提示
- 用户登出时会清除所有菜单权限数据
{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjA1ODMxMTEsImV4cCI6MTc2MDY2OTUxMX0.Z7TU3g66bc5n7BGEkndftCJFLyUGO9STJ_jviVZVJW8a7Otaarm4nr12WKKfE0-XawGPB4kApJcVFmWWvjxDsg",
"saved_at": 1760583111
"token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE3NjE1NTY0MTksImV4cCI6MTc2MTY0MjgxOX0.seWCAYzvRAMwpf3jld21oKC8rD8TzcB3h3rTJI8veBtlE9UjipUYadO38tgOPDI3K1l7v50Lu-k5CqOe0hxhKQ",
"saved_at": 1761556419
}
\ No newline at end of file
......