(未完,持续更新)
1.功能实现
1.1前端部分权限管理
1.1.1什么是权限管理
登录的人的角色可能是超级管理员、管理员、以及普通用户或者有更多的层级角色,拥有不同权限的用户登录系统之后看到的界面是不一样的。
若依系统中的权限分为以下几类:
1 菜单权限:用户登录系统之后能看到哪些菜单
2 按钮权限:用户在一个页面上能看到哪些按钮,比如新增、删除等按钮
3 接口权限:用户带着认证信息请求后端接口,是否有权限访问,该接口和前端页面上的按钮一一对应
4 数据权限:用户有权限访问后端某个接口,但是不同的用户相同的接口相同的入参,根据权限大小不同,返回的结果应当不一样——权限大的能够看到的数据更多。
1.1.2菜单权限
(1)获取用户角色、权限存入Vuex
permission.js文件中设置了导航守卫,每次路由发生变化的时候就会触发router.beforeEach的回调函数。当用户登录时,会调用代码如下:
//permission.js
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
useUserStore().getInfo().then(() => {
//...
}).catch(err => {
useUserStore().logOut().then(() => {
ElMessage.error(err)
next({ path: '/' })
})
})
将用户的权限字符存入数据库,当用户登录后根据用户的登录信息获取用户的所有信息(包括用户的权限信息),也就是调用Vuex里user模块的actions:getInfo(),将角色信息roles、权限标识存入user模块的states。
![](https://i-blog.csdnimg.cn/blog_migrate/16f4f139d3dcce1e1f78bab5b9208d0f.png)
//user.js(store)
// 获取用户信息
getInfo() {
return new Promise((resolve, reject) => {
getInfo().then(res => {
console.log(res)
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
this.roles = res.roles
this.permissions = res.permissions
} else {
this.roles = ['ROLE_DEFAULT']
}
this.name = user.userName
this.avatar = avatar;
resolve(res)
}).catch(error => {
reject(error)
})
})
},
(2)获取路由数据,动态路由遍历,验证是否具备权限
后端请求可以访问的路由数据,遍历后台传来的路由字符串,转换为组件对象,根据roles权限生成可访问的路由表。
动态路由遍历,验证是否具备权限,如果具备权限也加入路由表。
//permission.js(store)
generateRoutes(roles) {
return new Promise(resolve => {
// 向后端请求路由数据
getRouters().then(res => {
//...
// 遍历后台传来的路由字符串,转换为组件对象
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
//...
resolve(rewriteRoutes)
})
})
}
(3)获取路由实例,调用addRoute动态添加可访问路由表
添加路由配置具体可见:https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html
// 判断当前用户是否已拉取完user_info信息之后
useUserStore().getInfo().then(() => {
isRelogin.show = false
usePermissionStore().generateRoutes().then(accessRoutes => {
// 根据roles权限生成可访问的路由表
accessRoutes.forEach(route => {
if (!isHttp(route.path)) {
router.addRoute(route) // 动态添加可访问路由表
}
})
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
//router/index.js
const router = createRouter
此时如果访问没有权限的路由地址,相当于没有和已经定义好的路由匹配的,就会重定向到404page,pathMatch的作用就是使用正则进行路径匹配,当匹配的结果没有一个和已经定义好的路由相同的话,就会进行重定向。
{
path: "/:pathMatch(.*)*",
component: () => import('@/views/error/404'),
hidden: true
},
(4)菜单组件
1.1.2(2)步骤同时也将可访问的路由存入state中 的sidebarRouters,在菜单栏组件调用实现,没有权限的自然也不会显示,并且只会显示hidden: false的组件,其他例如/401、/404则不会显示。
![](https://i-blog.csdnimg.cn/blog_migrate/60c38c04b89bd094e7f343b5a4a25612.png)
1.1.3 按钮权限
封装了一个指令权限,能简单快速的实现按钮级别的权限判断。v-permission
(1)使用权限字符串 v-hasPermi
// 单个
<el-button v-hasPermi="['system:user:add']">存在权限字符串才能看到</el-button>
// 多个
<el-button v-hasPermi="['system:user:add', 'system:user:edit']">包含权限字符串才能看到</el-button>
(2)使用角色字符串 v-hasRole
// 单个
<el-button v-hasRole="['admin']">管理员才能看到</el-button>
// 多个
<el-button v-hasRole="['role1', 'role2']">包含角色才能看到</el-button>
提示
在某些情况下,它是不适合使用v-hasPermi,如元素标签组件,只能通过手动设置v-if。 可以使用全局权限判断函数,用法和指令 v-hasPermi 类似。
<template>
<div class="app-container">
<el-tabs type="border-card">
<el-tab-pane v-if="checkPermi(['system:user:add'])" label="用户管理" name="user">用户管理</el-tab-pane>
<el-tab-pane v-if="checkPermi(['system:user:add', 'system:user:edit'])" label="角色增改" name="menu">角色增改</el-tab-pane>
<el-tab-pane v-if="checkPermi(['system:config:list'])" label="参数管理" name="menu1">参数管理</el-tab-pane>
<el-tab-pane v-if="checkRole(['admin'])" label="角色管理" name="role">角色管理</el-tab-pane>
<el-tab-pane v-if="checkRole(['admin','common'])" label="定时任务" name="job">定时任务</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { checkPermi, checkRole } from "@/utils/permission"; // 权限判断函数
export default({
methods: {
checkPermi,
checkRole
}
})
</script>
![](https://i-blog.csdnimg.cn/blog_migrate/a98edd774fe62abea89d357c483114f8.png)
1.1.4 接口权限
前端鉴权只能保证可以隐藏或禁用菜单,并不能保证菜单关联的后端接口请求不被非法调用,若依支持在后端接口方法使用角色或权限字符声明权限:
以下代码表示必须拥有system:user:add和system:user:update权限才可访问
@RequiresPermissions({"system:user:add", "system:user:update"})
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
以下代码表示必须拥有admin角色才可访问
@RequiresRoles("admin")
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
若依没有为后端接口专门设计权限管理模块,它认为后端接口和菜单具有对应关系,可以直接使用菜单的角色或权限字符用于后端接口的权限声明。
1.1.5 数据权限
略
1.1.6 流程图
![](https://i-blog.csdnimg.cn/blog_migrate/f5338960d9efa47c897f3454ee115fab.png)
1.2 页签缓存
1.2.1 简介
由于目前 keep-alive 和 router-view 是强耦合的,而且查看文档和源码不难发现 keep-alive 的 include 默认是优先匹配组件的 name ,所以在编写路由 router 和路由对应的 view component 的时候一定要确保 两者的 name 是完全一致的。(切记 name 命名时候尽量保证唯一性 切记不要和某些组件的命名重复了,不然会递归引用最后内存溢出等问题
1.2.2 demo
//router 路由声明
{
path: 'config',
component: ()=>import('@/views/system/config/index'),
name: 'Config',
meta: { title: '参数设置', icon: 'edit' }
}
//路由对应的view system/config/index
export default {
name: 'Config'
}
一定要保证两者的名字相同,切记写重或者写错。默认如果不写 name 就不会被缓存。
提示
在系统管理-菜单管理-可以配置菜单页签是否缓存,默认为缓存
1.3 异常处理
1.3.1 实现
request.js 配置了vue的axios参数,对入参和出参进行判断、封装和处理,对于一些共通错误进行统一抛出,不管什么接口方法都需要用到 request.js ,之后如果对接口入参和出参有调整都需要去修改该文件。
axios配置:https://blog.csdn.net/JiangZhengyang7/article/details/129260624?spm=1001.2014.3001.5502
1.3.2 响应拦截的几种情况
登录过期,提示需要重新登录,利用vuex清除token数据
服务器报错500,提示对应异常、
接口正常执行,但返回非200状态码,提示对应异常
正常执行正常返回数据
1.4 全局样式
1.4.1 组件使用主题颜色样式实现
(1)定义一套主题色 css 变量,每种主题色对应不同的透明度
![](https://i-blog.csdnimg.cn/blog_migrate/1b272faa3e5ea4a3ea811c87ebe50470.png)
// 变浅颜色值
export function getLightColor(color, level) {
let rgb = hexToRgb(color)
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
}
return rgbToHex(rgb[0], rgb[1], rgb[2])
}
// 变深颜色值
export function getDarkColor(color, level) {
let rgb = hexToRgb(color)
for (let i = 0; i < 3; i++) {
rgb[i] = Math.floor(rgb[i] * (1 - level))
}
return rgbToHex(rgb[0], rgb[1], rgb[2])
}
(2)自定义html中的样式
声明一个自定义属性,属性名需要以两个减号(--)开始,属性值则可以是任何有效的 CSS 值。
//utils/theme.js
// 处理主题样式
export function handleThemeStyle(theme) {
document.documentElement.style.setProperty('--el-color-primary', theme)
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
}
}
document.documentElement 是一个会返回文档对象(document)的根元素的只读属性(如 HTML 文档的 <html> 元素)。
document.documentElement.style 属性定义了 当前浏览器支持的 所有 css 属性.
以document.documentElement.style.setProperty方式设置CSS变量会添加到html中。这样就可以在 HTML 文档的任何地方访问到它了
(3)组件color、background使用自定义属性
例子:修改TreeSelect组件中的样式
:deep(.el-tree-node__content:focus) {
color:var(--el-color-primary)
}
备注: 自定义属性名是大小写敏感的,--my-color 和 --My-color 会被认为是两个不同的自定义属性。
使用一个局部变量时用 var() 函数包裹以表示一个合法的属性值:
js中获取:
document.documentElement.style.getPropertyValue('--el-color-primary')
//#409EFF
1.4.2 主题样式自定义存储实现
(1)Vuex中定义了默认的主题样式
一开始进入后台,本地存储中不会存储有主题样式,此时会使用store定义的默认样式。
//store/setting.js
state: () => ({
title: '',
theme: storageSetting.theme || '#409EFF',
sideTheme: storageSetting.sideTheme || sideTheme,
showSettings: showSettings,
//...
}),
(2)用户修改主题风格、样式存储,保存后存入本地存储localStorage中
经过对Vuex的学习我们可以知道,网页刷新时state中的数据也会丢失,因此当用户在系统中修改主题样式并保存时,就会将样式保存至本地也就是localStorage中。
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
//...
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
setTimeout(proxy.$modal.closeLoading(), 1000)
}
(3)在公共组件中读取Vuex中存储的主题样式,判断
![](https://i-blog.csdnimg.cn/blog_migrate/dd3c6432ffdeb2d9b5176e8f07000814.png)
公共组件例如sidebar、TopNav,对Vuex中存储的主题样式进行判断,比如判断是否需要隐藏Navbar,或者是否是theme-dark风格之后,在全局样式variables.module.scss中获取对应不同风格的样式。当然,对于主题颜色的设置和1.4.1中的方法不一样。
需要注意的是,在 vite 创建的项目中,如果你想在 js 里引用 scss 文件,需要在后缀前加上 .module,如variables.module.scss
//sidebar/index.vue
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
mode="vertical"
>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
</script>
1.4.3 新增样式
页面的样式和组件是一个道理,全局的 @assets/styles 放置全局公用的样式,每一个页面的样式就写在当前 views下面,请记住加上scoped 就只会作用在当前组件内了,避免造成全局的样式污染。
1.5 使用图标
1.5.1 使用方式
<!-- icon-class 为 icon 的名字; class-name 为 icon 自定义 class-->
<svg-icon icon-class="password" class-name='custom-class' />
1.5.2 改变颜色
svg-icon 默认会读取其父级的 color fill: currentColor;
你可以改变父级的color或者直接改变fill的颜色即可。
提示
如果你是从 iconfont (opens new window)下载的图标,记得使用如 Sketch 等工具规范一下图标的大小问题,不然可能会造成项目中的图标大小尺寸不统一的问题。 本项目中使用的图标都是 128*128 大小规格的
2.二次封装组件
2.1 树形选择组件TreeSelect
2.1.1 示例
组件是直接使用,无需引入。
![](https://i-blog.csdnimg.cn/blog_migrate/d2691beef57075624ee019711eb1be86.png)
2.1.2 属性
属性名 | 说明 | 类型 | 可选值 | 默认值 |
objMap | 配置项,对应了options中的字段名,子级字段名以及显示名称 | Object | - | value: 'id', label: 'label', children: 'children' |
options | 传入要的分支节点以及叶节点
| Array | - | [] |
accordion | 是否自动收起 | Boolean | true/false | false |
value | 当前双向绑定的值 | String, Number | - | '' |
placeholder | 输入框内部的文字 | String | - | '' |
2.1.3 事件
事件 | 说明 |
update:value | 树形选择组件所选值更新,返回所选id |
2.2 工具栏右侧组件RightToolbar
2.2.1 示例
![](https://i-blog.csdnimg.cn/blog_migrate/38be5ad8971d88d5c6faec2482618122.png)
分别有搜索栏隐藏或显示、刷新搜索栏、table显隐列显示配置的功能。
![](https://i-blog.csdnimg.cn/blog_migrate/6fd85927a8902af5b18688fc03c445a3.png)
2.2.2 属性
属性名 | 说明 | 类型 | 可选值 | 默认值 |
showSearch | 双向绑定一个Boolean属性,点击工具栏第一个按钮则改变,将其v-show绑定搜索栏 | Boolean | true/false | true |
columns | 传入表格数据显隐列配置,将columns[].visible绑定表格列。
| Array | - | - |
search | 是否需要显示/隐藏搜索栏 | Boolean | true/false | false |
gutter | 组件样式配置 | Number | - | 10 |
2.2.3 事件
事件 | 说明 |
update:showSearch | 点击隐藏搜索/显示搜索按钮,返回Boolean值 |
queryTable | 点击刷新按钮 |
3.前端打包部署服务器
(未完)