diff --git a/src/config/menu.ts b/src/config/menu.ts index 4c35001..01ed255 100644 --- a/src/config/menu.ts +++ b/src/config/menu.ts @@ -1,24 +1,39 @@ -export default [ +export interface MenuItem { + title: string + path: string + permission: string // 需要的权限标识(list类型) +} + +export interface MenuGroup { + name: string + title: string + icon: string + child: MenuItem[] +} + +const menus: MenuGroup[] = [ { name: 'system', title: '系统管理', icon: 'Operation', child: [ - { title: '系统配置', path: '/system/config' }, - { title: '用户管理', path: '/system/user' }, - { title: '角色管理', path: '/system/role' }, - { title: '博客文章', path: '/system/article' }, - { title: '分析统计', path: '/system/statistics' } + { title: '系统配置', path: '/system/config', permission: 'config:list' }, + { title: '用户管理', path: '/system/user', permission: 'user:list' }, + { title: '角色管理', path: '/system/role', permission: 'role:list' }, + { title: '博客文章', path: '/system/article', permission: 'article:list' }, + { title: '分析统计', path: '/system/statistics', permission: 'article:list' } ] },{ name: 'api', title: 'API数据', icon: 'Histogram', child: [ - { title: '一言', path: '/api/hitokoto' }, - { title: '照片墙', path: '/api/photoWall' }, - { title: '图片资源库', path: '/api/sourceImage' }, - { title: '歌曲库', path: '/api/music' } + { title: '一言', path: '/api/hitokoto', permission: 'hitokoto:list' }, + { title: '照片墙', path: '/api/photoWall', permission: 'photoWall:list' }, + { title: '图片资源库', path: '/api/sourceImage', permission: 'sourceImage:list' }, + { title: '歌曲库', path: '/api/music', permission: 'music:list' } ] } -] \ No newline at end of file +] + +export default menus \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 956665a..a5b9d6a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import 'element-plus/theme-chalk/el-message-box.css' import 'element-plus/theme-chalk/el-image-viewer.css' import 'element-plus/theme-chalk/dark/css-vars.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import { permissionDirective } from '@/utils/permission' // 全局路由导航前置守卫 router.beforeEach(function (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) { @@ -32,4 +33,5 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.use(router) .use(store) .directive('loading', ElLoading.directive) + .directive('permission', permissionDirective) .mount('#app') diff --git a/src/model/system/system-role.ts b/src/model/system/system-role.ts index 564bfdc..791b2f7 100644 --- a/src/model/system/system-role.ts +++ b/src/model/system/system-role.ts @@ -2,9 +2,15 @@ export interface SystemRoleModel { _id: string | null name: string | null // 角色名称 description: string | null // 描述 - methods: string[] // 允许的请求类型(优先级3) - includeUri: string[] // 包含的URI(优先级2) - excludeUri: string[] // 排除的URI(优先级1) + permissions: string[] // 权限标识列表 createdAt?: Date // 创建时间 updatedAt?: Date // 更新时间 +} + +export interface PermissionTreeNode { + code: string + name: string + description: string + leaf: boolean + children: PermissionTreeNode[] } \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index 532b32f..d4fbd37 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,7 +8,8 @@ class Store { state: StateType = { loginInfo: { userInfo: null, - token: localStorage.getItem('login_token') + token: localStorage.getItem('login_token'), + permissions: [] }, tabs: [], activeTab: null, @@ -21,12 +22,15 @@ class Store { * @param {Object} state * @param {UserInfo} data 登录数据 */ - login(state: StateType, data: {token?: string, userInfo: UserInfo}): void { + login(state: StateType, data: {token?: string, userInfo: UserInfo, permissions?: string[]}): void { if (data.token) { localStorage.setItem('login_token', data.token) state.loginInfo.token = data.token } state.loginInfo.userInfo = data.userInfo + if (data.permissions) { + state.loginInfo.permissions = data.permissions + } }, /** * 注销 @@ -36,6 +40,7 @@ class Store { localStorage.removeItem('login_token') state.loginInfo.token = null state.loginInfo.userInfo = null + state.loginInfo.permissions = [] }, /** * 添加导航页签 diff --git a/src/store/types.ts b/src/store/types.ts index 01660ad..9e45b41 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -15,6 +15,7 @@ export interface StateType { loginInfo: { // 登录信息 userInfo: null | UserInfo token: null | string + permissions: string[] // 用户权限列表 } tabs: TabItem[] // 导航页签 activeTab: string | null // 当前激活的页签 diff --git a/src/utils/http.ts b/src/utils/http.ts index b626d3d..9823cc7 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -43,11 +43,7 @@ http.interceptors.response.use(res => { ElMessage.error('服务器内部错误') } else if (err.response?.status >= 400) { const message = err.response.data?.message - if (typeof message === 'string') { - ElMessage.error(message) - } else if (Array.isArray(message)) { - ElMessage.error(message.join('
')) - } + message && ElMessage.error(message) if (err.response.status === 403) { router.push('/login') } diff --git a/src/utils/permission.ts b/src/utils/permission.ts new file mode 100644 index 0000000..b7b58e8 --- /dev/null +++ b/src/utils/permission.ts @@ -0,0 +1,37 @@ +import type { Directive } from 'vue' +import store from '@/store' + +/** + * 检查用户是否具备指定权限 + * 支持上下级关系:如果用户拥有 'user' 权限,则 'user:list' 也视为拥有 + */ +export function hasPermission(permission: string): boolean { + const permissions: string[] = store.state.loginInfo.permissions + if (!permissions || !permissions.length) return false + // 直接匹配 + if (permissions.includes(permission)) return true + // 上级权限匹配:如果权限是 'user:list',检查用户是否拥有 'user' + const colonIndex = permission.indexOf(':') + if (colonIndex !== -1) { + const parent = permission.substring(0, colonIndex) + if (permissions.includes(parent)) return true + } + return false +} + +/** + * v-permission 自定义指令 + * 用法:v-permission="'user:save'" 或 v-permission="['user:save', 'user:delete']" + * 当传入数组时,满足其中任一权限即可显示 + */ +export const permissionDirective: Directive = { + mounted(el, binding) { + const value = binding.value + if (!value) return + const permissions = Array.isArray(value) ? value : [value] + const hasPerm = permissions.some(p => hasPermission(p)) + if (!hasPerm) { + el.parentNode?.removeChild(el) + } + } +} diff --git a/src/views/Home.vue b/src/views/Home.vue index 5845607..5e5eaac 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -81,10 +81,12 @@ import { ref, reactive, computed, nextTick } from 'vue' import { useStore } from 'vuex' import { useRouter, useRoute } from 'vue-router' -import menus from '../config/menu' +import allMenus from '../config/menu' +import type { MenuGroup } from '../config/menu' import http from '@/utils/http' import { ElMessage } from 'element-plus' import { VForm } from '@/types' +import { hasPermission } from '@/utils/permission' const store = useStore() const router = useRouter() @@ -97,6 +99,16 @@ const openMenuNames = ref([]) const mainViewReady = ref(false) const isDark = ref(false) + +const menus = computed((): MenuGroup[] => { + return allMenus + .map(group => ({ + ...group, + child: group.child.filter(item => hasPermission(item.permission)) + })) + .filter(group => group.child.length > 0) +}) + function applyDarkMode(dark: boolean) { isDark.value = dark document.documentElement.classList.toggle('dark', dark) @@ -170,7 +182,7 @@ if (!store.state.loginInfo.token) { router.push('/login') } else { http.post<{token: string}, any>('/common/verifyToken', {token: store.state.loginInfo.token}).then(data => { - store.commit('login', {userInfo: data.userInfo}) + store.commit('login', {userInfo: data.userInfo, permissions: data.permissions}) mainViewReady.value = true }).catch(() => { router.push('/login') diff --git a/src/views/api/Hitokoto.vue b/src/views/api/Hitokoto.vue index f4e3a6e..feae819 100644 --- a/src/views/api/Hitokoto.vue +++ b/src/views/api/Hitokoto.vue @@ -19,8 +19,8 @@
- 添加 - 删除 + 添加 + 删除
搜索 重置 diff --git a/src/views/api/Music.vue b/src/views/api/Music.vue index 55501db..d1486da 100644 --- a/src/views/api/Music.vue +++ b/src/views/api/Music.vue @@ -23,8 +23,8 @@
播放 - 上传音乐 - 歌单管理 + 上传音乐 + 歌单管理
搜索 重置 @@ -72,9 +72,9 @@ diff --git a/src/views/api/PhotoWall.vue b/src/views/api/PhotoWall.vue index 34554a2..e093a27 100644 --- a/src/views/api/PhotoWall.vue +++ b/src/views/api/PhotoWall.vue @@ -41,9 +41,9 @@ auto-upload :show-file-list="false" style="display: inline-block;margin-right: 10px;"> - 上传图片 + 上传图片 - 批量删除 + 批量删除
搜索 重置 diff --git a/src/views/api/SourceImage.vue b/src/views/api/SourceImage.vue index 9beb327..a880bff 100644 --- a/src/views/api/SourceImage.vue +++ b/src/views/api/SourceImage.vue @@ -16,9 +16,9 @@ auto-upload :show-file-list="false" style="display: inline-block;margin-right: 10px;"> - 上传图片 + 上传图片 - 批量删除 + 批量删除
@@ -41,7 +41,7 @@ diff --git a/src/views/system/Article.vue b/src/views/system/Article.vue index 1d73a8c..f058586 100644 --- a/src/views/system/Article.vue +++ b/src/views/system/Article.vue @@ -30,8 +30,8 @@
- 分词处理 - 拉取文章 + 分词处理 + 拉取文章 + style="display: inline-block;margin-left: 10px;" + v-permission="'system:deploy'"> 发布博客
diff --git a/src/views/system/SystemConfig.vue b/src/views/system/SystemConfig.vue index f2d86a6..e98b3e3 100644 --- a/src/views/system/SystemConfig.vue +++ b/src/views/system/SystemConfig.vue @@ -6,7 +6,7 @@
- 添加 + 添加
搜索 重置 @@ -37,8 +37,8 @@ diff --git a/src/views/system/SystemRole.vue b/src/views/system/SystemRole.vue index 37158bf..e628a18 100644 --- a/src/views/system/SystemRole.vue +++ b/src/views/system/SystemRole.vue @@ -6,7 +6,7 @@
- 添加 + 添加
搜索 重置 @@ -14,11 +14,7 @@
- - - +