# 权限系统增强实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 实现完整的权限系统增强,包括 Permission Store、v-permission 指令、动态菜单渲染和 API 权限检查。 **架构:** 使用 Pinia 统一管理权限数据,localStorage 持久化存储;通过 v-permission 指令实现按钮级权限控制;从后端 API 获取菜单数据动态渲染;在请求拦截器中添加 API 权限检查。 **技术栈:** Vue 3, Pinia, TypeScript, Element Plus, jwt-decode --- ## 文件结构 ### 新增文件 ``` novalon-manage-web/src/ ├── stores/ │ └── permission.ts # Permission Store ├── directives/ │ └── permission.ts # v-permission 指令 ├── components/ │ └── MenuItem.vue # 递归菜单组件 ├── utils/ │ └── permission-check.ts # API 权限检查工具 └── __tests__/ ├── stores/ │ └── permission.test.ts # Permission Store 测试 ├── directives/ │ └── permission.test.ts # v-permission 指令测试 ├── components/ │ └── MenuItem.test.ts # MenuItem 组件测试 └── utils/ └── permission-check.test.ts # API 权限检查测试 ``` ### 修改文件 ``` novalon-manage-web/src/ ├── main.ts # 注册权限指令 ├── views/system/Login.vue # 集成 Permission Store ├── layouts/DefaultLayout.vue # 使用动态菜单 └── utils/request.ts # 添加 API 权限检查 ``` --- ## 任务 1:创建 Permission Store **文件:** - 创建:`novalon-manage-web/src/stores/permission.ts` - 测试:`novalon-manage-web/src/__tests__/stores/permission.test.ts` - [ ] **步骤 1:编写 Permission Store 测试 - 基础功能** ```typescript // novalon-manage-web/src/__tests__/stores/permission.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { usePermissionStore } from '@/stores/permission' describe('Permission Store', () => { beforeEach(() => { setActivePinia(createPinia()) localStorage.clear() }) describe('基础功能', () => { it('应该正确初始化状态', () => { const store = usePermissionStore() expect(store.roles).toEqual([]) expect(store.permissions).toEqual([]) expect(store.menus).toEqual([]) expect(store.loaded).toBe(false) }) it('应该正确设置权限数据', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['admin'], permissions: ['user:read', 'user:delete'], menus: [ { id: 1, name: '仪表盘', path: '/dashboard', icon: 'Odometer', sort: 1 } ] }) expect(store.roles).toEqual(['admin']) expect(store.permissions).toEqual(['user:read', 'user:delete']) expect(store.menus).toHaveLength(1) expect(store.loaded).toBe(true) }) it('应该正确清除权限数据', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['admin'], permissions: ['user:read'], menus: [] }) store.clearPermissionData() expect(store.roles).toEqual([]) expect(store.permissions).toEqual([]) expect(store.menus).toEqual([]) expect(store.loaded).toBe(false) }) }) }) ``` - [ ] **步骤 2:运行测试验证失败** 运行:`cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts` 预期:FAIL,报错 "Cannot find module '@/stores/permission'" - [ ] **步骤 3:创建 Permission Store 基础结构** ```typescript // novalon-manage-web/src/stores/permission.ts import { defineStore } from 'pinia' export interface MenuItem { id: number name: string path: string icon?: string parentId?: number sort: number children?: MenuItem[] } interface PermissionState { roles: string[] permissions: string[] menus: MenuItem[] loaded: boolean } export const usePermissionStore = defineStore('permission', { state: (): PermissionState => ({ roles: [], permissions: [], menus: [], loaded: false }), actions: { setPermissionData(data: { roles: string[] permissions: string[] menus: MenuItem[] }) { this.roles = data.roles this.permissions = data.permissions this.menus = data.menus this.loaded = true this.saveToStorage() }, clearPermissionData() { this.roles = [] this.permissions = [] this.menus = [] this.loaded = false localStorage.removeItem('permission') }, saveToStorage() { const data = { roles: this.roles, permissions: this.permissions, menus: this.menus } localStorage.setItem('permission', JSON.stringify(data)) }, initFromStorage() { const stored = localStorage.getItem('permission') if (stored) { try { const data = JSON.parse(stored) this.roles = data.roles || [] this.permissions = data.permissions || [] this.menus = data.menus || [] this.loaded = true } catch (error) { console.error('从 localStorage 恢复权限数据失败:', error) } } } } }) ``` - [ ] **步骤 4:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts` 预期:PASS - [ ] **步骤 5:编写 Permission Store 测试 - 权限检查方法** ```typescript // 在 novalon-manage-web/src/__tests__/stores/permission.test.ts 中添加 describe('权限检查方法', () => { it('应该正确检查单个角色', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['admin', 'user'], permissions: [], menus: [] }) expect(store.hasRole('admin')).toBe(true) expect(store.hasRole('manager')).toBe(false) }) it('应该正确检查多个角色(满足任一即可)', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['user'], permissions: [], menus: [] }) expect(store.hasRole(['admin', 'user'])).toBe(true) expect(store.hasRole(['admin', 'manager'])).toBe(false) }) it('应该正确检查单个权限', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:read', 'user:delete'], menus: [] }) expect(store.hasPermission('user:read')).toBe(true) expect(store.hasPermission('user:create')).toBe(false) }) it('应该正确检查多个权限(满足任一即可)', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:read'], menus: [] }) expect(store.hasPermission(['user:read', 'user:create'])).toBe(true) expect(store.hasPermission(['user:create', 'user:update'])).toBe(false) }) }) ``` - [ ] **步骤 6:运行测试验证失败** 运行:`cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts` 预期:FAIL,报错 "store.hasRole is not a function" - [ ] **步骤 7:添加权限检查方法** ```typescript // 在 novalon-manage-web/src/stores/permission.ts 中添加 getters export const usePermissionStore = defineStore('permission', { state: (): PermissionState => ({ roles: [], permissions: [], menus: [], loaded: false }), getters: { hasRole: (state) => (role: string | string[]) => { if (Array.isArray(role)) { return role.some(r => state.roles.includes(r)) } return state.roles.includes(role) }, hasPermission: (state) => (permission: string | string[]) => { if (Array.isArray(permission)) { return permission.some(p => state.permissions.includes(p)) } return state.permissions.includes(permission) } }, actions: { // ... 已有的 actions } }) ``` - [ ] **步骤 8:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts` 预期:PASS - [ ] **步骤 9:编写 Permission Store 测试 - localStorage 持久化** ```typescript // 在 novalon-manage-web/src/__tests__/stores/permission.test.ts 中添加 describe('localStorage 持久化', () => { it('应该正确保存到 localStorage', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['admin'], permissions: ['user:read'], menus: [ { id: 1, name: '仪表盘', path: '/dashboard', sort: 1 } ] }) const stored = localStorage.getItem('permission') expect(stored).toBeTruthy() const data = JSON.parse(stored!) expect(data.roles).toEqual(['admin']) expect(data.permissions).toEqual(['user:read']) expect(data.menus).toHaveLength(1) }) it('应该正确从 localStorage 恢复', () => { localStorage.setItem('permission', JSON.stringify({ roles: ['user'], permissions: ['user:read:self'], menus: [] })) const store = usePermissionStore() store.initFromStorage() expect(store.roles).toEqual(['user']) expect(store.permissions).toEqual(['user:read:self']) expect(store.loaded).toBe(true) }) it('清除数据时应该同时清除 localStorage', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['admin'], permissions: [], menus: [] }) store.clearPermissionData() expect(localStorage.getItem('permission')).toBeNull() }) }) ``` - [ ] **步骤 10:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/stores/permission.test.ts` 预期:PASS - [ ] **步骤 11:Commit** ```bash cd novalon-manage-web git add src/stores/permission.ts src/__tests__/stores/permission.test.ts git commit -m "feat: 添加 Permission Store 实现权限数据管理" ``` --- ## 任务 2:创建 v-permission 指令 **文件:** - 创建:`novalon-manage-web/src/directives/permission.ts` - 测试:`novalon-manage-web/src/__tests__/directives/permission.test.ts` - [ ] **步骤 1:编写 v-permission 指令测试 - 角色检查** ```typescript // novalon-manage-web/src/__tests__/directives/permission.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { permissionDirective } from '@/directives/permission' import { usePermissionStore } from '@/stores/permission' describe('v-permission 指令', () => { beforeEach(() => { setActivePinia(createPinia()) localStorage.clear() }) describe('角色检查', () => { it('有角色时应该显示元素', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['admin'], permissions: [], menus: [] }) const wrapper = mount({ template: '', directives: { permission: permissionDirective } }) expect(wrapper.find('button').isVisible()).toBe(true) }) it('无角色时应该隐藏元素', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['user'], permissions: [], menus: [] }) const wrapper = mount({ template: '', directives: { permission: permissionDirective } }) expect(wrapper.find('button').isVisible()).toBe(false) }) it('支持数组参数(满足任一即可)', () => { const store = usePermissionStore() store.setPermissionData({ roles: ['user'], permissions: [], menus: [] }) const wrapper = mount({ template: '', directives: { permission: permissionDirective } }) expect(wrapper.find('button').isVisible()).toBe(true) }) }) }) ``` - [ ] **步骤 2:运行测试验证失败** 运行:`cd novalon-manage-web && pnpm test src/__tests__/directives/permission.test.ts` 预期:FAIL,报错 "Cannot find module '@/directives/permission'" - [ ] **步骤 3:创建 v-permission 指令基础结构** ```typescript // novalon-manage-web/src/directives/permission.ts import type { Directive, DirectiveBinding } from 'vue' import { usePermissionStore } from '@/stores/permission' export const permissionDirective: Directive = { mounted(el: HTMLElement, binding: DirectiveBinding) { const permissionStore = usePermissionStore() const { arg, value } = binding const checkType = arg || 'permission' if (!value) { console.warn('v-permission 指令需要提供权限值') el.style.display = 'none' return } let hasAccess = false if (checkType === 'role') { hasAccess = permissionStore.hasRole(value) } else if (checkType === 'permission') { hasAccess = permissionStore.hasPermission(value) } else { console.warn(`未知的权限检查类型: ${checkType}`) el.style.display = 'none' return } if (!hasAccess) { el.style.display = 'none' } } } ``` - [ ] **步骤 4:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/directives/permission.test.ts` 预期:PASS - [ ] **步骤 5:编写 v-permission 指令测试 - 权限检查** ```typescript // 在 novalon-manage-web/src/__tests__/directives/permission.test.ts 中添加 describe('权限检查', () => { it('有权限时应该显示元素', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:delete'], menus: [] }) const wrapper = mount({ template: '', directives: { permission: permissionDirective } }) expect(wrapper.find('button').isVisible()).toBe(true) }) it('无权限时应该隐藏元素', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:read'], menus: [] }) const wrapper = mount({ template: '', directives: { permission: permissionDirective } }) expect(wrapper.find('button').isVisible()).toBe(false) }) it('支持简写形式(默认权限检查)', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:create'], menus: [] }) const wrapper = mount({ template: '', directives: { permission: permissionDirective } }) expect(wrapper.find('button').isVisible()).toBe(true) }) }) ``` - [ ] **步骤 6:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/directives/permission.test.ts` 预期:PASS - [ ] **步骤 7:在 main.ts 中注册指令** ```typescript // novalon-manage-web/src/main.ts import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import zhCn from 'element-plus/es/locale/lang/zh-cn' import 'element-plus/dist/index.css' import router from './router' import App from './App.vue' import './assets/styles.css' import { permissionDirective } from './directives/permission' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.use(router) app.use(ElementPlus, { locale: zhCn, }) app.directive('permission', permissionDirective) app.mount('#app') ``` - [ ] **步骤 8:Commit** ```bash cd novalon-manage-web git add src/directives/permission.ts src/__tests__/directives/permission.test.ts src/main.ts git commit -m "feat: 添加 v-permission 指令实现按钮级权限控制" ``` --- ## 任务 3:集成 Permission Store 到登录流程 **文件:** - 修改:`novalon-manage-web/src/views/system/Login.vue` - [ ] **步骤 1:修改 Login.vue 集成 Permission Store** ```typescript // novalon-manage-web/src/views/system/Login.vue // 在 ``` - [ ] **步骤 4:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/components/MenuItem.test.ts` 预期:PASS - [ ] **步骤 5:Commit** ```bash cd novalon-manage-web git add src/components/MenuItem.vue src/__tests__/components/MenuItem.test.ts git commit -m "feat: 添加递归菜单组件 MenuItem" ``` --- ## 任务 5:修改 DefaultLayout 使用动态菜单 **文件:** - 修改:`novalon-manage-web/src/layouts/DefaultLayout.vue` - [ ] **步骤 1:修改 DefaultLayout.vue** ```vue ``` - [ ] **步骤 2:Commit** ```bash cd novalon-manage-web git add src/layouts/DefaultLayout.vue git commit -m "feat: 修改 DefaultLayout 使用动态菜单渲染" ``` --- ## 任务 6:创建 API 权限检查工具 **文件:** - 创建:`novalon-manage-web/src/utils/permission-check.ts` - 测试:`novalon-manage-web/src/__tests__/utils/permission-check.test.ts` - [ ] **步骤 1:编写 API 权限检查测试** ```typescript // novalon-manage-web/src/__tests__/utils/permission-check.test.ts import { describe, it, expect, beforeEach } from 'vitest' import { createPinia, setActivePinia } from 'pinia' import { usePermissionStore } from '@/stores/permission' import { canAccessApi } from '@/utils/permission-check' describe('API 权限检查', () => { beforeEach(() => { setActivePinia(createPinia()) localStorage.clear() }) it('有权限时应该返回 true', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:read'], menus: [] }) expect(canAccessApi('/api/users', 'GET')).toBe(true) }) it('无权限时应该返回 false', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:read'], menus: [] }) expect(canAccessApi('/api/users', 'POST')).toBe(false) }) it('未定义权限要求的 API 应该默认允许', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: [], menus: [] }) expect(canAccessApi('/api/unknown', 'GET')).toBe(true) }) it('应该正确匹配通配符路径', () => { const store = usePermissionStore() store.setPermissionData({ roles: [], permissions: ['user:update'], menus: [] }) expect(canAccessApi('/api/users/123', 'PUT')).toBe(true) }) }) ``` - [ ] **步骤 2:运行测试验证失败** 运行:`cd novalon-manage-web && pnpm test src/__tests__/utils/permission-check.test.ts` 预期:FAIL,报错 "Cannot find module '@/utils/permission-check'" - [ ] **步骤 3:创建 API 权限检查工具** ```typescript // novalon-manage-web/src/utils/permission-check.ts import { usePermissionStore } from '@/stores/permission' const apiPermissionMap: Record = { '/api/users:GET': 'user:read', '/api/users:POST': 'user:create', '/api/users/*:PUT': 'user:update', '/api/users/*:DELETE': 'user:delete', '/api/roles:GET': 'role:read', '/api/roles:POST': 'role:create', '/api/roles/*:PUT': 'role:update', '/api/roles/*:DELETE': 'role:delete', '/api/menus:GET': 'menu:read', '/api/menus:POST': 'menu:create', '/api/menus/*:PUT': 'menu:update', '/api/menus/*:DELETE': 'menu:delete', '/api/config:GET': 'config:read', '/api/config:POST': 'config:create', '/api/config/*:PUT': 'config:update', '/api/config/*:DELETE': 'config:delete', '/api/dict:GET': 'dict:read', '/api/dict:POST': 'dict:create', '/api/dict/*:PUT': 'dict:update', '/api/dict/*:DELETE': 'dict:delete', '/api/files:GET': 'file:read', '/api/files:POST': 'file:create', '/api/files/*:DELETE': 'file:delete', '/api/notices:GET': 'notice:read', '/api/notices:POST': 'notice:create', '/api/notices/*:PUT': 'notice:update', '/api/notices/*:DELETE': 'notice:delete', '/api/logs/login:GET': 'log:read', '/api/logs/operation:GET': 'log:read', '/api/logs/exception:GET': 'log:read' } function findRequiredPermission(path: string, method: string): string | null { const exactKey = `${path}:${method}` if (apiPermissionMap[exactKey]) { return apiPermissionMap[exactKey] } for (const [key, permission] of Object.entries(apiPermissionMap)) { const [pattern, reqMethod] = key.split(':') if (reqMethod !== method) continue const regex = new RegExp('^' + pattern.replace('*', '.*') + '$') if (regex.test(path)) { return permission } } return null } export function canAccessApi(path: string, method: string): boolean { const permissionStore = usePermissionStore() const required = findRequiredPermission(path, method) if (!required) { return true } return permissionStore.hasPermission(required) } ``` - [ ] **步骤 4:运行测试验证通过** 运行:`cd novalon-manage-web && pnpm test src/__tests__/utils/permission-check.test.ts` 预期:PASS - [ ] **步骤 5:Commit** ```bash cd novalon-manage-web git add src/utils/permission-check.ts src/__tests__/utils/permission-check.test.ts git commit -m "feat: 添加 API 权限检查工具" ``` --- ## 任务 7:集成 API 权限检查到请求拦截器 **文件:** - 修改:`novalon-manage-web/src/utils/request.ts` - [ ] **步骤 1:修改 request.ts 添加权限检查** ```typescript // novalon-manage-web/src/utils/request.ts import axios, { AxiosRequestConfig } from 'axios' import { generateSignatureHeaders } from './signature' import { canAccessApi } from './permission-check' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use( (config: AxiosRequestConfig) => { const path = config.url || '' const method = config.method?.toUpperCase() || 'GET' if (!canAccessApi(path, method)) { console.warn(`无权限访问 API: ${method} ${path}`) return Promise.reject(new Error(`无权限访问此 API: ${method} ${path}`)) } const token = localStorage.getItem('token') if (token) { config.headers = config.headers || {} config.headers.Authorization = `Bearer ${token}` } const methodForSignature = config.method?.toUpperCase() || 'GET' let url = config.url || '' const body = config.data if (config.params && Object.keys(config.params).length > 0) { const queryParams = new URLSearchParams() Object.entries(config.params).forEach(([key, value]) => { if (value !== undefined && value !== null) { queryParams.append(key, String(value)) } }) const queryString = queryParams.toString() if (queryString) { url += (url.includes('?') ? '&' : '?') + queryString } } const fullPath = `/api${url.startsWith('/') ? url : '/' + url}` const signatureHeaders = generateSignatureHeaders(methodForSignature, fullPath, body) config.headers = config.headers || {} Object.assign(config.headers, signatureHeaders) return config }, (error) => Promise.reject(error) ) request.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { localStorage.removeItem('token') if (!window.location.pathname.includes('/login')) { window.location.href = '/login' } } return Promise.reject(error) } ) export default request ``` - [ ] **步骤 2:Commit** ```bash cd novalon-manage-web git add src/utils/request.ts git commit -m "feat: 集成 API 权限检查到请求拦截器" ``` --- ## 任务 8:运行完整测试套件 - [ ] **步骤 1:运行所有单元测试** 运行:`cd novalon-manage-web && pnpm test` 预期:所有测试通过 - [ ] **步骤 2:运行 TypeScript 类型检查** 运行:`cd novalon-manage-web && pnpm type-check` 预期:类型检查通过(忽略已存在的其他文件错误) - [ ] **步骤 3:最终 Commit** ```bash cd novalon-manage-web git add . git commit -m "feat: 完成权限系统增强功能实现" ``` --- ## 后端 API 实现说明 本计划需要后端新增以下 API: **接口**: `GET /api/menus/user` **功能**: 获取当前登录用户可访问的菜单和权限 **实现要点**: 1. 从 token 获取用户 ID 2. 查询用户角色 3. 根据角色查询菜单和权限 4. 构建菜单树结构 5. 返回菜单和权限列表 **响应格式**: ```json { "code": 200, "data": { "menus": [ { "id": 1, "name": "仪表盘", "path": "/dashboard", "icon": "Odometer", "parentId": null, "sort": 1 } ], "permissions": [ "user:read", "user:create" ] } } ``` 后端实现不在本计划范围内,需要单独开发。