Skip to content

菜单权限

按钮和菜单权限介绍

用户绑定角色,角色绑定菜单权限和按钮权限

ts
# 用户管理页面
	# 创建用户,分配角色,编辑用户,删除用户
# 角色管理(岗位)页面
	添加角色,删除角色,编辑角色,角色下发菜单权限设置和按钮的权限(增删改查)
# 菜单管理页面
	添加菜单,删除菜单,编辑菜单,

权限管理业务

配置权限管理模块静态页面

ts
# 配置路由

# 发送请求

# 获取数据

# 渲染页面

重构动态路由

src/router/routes.ts

ts
import type { RouteRecordRaw } from 'vue-router';

//静态路由:常量路由
export const staticRoutes: Array<RouteRecordRaw> = [
	{
		// 路径全是小写的
		path: '/login',
		name: 'Login',
		component: () => import('@/views/login/index.vue'),
		// 路由元数据
		meta: {
			hidden: true
		}
	},

	{
		path: '/404',
		name: '404',
		component: () => import('@/views/error/404.vue'),
		meta: {
			hidden: true
		}
	},

	{
		path: '/',
		component: () => import('@/layout/index.vue'),
		redirect: '/home',
		children: [{
			path: 'home',
			name: 'Home',
			component: () => import('@/views/home/index.vue'),
			meta: {
				title: '首页',
				icon: 'ele-HomeFilled',
			}
		}]
	},
	

	
];



// 定义动态路由
export const asyncRoutes: Array<RouteRecordRaw> = [
	// 商品管理模块 一级路由
	{
		path: '/product', // 一级路由展示 layout组件
		component: () => import('@/layout/index.vue'),
		name: 'Product',
		meta: {
			title: '商品管理',
			icon: 'ele-Goods'
		},
		children: [
			{
				path: 'trademark',
				component: () => import('@/views/product/trademark/index.vue'),
				name: 'Trademark',
				meta: {
					title: '品牌管理',
					icon: 'ele-Apple'
				}
			},
			{
				path: 'attr',
				component: () => import('@/views/product/attr/index.vue'),
				name: 'Attr',
				meta: {
					title: '属性管理',
					icon: 'ele-IceTea'
				}
			},
			{
				path: 'spu',
				component: () => import('@/views/product/spu/index.vue'),
				name: 'Spu',
				meta: {
					title: 'SPU管理',
					icon: 'ele-Burger'
				}
			},
			{
				path: 'sku',
				component: () => import('@/views/product/sku/index.vue'),
				name: 'Sku',
				meta: {
					title: 'SKU管理',
					icon: 'ele-Goblet'
				}
			}
		]
	},

	// 权限管理 一级路由
	{
		name: 'Acl',
		path: '/acl',
		component: () => import('@/layout/index.vue'),
		redirect: '/acl/user/list',
		meta: { title: '权限管理', icon: 'ele-Setting' },
		children: [
			{
				name: 'User',
				path: '/acl/user/list',
				component: () => import('@/views/acl/user/index.vue'),
				meta: { title: '用户管理' }
			},
			{
				name: 'Role',
				path: '/acl/role/list',
				component: () => import('@/views/acl/role/index.vue'),
				meta: {
				title: '角色管理',
				},
			},
			{
				name: 'RoleAuth',
				path: '/acl/role/auth',
				component: () => import('@/views/acl/role/roleAuth.vue'),
				meta: {
				title: '角色管理',
				hidden: true,
				activeMenu: '/acl/role/list',
				},
			},
			{
				name: 'Permission',
				path: '/acl/permission/list',
				component: () => import('@/views/acl/permission/index.vue'),
				meta: {
				title: '菜单管理',
				}
			}
		]
	},
];

// 任意路由
export const anyRoute = [
	/* 匹配任意的路由 必须最后注册 */
	{
		path: '/:pathMatch(.*)',
		name: 'Any',
		redirect: '/404',
		meta: {
			hidden: true
		}
	}
]

用户管理页面

封装接口请求

src/api/acl/user.ts

ts
import request from '@/utils/request'
import type { UserObj, UserPageArr } from './type/user'
import type { UserRolesArr } from './type/role'

// 定义请求地址的枚举
enum Api {
    GetUserList = '/admin/acl/user', // 获取用户列表的接口地址
    RemoveUser = '/admin/acl/user/remove', // 移除用户的接口地址
    RemoveUsers = '/admin/acl/user/batchRemove', // 批量移除用户的接口地址
    SaveUser = '/admin/acl/user/save', // 保存用户的接口地址
    UpdateUser = '/admin/acl/user/update', // 更新用户的接口地址
    GetUserRoleList = '/admin/acl/user/toAssign', // 获取用户角色列表的接口地址
    AssignUserRoleList = '/admin/acl/user/doAssign', // 设置用户角色的接口地址
}

// 获取用户列表
export const getUserListApi = (page: number, limit: number, searchParams: object) => {
    return request.get<any, UserPageArr>(Api.GetUserList + `/${page}/${limit}`, {
        params: searchParams
    })
}

// 编辑和添加用户
export const saveOrUpdateUserApi = (user: UserObj) => {
    if (user.id) {
        return request.put<any, null>(Api.UpdateUser, user);
    }else{
        return request.post<any, null>(Api.SaveUser, user);
    }
}

// 删除用户(单个)
export const removeUserApi = (id: string) => {
    return request.delete<any, null>(Api.RemoveUser + `/${id}`);
};

// 删除用户(批量)
export const removeUsersApi = (ids: string[]) => {
    return request.delete<any, null>(Api.RemoveUsers, {
        data: ids
    })
}

// 获取用户角色
export const getUserRoleListApi = (userId: string) => {
    return request.get<any, UserRolesArr>(Api.GetUserRoleList + `/${userId}`);
};
  
/**
 * 给用户分配角色
 * @param userId 用户id
 * @param roleId 包含所有角色id的字符串 3,5,6
 * @returns 
 */
export const assignUserRoleListApi = (userId: string, roleId: string) => {
    return request.post<any, void>(Api.AssignUserRoleList, null, {
        params: {
            userId,
            roleId
        }
    });
};

封装接口类型

src/api/acl/type/user.ts

ts
/*******************************************************************/

//登录接口需要携带请求体参数-对象
export interface loginData {
    username: string,
    password: string
}
//登录接口返回值的类型
export interface loginResponseData {
    token: string
}
//用户信息接口返回数据类型
export interface userInfoResponseData {
    name:string,
    avatar:string,
    roles:string[],
    buttons:string[],
    routes:string[]
}

/*******************************************************************/
export interface UserObj {
    id?: string; // 用户id
    roleName?: string[]; // 用户的角色数组名字
    username: string; // 用户名字
    nickName: string; // 昵称
    password: string; // 密码
}
  
// 管理员用户列表
export type UserArr = UserObj[];
  
// 管理员用户分页列表
export interface UserPageArr {
    items: UserArr; // 用户列表数据
    total: number; // 总条数
}

数据渲染(增删改查)

src/views/acl/user/index.vue

vue
<template>
    <el-card>
        <el-form inline>
            <el-form-item>
                <el-input  placeholder="用户名" v-model="paramsState.username"/>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" :icon="Search" @click="getUsers()">查询</el-button>
                <el-button type="default" @click="resetInp">清空</el-button>
            </el-form-item>    
        </el-form>

        <div style="margin-bottom: 10px;">
            <el-button type="primary" @click="showAddUser">添 加</el-button>
            <el-button type="danger" @click="removeUsersBat" :disabled="tableState.selectedIds.length===0">批量删除</el-button>
        </div>

        <el-table border stripe :data="tableState.users" v-loading="listLoading" @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55" />
            <el-table-column type="index" label="序号" width="80" align="center" />
            <el-table-column prop="username" label="用户名" width="150" />
            <el-table-column prop="nickName" label="用户昵称" />
            <el-table-column prop="roleName" label="角色列表" width="200" />
            <el-table-column prop="gmtCreate" label="创建时间" width="180" />
            <el-table-column prop="gmtModified" label="更新时间" width="180" />
            <el-table-column label="操作" width="200" align="center" fixed="right">
                <template v-slot="{row}">
                    <el-button type="info" size="small" :icon="UserFilled" title="分配角色" @click="showAssignRole(row)"/>
                    <el-button type="primary" size="small" :icon="Edit" title="修改用户" @click="showEditUser(row)" />
                    <el-popconfirm  :title="`确定删除 ${row.username} 吗?`" @confirm="removeUser(row.id)">
                        <template #reference>
                            <el-button style="margin-left:10px" type="danger" size="small" :icon="Delete" title="删除用户" />
                        </template>
                    </el-popconfirm>
                </template>
            </el-table-column>
        </el-table>

        
        <el-pagination
            v-model:current-page="paramsState.page"
            v-model:page-size="paramsState.limit"
            :page-sizes="[3, 5, 7]"
            style="margin-top: 10px;"
            background
            layout="prev, pager, next, jumper,->,sizes,total"
            :total="tableState.total"
            @current-change="getUsers" 
            @size-change="SizeChange"
        />
        <!-- 添加用户 -->
        <el-dialog v-model="userState.dialogUserVisible"
            :title="userState.user?.id ? '修改用户' : '添加用户'">
            <el-form ref="userFormRef" 
                label-width="120px" 
                :model="userState.user" 
                :rules="userRules">
                <el-form-item label="用户名" prop="username">
                    <el-input  v-model="userState.user.username"/>
                </el-form-item>
                <el-form-item label="用户昵称">
                    <el-input  v-model="userState.user.nickName"/>
                </el-form-item>
                <el-form-item  label="用户密码" v-if="!userState.user.id" prop="password">
                    <el-input  v-model="userState.user.password"/>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button @click="cancelUser">取 消</el-button>
                    <el-button :loading="userState.userLoading" @click="addOrUpdate" type="primary" >确 定</el-button>
                </div>
            </template>
        </el-dialog>
        <!-- 设置角色 -->
        <el-dialog title="设置角色" v-model="roleState.dialogRoleVisible" :before-close="resetRoleData">
            <el-form label-width="80px">
                <el-form-item label="用户名">
                    <el-input disabled :value="userState.user.username"></el-input>
                </el-form-item>
                <el-form-item label="角色列表">
                    <el-checkbox 
                        :indeterminate="roleState.isIndeterminate" 
                        @change="handleCheckAllChange"
                        v-model="roleState.checkAll">全选</el-checkbox>
                        <div style="margin:15px 0;"></div>
                        <el-checkbox-group v-model="roleState.userRoleIds" @change="handleCheckedChange">
                            <el-checkbox v-for="role in roleState.allRoles" :key="role.id" :label="role.id" :value="role.id" >{{role.roleName}}</el-checkbox>
                        </el-checkbox-group>
                </el-form-item>
            </el-form>

            <template #footer>
                <el-button :loading="roleState.roleLoading" type="primary" @click="assignRole">保存</el-button>
                <el-button @click="resetRoleData">取消</el-button>
            </template>
        </el-dialog>
    </el-card>
</template>

<script setup lang='ts'>
import { ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
import { Search, Edit, UserFilled, Delete } from '@element-plus/icons-vue'
import { reactive, ref, onMounted, nextTick } from 'vue'
import { assignUserRoleListApi, getUserListApi, getUserRoleListApi, removeUserApi, removeUsersApi, saveOrUpdateUserApi } from '@/api/acl/user';
import type { UserArr, UserObj } from '@/api/acl/type/user';
import type { AllRolesArr } from '@/api/acl/type/role';
const userFormRef = ref()
const listLoading = ref(false) // 是否显示列表加载的提示
// 定义请求体数据
const paramsState = reactive({
    username: '',
    page: 1, // 当前页码
    limit: 5 // 每页数量
})

// TableState类型
interface TableState {
    total: number
    users: UserArr
    selectedIds: string[]
}
// TableState数据
const tableState = reactive<TableState>({
    total: 0,
    users: [],
    selectedIds: []
})
// 用户表单类型
interface UserState {
  dialogUserVisible: boolean
  userLoading: boolean
  user: UserObj
}
const userState = reactive<UserState>({
    dialogUserVisible: false,
    userLoading: false,
    user: {
        username: '',
        nickName: '',
        password: ''
    }
})
// 用户校验规则
const validatePassword = (rule: any, value: any, callback: any) => {
    if (!value) {
        callback('密码必须输入')
    } else if (!value || value.length < 6) {
        callback('密码不能小于6位')
    } else {
        callback()
    }
}
const userRules = {
    username: [
        { required: true, message: '用户名必须输入' },
        { min: 4, message: '用户名不能小于4位' }
    ],
    password: [{ required: true, validator: validatePassword }]
}

interface RoleState {
    dialogRoleVisible?: boolean
    roleLoading: boolean
    checkAll: boolean
    isIndeterminate: boolean
    userRoleIds: string[]
    allRoles: AllRolesArr
}
const roleState = reactive<RoleState>({
    dialogRoleVisible: false,
    roleLoading: false,
    checkAll: false,
    isIndeterminate: false,
    userRoleIds: [],
    allRoles: []
})

// 生命周期
onMounted(() => {
    // 调用获取用户方法
    getUsers()
})

// 定义搜索的方法
const getUsers = async (page = paramsState.page, limit = paramsState.limit) => {
    // 修改分页数据
    paramsState.page = page
    paramsState.limit = limit
    // 设置loading状态
    listLoading.value = true
    // 发送请求
    const result = await getUserListApi(page, limit, {
        username: paramsState.username
    })
    // loading为false
    listLoading.value = false
    // 设置表格数据
    const { items, total } = result
    tableState.users = items
    tableState.total = total
    tableState.selectedIds = []
}

// 分页变化
const SizeChange = (val:number)=>{
    paramsState.limit = val;
    getUsers();
}

// 清空搜索input
const resetInp = ()=>{
    paramsState.username = ''
    getUsers()
}

// 添加用户
const showAddUser = ()=>{
    // 初始化数据
    resetUserList()
    // 显示对话框
    userState.dialogUserVisible = true
    // 清除校验规则结果
    nextTick(() => userFormRef.value?.clearValidate())
}

// 编辑用户
const showEditUser = (row:UserObj) =>{
    // 获取行对象中的数据并赋值
    userState.user = { ...row }
    // 显示对话框
    userState.dialogUserVisible = true
}

// 提交按钮
const addOrUpdate = async ()=>{
    // 进行表单校验
    await userFormRef.value?.validate()
    const { user } = userState
    if (user.username === 'admin') {
        ElMessage({
            type: "error",
            message: "admin账号不能添加或更新",
        })
        return
    }
    // 加载
    userState.userLoading = true
    try {
        // 发送请求
        await saveOrUpdateUserApi(user)
        // 加载状态取消
        userState.userLoading = false
        // 提示消息
        ElMessage({
            type: "success",
            message: user.id ? "更新成功!":"添加成功!",
        })
        // 更新停留在本页,否则回到第一页
        getUsers(user.id ? paramsState.page : 1)
        // 清空表单内容
        resetUserList()
        // 隐藏对话框
        userState.dialogUserVisible = false
    } catch (error) {
        console.log(error)
        userState.userLoading = false
    }
}
// 取消按钮
const cancelUser = () =>{
    userState.dialogUserVisible = false
    resetUserList()
}
// 清空用户表单数据
const resetUserList = () =>{
    userState.user = {
        username: '',
        nickName: '',
        password: ''
    }
}
// 删除用户(单个)
const removeUser = async (id:string) =>{
    await removeUserApi(id)
    ElMessage({
        type: "success",
        message: "删除成功",
    })
    // 获取用户数据,当用户只剩一个时
    getUsers(
        tableState.users.length === 1 ? paramsState.page - 1 : paramsState.page
    )
}
// 用户复选框勾选
const handleSelectionChange = (selection: any[]) => {
    // 由id作为数组元素的 列表
    tableState.selectedIds = selection.map((item) => item.id)
}

// 批量删除()
const removeUsersBat = ()=>{
    ElMessageBox.confirm('确定删除吗?').then( async () => {
        await removeUsersApi(tableState.selectedIds)
        ElMessage.success('用户删除成功!')
        getUsers()
    }).catch(() => {
        ElMessage.info('取消删除')
    })
}

// 获取角色列表
const getRoles = async() =>{
    // 从当前用户重解构数据id 发请求
    const result = await getUserRoleListApi(userState.user?.id as string)
    // 解构
    const { allRolesList, assignRoles } = result
    // 修改数据
    roleState.allRoles = allRolesList
    roleState.userRoleIds = assignRoles.map((item) => item.id as string)
    // 设置全选和 未全选标记
    roleState.checkAll = allRolesList.length === assignRoles.length
    roleState.isIndeterminate =
    assignRoles.length > 0 && assignRoles.length < allRolesList.length
}

// 展示分配角色
const showAssignRole =(row:UserObj) => {
    // 存储当前选中用户
    userState.user = row
    // 显示角色对话框
    roleState.dialogRoleVisible = true
    // 获取角色
    getRoles()
}

// 关闭前钩子
const resetRoleData = () =>{
    // 重置
    Object.assign(roleState, {
        dialogRoleVisible: false,
        allRoles: [],
        userRoleIds: [],
        isIndeterminate: false,
        checkAll: false
    })
}

// 全选钩子函数
const handleCheckAllChange = (value: string | number | boolean)=>{
    // value 当前勾选状态true/false
    // 如果当前全选, userRoleIds就是所有角色id的数组, 否则是空数组
    if(value){
        roleState.userRoleIds = roleState.allRoles.map((item) => item.id as string)
    }else{
        roleState.userRoleIds = []
    } 
    // 不确定状态 设为false 
    roleState.isIndeterminate = false
}

// 改变勾选钩子
const handleCheckedChange =()=>{
    // 解构用户角色ids
    const { userRoleIds, allRoles } = roleState
    // 设置全选标记
    roleState.checkAll =
    userRoleIds.length === allRoles.length && allRoles.length > 0
    // 设置不确定标记
    roleState.isIndeterminate =
    userRoleIds.length > 0 && userRoleIds.length < allRoles.length
}
// 提交角色分配
const assignRole = async () =>{
    // 从当前用户中解构出用户id
    const userId = userState.user?.id
    // 在用户 角色数组中,将数组转为字符串
    const roleIds = roleState.userRoleIds.join(',')
    // 加载
    roleState.roleLoading = true
    // 发请求
    await assignUserRoleListApi(userId as string, roleIds)
    // 加载完成
    roleState.roleLoading = false
    // 提示
    ElMessage({
        type: "success",
        message: "角色分配成功",
    })
    // 清空数据返回
    resetRoleData()
}
</script>

<script lang="ts">
export default {
    name: 'User'
}
</script>

<style scoped lang="less">
</style>

角色管理页面

封装接口请求 src/api/role.ts

ts
import request from '@/utils/request';
import type { Role, RolePageResponseData } from './type/role';

// 定义请求地址的枚举
enum Api {
    GetRoleList = '/admin/acl/role', // 获取角色列表数据
    RemoveRole = '/admin/acl/role/remove', // 移除角色
    RemoveRoles = '/admin/acl/role/batchRemove', // 批量移除角色
    SaveRole = '/admin/acl/role/save', // 保存角色
    UpdateRole = '/admin/acl/role/update', // 删除角色
    AssignRole = '/admin/acl/permission/doAssign', // 授权角色
    GetAssignRole = '/admin/acl/permission/toAssign', // 获取角色列表
}

// 获取角色分页列表(带搜索)
export const getRoleListApi = (page: number, limit: number, roleName: string) => {
    return request.get<any, RolePageResponseData>(Api.GetRoleList + `/${page}/${limit}`, {
        params: {
            roleName
        }
    })
}

/**
 * 保存一个新角色
 * @param role 角色对象
 * @returns null
 */
export const saveRoleApi = (role: Role) => {
    return request.post<any, null>(Api.SaveRole, role);
}

/**
 * 获取一个角色的所有权限列表
 * @param roleId 角色id
 * @returns PermissionModel
 */
export const getAssignRoleApi = (roleId: string) => {
    return request.get<any, any>( Api.GetAssignRole + `/${roleId}`);
};


/**
 * 给某个角色授权
 * @param roleId 角色ID
 * @param permissionId 多个权限id组成的字符串  2,3,4
 * @returns null
 */
export const assignRoleApi = (roleId: string, permissionId: string) => {
    return request.post<any, null>(Api.AssignRole, null, {
        params: {
            roleId,
            permissionId
        },
    });
};

/**
 * 更新一个角色
 * @param role 角色对象
 * @returns null
 */
export const updateRoleApi = (role: Role) => {
    return request.put<any, null>( Api.UpdateRole, role);
};

/**
 * 删除某个角色
 * @param id 角色id
 * @returns null
 */
export const removeRoleApi = (id: string) => {
    return request.delete<any, null>( Api.RemoveRole + `/${id}`);
};

/**
 * 批量删除多个角色
 * @param ids 角色id的数组
 * @returns null
 */
export const removeRolesApi = (ids: string[]) => {
    return request.delete<any, null>(Api.RemoveRoles, {
        data: ids
    })
}

封装接口类型

角色类型 src/api/type/role.ts

ts
/********角色*********/

export interface Role{
    id?: string; // id
    roleName: string; // 角色名字
    originRoleName?: string // 页面是否需要的标识
    edit?: boolean // 是否可进行编译
}
// 角色列表
export type AllRolesArr = Role[]

// 角色分页
export interface RolePageResponseData{
    items: AllRolesArr
    total: number
}

// 某个用户的角色列表
export interface UserRolesArr{
    // 所有角色列表
    allRolesList: AllRolesArr;
    // 分配的角色列表
    assignRoles: AllRolesArr;
}

权限类型 src/api/type/permission.ts

ts
import request from '@/utils/request';
import type { PermissionListResponseModel, PermissionModel } from './type/permission';

// 定义请求地址的枚举
enum Api {
    GetPermissionList = '/admin/acl/permission', // 获取权限列表数据
    RemovePermission = '/admin/acl/permission/remove', // 移除
    SavePermission = '/admin/acl/permission/save', // 添加
    UpdatePermission = '/admin/acl/permission/update', // 更新
}


/**
 * 获取权限列表
 * @returns PermissionListResponseModel
 */
export const getPermissionListApi = () => {
    return request.get<any, PermissionListResponseModel>(Api.GetPermissionList);
  };
  
  /**
   * 删除权限
   * @param id 权限id
   * @returns null
   */
  export const removePermissionApi = (id: string) => {
    return request.delete<any, null>(Api.RemovePermission + `/${id}`);
  };
  
  /**
   * 添加或更新权限
   * @param permission 权限对象 
   * @returns null
   */
  export const saveOrUpdatePermissionApi = (permission: PermissionModel) => {
    return request.put<any, null>(permission.id ? Api.UpdatePermission : Api.SavePermission, permission);
  };

数据渲染(增删改查) src/views/acl/role/index.vue

vue
<template>
    <el-card>
        <el-form inline>
            <el-form-item>
                <el-input placeholder="角色名称" v-model="roleNames.roleName"/>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" :icon="Search" @click="getRoles()">查询</el-button>
                <el-button @click="resetSearch">清空</el-button>
            </el-form-item>
        </el-form>
        
        <div style="margin-bottom: 10px">
            <el-button type="primary" @click="addRole">添加</el-button>
            <el-button type="danger" @click="removeRoles" :disabled="selectedRoles.length === 0">批量删除</el-button>
        </div>
  
        <el-table border stripe style="width: 960px" v-loading="listLoading" :data="rolesList" @selection-change="handleSelectionChange">
            <el-table-column type="selection" width="55" />
            <el-table-column type="index" label="序号" width="80" align="center"></el-table-column>
            <el-table-column label="角色名称">
                <template v-slot="{row}">
                    <template v-if="row.edit">
                    <el-input v-model="row.roleName" class="edit-input" size="small" @blur="toLook(row)" ref="inputRef" />
                    <el-button class="cancel-btn" size="small" :icon="Refresh" type="warning" @click="cancelEdit(row)">
                        取 消
                    </el-button>
                    </template>
                    <span v-else>{{ row.roleName }}</span>
                </template>
            </el-table-column>
            <el-table-column label="操作" width="300" align="center">
                <template v-slot="{row}">
                    <el-button size="small" type="info" :icon="InfoFilled" title="分配权限"
                        @click="$router.push(`/acl/role/auth?id=${row.id}&roleName=${row.roleName}`)"
                    />
                    <el-button size="small" type="primary" :icon="Check" title="确定" @click="updateRole(row)" />
                    <el-button size="small" type="primary" :icon="Edit" title="修改角色"  @click="EditRule(row)"/>
                    <el-button size="small" type="danger" :icon="Delete" title="删除角色" @click="removeRole(row)"/>
                </template>
            </el-table-column>
        </el-table>
  
        <!-- 分页组件 -->
        <el-pagination
            v-model:current-page="page"
            v-model:page-size="limit"
            :page-sizes="[3, 5, 7]"
            style="margin-top: 10px;"
            background
            layout="prev, pager, next, jumper,->,sizes,total"
            :total="total"
            @current-change="getRoles"
            @size-change="SizeChange"
        />
    </el-card>
</template>
<script lang="ts">
    export default {
        name: 'Role'
    }
</script>

<script lang="ts" setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { Search,Edit,Delete,Refresh,InfoFilled,Check} from '@element-plus/icons-vue'
import type { AllRolesArr } from '@/api/acl/type/role';
import { getRoleListApi, removeRoleApi, removeRolesApi, saveRoleApi,updateRoleApi } from '@/api/acl/role';
import { ElMessage, ElMessageBox } from 'element-plus';

const selectedRoles = ref<AllRolesArr>([]) // 所有选中的角色列表
const listLoading = ref(false) // 数据是否正在加载
const rolesList = ref<AllRolesArr>([]) // 角色列表
const total = ref(0) // 总记录数
const page = ref(1) // 当前页码
const limit = ref(5) // 每页记录数
const inputRef = ref() // input输入框对象
const roleNames = reactive({
    // 用户搜索的用户名
    roleName: '',
    searchRoleName: ''
})

// 获取角色分页列表
const getRoles = (p = page.value, size = limit.value) => {
    // 设置页码,每页显示
    page.value = p
    limit.value = size
    // 加载
    listLoading.value = true
    // 发请求
    getRoleListApi(p, size, roleNames.roleName)
    .then((result) => {
        // 存储角色列表数据
        rolesList.value = result.items.map((item) => {
            item.edit = false // 用于标识是否显示编辑输入框的属性
            item.originRoleName = item.roleName // 缓存角色名称, 用于取消
            return item
        })
        // 存储总数
        total.value = result.total
    })
    .finally(() => {
        // 取消加载
        listLoading.value = false
    })
}
// 重置搜索内容
const resetSearch = () => {
    roleNames.roleName = ''
    getRoles()
}
// 分页变化
const SizeChange = (val:number)=>{
    limit.value = val;
    getRoles();
}
// 添加角色
const addRole = () =>{
    // 显示添加界面
    ElMessageBox.prompt('请输入新名称', '添加角色', {
        confirmButtonText: '确定',
        cancelButtonText: '取消'
    })
    .then(({ value }) => {
        saveRoleApi({ roleName: value }).then(() => {
            ElMessage.success('添加角色成功')
            getRoles()
        }).catch(()=>{
            ElMessage.success('添加角色失败')
        })
    })
    .catch(() => {
        ElMessage.warning('取消添加')
    })
}
/* 
  删除指定的角色
*/
const removeRole = (role: any) => {
    ElMessageBox.confirm( `确定删除 '${role.roleName}' 吗?`, '提示', {
        type: 'warning'
    })
    .then(async () => {
        // 发送删除角色请求
        await removeRoleApi(role.id as string)
        // 重新获取数据,如果本页只有一个 回到前一页
        getRoles(rolesList.value.length === 1 ? page.value - 1 : page.value)
        ElMessage.success('删除成功!')
    })
    .catch(() => {
        ElMessage.info('已取消删除')
    })
}

// 更新角色
const updateRole = (role: any) => {
    updateRoleApi(role).then(() => {
        ElMessage.success('更新角色成功!')
        getRoles()
    })
}
// 批量删除
const removeRoles = () => {
    ElMessageBox.confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        type: 'warning'
    })
    .then(async () => {
        // 获取id数组
        const ids = selectedRoles.value.map((role) => role.id as string)
        // 发送请求
        await removeRolesApi(ids)
        // 重新获取数据,如果本页只有一个 回到前一页
        getRoles(rolesList.value.length === 1 ? page.value - 1 : page.value)
        // 提示信息
        ElMessage({
            type: 'success',
            message: '批量删除成功!'
        })
    })
    .catch((error) => {
        if (error === 'cancel') {
            ElMessage({
                type: 'info',
                message: '已取消删除'
            })
        }
    })
}
// table勾选变化
const handleSelectionChange = (selection: AllRolesArr) =>{
    selectedRoles.value = selection
}
// 取消编辑
const cancelEdit = (row:any) =>{
    // 取出之前存的
    row.roleName = row.originRoleName as string
    // 编辑标记为false
    row.edit = false
    // 提示
    ElMessage.warning('取消角色修改')
}

// 编辑
const EditRule = (row:any) =>{
    // 设置编辑标记
    row.edit= true
    // 获取光标
    nextTick(() => {
        inputRef.value.focus();
    });
} 

// 确认编辑
const toLook = (row:any) =>{
    row.edit = false
}

onMounted(() => {
    getRoles()
})

</script>

编辑角色列表 src/views/acl/role/roleAuth.vue

vue
<template>
    <el-card>
        <el-input disabled :value="$route.query.roleName"></el-input>
        <el-tree
            style="margin: 20px 0"
            ref="treeRef"
            :data="allPermissions"
            node-key="id"
            show-checkbox
            default-expand-all
            :props="defaultProps"
            v-loading="listLoading"
        />
        <el-button :loading="loading" type="primary" @click="save">保存</el-button>
        <el-button @click="cancel">取消</el-button>
    </el-card>
  </template>

<script lang="ts">
export default {
    name: 'roleAuth'
}
</script>

<script lang="ts" setup>

import { useRouter, useRoute } from 'vue-router'
import {ElMessage } from 'element-plus'
import { ref, onMounted } from 'vue'

import { getAssignRoleApi, assignRoleApi } from '@/api/acl/role'
import { useUserInfoStore } from '@/stores/userInfo'
// 用户数据仓库
const userInfoStore = useUserInfoStore()
// 路由器对象
const router = useRouter()
// 路由配置对象
const route = useRoute()
// 加载标记
const loading = ref(false) 
// 完整的权限
const allPermissions = ref<PermissionListModel>([])
// 树形结构对象
const treeRef = ref<InstanceType<typeof ElTree>>()
// 设置属性结构别名   
const defaultProps = {
  children: 'children',
  label: 'name'
}
// 设置loading状态
const listLoading = ref<boolean>(false)
// 设置勾选数组状态
const checkedIds = ref<any>()

onMounted(() => init())

/* 
  初始化
*/
const init = () => {
    // 获取路由地址中的id
    const roleId = route.query.id
    listLoading.value = true
    // 调用获取权限的方法
    getPermissions(roleId as string)
    listLoading.value = false
}

/* 
  获取指定角色的权限列表
*/
const getPermissions = (roleId: string) => {
    // 发送请求
    getAssignRoleApi(roleId).then((result) => {
        // 存储服务器数据
        allPermissions.value = result.children as PermissionListModel
        // 存储勾选数组
        checkedIds.value = getCheckedIds(allPermissions.value)
        // console.log('getPermissions() checkedIds', checkedIds)
        // 设置勾选状态
        treeRef.value?.setCheckedKeys(checkedIds.value)
    })
}

/* 
  得到所有选中的id列表
  只用得到所有选中的按钮权限数据的id
*/
const getCheckedIds = (auths: PermissionListModel, initArr: string[] = []) => {
    // 遍历权限列表数组
    auths.forEach((item) => {
        // 判断如果元素存在 select标记 并且 等级是4
        if (item.select && item.level === 4) {
            // 在新数组中添加元素
            initArr.push(item.id as string)
        } else if (item.children) { // 如果存在孩子
            // 递归调用
            getCheckedIds(item.children, initArr)
        }
    })
    // 返回新数组
    return initArr
}

/* 
  保存权限列表
*/
const save = () => {
    // 得到所有全选的id
    const checkedIds = treeRef.value?.getCheckedKeys()
    // console.log("选中状态的数据",checkedIds)
    // 得到所有半选的id
    const halfCheckedIds = treeRef.value?.getHalfCheckedKeys()
    // console.log("半选状态的数据",halfCheckedIds)
    // 合并全选和半选的id, 并用逗号连接成串
    var ids = checkedIds?.concat(halfCheckedIds as []).join(',')

    loading.value = true
    // 发送请求
    assignRoleApi(route.query.id as string, ids as string).then(async () => {
        loading.value = false
        ElMessage.success('分配权限成功')
        // 跳转到角色列表页面
        await router.replace('/acl/role/list')
        // 跳转完成后, 如果分配的是当前用户角色的权限, 刷新一下浏览器
        const roleName = route.query.roleName
        // if (userInfoStore.roles.includes(roleName as string)) {
        //   window.location.reload()
        // }
    })
}

/* 
  取消
  */
const cancel = () => {
  // 跳转到角色列表
  router.replace('/acl/role/list')
}
</script>

菜单管理页面

封装接口请求 src/api/acl/permission.ts

ts
import request from '@/utils/request';
import type { PermissionListResponseModel, PermissionModel } from './type/permission';

// 定义请求地址的枚举
enum Api {
    GetPermissionList = '/admin/acl/permission', // 获取权限列表数据
    RemovePermission = '/admin/acl/permission/remove', // 移除
    SavePermission = '/admin/acl/permission/save', // 添加
    UpdatePermission = '/admin/acl/permission/update', // 更新
}


/**
 * 获取权限列表
 * @returns PermissionListResponseModel
 */
export const getPermissionListApi = () => {
    return request.get<any, PermissionListResponseModel>(Api.GetPermissionList);
  };
  
  /**
   * 删除权限
   * @param id 权限id
   * @returns null
   */
  export const removePermissionApi = (id: string) => {
    return request.delete<any, null>(Api.RemovePermission + `/${id}`);
  };
  
  /**
   * 添加或更新权限
   * @param permission 权限对象 
   * @returns null
   */
  export const saveOrUpdatePermissionApi = (permission: PermissionModel) => {
    return request.put<any, null>(permission.id ? Api.UpdatePermission : Api.SavePermission, permission);
  };

封装接口类型: src/api/acl/type/permission.ts

ts
/* 
权限数据
*/
export interface PermissionModel {
    id?: string; 
    pid?: string; // 上一级id 
    code?: string; // 按钮id
    select?: boolean; // 是否勾选
    toCode?: string; // 
    children?: PermissionListModel; // 是否是菜单
    name: string; // 名字
    level: PermissionLevelModel; // 等级
    type: 1 | 2; // 类型
}

// 1. 树行表格
//    * table展示数据menuPermissionList, 每一行是一个row, 需要数据有 row 中包含 children
//    * 需要给 el-table 组件设置 row-key 属性, row-key属性的作用作为表格中数据的唯一标识
//      row-key="id" ---->   将 row(每行展示的数据)中的字段 id 作为这一行数据的唯一标识
//    * expand-row-keys 设置表格默认展开的是哪一行
//      expand-row-keys 这个的属性值是一个数组,数字中放需要展开行的 row-key 标识(唯一标识)
//      expand-row-keys="menuPermissionList[0].id"
// 2. 保存数据
// level 字段 代表的是等级的意思
// level 支持 1, 2, 3, 4
//    1, 2, 3 代表的都是菜单,此时type值应该是1
//    4 代表的是按钮,此时type值应该是2
// type值是1,代表的是菜单,最后拥有这个权限的时候,数据会展示在当前用户返回的个人信息的 routes 中
// type值是2,代表的是按钮,最后拥有这个权限的时候,数据会展示在当前用户返回的个人信息的 buttons 中

// 权限等级
export type PermissionLevelModel = 0 | 1 | 2 | 3 | 4

// 权限列表
export type PermissionListModel = PermissionModel[]

// 权限列表接口返回的数据
export interface PermissionListResponseModel {
    children: PermissionListModel;
}

数据渲染(增删改查) src/views/acl/menu/index.vue

vue
<template>
    <el-card>
        <!-- 表格 -->
        <el-table border style="margin-bottom: 20px;" :data="menuPermissionList" :expand-row-keys="expandKeys" row-key="id" v-loading="listLoading">
            <el-table-column prop="name" label="名称" />
            <el-table-column prop="code" label="权限值" />
            <el-table-column prop="toCode" label="跳转权限值" />
            <el-table-column label="操作">
                <template v-slot="{row}">
                    <el-button :disabled="row.level===4" type="primary" :icon="Plus" size="small" @click="toAddPermission(row)" :title="getAddTitle(row.level)" />
                    <el-button type="primary" :icon="Edit" size="small" :disabled="row.level===1" @click="toUpdatePermission(row)" :title="row.level===4 ? '修改功能' : '修改菜单'" />
                    <el-button :disabled="row.level===1" type="danger" :icon="Delete" size="small" @click="removePermission(row)" title="删除" />
                </template>
            </el-table-column>
        </el-table>
        <!-- 对话框 -->
        <el-dialog v-model="dialogVisible" :title="dialogTitle" @close="resetData">
            <el-form ref="permissionRef" :model="permission" :rules="permissionRules" label-width="120px">
                <el-form-item label="名称" prop="name">
                    <el-input v-model="permission.name" />
                </el-form-item>
                <el-form-item label="功能权限值" prop="code">
                    <el-input v-model="permission.code" />
                </el-form-item>
                <el-form-item label="跳转路由权限值" prop="toCode" v-if="permission.level===4">
                    <el-input v-model="permission.toCode" />
                </el-form-item>
            </el-form>
            <template #footer>
                <el-button @click="resetData">取 消</el-button>
                <el-button type="primary" @click="addOrUpdatePermission">确 定</el-button>
            </template>
        </el-dialog>
    </el-card>
</template>

<script setup lang='ts'>
import { ElMessage, ElMessageBox } from "element-plus";
import {Plus, Edit, Delete} from '@element-plus/icons-vue'
import { reactive, ref, computed, onMounted, nextTick } from "vue";
import type { PermissionListModel, PermissionModel, PermissionLevelModel } from "@/api/acl/type/permission";
import { getPermissionListApi, removePermissionApi, saveOrUpdatePermissionApi } from "@/api/acl/permission";
import cloneDeep from 'lodash/cloneDeep'
// 权限设置表单对象
const permissionRef = ref<FormInstance>()
// 菜单权限列表
const menuPermissionList = ref<PermissionListModel>([])
// 展开的值
const expandKeys = ref<string[]>([])
// 对话框的显示与隐藏
const dialogVisible = ref<boolean>(false)
// 权限对象 默认值
const permission = reactive<PermissionModel>({ // 要操作的菜单权限对象
    type: 1,
    level: 0, // 菜单级别
    name: '', // 菜单名字
    code: '', // 菜单按钮
    toCode: '' // 菜单
})
// 加载状态
const listLoading = ref<boolean>(false)

onMounted(() => {
    fetchPermissionList()
})

/* 
  动态计算得到Dialog的标题
*/
const dialogTitle = computed(() => {
    const {id, level} = permission
    if (id) {
        return level===4 ? '修改功能' : '修改菜单'
    } else {
        return level===4 ? '添加功能' : '添加菜单'
    }
})

/* 
    根据权限的等级来计算确定校验规则
*/
const permissionRules = computed(() => {
    // 菜单权限校验的规则
    const menuRules = { 
        name: [{required: true, message: '名称必须输入'}],
        code: [{required: true, message: '权限值必须输入'}],
    }
    // 按钮功能权限校验的规则
    const btnRules = { 
        name: [{required: true, message: '名称必须输入'}],
        code: [{required: true, trigger: 'blur', message: '功能权限值必须输入'}]
    }
    return permission.level===4 ? btnRules : menuRules
})

/* 
    根据级别得到要显示的添加dialog的标题
*/
const getAddTitle = (level: number) => {
    if (level===1 || level===2) {
        return '添加菜单'
    } else if (level===3){
        return '添加功能'
    }
}

/* 
请求获取权限菜单数据列表
*/
const fetchPermissionList = async () => {
    // 设置状态
    listLoading.value = true
    // 发送请求
    const result = await getPermissionListApi()
    // 存储数据
    menuPermissionList.value = result.children
    listLoading.value = false
    // 默认展开行 展开第一行
    expandKeys.value = [menuPermissionList.value[0].id as string]
}

/* 
    显示添加权限的界面(菜单或功能)
*/
const toAddPermission = (row: PermissionModel) => {
    // 打开添加对话框
    dialogVisible.value = true
    permission.id = ''
    // 设置pid为当前行对象id
    permission.pid = row.id    // pid ==> parentId 父权限数据的id
    // 设置权限对象 等级为 顺等加1
    permission.level = (row.level + 1) as PermissionLevelModel
    // 设置权限类型
    permission.type = permission.level===4 ? 2 : 1
    
    // 清除校验(必须在界面更新之后)
    // 注意: 校验规则是动态的, nextTick清除后还会显示错误信息, 应该是在nextTick后又进行了校验
    // nextTick(() => {
    //     permissionRef.value?.clearValidate()
    // })
    setTimeout(() => {
        permissionRef.value?.clearValidate()
    }, 0);
}

/* 
    显示菜单添加或更新的dialog
*/
const toUpdatePermission = (row: PermissionModel) => {
    // 设置对话框显示
    dialogVisible.value = true
    // 把行对象内容显示
    Object.assign(permission, row)
    // 设置permission的类型
    permission.type = permission.level===4 ? 2 : 1

    // 清除校验(必须在界面更新之后)
    // nextTick(() => permissionRef.value?.clearValidate())
    // 注意: 校验规则是动态的, nextTick清除后还会显示错误信息, 应该是在nextTick后又进行了校验
    setTimeout(() => {
        permissionRef.value?.clearValidate()
    }, 0);
}

/* 
  删除某个权限节点
*/ 
const removePermission = (permission: PermissionModel) => {
    
    ElMessageBox.confirm('此操作将永久删除该记录, 是否继续?', '提示', {
        type: 'warning'
    }).then(async () => {
        // 发送删除请求
        await removePermissionApi(permission.id as string)
        ElMessage.success('删除成功!')
        // 重新获取数据
        fetchPermissionList()
    }).catch((error) => {
        // 提示错误
        if (error==='cancel') {
            ElMessage({
            type: 'info',
            message: '已取消删除'
            })
        }
    })
}

/* 
  添加或更新功能权限
*/
const addOrUpdatePermission = () => {
    // 校验表单数据
    permissionRef.value?.validate(async valid => { // 校验的回调函数
        if (valid) {
            // 解构出id
            const { id } = permission
            // console.log("valid",valid); // 通过校验返回 true
            // 发送请求
            await saveOrUpdatePermissionApi(permission)
            // 提示信息
            ElMessage.success(`${id ? '修改' : '添加'}成功!`)
            // 重置数据
            resetData()
            // 重新获取请求
            fetchPermissionList()
        }
    })
}

/* 
  重置数据
*/
const resetData = () => {
    // 关闭对话框
    dialogVisible.value = false
    // 合并属性
    Object.assign(permission,cloneDeep({
        id:null,
        type: 1,
        level: 0,
        name: '',
        code: '',
        toCode: ''
    }))
}
</script>
<script lang="ts">
    export default {
        name: 'Permission'
    }
</script>
<style scoped lang="less">
</style>

动态校验清除校验

ts
/* 
    根据权限的等级来计算确定校验规则
*/
const permissionRules = computed(() => {
    // 菜单权限校验的规则
    const menuRules = { 
        name: [{required: true, message: '名称必须输入'}],
        code: [{required: true, message: '权限值必须输入'}],
    }
    // 按钮功能权限校验的规则
    const btnRules = { 
        name: [{required: true, message: '名称必须输入'}],
        code: [{required: true, trigger: 'blur', message: '功能权限值必须输入'}]
    }
    return permission.level===4 ? btnRules : menuRules
})

// 清除校验(必须在界面更新之后)
// 注意: 校验规则是动态的, nextTick清除后还会显示错误信息, 应该是在nextTick后又进行了校验


// nextTick(() => {
//     permissionRef.value?.clearValidate()
// })
// 使用 定时器异步 清除校验
setTimeout(() => {
    permissionRef.value?.clearValidate()
}, 0);

按钮权限控制

用v-if不要用v-show(有风险)

ts
按钮的权限:有的账号有,有的人没有(没有权限)
	控制按钮的权限是根据用户的信息来控制的
    用户信息中(在用户数据仓库中的登陆方法中获得),包含了用户名,用户头像,角色,按钮(数据是一个数组["btn.Category.add",""]),路由(数据是一个数组["",""])。
    数组中有这个属性标识才展示,没有则不展示
	把按钮数组数据存放在用户仓库中(在state添加buttons属性)
	然后在每个按钮上设置v-if判断显示与隐藏
		在页面中引入小仓库,然后在按钮上设置 v-if="userStore.buttons.includes('btn.Trademark.add')"

方式一:使用v-if设置按钮权限

vue
在按钮上通过 v-if 设置是否渲染按钮

方式二:使用自定义指令全局设置按钮权限(优点在于不需要在每个页面引入用户仓库数据)

ts
# 在 src/utils/文件夹中新增 directive.ts 文件
	
# 在用户信息仓库中 存储 按钮数组

# 在按钮上通过 v-自定义 设置是否渲染按钮

项目代码:(方式二)

修改用户仓库中的用户数据类型 添加按钮类型

src/store/interface/index.ts

ts
import type { RouteRecordRaw } from "vue-router";

// 用户信息包括权限数据
export interface UserInfoState {
  token: string;
	avatar: string;
	name: string;
  menuRoutes: RouteRecordRaw[] // 用于生成导航菜单的路由列表
  buttons:string[]
}

定义自定义指令:src/utils/directive.ts

ts
// 在非组件页面下使用小仓库,需要先引入大仓库
import pinia from "@/stores"
import {useUserInfoStore} from "@/stores/userInfo"
// 注入大仓库
const userStore = useUserInfoStore(pinia)
// 暴漏一个函数
//对外暴露一个函数
export const has = (app:any)=>{
    //生成按钮权限全局自定义指令
    //第一个参数:自定义指令的名字
    app.directive("has",{
      //当使用v-has这个全局自定义指令的DOM|组件挂载完毕的时候会立即执行一次
      //element:使用这个指令的DOM元素
      //options:能获取有自定义只有右侧数值  v-has="btn.Trademark.add"
        mounted(element:any,options:any) {
          //判断自定义指令右侧数值在仓库的buttons数组当中是否出现!!!
            if(!userStore.buttons.includes(options.value)){
            //获取当前绑定自定义指令DOM元素父组件,通过removeChild方法将当前元素删除!!!
                element.parentNode.removeChild(element);
            }
            console.log("vue ele is mounted",options)
        },
    })
}

在用户仓库中存储 按钮 数组数据

src/store/userinfo.ts

ts
// 引入定义小仓库方法
import { defineStore } from 'pinia';
// 本地存储操作token
import { getToken, removeToken, setToken } from '../utils/token-utils';
// state类型的数据类型
import type { UserInfoState } from './interface';
// 消息提示
import { ElMessage } from 'element-plus'
// 静态路由
import { staticRoutes } from '@/router/routes'
// 导入用户相关的API
import { getUserInfo, reqUserLogin, reqUserlogout } from '@/api/user';
// 引入类型
import type {LoginResponseData, UserInfoResponseData} from '@/api/user/type/index'
/**
 * 用户信息
 * @methods setUserInfos 设置用户信息
 */
export const useUserInfoStore = defineStore('userInfo', {

  state: (): UserInfoState => {
    return {
      token: getToken() as string,
      name: '',
      avatar: '',
      menuRoutes: [],
      buttons:[]
    }
  },

  actions: {
    // 登陆
    async login(username: string, password: string) {
      // 定义请求体
      const data = {
        username,
        password
      }
      // 获取响应数据
      const result:LoginResponseData = await reqUserLogin(data)
      // 存储token数据(pinia)
      this.token = result.token
      // 存储token数据(localstore)
      setToken(result.token)
      
    },
    // 获取用户信息(路由鉴权那里进行调用)
    async getInfo() {
      const result:UserInfoResponseData = await getUserInfo()
      // console.log("result ",result)
      // 设置小仓库中的数据状态
      this.name = result.name
      this.avatar = result.avatar
      //存储当前用户拥有哪些按钮权限标识 ['btn.模块的名字.xxxx']
      this.buttons = result.buttons;
      // 设置用户的路由
      this.menuRoutes = staticRoutes
    },
    // 退出登陆
    async reset() {
      // 发送请求
      await reqUserlogout()
      // 删除local中保存的token
      removeToken()
      // 提交重置用户信息的mutation
      this.token = ''
      this.name = ''
      this.avatar = ''
    },
  },
});

在入口文件 注册为全局指令

src/main.ts

ts
import {has} from '@/utils/directive';
//引入自定义指令文件函数
has(app);

在组件中使用:src/views/product/spu/index.vue

vue
<!-- 添加按钮 -->
<el-button
    type="primary"
    :icon="Plus"
    @click="addSpu"
    v-has="'btn.Spu.add'"
    :disabled="categoryStore.c3Id ? false : true">添加SPU
</el-button>

菜单权限控制

ts
用户仓库中的 this.menuRoutes 属性能决定左侧菜单展示有哪些路由

菜单的权限
# 对路由进行拆分

	静态路由:(大家都拥有的路由)
    异步路由:(有的人有,有的没有)
	任意路由:

# 管理员账号下发给用户的权限包含了异步路由

# 最后要把静态路由,异步路由,任意路由合并在一起

1. 路由拆分

src/routes/routes.ts

ts
静态路由

异步路由

任意路由

封装函数过滤路由

2. 从用户信息中过滤异步路由

ts
// 定义一个函数 用于过滤路由
//项目的异步路由
	// 定义一个完整异步路由
    const asyncRoute = [
        {
            path: '/acl',
            name: 'Acl',
            children: [
                {
                    path: 'user',
                    name: 'User'
                },
                {
                    path: 'role',
                    name: 'Role'
                },
                {
                    path: 'permisstion',
                    name: 'Permisstion'
                }
            ]
        },
        {
            path: '/product',
            name: 'Product',
            children:[
                {
                    path: 'trademark',
                    name: 'Trademark'
                },
                {
                    path: 'attr',
                    name: "Attr"
                },
                {
                    path: 'spu',
                    name: 'Spu'
                },
                {
                    path: "sku",
                    name: 'Sku'
                }
            ]
        } 
    ];
// 定义一个数组,里边存放用户路由标识,(第一个元素是一级路由,第二个元素之后都是二级路由)
const routes = ["Product", "Trademark", "Spu"];

//封装一个过滤函数,参数是(完整异步路由 , 用户路由数组) 
function findUserAsycRoute(asyncRoute, routes) {
    //过滤路由
    const result = asyncRoute.filter((item) => {
        if (routes.includes(item.name)) {
            if (item.children && item.children.length > 0) {
                // 递归调用,并把递归的结果(二级路由)返回给当前对象的 children属性(二级路由)
                // 这行语句 会 修改原数组asyncRoute中的数据
                item.children = findUserAsycRoute(item.children, routes);
            }
            // 返回为真
            return true;
        }
    })
    // 把当前元素加入到数组中
    return result;
}
// 调用函数,在这里使用深拷贝,传入一个备份就不会影响原数组
const newArr = findUserAsycRoute(cloneDeep(asyncRoute),routes);

3. 在用户仓库中过滤路由

ts
# 导入静态路由,异步路由,任意路由

# 定义方法

# 调用方法(在获取用户信息时)

# 合并路由

# 在路由器注册异步路由,动态追加路由

# 登出账号时,路由器注销全部异步路由注册

# 然后再把静态路由注册进来

修改路由配置

src/router/routes.ts

ts
import type { RouteRecordRaw } from 'vue-router';

//静态路由:常量路由
export const staticRoutes: Array<RouteRecordRaw> = [
	//登录
	{
		path: '/login',
		component: () => import('@/views/login/index.vue'),
		name: 'Login',
		meta: {
			//hidden:决定当前路由是否在左侧菜单展示 true:隐藏 false:显示
			//title:决定路由在左侧展示标题
			//icon:左侧菜单文字前面的图标
			hidden: true,
		}
	},
	//404 
	{
		path: '/404',
		name: '404',
		component: () => import('@/views/error/404.vue'),
		meta: {
			hidden: true
		}
	},
	// 首页
	{
		path: '/',
		component: () => import('@/layout/index.vue'),
		redirect: '/home',
		children: [{
			path: 'home',
			name: 'Home',
			component: () => import('@/views/home/index.vue'),
			meta: {
				title: '首页',
				icon: 'ele-HomeFilled',
			}
		}]
	},
];

//存储异步路由
export const asyncRoute = [
	// 权限管理
	{
		name: 'Acl',
		path: '/acl',
		component: () => import('@/layout/index.vue'),
		redirect: '/acl/user/list',
		meta: { title: '权限管理', icon: 'ele-Setting' },
		children: [
			{
				name: 'User',
				path: '/acl/user/list',
				component: () => import('@/views/acl/user/index.vue'),
				meta: { title: '用户管理' }
			},
			{
				name: 'Role',
				path: '/acl/role/list',
				component: () => import('@/views/acl/role/index.vue'),
				meta: {
					title: '角色管理',
				},
			},
			{
				name: 'RoleAuth',
				path: '/acl/role/auth',
				component: () => import('@/views/acl/role/roleAuth.vue'),
				meta: {
					title: '角色管理',
					hidden: true,
					activeMenu: '/acl/role/list',
				},
			},
			{
				name: 'Permission',
				path: '/acl/permission/list',
				component: () => import('@/views/acl/menu/index.vue'),
				meta: {
					title: '菜单管理',
				}
			}
		]
	},
	//商品管理模块的路由
	{
		path: '/product',
		component: () => import('@/layout/index.vue'),
		name: 'Product',
		meta: {
			title: '商品管理',//菜单的标题
			icon: "ele-Goods"
		},
		children: [
			{
				path: 'trademark',
				component: () => import('@/views/product/trademark/index.vue'),
				name: 'Trademark',
				meta: {
					title: '品牌管理',
					icon: 'ele-Operation'
				}
			},
			{
				path: 'attr',
				component: () => import('@/views/product/attr/index.vue'),
				name: 'Attr',
				meta: {
					title: '属性管理',
					icon: 'ele-Files'
				}
			},
			{
				path: 'spu',
				component: () => import('@/views/product/spu/index.vue'),
				name: 'Spu',
				meta: {
					title: 'Spu管理',
					icon: 'ele-Platform'
				}
			}
			,
			{
				path: 'sku',
				component: () => import('@/views/product/sku/index.vue'),
				name: 'Sku',
				meta: {
					title: 'sku管理',
					icon: 'ele-Football'
				}
			}

		]

	}
]


//存储任意路由
export const anyRoute = {
	path: '/:pathMatch(.*)',
	name: 'Any',
	redirect: '/404',
	meta: {
		hidden: true
	}
}

用户仓库中定义方法,调用

src/store/userInfo.ts

ts
// 引入定义小仓库方法
import { defineStore } from 'pinia';
// 本地存储操作token
import { getToken, removeToken, setToken } from '../utils/token-utils';
// state类型的数据类型
import type { UserInfoState } from './interface';
// 消息提示
import { ElMessage } from 'element-plus'
//引入常量路由、异步路由、任意路由
import { staticRoutes, asyncRoute, anyRoute } from '@/router/routes'
// 导入用户相关的API
import { getUserInfo, reqUserLogin, reqUserlogout } from '@/api/user';
// 引入类型
import type {LoginResponseData, UserInfoResponseData} from '@/api/user/type/index'

//引入路由器
import router from '@/router';
//引入lodash深拷贝
import cloneDeep from "lodash/cloneDeep";

//过滤异步路由
/**
 * 
 * @param asyncRoute 所有异步路由
 * @param routes 用户信息中包含的路由数组
 * @returns 返回一个路由配置对象
 */
function findUserAsyncRoute(asyncRoute: any, routes: any) { // 返回一个路由配置对象
  // 遍历异步路由
  return asyncRoute.filter((route: any) => {
    // 如果用户路由数组中包含
    if (routes.includes(route.name)) {
      // 存在children属性,并children属性长度大于0
      if (route.children && route.children.length > 0) {
        // 递归调用 把递归的结果(二级路由)返回给当前对象的 children属性(二级路由)
        // 这行语句 会 修改原数组asyncRoute中的数据
        route.children = findUserAsyncRoute(route.children, routes);
      }
      // 返回为真
      return true;
    }
  })
}

/**
 * 用户信息
 * @methods setUserInfos 设置用户信息
 */
export const useUserInfoStore = defineStore('userInfo', {

  state: (): UserInfoState => {
    return {
      token: getToken() as string,
      name: '',
      avatar: '',
      menuRoutes: [],
      buttons:[]
    }
  },

  actions: {
    // 登陆
    async login(username: string, password: string) {
      // 定义请求体
      const data = {
        username,
        password
      }
      // 获取响应数据
      const result:LoginResponseData = await reqUserLogin(data)
      // 存储token数据(pinia)
      this.token = result.token
      // 存储token数据(localstore)
      setToken(result.token)
      
    },
    // 获取用户信息(路由鉴权那里进行调用)
    async getInfo() {
      const result:UserInfoResponseData = await getUserInfo()
      // console.log("result ",result)
      // 设置小仓库中的数据状态
      this.name = result.name
      this.avatar = result.avatar
      //存储当前用户拥有哪些按钮权限标识 ['btn.模块的名字.xxxx']
      this.buttons = result.buttons;
      //过滤出这个用户需要展示异步路由 在这里使用深拷贝,传入一个备份就不会影响原数组
      const userAsyncRoute = findUserAsyncRoute(cloneDeep(asyncRoute), result.routes);
      //左侧菜单展示:决定左侧菜单(导航)显示与否
      //router注册路由只有静态路由 
      this.menuRoutes = [...staticRoutes,...userAsyncRoute,anyRoute];
      //动态追加路由:异步路由、任意路由即可!!! router.getRoutes获取路由器全部的路由!!!
      [...userAsyncRoute,anyRoute].forEach((route:any)=>{
          // 动态追加路由
          router.addRoute(route);
      })
    },
    // 退出登陆
    async reset() {
      // 发送请求
      await reqUserlogout()
      // 删除local中保存的token
      removeToken()
      // 提交重置用户信息的mutation
      this.token = ''
      this.name = ''
      this.avatar = ''
      //清空当前用户全部的路由
      router.getRoutes().forEach((route:any)=>{
        //退出登录清空全部路由
        router.removeRoute(route.name);
      });
      //退出登录保留全部的常量路由
      staticRoutes.forEach((route:any)=>{
          router.addRoute(route);
      })
    },
  },
});