Vue3+Vite+TypeScript+Element Plus开发-20.按钮权限
系列文档目录
Vue3+Vite+TypeScript安装
Element Plus安装与配置
主页设计与router配置
静态菜单设计
Pinia引入
Header响应式菜单缩展
Mockjs引用与Axios封装
登录设计
登录成功跳转主页
多用户动态加载菜单
Pinia持久化
动态路由 -动态增加路由
动态路由-动态删除路由
路由守卫-无路由跳转404
路由守卫-未登录跳转登录界面
登录退出
Tags-组件构建
Tags-与菜单联动
Pinia持久化优化
按钮权限
客制按钮组件
客制Table组件
客制Form组件
国际化
配置文件
文章目录
目录
系列文档目录
文章目录
前言
权限数据模拟
Mock模拟
增加API
菜单增加
stores存储
权限处理
权限展示
运行效果
后续
前言
前面已讲解基础架构构造,实战中按钮权限是关键环节,本章重点讲解按钮权限构建。
权限数据模拟
在src/mock/mockData下新建permissionsData.ts文件
数据结构:菜单信息与权限信息。 permissionsInfoOne 下,该账号具有 demo1:view 、 demo1:create 、 demo1:edit 、 demo1:delete 权限。
{ index: 'permissionsInfoOne', label: '权限演示-1',path: 'views/SysSettings/UserInfo',permissions: ["demo1:view","demo1:create","demo1:edit","demo1:delete"]},
完整代码:
// src/mock/mockData/permissionsData.ts
import Mock from 'mockjs';// 动态生成菜单数据
export default (data: any) => {// 解析传入的 data 参数const { username, password } = data;// 根据用户名和密码生成不同的响应if (username === 'admin') {return Mock.mock({status_code: 200,status: 'success',message: 'Operation successful.',data: [{ index: 'permissionsInfoOne', label: '权限演示-1',path: 'views/SysSettings/UserInfo',permissions: ["demo1:view","demo1:create","demo1:edit","demo1:delete"]},{ index: 'permissionsInfoTow', label: '权限演示-2',path: 'views/SysSettings/AccountSetting' ,permissions: ["demo2:view","demo2:create","demo2:edit","demo2:delete"]},],});} else if (username === 'user' ) {return Mock.mock({status_code: 200,status: 'success',message: 'Operation successful.',data: [{ index: 'permissionsInfoOne', label: '权限演示-1',path: 'views/SysSettings/UserInfo',permissions: ["demo1:view","demo1:create","demo1:edit" ]},{ index: 'permissionsInfoTow', label: '权限演示-2',path: 'views/SysSettings/AccountSetting' ,permissions: ["demo2:view","demo2:create", "demo2:delete"]},],});} else {return Mock.mock({status_code: 401,status: 'fail',message: 'Invalid username ,No permissions Data.',data: [],});}
};
Mock模拟
在/src/mock/index.ts文件增加permission
import permissionData from '@/mock/mockData/permissionData' ;
// 权限接口
Mock.mock(/permission/, 'get', (options) => {const { url } = options;// 使用 URLSearchParams 解析查询字符串const queryParams = new URLSearchParams(new URL(url).search);const username = queryParams.get('username');const password = queryParams.get('password');// console.log('username:', username);// console.log('password:', password);return permissionData({ username, password });
});
增加API
在 src/api/ 目录下新增 permission.ts 文件。
代码如下
// src/api/permission.ts// 引入 request、post 和 get 函数import { get } from '@/api/request'; // 绝对路径// 权限接口 增加dataexport const permissionAPI = async (data: any) => {try {const result = await get('/permission',data); // 使用封装的 get 方法// console.log('result',result);// console.log('result data',result.data );return result.data ;// result.data 为了返回值统一,增加data} catch (error) {console.error('获取权限数据失败:', error);return [];}};
菜单增加
修改src/mock/mockData/menuData.ts文件在admin与User中增加以下菜单
{index: 'Demo',label: '案例演示',icon: Setting,children: [{ index: 'permissionsInfoOne', label: '权限演示-1',path: 'views/Demos/permissionsInfoOne' },{ index: 'permissionsInfoTow', label: '权限演示-2',path: 'views/Demos/permissionsInfoTow' },],},
stores存储
在src/stores/index.ts中增加permissions、currentPagePath定义与赋值
permissions: any[];// 用户的权限列表currentPagePath: string;// 当前页面路径// 权限setPermissions(val: any) {this.permissions = val;},getPermissions(): [] {return this.permissions ;},// 当前页面路径setCurrentPagePath(val: any) {this.currentPagePath = val;}, getCurrentPagePath() {return this.currentPagePath ;},
完整代码
// src/stores/index.tsimport { defineStore } from 'pinia';
import router from '../router';
import type { Component } from 'vue';
import { get } from 'http';type Modules = Record<string, () => Promise<{ default: Component }>>;function stateIni(): {isCollapse: boolean;username: string;token_key: string;menuData: any[];tabs: { path: string; index: string; label: string; icon: string }[];currentMenu: any;permissions: any[];// 用户的权限列表currentPagePath: string;// 当前页面路径
} {return {isCollapse: false,username: '',token_key: '',menuData: [],tabs: [{path: "/home",index: "Home",label: "home",icon: "home"}],currentMenu: null,permissions: [], // 用户的权限列表currentPagePath: '/' ,// 当前页面路径};
}// 定义公共 storeexport const useAllDataStore = defineStore('useAllData', {// 定义状态state: stateIni,// 定义 actionsactions: {// 设置用户名setUsername(username: string) {this.username = username;},// 获取用户名getUsername(): string {return this.username;},// 设置 token_keysetTokenKey(token_key: string) {// sessionStorage.setItem('useAllData-session-store', JSON.stringify({ token_key: token_key, menuData: this.menuData}));this.token_key = token_key;},// 获取 token_keygetTokenKey(): string {/*const sessionData = sessionStorage.getItem('useAllData-session-store');console.log(sessionData)if (sessionData) {const data = JSON.parse(sessionData);this.token_key = data.token_key;this.menuData = data.menuData;}else{this.token_key = ''}*/return this.token_key;},// 设置菜单数据setMenuData(menuData: any){// sessionStorage.setItem('useAllData-session-store', JSON.stringify({ token_key: this.token_key, menuData: menuData}));addRouter(menuData)this.menuData = menuData},// 获取菜单数据getMenuData(): [] {return this.menuData;},// tabssetTabsData(val: any) {console.log('val',val)if(val.name==='home'){this.currentMenu=null;}else{let index=this.tabs.findIndex((item:any)=>item.index===val.index);console.log(index)index===-1? this.tabs.push(val):"";console.log('tabs:',this.tabs)}},getTabsData(): [] {return this.tabs;},removeTagsData(val: any){let index=this.tabs.findIndex((item:any)=>item.index===val.index);this.tabs.splice(index,1)console.log('removeTagsData',this.tabs)},resetStore(){//console.log('sessionStorage clear:', sessionStorage.length === 0); // 应该输出 true this.$reset(); // 重置 Pinia 状态//console.log('sessionStorage reset:', sessionStorage.length === 0); // 应该输出 true //sessionStorage.clear(); // 清空 sessionStoragesessionStorage.removeItem('__persisted__useAllData-store');//console.log('sessionStorage cleared:', sessionStorage.length === 0); // 应该输出 true // Object.assign(this, stateIni()); // 复制 Pinia 状态// sessionStorage.removeItem('useAllData-store'); // 删除 sessionStorage 中的数据},// 登出方法logout() {this.resetStore() // console.log('sessionStorage reset:', sessionStorage.length === 0); // 应该输出 true // sessionStorage.removeItem('useAllData-store'); // 删除 sessionStorage 中的数据// console.log('sessionStorage clear:', sessionStorage.length === 0); // 应该输出 true // router.push({ name: 'login' }); // 重定向到登录页面// 检查登录后的逻辑router.push({ name: 'login' }).then(() => {// console.log('Navigated to login page');// console.log('sessionStorage after navigation:', sessionStorage.length === 0); // 确认是否仍然为 true});},// 权限setPermissions(val: any) {this.permissions = val;},getPermissions(): [] {return this.permissions ;},// 当前页面路径setCurrentPagePath(val: any) {this.currentPagePath = val;}, getCurrentPagePath() {return this.currentPagePath ;},}, persist: [{enabled: true,key: 'useAllData-store',storage: sessionStorage, // // localStorage sessionStoragepick: ['token_key','tabs','menuData','permissions','currentPagePath'], // 指定持久化的字段},{key: 'useAllData-store',storage: localStorage, // // localStorage sessionStoragepick: ['username','isCollapse'], // 指定持久化的字段},],/*persist: [{pick: ['menuData'],storage: localStorage,},{pick: ['token_key'],storage: sessionStorage,},],*//*persist: {enabled: true,key: 'useAllData-store',storage: sessionStorage, // // localStorage sessionStoragepaths: ['token_key','tabs'], // 指定持久化的字段},*//*persist: {enabled: true,strategies: [{key: 'useAllData-store',storage: sessionStorage,// localStorage sessionStoragepaths: ['token_key','menuData'], // 指定需要持久化的字段},],},*/});function addRouter(menuData: any){const routerList=router.getRoutes()const modules: Modules = import.meta.glob('../views/**/*.vue') as Modules;const routerArr=[]menuData.forEach((item:any) => {// console.log(item) if(item.children){item.children.forEach((child:any) => {const componentPath = `../${child.path}.vue`;const module = modules[componentPath];if (module) {/*module().then(({ default: component }) => {child.component = component;});*/child.component = module;routerArr.push(child)}});}else{const componentPath = `../${item.path}.vue`;const module = modules[componentPath];if(module){item.component = module;routerArr.push(item)}}});// 增加删除路由routerList.forEach((item:any) => {if (item.name === 'main' || item.name === 'home' || item.name === '404' || item.name === 'login'|| item.name === 'error'|| item.name === 'undefined'|| item.path === '/'|| item.path === '/main') returnrouter.removeRoute(item.name)});routerArr.forEach((item:any) => {router.addRoute('main',{path: item.index,name: item.label,component: item.component,});})
const routerListLast=router.getRoutes()
// console.log(routerListLast)}export function ReloadData() {const store = useAllDataStore();const menuData = store.getMenuData();addRouter(menuData);}
登录界面
修改login.vue中fetchMenuData中增加获取权限资料并存储。
// 获取菜单数据
const fetchMenuData = async () => {try {const resultMenu = await menuAPI(loginForm); // 假设 loginForm 包含必要的参数store.setMenuData(resultMenu);const resultPermission = await permissionAPI(loginForm); // 假设 loginForm 包含必要的参数store.setPermissions(resultPermission);// console.log('login result 返回的数据:', result);// console.log('login menuAPI 返回的数据:', store.getMenuData());} catch (error) {console.error('获取菜单数据失败:', error);}
};
以上代码 在后期被修改引入Promise,具体代码如下:
async function getMenu(): Promise<any> {const resultMenu = await menuAPI(loginForm); // 假设 loginForm 包含必要的参数store.setMenuData(resultMenu);}async function getPermission(): Promise<any> {const resultPermission = await permissionAPI(loginForm); // 假设 loginForm 包含必要的参数store.setPermissions(resultPermission);}
// 获取菜单数据
const fetchLoginAfterData = async () => {try {await Promise.all([getMenu(), getPermission()]);}catch (error) {ElMessage.error(`菜单或权限数据获取失败,请稍后再试: ${error.message || '未知错误'}`);}
};
权限处理
新增src/utils/permissionUtils.ts文件,主要对权限处理。
// src/utils/permissionUtils.ts
import { computed } from 'vue';
import { useAllDataStore } from '@/stores';
import { useRouter } from 'vue-router';function usePermission(): (permission: string) => boolean {const Store = useAllDataStore();const Router = useRouter();// 获取当前页面路径并去掉开头的 '/'let currentPagePath = Router.currentRoute.value.path;currentPagePath = currentPagePath.startsWith('/') ? currentPagePath.slice(1) : currentPagePath;// 获取用户权限列表,并添加默认值处理const Permissions = Store.getPermissions() || [];// 获取当前页面的按钮权限const userPermissions = computed(() => {if (!Array.isArray(Permissions)) {console.error('Permissions is not an array:', Permissions);return []; // 如果 Permissions 不是数组,返回空数组}const currentMenu = Permissions.find(permissions => permissions.index === currentPagePath);return currentMenu?.permissions || []; // 确保返回的是一个数组});// 返回一个函数,用于检查用户是否具有某种权限return (permission: string): boolean => {// 解包 ComputedRef 的值后再调用 includes 方法return userPermissions.value.includes(permission);};
}export { usePermission };
权限展示
新增views\Demos\permissionsInfoOne.vue文件,在权限部分,新增、demo2:view刻意设定'permissionsInfoTwo中权限,预期效果是permissionsInfoOne不能显示,在实战中权限可以自定义。
<template ><div><div>权限演示-1</div><div v-if="hasPermission('demo1:view')">demo1:view</div><div v-if="hasPermission('demo2:view')">demo2:view</div><div><el-button :icon="Search" circle /><el-button v-if="hasPermission('demo2:create')" type="primary" :icon="Plus" circle /><el-button v-if="hasPermission('demo1:edit')" type="primary" :icon="Edit" circle /><el-button type="success" :icon="Check" circle /><el-button type="info" :icon="Message" circle /><el-button type="warning" :icon="Star" circle /><el-button v-if="hasPermission('demo1:delete')" type="danger" :icon="Delete" circle /></div></div></template>
<script lang="ts" setup>
import { usePermission } from "@/utils/permissionUtils";
import {Plus,Check,Delete,Edit,Message,Search,Star,
} from '@element-plus/icons-vue'
const hasPermission = usePermission();</script>
运行效果
用admin登录,新增按钮未显示
用user登录,删除与新增按钮未显示
后续
本章重点讲解按钮权限构建,一下章节把按钮构建一个组件并用权限管控。