人力资源管理项目 一、前端项目搭建 1、搭建项目前的一些基本准备 前端使用的是Vue3
框架,所以开始先使用Vue
脚手架来创建前端的项目。
确保在电脑中安装了对应的脚手架vue/cli
当然,首先必须在电脑中安装node.js
环境
查看node
和 npm
的版本
1 2 $ node -v #查看node版本 $ npm -v #查看npm版本
**npm
淘宝镜像 **
npm
是非常重要的npm
管理工具,由于npm
的服务器位于国外, 所以一般建议 将 npm
设置成国内的淘宝镜像
设置淘宝镜像
1 2 $ npm config set registry https://registry.npm.taobao.org/ #设置淘宝镜像地址 $ npm config get registry #查看镜像地址
2、使用脚手架搭建Vue3
开发环境 在指定的目录下面执行
使用以上命令创建项目的时候,出现了如下错误
1 vue : 无法加载文件 C:\XXX\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本。
参考解决方案:
1 https://blog.csdn.net/ying456baby/article/details/126379392
第一:以管理身份启动命令窗口
第二:在终端窗口输入 **set-ExecutionPolicy RemoteSigned**
输入命令查看 **get-ExecutionPolicy**
为 “**RemoteSigned**”
项目创建的步骤如下
第一步:打开命令行窗口(以管理员身份)
第二步:在打开的命令行窗口中输入vue create hr-front
第三步:选择自定义创建
第四:选中vue-router
,vuex
,css Pre-processors
选项
第五步:选择vue3.0
版本
第六步:选择hash
模式的路由
第七步:选择less
作为预处理器
第八步:选择 standard
标准代码风格
第九步:保存代码校验代码风格,代码提交时候校验代码风格
第十步:依赖插件或者工具的配置文件分文件保存
第十一步:是否记录以上操作,选择否
第十二步:等待安装…
最后:安装完毕
这里可以cd
到hr-front
目录,然后通过执行npm run serve
命令来启动项目
3、API
模块和请求封装模块介绍
在src
目录下面创建utils
目录,在该目录下面创建request.js
文件,该文件中的初步代码如下所示:
先创建axios
的基本结构,后面在完善
1 2 3 4 5 6 import axios from 'axios' const service = axios.create (); service.interceptors .request .use (); service.interceptors .response .use (); export default service;
api
模块的单独封装
我们习惯性的将所有的网络请求 放置在api
目录下统一管理,按照模块进行划分
在src
目录下面创建api
目录,在该目录下面创建user.js
文件,该文件中封装的就是获取用户信息,或者是对用户信息进行操作的api
接口。
初步代码如下所示:
1 2 3 4 5 6 7 import request from "@/utils/request" ;export function login (data ) {}export function getInfo ( ) {}export function logout ( ) {}
4、公共资源图片和统一样式
将文件夹拷贝到assets
目录下面
在src
目录下面创建styles
目录,把样式都拷贝到该目录下面
5、修改网站名称
在public/index.html
文件中,可以在title
标签中看到如下的代码:
1 <title><%= htmlWebpackPlugin.options.title %></title>
这里获取的是webpack
中的htmlWebpackPlugin
插件中配置的标题
默认情况下,项目显示的标题为项目路径对应的名称,下面介绍修改htmlWebpackPlugin.options.title
对应的值。
修改项目中vue.config.js
文件中的配置
vue.config.js
就是vue
项目相关的编译,配置,打包,启动服务相关的配置文件.
在文件中添加如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 module .exports = { lintOnSave : false , chainWebpack : (config ) => { config.plugin ("html" ).tap ((args ) => { args[0 ].title = "人力资源管理平台" ; return args; }); }, };
args
参数的值为
1 2 3 4 5 6 7 args=== [ { title : 'hr-front' , templateParameters : [Function : templateParameters], template : 'index.html文件的路径' } ]
所以说args
参数相当于html
模板中的htmlWebpackplugin.options
.
配置完成后一定要重新启动项目。
返回浏览器,查看标题。
6、修改端口号
项目启动以后,对应的端口号是8080
如果想修改默认的端口号应该怎样处理呢?
在项目的根目录 下面创建两个文件:
分别是.env.development
,.env.production
development
=> 开发环境
production
=> 生产环境
当我们运行**npm run serve
进行开发调试的时候,此时会加载执行 .env.development
**文件内容
当我们运行**npm run build
进行生产环境打包的时候,会加载执行 .env.production
**文件内容
所以,如果想要设置开发环境的接口,直接在**.env.development
**中写入对于port变量的赋值即可
问题是怎样获取该变量的值?
下面继续修改vue.config.js
文件中的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const port = process.env .port || 8080 ;module .exports = { lintOnSave : false , chainWebpack : (config ) => { config.plugin ("html" ).tap ((args ) => { args[0 ].title = "人力资源管理平台" ; return args; }); }, devServer : { port : port, }, };
这里首先通过process.env.port
获取到.env.development
文件中所定义的port
变量值。
然后把该变量的值赋值给devServer
配置项中的port
属性。
重新启动项目。
二、登录模块 1、登录布局实现 这里我们会使用Element Plus
组件来实现登录界面布局。
https://element-plus.gitee.io/zh-CN/
1 npm install element-plus --save
安装好以后,在项目中使用Element Plus
组件,这里需要按需导入:
https://element-plus.gitee.io/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5
修改vue.config.js
这个配置文件,实现组件的按需导入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const AutoImport = require ("unplugin-auto-import/webpack" );const Components = require ("unplugin-vue-components/webpack" );const { ElementPlusResolver } = require ("unplugin-vue-components/resolvers" );const { defineConfig } = require ('@vue/cli-service' )module .exports = defineConfig ({ transpileDependencies : true , lintOnSave : false , chainWebpack : (config ) => { config.plugin ("html" ).tap ((args ) => { args[0 ].title = "人力资源管理平台" ; return args; }); }, configureWebpack : { plugins : [ AutoImport ({ resolvers : [ElementPlusResolver ()], }), Components ({ resolvers : [ElementPlusResolver ()], }), ], }, })
这里你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
1 npm install -D unplugin-vue-components unplugin-auto-import
下面进行测试:在views
目录下面创建login
目录,在该目录下面创建index.vue
组件,该组件中的初步代码如下:
1 2 3 4 5 6 <template> <div> <el-button>按钮</el-button> hello world </div> </template>
在模板中直接使用了按钮组件。
这里还需要配置一下路由:
修改src/router/index.js
文件中的代码,添加如下路由规则
1 2 3 4 5 6 7 8 9 10 11 12 const routes = [ { path : "/" , name : "Home" , component : Home , }, { path : "/login" , name : "Login" , component : () => import ("@/views/login/index.vue" ), },
在上面添加了登录的路由规则。
由于修改了配置文件,需要重新启动项目
http://localhost:8080/#/login
进行访问
下面进行布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 <template> <div class="login-container"> <el-form class="login-form"> <div class="title-container"> <h3 class="title"> <!-- <img src="@/assets/common/login-logo.png" alt="" /> --> </h3> </div> <el-form-item> <el-input placeholder="请输入手机号" type="text" v-model="mobile" tabindex="1" /> </el-form-item> <el-form-item> <el-input placeholder="请输入密码" name="password" type="password" tabindex="2" v-model="password" /> </el-form-item> <el-button class="loginBtn" type="primary" style="width: 100%; margin-bottom: 30px" @click="submit">登录</el-button> <div class="tips"> <span style="margin-right: 20px">账号: 13811111111</span> <span> 密码: 123</span> </div> </el-form> </div> </template> <script> import { ref } from "vue"; export default { name: "Login", setup() { const mobile = ref(""); const password = ref(""); const submit = () => { console.log(mobile.value + " " + password.value); }; return { mobile, password, submit, }; }, }; </script> <style lang="scss" scoped> $bg: #2d3a4b; $dark_gray: #889aa4; $light_gray: #eee; .login-container { min-height: 100%; width: 100%; background-color: $bg; overflow: hidden; background-image: url("~@/assets/common/login.jpg"); // 设置背景图片,注意:如需要在样式表中使用**`@`**别名的时候,需要在@前面加上一个**`~`**符号,否则不识别 background-position: center; // 将图片位置设置为充满整个屏幕 .login-form { position: relative; width: 520px; max-width: 100%; padding: 160px 35px 0; margin: 0 auto; overflow: hidden; } .el-form-item { border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.7); // 输入登录表单的背景色 border-radius: 5px; color: #454545; } .el-form-item__error { color: #fff; } .loginBtn { background: #407ffe; height: 64px; line-height: 32px; font-size: 24px; } .tips { font-size: 14px; color: #fff; margin-bottom: 10px; span { &:first-of-type { margin-right: 16px; } } } .title-container { position: relative; .title { font-size: 26px; color: $light_gray; margin: 0px auto 40px auto; text-align: center; font-weight: bold; } } .show-pwd { position: absolute; right: 10px; top: 7px; font-size: 16px; color: $dark_gray; cursor: pointer; user-select: none; } } </style>
1 2 Failed to resolve loader: sass-loader Can't resolve 'sass-loader'
编译项目的时候,会出现如上的错误,原因:我们使用的是sass
来编写样式,当时我们使用脚手架搭建项目的时候,使用的是less
.
所以我们现在搭建的项目无法处理sass
。这里需要安装sass-loader
这个包来进行处理。
1 2 npm i sass npm i sass-loader
在上面的代码中,完成了表单的布局。(注意:如需要在样式表中使用**@
别名的时候,需要在@前面加上一个 ~
**符号,否则不识别)
同时文本框通过v-model
进行了双向绑定。
在setup
入口函数中,通过ref
创建响应式对象,由于与文本框进行了双向绑定,所以在单击登录按钮的时候,在所触发的方法中可以获取到文本框中的值。
返回到浏览器中进行测试。
当然我们也可以按照文档中的使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <script> import { reactive } from "vue"; export default { name: "Login", setup() { const ruleForm = reactive({ mobile: "", password: "", }); const submit = () => { console.log(ruleForm.mobile + "...." + ruleForm.password); // 打印在文本框与密码框中输入的内容 }; return { submit, ruleForm, // 注意:这里将响应式对象返回到模版中,模版中才会使用 }; }, }; </script>
通过reactive
创建一个响应式对象,让后返回到模板中
1 <el-form class="login-form" :model="ruleForm">
将ruleForm
与el-form
中的model
进行关联。
model
是el-form
表单提供的属性,含义是:表单数据对象
1 2 3 4 5 6 7 8 9 10 11 12 13 <el-input placeholder="请输入手机号" type="text" v-model="ruleForm.mobile" tabindex="1" /> <el-input placeholder="请输入密码" name="password" type="password" tabindex="2" v-model="ruleForm.password" />
通过v-model
与ruleForm
中的属性进行了双向绑定
返回浏览器中进行测试
官方文档案例:https://element-plus.gitee.io/zh-CN/component/form.html#%E8%A1%A8%E5%8D%95%E6%A0%A1%E9%AA%8C
2、表单校验 关于对手机号码的正则表达式的校验,单独的封装到了一个js
文件中。
在utils
目录下面创建validate.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 export function validMobile (str ) { return /^1[3-9]\d{9}$/ .test (str); }
返回到views/login/index.vue
文件中继续修改代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <script> import { reactive } from "vue"; import { validMobile } from '@/utils/validate' // 导入validMobile export default { name: "Login", setup() { const ruleForm =reactive({ mobile:"", password:"" }); // 手机号码的验证: const validateMobile =(rule,value,callback)=>{ console.log('rule =',rule); console.log("value = ",value); console.log("callback = ",callback); // 调用所导入的validMobile这个函数,对传递过来的手机号进行校验 validMobile(value)?callback():callback(new Error("手机号码错误")); } // 定义校验的规则 const loginRules =reactive({ mobile:[{ required:true, message:'请输入手机号码', trigger:"blur" },{ trigger:"blur", validator: validateMobile // 当手机号文本框失去焦点以后,会自动调用validateMobile这个函数。 }], password:[{ required:true, message:'请输入密码', trigger:"blur" },{ min:6, max:16, message:'密码的长度在6-16位之间', trigger:"blur" }] }); const submit = () => { console.log(ruleForm.mobile + " " + ruleForm.password); }; return { ruleForm, submit, loginRules // 返回定义的校验规则 }; }, }; </script>
在上面的代码中,先将validMobile
方法导入进来。
定义了校验的响应式对象loginRules
,并且返回
在该对象中定义了校验规则,注意mobile
和password
一定要与el-form-item
中的prop
属性保持一致,校验才会起作用。
在校验手机号码的时候,要求必填,同时指定了validator
属性,在失去焦点后,会执行validator
属性对应的validateMobile
方法,
1 2 3 4 5 6 7 8 const validateMobile =(rule,value,callback )=>{ console .log ('rule =' ,rule); console .log ("value = " ,value); console .log ("callback = " ,callback); validMobile (value)?callback ():callback (new Error ("手机号码错误" )); }
validateMobile
方法有三个参数,第一个参数:表示校验的规则,第二个参数,用户输入的手机号码,第三个参数callback
是一个函数。
在validateMobile
方法内部,调用validMobile
方法校验手机号码,如果通过校验了,调用callback
函数,如果没有通过校验,也会调用callback
函数,只不过会传递错误对象。
模板中的代码
1 <el-form class="login-form" :model="ruleForm" :rules="loginRules">
这里将定义的规则对象与el-form
组件中的rules
属性关联在一起。
1 2 3 4 5 6 7 <el-form-item prop ="mobile"> <el-input placeholder="请输入手机号" type="text" v-model="ruleForm.mobile" tabindex="1" /> </el-form-item> <el-form-item prop ="password"> <el-input placeholder="请输入密码" name="password" type="password" tabindex="2" v-model="ruleForm.password" /> </el-form-item>
注意:el-form-item
中的prop
属性一定要与校验规则对象loginRules
中定义的属性保持一致,校验才起作用。
返回浏览器中进行测试。
修饰符
这里还有一个小问题就是,用户一般都是输入完密码以后,按下回车键,就可以发送请求,也就是相当于单击了登录
按钮。
要想实现这样的效果,需要用到修饰符
1 https://cn.vuejs.org/guide/essentials/event-handling.html#event-modifiers
1 <el-input placeholder="请输入密码" name="password" type="password" tabindex="2" v-model="ruleForm.password" @keyup.enter="submit"/>
在上面的代码中添加了@keyup.enter
修饰符
输入完密码,按下回车键,也要调用submit
方法。
返回到浏览器中进行测试,在浏览器的控制台中查看输入的结果。
这里有一个小的问题,当在密码框中输入完内容以后,按下回车键会调用submit
打印所输入的手机号码与密码。
但是,如果密码没有按照上面定义的验证规则进行输入,按下回车键以后,也会打印出来,这样就不行了,而这种情况应该
给出相应的错误提示。也就是说,在按下了登录按钮把用户在表单中所输入的内容发送到服务端之前,也需要进行校验
这里也是参考element-ui
文档中的校验方式
1 2 3 4 5 6 7 8 9 10 import { reactive,ref } from "vue" ; import { validMobile } from '@/utils/validate' export default { name : "Login" , setup ( ) { const ruleForm =reactive ({ mobile :"" , password :"" }); const ruleFormRef=ref ();
上面的代码中,我们创建了一个ruleFormRef
对象,创建该对象的目的就是与form
表单进行关联,通过该对象获取form
表单,完成校验。
所以这里需要将ruleFormRef
这个对象返回
1 2 3 4 5 6 return { ruleForm, submit, loginRules, ruleFormRef };
1 <el-form class="login-form" :model="ruleForm" :rules="loginRules" ref="ruleFormRef">
给el-form
这个表单组件,添加ref
属性与ruleFormRef
进行关联。
1 2 3 4 <el-input placeholder="请输入密码" name="password" type="password" tabindex="2" v-model="ruleForm.password" @keyup.enter="submit(ruleFormRef)"/> <el-button class="loginBtn" type="primary" style="width: 100%; margin-bottom: 30px" @click="submit(ruleFormRef)">登录</el-button>
不管是在按下回车键的时候,还是单击登录
按钮的时候,都把ruleFormRef
这个对象,传递到submit
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const submit = (loginForm ) => { loginForm.validate ((valid )=> { if (valid){ console .log (ruleForm.mobile .trim () + " " + ruleForm.password ); }else { ElMessageBox .alert ("密码没有通过验证!" ,"数据校验" ,{ confirmButtonText :"OK" }); } } ) };
在submit
这个方法中,接收传递过来的ruleFormRef
对象,也就是form
表单,然后进行校验。
启动项目进行测试。
3、服务端环境搭建 这里的服务端架构还是以前的架构设计。
这里首先修改一下数据库链接字符串appsettings.json
和MyDbContextDesign.cs
两个文件中的字符串都需要修改
第二点:创建一个WebApi
项目,并且添加如下的包
1 2 3 4 5 6 7 8 9 <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Autofac" Version="7.0.0" /> <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
第三点:把Attributes,AutofaceDI,Filters
三个文件夹都拷贝到WebApi
项目中,并且修改这些文件夹中所有类的命名空间。
第四点:在Program.cs
文件中完成相应的注册操作
4、服务端登录接口开发 创建一个login
控制器
并且在WebApi
项目中,安装Jwt
所需要的包
1 Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
这里为了简化项目的复杂度,我们不再创建DTO
数据传输对象,大家在做的时候,可以根据情况自行添加
针对登录,很难创建一个标注的Restful
的接口,但是我们前面也已经讲解过,不用为了Restful
而Restful
。
这里创建一个LoginController.cs
控制器,该控制器中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 using Cms.Entity;using Cms.IService;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Text;namespace Cms.WebApi.Controllers { [Route("api/[controller]" ) ] [ApiController ] public class LoginController : ControllerBase { private readonly IUserInfoService _userInfoService; private readonly IConfiguration configuration; public LoginController (IUserInfoService userInfoService, IConfiguration configuration ) { this ._userInfoService = userInfoService; this .configuration = configuration; } [HttpPost ] public async Task<IActionResult> login ([FromBody] UserInfo userInfo ) { if (string .IsNullOrWhiteSpace(userInfo.UserPhone) || string .IsNullOrWhiteSpace(userInfo.UserPassword)) { return BadRequest("手机号与密码不能为空!" ); } var loginUser = await _userInfoService.LoadEntities(u=>u.UserPhone==userInfo.UserPhone&&u.UserPassword==userInfo.UserPassword).FirstOrDefaultAsync(); if (loginUser == null ) { return BadRequest("手机号与密码错误" ); } var securityAlgorithm = SecurityAlgorithms.HmacSha256; var claims = new [] { new Claim(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub,loginUser.Id.ToString()) }; var secretByte = Encoding.UTF8.GetBytes(configuration["Authentication:SecretKey" ]!); var signingKey = new SymmetricSecurityKey(secretByte); var signingCredentials = new SigningCredentials(signingKey, securityAlgorithm); var token = new JwtSecurityToken( issuer: configuration["Authentication:Issuer" ], audience: configuration["Authentication:Audience" ], claims, notBefore: DateTime.Now, expires: DateTime.Now.AddDays(1 ), signingCredentials ); var tokenStr = new JwtSecurityTokenHandler().WriteToken(token); return Ok(tokenStr); } } }
在上面的代码中,我们构建数字签名的时候,使用了私钥,私钥必须保存到服务端,同时这里我们将其保存在了appsettings.json
文件中
1 2 3 4 5 6 7 8 "ConnectionStrings" : { "StrConn" : "server=localhost;database=HrDb;uid=sa;pwd=123456;TrustServerCertificate=true" }, "Authentication" : { "SecretKey" : "abc123456789@126.com" , "Issuer" : "xxx.com" , "Audience" : "xxx.com" }
启动授权
当用户访问某些资源的时候,需要进行验证。
这就是使用前面所生成的token
.
当然,这里需要注入jwt
的认证服务。
修改Program.cs
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { var secretByte = Encoding.UTF8.GetBytes(builder.Configuration["Authentication:SecretKey" ]!); options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true , ValidIssuer = builder.Configuration["Authentication:Issuer" ], ValidateAudience = true , ValidAudience = builder.Configuration["Authentication:Audience" ], ValidateLifetime = true , IssuerSigningKey = new SymmetricSecurityKey(secretByte) }; }); builder.Services.AddDbContext<MyDbContext>(opt => {
以上代码就是在注入DbContext
服务的上面,注入了JWT
的认证服务。
1 2 3 4 5 6 7 8 9 10 11 12 if (app.Environment.IsDevelopment()){ app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
同时,在Program.cs
文件的下面添加了app.UseAuthentication(),app.UseAuthorization();app.MapControllers();
下面,我们进行测试。
这里我们可以把登录成功后,返回前端的数据格式修改一下:
在WebApi
项目中创建models
文件夹,在该文件夹中创建ApiResult.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 namespace Cms.WebApi.models { public class ApiResult <T > { public bool Success { get ; set ; } public string ? Message { get ; set ; } public T? Data { get ; set ; } } }
下面修改一下LoginController
控制器中的login
方法中的代码,如下所示:
1 2 3 4 var tokenStr = new JwtSecurityTokenHandler().WriteToken(token); return Ok( new ApiResult<string > (){ Success=true , Message="登录成功" ,Data=tokenStr});
在上面的代码中,返回的就是ApiResult
定义的格式。
5、前端–封装单独的登录接口 在src/api/user.js
文件中,完善login
方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import request from "@/utils/request" ;export function login (data ) { return request ({ url :'/api/login' , method :'post' , data }) } export function getInfo ( ) {}export function logout ( ) {}
6、Vuex
中对token
进行管理 关于token
信息,我们需要存储到Vuex
中,因为需要在不同的组件中使用。将Token
信息存储到Vuex
中可以实现Token
信息的共享,更方便读取。
当然,存储到Vuex
中的Token
信息,在刷新浏览器后会丢失,所以需要持久化到本地。
在src/utils/
目录下面创建storage.js
文件,该文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const TOKEN_KEY = "hr-token" ;export const setTokenInfo =(tokenInfo )=>{ localStorage .setItem (TOKEN_KEY ,JSON .stringify (tokenInfo)); } export const getTokenInfo =( )=>{ return JSON .parse (localStorage .getItem (TOKEN_KEY )); } export const removeTokenInfo =( )=>{ localStorage .removeItem (TOKEN_KEY ); }
以上定义了相关方法,实现将token
信息存储到本地,以及从本地读取token
信息和删除token
信息。
在store
目录下面创建modules
目录,在该目录下面创建的文件都是Vuex
的模块。
在这里我们先创建user.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { getTokenInfo, removeTokenInfo, setTokenInfo } from "@/utils/storage" ;export default { namespaced : true , state ( ) { return { token : getTokenInfo (), }; }, mutations : { setToken (state, payload ) { state.token = payload; setTokenInfo (payload); }, removeToken (state ) { state.token = null ; removeTokenInfo (); }, }, };
这里我们需要将上面创建的user
模块,注册到vuex
容器中。
修改src/store/index.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 import { createStore } from "vuex" ;import user from "./modules/user" ;export default createStore ({ modules : { user, }, });
将创建的user
模块,添加到了modules
这个选项中,完成了模块的注册操作。
当然,在入口文件main,js
文件中,也已经将vuex
容器注册到了应用实例
中。
1 2 3 4 5 6 import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' createApp (App ).use (store).use (router).mount ('#app' )
1 2 3 4 5 6 7 8 9 10 localstorage和cookie都可以用来做本地存储,实现数据持久化。它们的区别还是很多的,主要体现在以下几个方面。 第一:能保存的数据大小不同。localstorage能保存的内容更多一些,查资料差不多是5M;cookie能保存的内容少一些,差不多4K 第二: 有效时间不同。cookie的有效期可以自行设置,而localstorage可以一直生效。 第三:在请求时,cookie会被携带,而localstorage不会。同源的cookie信息会自动作为请求头的一部分发给服务器,如果过多设置cookie,会额外增加通信负荷。而localstorage没有这个问题,它会一直存在于浏览器端。 在实际开发中,我会根据具体情况来选择使用它们。如果不需要与服务器通信并且可以长时间保存在客户端的信息就可以采用localstorage来保存,例如:网站中,提供的个人设置信息。
7、创建登录的Action
修改store/modules/user.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { getTokenInfo, removeTokenInfo, setTokenInfo } from "@/utils/storage" ;import { login } from "@/api/user" ;export default { namespaced : true , state ( ) { return { token : getTokenInfo (), }; }, mutations : { setToken (state, payload ) { state.token = payload; setTokenInfo (payload); }, removeToken (state ) { state.token = null ; removeTokenInfo (); }, }, actions : { async userLogin (context, payload ) { const result = await login (payload); if (result.data .success ) { context.commit ("setToken" , result.data .data ); } }, }, };
在上面的代码中,导入了api/user.js
文件中定义的login
方法。
然后创建actions
,在actions
中创建userLogin
方法,在调用该方法的时候,会传递手机号和密码。
在该方法中调用login
方法发送请求,实现登录。
登录成功以后,提交setToken
这个mutations
方法,这样就可以将服务端返回的token
数据存储到vuex
中,同时也实现了本地持久化操作。
除此之外,为了更好的让其他模块和组件更好的获取token数据,我们可以在**store/getters.js
**中将token值作为公共的访问属性放出.
在store
目录下面创建getters.js
文件,该文件中的代码如下所示:
1 2 3 4 5 const getters = { token : (state ) => state.user .token , }; export default getters;
在上面的代码中从user
模块中获取token
这个状态数据。
当然,也需要将上面定义的getters
注册到vuex
容器中。
所以还需要修改store/index.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 import { createStore } from "vuex" ;import user from "./modules/user" ;import getters from "./getters" ;export default createStore ({ modules : { user, }, getters, });
在上面的代码中,导入了getters
,同时在创建store
容器的时候,完成了对应的注册操作。
8、完善axios
配置 8.1 区分axios
在不同环境中的请求地址 在开发环境与生产环境中请求的服务端接口是不一样的。
怎样进行区分呢?
通过**.env.development
和 .env.production
**两个文件进行区分。
.env.development
是在开发环境中执行的文件,而.env.production
是在生产环境中执行的文件。
所以:我们可以在**.env.development
和 .env.production
**定义变量,变量自动就为当前环境的值
下面修改根目录 下面的.env.development
文件中的配置:
1 2 3 4 port = 8888 # 开发环境的基础地址和代理对应 VUE_APP_BASE_API = 'http://localhost:5105/api'
但是我们的开发环境代理是**/api
**,所以可以采用以上的统一方式。
如果是生产环境,需要修改.env.production
文件,如下所示:
1 2 # 生产环境的基础地址和代理对应 VUE_APP_BASE_API = 'http://localhost:8105/api'
以上是生产环境下的接口地址。
下面修改utils/request.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 import axios from "axios" ;const service = axios.create ({ baseURL : process.env .VUE_APP_BASE_API , timeout : 5000 , }); service.interceptors .request .use (); service.interceptors .response .use (); export default service;
8.2 响应拦截器配置 修改utils/request.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import axios from "axios" ; const service = axios.create ({ baseURL : process.env .VUE_APP_BASE_API , timeout : 5000 , }); service.interceptors .request .use (); service.interceptors .response .use ( (response ) => { const { success, message, data } = response.data ; if (success) { return data; } else { ElMessage .error (message); return Promise .reject (new Error (message)); } }, (error ) => { ElMessage .error (error.message ); return Promise .reject (error); } ); export default service;
在上面的代码中,我们实现了响应拦截,这里use
方法需要两个参数,都是函数。
第一个表示处理成功的响应
第二个表示处理失败的响应。
如果处理失败了,首先通过ElMessage
弹出错误的消息,同时执行 return Promise.reject(error)
,表示返回执行的错误,让当前的执行链跳出成功,直接进入catch
.
例如:登录的时候,我们执行的是login().then().catch()
,如果成功了会进入到then
方法,但是如果服务端处理失败了,会执行catch
,
在第一个处理函数中,我们解构出了服务端返回内容,当前服务端接口,返回的内容包括了success,message,data
这里我们判断一下success
如果为true
,表示服务端返回了正确的响应数据,这里直接将data
返回。
否则表示业务执行错误,需要进行错误处理。
如果响应是成功的,这里我们直接将服务端返回的响应数据返回了,所以在store/modules/user.js
文件中的userLogin
这个actions
方法中,不需要添加判断,并且result
中存储的就是服务端返回的响应数据。所以修改成如下的形式。
1 2 3 4 5 6 actions : { async userLogin (context, payload ) { const result = await login (payload); context.commit ("setToken" , result); }, },
9、实现登录 下面修改views/login/index.vue
中的代码
我们知道当我们单击登录
按钮的时候,会调用submit
方法
在该方法中,我们需要派发action
,来发送请求。
修改submit
方法中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const submit = (loginForm ) => { loginForm.validate (async (valid)=>{ if (valid){ const loginForm={ userPhone :ruleForm.mobile , userPassword :ruleForm.password } await store.dispatch ("user/userLogin" , loginForm); router.push ("/" ); }else { ElMessageBox .alert ("密码没有通过验证!" ,"数据校验" ,{ confirmButtonText :"OK" }); } } )
通过通过校验了这里通过store.dispatch
派发的是user
模块中的userLogin
这个actions
方法,同时为该方法传递的是用户在表单中输入的数据.
成功以后进行跳转。
1 2 import { useStore } from "vuex" ; import { useRouter } from "vue-router" ;
导入useStore和useRouter
两个方法。
1 2 3 const formRef = ref (null ); const store = useStore (); const router = useRouter ();
调用useStore
方法创建store
实例,调用useRouter
创建router
实例。
返回到浏览器中进行测试。
先输入错误的密码进行测试
然后输入正确的手机号和密码进行测试。
当登录成功以后,可以查看本地存储的token
信息。
注意:在进行测试的时候,一定要重新启动项目,因为前面修改了配置。
但是这里出错了,出错的原因是出现了跨域的问题。
10、跨域 1 2 3 跨源(跨域) HTTP 请求的一个例子:运行在 https://domain-a.com 的 JavaScript 代码使用 XMLHttpRequest 来发起一个到 https://domain-b.com/data.json 的请求。 出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求
它是浏览器同源策略 造成的,是浏览器对JS
实施的安全限制。
浏览器安全性可防止网页向不处理网页的域发送请求。 此限制称为同域(同源)策略。 同域(同源)策略可防止恶意站点从另一站点读取敏感数据
*什么是同源策略?* (所谓同源是指:“域名”、“协议”、“端口”均为相同)
具体参考微软官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/security/cors?view=aspnetcore-7.0
常见的跨域场景
跨域资源共享CORS
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
基本流程:
1 2 3 4 5 6 7 8 9 对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。 GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0
Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
1 2 3 4 5 6 7 8 9 10 11 12 Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8 上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头 Access-Control-Allow-Origin :该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求 Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。 Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
参考:https://juejin.cn/post/6844903521163182088
这里服务端进行解决:(修改Program.cs
文件中的代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 builder.Services.AddSwaggerGen(); var MyAllowSpecificOrigins = "_myAllowSpecificOrigins" ;builder.Services.AddCors(options => { options.AddPolicy(name: MyAllowSpecificOrigins, policy => { policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); }); }); var app = builder.Build();app.UseCors(MyAllowSpecificOrigins);
测试完成,查看本地存储中的token
信息。
11、Loading
效果实现 1 2 3 4 5 6 7 8 <el-button class="loginBtn" type="primary" style="width: 100%; margin-bottom: 30px" @click="submit(formRef)" :loading="loading" >登录</el-button >
给el-button
组件添加loading
属性,关联的是loading
对象。
1 2 const router = useRouter (); const loading = ref (false );
创建loading
对象,默认值是false
1 2 3 4 5 6 7 return { ruleForm, submit, loginRules, ruleFormRef, loading };
并且将loading
返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const submit = (loginForm ) => { console .log ("loginForm = " ,loginForm); loginForm.validate (async (valid)=>{ if (valid){ loading.value = true ; const loginForm={ userPhone :ruleForm.mobile , userPassword :ruleForm.password } try { await store.dispatch ("user/userLogin" , loginForm); router.push ("/" ); }catch (error){ console .log (error) } finally { loading.value = false ; } }else { ElMessageBox .alert ("密码没有通过验证!" ,"数据校验" ,{ confirmButtonText :"OK" }); } } ) };
在submit
方法中,通过表单校验后将loading
设置为true
.
不管是登录成功还是失败都需要将loading
设置为false
,也就是关闭loading
的效果。
所以这里在finally
中将loading
设置为false
。
返回到浏览器中进行测试。(把浏览器的网络调慢,来查看loading
的效果)
在测试的时候,可以输入错误的手机号或者是密码,查看控制台中的输出,这时候服务端返回的状态码是400,所以前端项目也需要针对这种这种进行处理,这里我们没有处理,只是打印到了浏览器的控制台中,大家可以自行完善。
三、主页模块 1、主页的token拦截处理 我们已经完成了登录的过程,并且存储了token,但是此时主页并没有因为token的有无而被控制访问权限,
也就是说,如果用户没有登录,是不能直接访问主页的,但是现在却可以。
所以下面我们看一下,怎样对用户是否登录进行判断,从而决定是否允许访问主页。
通过上图,可以看到整个的校验流程
首先判断是否有token
,如果有token
说明用户已经登录,但是如果用户又访问了登录页面,这时候会直接跳转到主页,但是如果访问的不是登录页面,而是其他的页面,放行,运行访问。也就是说,用户如果登录了,在访问登录页面,是不会出现登录表单的,直接就进入主页了。
如果没有token
信息,说明用户没有登录,这时候,我们就需要看一下用户访问的地址是否是在白名单中,所谓的白名单指的就是不需要登录也能够访问的页面。例如404页面,登录页面,这些页面用户不用登录也可以访问。如果访问的地址是在白名单中,就允许访问。如果访问的地址不在白名单中,说明用户访问的地址只能是登录以后才能访问,而这时候用户又没有登录,所以只能跳转到登录页面,进行登录。
以上就是关于用户是否登录的判断流程。
当输入地址访问页面的时候,会执行路由守卫,所以在路由守卫中进行判断。
下面看一下具体代码的实现。
在项目的src
目录下面创建permission.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import router from "@/router" ;import store from "@/store" ;const whiteList =["/login" ,"/404" ]; router.beforeEach ((to,from ,next )=> { if (store.getters .token !=null && Object .keys (store.getters .token ).length ) { if (to.path === "/login" ) { next ("/" ); } else { next (); } } else { if (whiteList.indexOf (to.path ) > -1 ) { next (); } else { next ("/login" ); } } })
同时在main.js
文件中导入permission.js
,如下所示:
1 2 3 4 5 6 7 import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' import "./permission" createApp (App ).use (store).use (router).mount ('#app' )
返回到浏览器中进行测试,按照上图的流程进行测试。
下面添加进度条效果,在路由切换的时候,如果加载比较慢,可以出现进度条。
继续修改permission.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import router from "@/router" ;import store from "@/store" ;import NProgress from "nprogress" ; import "nprogress/nprogress.css" ; const whiteList =["/login" ,"/404" ]; router.beforeEach ((to,from ,next )=> { NProgress .start (); if (store.getters .token !=null && Object .keys (store.getters .token ).length ) { if (to.path === "/login" ) { next ("/" ); } else { next (); } } else { if (whiteList.indexOf (to.path ) > -1 ) { next (); } else { next ("/login" ); } } }); router.afterEach (function ( ) { NProgress .done (); });
在最开始导入进度条插件,同时导入进度条所需要的样式。
在前置守卫中开启进度条
在后置守卫中关闭进度条。
返回到浏览器中进行测试。
2、主页布局实现
在src
目录下面创建layout
目录,在该目录下面创建index.vue
组件,该组件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 <template> <el-container class="el-container"> <el-header> <div class="header-div"> <img src="@/assets/logo.png" alt="logo" /> <span><router-link to="/">后台管理系统</router-link></span> <el-button style="float: right" type="info">退出</el-button> </div> </el-header> <el-container> <el-aside :width="isCollapse ? '60px' : '200px'"> <div class="toggle-button" @click="openCollapse">|||</div> <el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo" default-active="2" text-color="#fff" @open="handleOpen" @close="handleClose" > <el-sub-menu index="1"> <template #title> <el-icon><location /></el-icon> <span>Navigator One</span> </template> <el-menu-item-group title="Group One"> <el-menu-item index="1-1">item one</el-menu-item> <el-menu-item index="1-2">item one</el-menu-item> </el-menu-item-group> <el-menu-item-group title="Group Two"> <el-menu-item index="1-3">item three</el-menu-item> </el-menu-item-group> <el-sub-menu index="1-4"> <template #title>item four</template> <el-menu-item index="1-4-1">item one</el-menu-item> </el-sub-menu> </el-sub-menu> <el-menu-item index="2"> <el-icon><icon-menu /></el-icon> <span>Navigator Two</span> </el-menu-item> <el-menu-item index="3" disabled> <el-icon><document /></el-icon> <span>Navigator Three</span> </el-menu-item> <el-menu-item index="4"> <el-icon><setting /></el-icon> <span>Navigator Four</span> </el-menu-item> </el-menu> </el-aside> <!-- 所单击菜单对应的组件在该位置进行展示 --> <el-main><router-view /></el-main> </el-container> </el-container> </template> <script> import { ref } from "vue"; export default { name: "Layout", setup() { const isCollapse = ref(false); function openCollapse() { isCollapse.value = !isCollapse.value; } return { isCollapse, openCollapse, }; }, }; </script> <style> .el-container { height: 1000px; } .el-aside { background-color: #333744; } .el-main { background-color: #eaedf1; } .el-header { background-color: #373d41; color: #fff; font-size: 20px; } a { text-decoration: none; color: #fff; } .header-div img { /* 让文字在图片中间对齐 */ vertical-align: middle; width: 60px; height: 60px; margin-right: 10px; } .toggle-button { background-color: #4a5064; font-size: 10px; line-height: 24px; color: #fff; text-align: center; letter-spacing: 0.2em; cursor: pointer; } </style>
在上面的代码中指定了头部区域,左侧的区域。以及中间区域
左侧区域展示菜单,所以使用到了menu
组件。
中间区域,会展示当单击了菜单后对应的组件。
同时通过isCollapse
控制了左侧区域的显示与隐藏。
路由规则配置:
修改router/index.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { createRouter, createWebHashHistory } from 'vue-router' import Layout from '@/layout' const routes = [ { path : '/' , redirect :'/dashboard' , component : Layout , children :[ { path :'dashboard' , name :'dashboard' , component :()=> import ("@/views/dashboard/index" ), meta :{title :"首页" ,icon :'dashboard' } } ] }, { path :'/login' , name :'login' , component :()=> import ("@/views/login/index.vue" ) },
在上面的代码中,导入了Layout
组件,同时配置了相应的路由规则。
这里配置了子路由。
在views
目录下面创建dashboard
目录,在该目录下面创建index.vue
组件,该组件中的初步代码如下所示:
1 <template>后台布局</template>
返回到浏览器中进行查看。
修改一下菜单(可以先将菜单修改成如下的形式)
3、获取用户资料信息-前端实现 在登录成功后,可以将用户的信息返回。
当然,这里我们也可以再查询一次。
在前端login/index.vue
组件的的submit
方法中,我们可以先将用户的手机号码存储到localStorage
中,然后根据手机号查询用户的信息。
1 2 3 4 5 6 7 8 9 try { await store.dispatch ("user/userLogin" , loginForm); localStorage .setItem ("userPhone" ,loginForm.userPhone ); router.push ("/" ); }catch (error){ console .log (error) } finally { loading.value = false ; }
下面要做的就是定义前端获取用户信息资料的api
接口。
修改api/user.js
文件中的代码,如下所示:
1 2 3 4 export function getUserInfo (mobile ){ return request ("/users?userPhone=" +mobile); }
当调用上面的方法发送请求的时候,请求中需要带上token
数据。而我们知道后期会有很多的请求接口
都需要带上token
信息,所以可以在请求拦截器中添加。修改utils/request.js
文件中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import store from '@/store' ; import axios from 'axios' const service = axios.create ({ baseURL :process.env .VUE_APP_BASE_API , timeout :5000 }); service.interceptors .request .use ( (config )=> { if (store.getters .token ){ config.headers ["Authorization" ]=`Bearer ${store.getters.token} ` ; } return config; },(error )=> { return Promise .reject (error); } );
问题:在什么地方调用api/user.js
文件中的getUserInfo
方法发送请求呢?
我们知道,用户信息资料,会在很多组件中使用,所以是共享的数据。那么就应该存储到vuex
中,所以需要在actions
中发送请求。
下面修改store/modules/user.js
文件中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 actions : { async userLogin (context, payload ) { const result = await login (payload); context.commit ("setToken" , result); }, async getUserInfo (context ){ const userPhone =localStorage .getItem ("userPhone" ); const result = await getUserInfo (userPhone); context.commit ("setUserInfo" ,result); return result; } },
在actions
中也定义了一个getUserInfo
方法(从localStorage
中获取用户的手机号,传递给该方法),然后调用api/user.js
文件中的getUserInfo
方法,发送请求获取数据,将获取到的数据提交到mutations
中的setUserInfo
这个方法中,完成状态的更新。
1 import { login, getUserInfo } from "@/api/user" ;
这里需要从api/user.js
文件中导入getUserInfo
方法。
下面要做的就是在state
中定义状态,存储从服务端返回的用户信息。
1 2 3 4 5 6 7 namespaced : true , state ( ) { return { token : getTokenInfo (), userInfo :{} }; },
在上面的代码中,定义了userInfo
这个状态属性,默认值是一个空对象。为什么是空对象,而不是null
呢?因为后面我们需要从该状态中获取具体的数据,例如用户名,如果userInfo
没有获取到服务端的数据,并且默认值是null
,哪么就会变成null.name
,这样就出错了。
下面实现对应的mutations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mutations : { setToken (state, payload ) { state.token = payload; setTokenInfo (payload); }, removeToken (state ) { state.token = null ; removeTokenInfo (); }, setUserInfo (state,payload ){ state.userInfo = payload; }, removeUserInfo (state ){ state.userInfo ={}; }
在上面的mutations
中定义了setUserInfo
方法,将用户信息资料存储到了userInfo
这个状态属性中。
同时,添加了一个removeUserInfo
的方法,删除用户信息,这个会在后面退出的时候使用到。
下面在getters
中创建关于用户信息的快捷访问。
这里我们先创建对用户名的快捷访问。
修改store/getters.js
文件中的代码,如下所示:
1 2 3 4 5 const getters = { token : (state ) => state.user .token , name :(state )=> state.user .userInfo .userName }; export default getters;
问题:在actions
中的getUserInfo
方法什么时候调用呢?
在下一小节中将实现对该方法的调用。
4、获取用户资料信息2-前端实现 在这一小节中,我们需要实现的就是对store/modules/user.js
文件中定义的getUserInfo
这个actions
方法进行调用。
我们知道,调用该方法,会发送请求获取用户信息,而且请求头中必须有token
信息才可以。
所以在调用getUserInfo
这个actions
方法的时候,必须确保已经获取到了token
信息。
下面修改src/permission.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 router.beforeEach (async (to,from ,)=>{ NProgress .start (); if (store.getters .token !=null && Object .keys (store.getters .token ).length ) { if (to.path === "/login" ) { next ("/" ); } else { if (!store.state .user .userInfo .Id ){ await store.dispatch ("user/getUserInfo" ); } next (); } } else {
如果有token
信息了,才会去获取用户的信息资料
同时是在用户访问其它页面的时候判断一下是否能够获取到用户的编号(Id
是服务端返回的属性),如果能够获取到说明vuex
中已经存储了用户的信息资料,这里就没有必要发送请求了。
否则调用user
模块中的getUserInfo
这个actions
方法进行请求的发送,获取用户的信息资料。
同时还需要注意一个问题,这里我们添加了await
,含义就是一定是等待获取到了用户信息资料以后,才会执行next()
方法,访问其它页面。
原因是,在其它页面中有可能会使用用户信息资料。如果这里不添加await
,就会导致请求还没有结束,就执行了next
方法,请求是异步的。
这里还有一个问题,就是用户的资料信息为什么没有存储到本地的localStorage
中呢?
因为,我们是在beforeEach
方法中发送请求,不管是刷新浏览器还是切换路由,都会重新发送请求来获取用户的资料信息,所以不需要存储到本地的localStoreage
中。
下面,在头部展示用户名
1 2 3 4 <span><router-link to="/">HR后台管理系统</router-link></span> <span style="font-size: 16px; float: right">{{ $store.getters.name }}</span>
返回到浏览器中进行测试。
这里测试的时候,有可能token
已经失效了,所以将本地存储的token
信息删除,然后重新登录,监视请求。
5、获取用户资料信息3–服务端接口实现 下面我们需要实现的就是服务端获取用户资料信息的接口。
创建UsersController.cs
控制器,该控制器中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [Route("api/[controller]" ) ] [ApiController ] public class UsersController : ControllerBase { private IUserInfoService _userInfoService; public UsersController (IUserInfoService userInfoService ) { this ._userInfoService = userInfoService; } [HttpGet ] [Authorize ] public async Task<IActionResult> GetUserInfo ([FromQuery]string userPhone ) { if (string .IsNullOrWhiteSpace(userPhone)) { return BadRequest("手机号不能为空" ); } var user = await _userInfoService.LoadEntities(u => u.UserPhone == userPhone).FirstOrDefaultAsync(); if (user == null ) { return BadRequest("没有查找到对应用户" ); } return Ok(new ApiResult<UserInfo>() { Success = true , Message = "获取用户成功" , Data = user }); } }
接收前端传递过来的手机号码
然后根据手机号码查询用户的信息。
返回到浏览器中进行测试。
这里测试的时候,有可能token
已经失效了,所以将本地存储的token
信息删除,然后重新登录,监视请求。
当登录成功以后,可以看到顶部展示了用户名
下面将头像也展示出来。
6、展示用户头像 这里我们已经将登录的用户名展示出来了,下面展示用户的头像
第一:服务端设置
在WebApi
项目中创建wwwroot
目录,在该目录下面创建images
文件夹,上传成功的图片都是存储在服务端的,也就是当前的images
文件夹中。
1 2 3 4 5 app.UseCors(MyAllowSpecificOrigins); app.UseStaticFiles(); app.UseAuthentication();
同时在Program.cs
文件中启用处理静态资源文件的中间件。
一定要将服务端项目重新启动。
第二:前端设置
我们知道,现在在前端已经获取到了登录成功的用户资料信息。
在用户资料信息中有一个属性是photoUrl
,存储的是用户的头像地址。
所以这里,我们先修改store/getter.js
文件中的代码,如下所示:
1 2 3 4 5 6 const getters = { token : (state ) => state.user .token , name :(state )=> state.user .userInfo .userName , photoUrl :(state )=> state.user .userInfo .photoUrl }; export default getters;
这里了能够更好的获取用户的头像,可以把获取用户头像 的代码写到getters
中,也就是建立getters
的快捷访问的方式。
这里从user
模块中找到userInfo
对象,获取photoUrl
属性的值,也就是头像的路径地址。
返回到layout/index.vue
组件中,在头部区域展示登录成功的用户头像。
但是,问题是,我们这里获取到的photoUrl
属性的值是数据库中存储的头像路径的地址,是相对路径,例如:/images/f.jpg
而具体的图片文件是存储在服务端的,所以这里我们需要添加上服务端的地址,才能获取到真正的图片文件。
.env.development
文件,如下所示:
1 2 3 4 5 port = 8888 # 开发环境的基础地址和代理对应 VUE_APP_BASE_API = 'http://localhost:5105/api' VUE_APP_BASE_URL='http://localhost:5105' # --------------这里通过该值获取服务端的地址,
注意:一定要以VUE_APP_
开头。
同时,.env.production
文件中也添加该项
1 2 3 # 生产环境的基础地址和代理对应 VUE_APP_BASE_API = '/api' VUE_APP_BASE_URL='http://localhost:5105'
下面返回到layout/index.vue
中进行代码的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { ref } from "vue" ; import { useStore } from "vuex" ; export default { name : "Layout" , setup ( ) { const isCollapse = ref (false ); const store = useStore (); const baseUrl = process.env .VUE_APP_BASE_URL ; console .log ("baseUrl=" ,baseUrl); const imgUrl = baseUrl+store.getters .photoUrl ; function openCollapse ( ) { isCollapse.value = !isCollapse.value ; } return { isCollapse, openCollapse, imgUrl };
下面看一下模版代码的修改
1 2 3 4 5 <span style="font-size: 16px;color:darkred; float: right">{{ $store.getters.name }}</span> <img :src="imgUrl" style="float: right" /> <!----添加img标签,其src属性动态绑定了imgUrl属性的值---> <el-button style="float: right" type="info">退出</el-button>
注意:由于前端项目也修改了配置文件,也需要重新启动前端项目
返回浏览器中查看效果。
7、用户退出登录 在用户单击了退出
按钮后,进行退出。
退出的时候需要删除用户的token
以及vuex
中存储的当前登录用户的信息。
修改src/store/modules/user.js
文件中的代码
1 2 3 4 5 6 7 8 9 logout (context ){ context.commit ("removeToken" ); context.commit ("removeUserInfo" ); localStorage .removeItem ("userPhone" ); }
在actions
中定义了logout
方法。
在该方法中提交了removeToken
这个mutations
方法,删除了token
信息,这里不仅删除了vuex
中存储的token
信息,同时也删除了本地存储的token
信息,以及本地存储的手机号码信息
关于removeToken
这个mutations
方法,在前面我们已经定义好了。
下面紧跟着提交了removeUserInfo
这个mutations
方法,删除了用户的资料信息,该方法在前面也已经定义了。
问题是logout
这个actions
方法在哪调用。
下面修改layout/index.vue
组件中的代码
1 2 <img :src="imgUrl" style="float: right" /> <el-button style="float: right" type="info" @click="logout">退出</el-button>
当单击了退出按钮的时候,调用上面所定义的logout
这个actions
方法。
所以这里我们先给退出
按钮注册了单击事件,事件触发调用logout
方法,下面在setup
入口函数中实现该方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import { ref } from "vue" ;import { useStore } from "vuex" ;import { useRouter } from "vue-router" ; export default { name : "Layout" , setup ( ) { const isCollapse = ref (false ); const store = useStore (); const router = useRouter (); const baseUrl = process.env .VUE_APP_BASE_URL ; console .log ("baseUrl=" ,baseUrl); const imgUrl = baseUrl+store.getters .photoUrl ; function openCollapse ( ) { isCollapse.value = !isCollapse.value ; } const logout =( )=>{ ElMessageBox .confirm ("确定要退出系统吗?" ,"退出系统" ,{ confirmButtonText :"确定" , cancelButtonText :"取消" , type :"questions" }).then (()=> { store.dispatch ("user/logout" ); router.push ("/login" ); ElMessage ({type :'success' ,message :'退出成功' }); }) .catch (()=> { ElMessage ({ type :'info' , message :'取消退出' }) }) } return { isCollapse, openCollapse, imgUrl, logout }; },
进行测试。
8、Token
失效的处理 服务端处理
问题:我们知道在登录的时候,我们创建的token
数据,并且设置了过期时间,问题是,怎样判断token
过期了呢?
这里我们需要注册事件,修改Program.cs
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { var secretByte = Encoding.UTF8.GetBytes(builder.Configuration["Authentication:SecretKey" ]!); options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true , ValidIssuer = builder.Configuration["Authentication:Issuer" ], ValidateAudience = true , ValidAudience = builder.Configuration["Authentication:Audience" ], ValidateLifetime = true , ClockSkew = TimeSpan.Zero, IssuerSigningKey = new SymmetricSecurityKey(secretByte) }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { if (context.Exception.GetType() == typeof (SecurityTokenExpiredException)) { context.Response.Headers.Add("Access-Control-Expose-Headers" , "act" ); context.HttpContext.Response.Headers.Add("act" , "expired" ); context.Response.Headers.AccessControlAllowOrigin = "*" ; } return Task.CompletedTask; } }; });
在上面的代码中,当授权失败以后,判断错误的类型是否是token
过期,如果是token
过期,这里会向前端返回act
这个响应头,该响应头的值是expired
,当然,该响应头大家可以随意命名。目的是,将自定义act
这个响应头返回到前端以后,前端获取到该响应头并判断其值是expired
,表示的是token
过期了,这样前端路由就可以跳转到登录页面。
但是这里有一个问题:要想前端的axios
获取到自定义的响应头act
,需要添加Access-Control-Expose-Headers
,它的值是act
这个自定义的响应头,这样前端的axios
就可以过去该自定义的响应头了。
为了进行测试,我们修改一下LoginController
这个控制器中创建的token
的过期时间,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var token = new JwtSecurityToken( issuer: configuration["Authentication:Issuer" ], audience: configuration["Authentication:Audience" ], claims, notBefore: DateTime.Now, expires: DateTime.Now.AddSeconds(30 ), signingCredentials );
这里关于过期时间在强调一下,如果不设置token
的过期时间,默认过期时间是5分钟,当然,即使过期的时间到了,也不会马上失效,而是再过5分钟才会失效。
但是,这里我们为了方便测试,在Program.cs
中修改了配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { var secretByte = Encoding.UTF8.GetBytes(builder.Configuration["Authentication:SecretKey" ]!); options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true , ValidIssuer = builder.Configuration["Authentication:Issuer" ], ValidateAudience = true , ValidAudience = builder.Configuration["Authentication:Audience" ], ValidateLifetime = true , ClockSkew = TimeSpan.Zero, IssuerSigningKey = new SymmetricSecurityKey(secretByte) };
在上面的jwt
的认证配置中,我们添加了ClockSkew
属性,该属性的值设置为了TimeSpan.Zero
表示的含义就是token
到期后就失效,没有缓冲期。
以上是服务端的配置。
下面看一下前端代码的修改
前端处理
修改utils/request.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 service.interceptors .response .use ( (response ) => { const { success, message, data } = response.data ; if (success) { return data; } else { ElMessage .error (message); return Promise .reject (new Error (message)); } }, (error ) => { if (error.response && error.response .headers ["act" ]&&error.response .headers ["act" ]==='expired' ){ store.dispatch ("user/logout" ); router.push ("/login" ); }else { ElMessage .error (error.message ); } return Promise .reject (error); });
在上面的代码中,我们修改了响应拦截器中的代码
如果服务端返回的内容有错误,会执行响应拦截器中错误的处理程序,在这里判断error.response
对象中是否有act
这个响应头,并且该响应头的值是否是expired
,如果条件成立,则表示服务端的token
信息过期了,这里我们需要退出登录,并且重新跳转到登录页面。
如果以上条件不满足,这里我们只是通过ElMessage
这个组件展示错误信息就可以了。
启动项目进行测试,这里可以先将以前登录所保存的localStorage
中信息删除,重新进行登录后,等待30秒钟后重新刷新浏览器进行测试。
四、路由与菜单模块 1、路由说明 简单项目中的路由设置
当前项目结构
为什么要拆成若干个路由模块呢?
因为复杂项目的页面众多,不可能把所有的业务都集中在一个文件上进行管理和维护,并且还有最重要的,前端的页面中主要分为两部分,一部分是所有人都可以访问的, 一部分是只有有权限的人才可以访问的,拆分多个模块便于更好的控制
这里我们将所有用户都可以访问的路由称作静态路由
而具有了一定的权限才能访问的路由就是动态路由
**注意
**这里的动态路由并不是 路由传参 的动态路由
最后,在路由中把404
规则添加上。
修改router/index.js
文件中的路由规则,如下 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 { path : "/login" , name : "Login" , component : () => import ("@/views/login/index.vue" ), }, { path : "/404" , component : () => import ("../views/404.vue" ), }, { path : "/:pathMatch(.*)*" , redirect : "/404" , },
在上面的代码中,需要指定404
路由规则。
在views
目录下面创建404.vue
返回到浏览器中进行测试。
2、前端项目模块说明 下面,我们将本项目中所要做的模块快速搭建相应的页面和路由
1 2 3 4 5 6 7 8 9 10 11 ├── dashboard ├── login ├── 404 ├── departments ├── employees ├── setting ├── salarys ├── social ├── attendances ├── approvals ├── permission
根据上图中的结构,在views目录下,建立对应的目录,给每个模块新建一个**index.vue
**,作为每个模块的主页
快速新建文件夹
1 mkdir departments employees setting salarys social attendances approvals permission
注意:找到项目中的views
文件夹,按住ctrl
键右击,在弹出的快捷菜单中选择Git Bash Here
,执行以上创建文件夹的命令。(在win11
中以上命令有问题)
每个模块的内容,可以先按照标准的模板建立,如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <div class="dashboard-container"> <div class="app-container"> <h2> 员工 </h2> </div> </div> </template> <script> export default { } </script> <style> </style>
根据以上的标准建立好对应页面之后,接下来建立每个模块的路由规则
路由模块目录结构
1 2 3 4 5 6 7 8 9 10 11 ├── router ├── index.js ├── modules ├── departments.js ├── employees.js ├── setting.js ├── salarys.js ├── social.js ├── attendances.js ├── approvals.js ├── permission.js
3、设置每个模块的路由规则 每个模块导出的内容表示该模块下的路由规则
如果员工的路由规则:router/modules/employees.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import Layout from "@/layout" ;export default { path : "/employees" , name : "employees" , component : Layout , children : [ { path : "" , component : () => import ("@/views/employees" ), meta : { title : "员工管理" , }, }, ], };
上述代码中,我们用到了meta属性,该属性为一个对象,里面可放置自定义属性,主要用于读取一些配置和参数,并且值得**注意
的是:我们的meta写在了二级默认路由上面,而不是一级路由,因为当存在二级路由的时候,访问当前路由信息访问的就是 二级默认路由
**
4、路由合并 将静态路由和动态路由的路由表进行临时合并
什么叫临时合并?
在第一个小节中,我们讲过了,动态路由是需要权限进行访问的,但是权限的动态路由访问是很复杂的,我们稍后在进行讲解,所以为了更好地看到效果,我们可以先将 静态路由和动态路由进行合并
修改src/router/index.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import { createRouter, createWebHashHistory } from 'vue-router' import Layout from '@/layout' import employeesRouter from './modules/employees' const routes = [ { path : '/' , redirect :'/dashboard' , component : Layout , children :[ { path :'dashboard' , name :'dashboard' , component :()=> import ("@/views/dashboard/index" ), meta :{title :"首页" ,icon :'dashboard' } } ] }, { path :'/login' , name :'login' , component :()=> import ("@/views/login/index.vue" ) }, { path : "/404" , component : () => import ("../views/404.vue" ), }, { path : "/:pathMatch(.*)*" , redirect : "/404" , }, { path : '/about' , name : 'about' , component : () => import ( '../views/AboutView.vue' ) } ] export const asyncRoutes =[ employeesRouter ]; const router = createRouter ({ history : createWebHashHistory (), routes :[...routes,...asyncRoutes] }) export default router
首先导入模块中定义的动态路由规则
然后定义一个asyncRoutes
数组,数组中存储的就是定义的动态路由规则对象,
在createRouter
中将静态路由规则和动态路由规则进行合并。
可以在浏览器中输入:http://localhost:8888/#/employees
来进行访问。
5、菜单展示 在这一小节中,我们将所有路由规则中的meta
中的title
属性的值获取到,然后将其作为左侧的菜单项。
先修改router/index.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const routes = [ { path : '/' , redirect :'/dashboard' , component : Layout , children :[ { path :'dashboard' , name :'dashboard' , component :()=> import ("@/views/dashboard/index" ), meta :{title :"首页" ,icon :'dashboard' } } ] }, { path :'/login' , name :'login' , hidden : true , component :()=> import ("@/views/login/index.vue" ) }, { path : "/404" , hidden : true , component : () => import ("../views/404.vue" ), }, { path : "/:pathMatch(.*)*" , redirect : "/404" , }, { path : '/about' , name : 'about' , component : () => import ( '../views/AboutView.vue' ) } ]
在上面定义的静态路由
规则中,每个路由规则都添加了hiddren
属性,并且该属性的取值为true
.
设置为true
的目的表示该项不会在左侧进行展示,也就是不是菜单项。
下面修改src/layout/index.vue
组件中的代码,如下所示:
1 2 3 4 5 setup ( ) { const isCollapse = ref (false ); const store = useStore (); const router = useRouter (); let routes = ref ([]);
在setup
入口函数中,定义routes
数组,该数组中会存储获取到的路由规则信息
1 2 3 4 5 6 7 8 9 10 11 12 import { ref,computed } from "vue" ; routes = computed (()=> { return router.options .routes ; }) return { isCollapse, openCollapse, imgUrl, logout, routes };
这里添加了计算属性,在计算属性中通过router
对象中的options
中的routes
属性获取对应的路由规则信息,然后将这些信息存储到了rotues
这个数组中,并且返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo" default-active="2" text-color="#fff" @open="handleOpen" @close="handleClose" > <template v-for="route in routes" > <el-menu-item v-if="!route.hidden && route.children" :index="route.path" > <el-icon><icon-menu /></el-icon> <span> <router-link :to="route.path"> {{ route.children[0].meta.title }} </router-link> </span> </el-menu-item> </template> </el-menu>
这里在el-menu-item
上面添加了template
标签,对routes
进行遍历,然后在el-menu-item
组件上添加判断,判断hidden
是否为false
,并且是否有children
,该条件满足,展示router-link
组件,并且该组件中展示的就是路由规则
中的title
属性的值
返回到浏览器中进行查看。
6、菜单图标处理 第一:在src
目录下面创建icons
目录,在该目录下面创建svg
目录,将图标拷贝到该目录下面。
第二:修改src/router/index.js
文件中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 { path : '/' , redirect :'/dashboard' , component : Layout , children :[ { path :'dashboard' , name :'dashboard' , component :()=> import ("@/views/dashboard/index" ), meta :{title :"首页" ,icon :'dashboard' } } ] },
这里给首页的meta
中添加了icon
属性,该属性的取值就是图标的名称
。
下面需要给router/modules
目录下面的所有的模块中的meta
都添加icon
属性,指定图标的名称。
对应的关系如下所示(模块对应的icon
图标名称):
1 2 3 4 5 6 7 8 9 ├── dashboard ├── departments ├── employees ├── setting ├── salarys ├── social ├── attendances ├── approvals ├── permission
第三:在src/icons/
目录下面创建index.js
文件
该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const req = require .context ("./svg" , false , /\.svg$/ );const requireAll = (requireContext ) => { const result = requireContext.keys ().map (requireContext); return result; }; requireAll(req);
什么时候执行上面的代码,把svg
目录下的图标文件打包到项目中呢?
在main.js
这个入口文件中。
修改main.js
文件中的代码如下所示:
1 2 3 import store from './store' import "./permission" import "@/icons" ;
导入ioncs
,这时候会执行该目录下的index.js
文件
第四:创建图标组件
在项目中需要多次使用图标,可以将其单独的封装成一个公共的组件。
所以在src/components
目录下面创建SvgIcon
目录,在该目录下面创建index.vue
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <template> <svg :class="className" aria-hidden="true"> <use :xlink:href="'#icon-' + `${iconClass}`" /> </svg> </template> <script> // import { computed } from "vue"; export default { name: "SvgIcon", props: { iconClass: { type: String, required: true, }, className: { type: String, default: "svg-icon", }, }, }; </script> <style scoped> .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } .svg-external-icon { background-color: currentColor; mask-size: cover !important; display: inline-block; } </style>
当使用上面的图标组件的时候可以传递className
,如果不传递默认是svg-icon.
该组件中的重点就是使用了svg
标签,然后通过内部的标签use
标签的指定要插入的图标。这里通过iconClass
接收传递过来的图标名称。
问题:怎样使用该图标组件?
在src/layout/index.vue
文件中使用该图标组件。原因是在layout
中使用了菜单组件。
思考:由于公共组件使用比较频繁,如果每次使用都导入比较麻烦,所以可以完成统一的注册。
在src/components
目录下面创建index.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 import SvgIcon from "@/components/SvgIcon" ;export default { install (app ) { app.component (SvgIcon .name , SvgIcon ); }, };
这里导入了图标组件,并且导入一个对象,该对象中有一个install
方法(注意必须是install
方法),这里相当于做了一个插件,插件可以扩展vue
的功能。而插件中必须有install
方法,参数是应用实例。这里调用了应用实例中的component
方法,进行组件的注册。第一个参数表示要注册的组件的名称,这里获取了其name
属性,所以要求定义公共组件的时候,一定要有name
名称。第二个参数就是要注册的组件。
问题:以上install
方法什么时候被调用呢?
在main.js
文件中进行代码的修改
1 2 3 4 import "@/icons" ; import UI from "@/components/index" ;createApp (App ).use (store).use (router).use (UI ).mount ('#app' )
这里导入了components/index
,然后use
到了app
这个应用实例上。
这样到项目启动后,公共组件都挂载到了app
这个应用实例上了,也就是完成了组件的注册。
下面使用该图标组件。
在src/layout/index.vue
文件中使用以上图标组件。
1 2 3 4 5 6 7 8 9 10 11 12 <template v-for="route in routes" > <el-menu-item v-if="!route.hidden && route.children" :index="route.path" > <el-icon><SvgIcon :iconClass="route.children[0].meta.icon" <!--使用SvgIcon组件---> /></el-icon> <span> <router-link :to="route.path"> {{ route.children[0].meta.title }} </router-link> </span> </el-menu-item> </template>
在layout/index.vue
中不需要导入图标组件,直接使用就可以了。
这里指定了inconClass
,属性。其值就是所获取到的在路由规则中定义的meta
对象中的icon
属性。
第五:配置loader
这里需要安装第三方包,解析svg
文件
1 npm install svg-sprite-loader
修改vue.config.js
文件中的配置:
代码如下所示:
1 2 3 4 5 6 const { defineConfig } = require ('@vue/cli-service' )const path =require ("path" );function resolve (dir ){ return path.join (__dirname,dir) }
导入path
,定义resolve
方法,拼接目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 module .exports = defineConfig ({ transpileDependencies : true , lintOnSave : false , chainWebpack : (config ) => { config.plugin ("html" ).tap ((args ) => { args[0 ].title = "人力资源管理平台" ; return args; }); config.module .rule ("svg" ).exclude .add (resolve ("src/icons" )).end (); config.module .rule ("icons" ) .test (/\.svg$/ ) .include .add (resolve ("src/icons" )) .end () .use ("svg-sprite-loader" ) .loader ("svg-sprite-loader" ) .options ({ symbolId : "icon-[name]" , }) .end (); },
在chainWebpack
中完成对svg
的配置。
由于修改了配置文件需要重新启动项目
返回到浏览器中进行测试。
五、组织架构模块 1、组织架构简介
2、组织架构头部布局 通过上图可以看到,头部分为两部分,左侧是公司的名称,右侧是负责人
这里我们可以使用element-plus
中的Layout
布局组件进行布局。
修改src/views/departments/index.vue
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div class="dashboard-container"> <div class="app-container"> <!-- 组织架构--头部布局 --> <el-card> <el-row> <el-col :span="12"> <!--span表示宽度--> <span>xx教育集团</span> </el-col> <el-col :span="12">123</el-col> </el-row> </el-card> </div> </div> </template>
el-card
就是一个div
el-row:表示行
el-col
表示列
上面一行中分为了两部分
但是以上123
的内容没有在右侧。
下面,我们查看一下该页面的效果。
需要配置路由, 按照前面的方式进行配置
在src/router/modules
目录下面创建departments.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import Layout from "@/layout" ;export default { path : "/departments" , name : "departments" , component : Layout , children : [ { path : "" , name :'departmentsChild' , component : () => import ("@/views/departments" ), meta : { title : "部门管理" , icon :"tree" }, }, ], };
下面在src/router
中index.js
文件中添加如下代码
1 2 3 import Layout from '@/layout' import employeesRouter from './modules/employees' import departmentsRouter from './modules/departments'
1 2 3 4 5 export const asyncRoutes =[ employeesRouter, departmentsRouter ];
返回到浏览器中查看效果。
下面需要指定对齐方式
这里给第二列又添加了el-row
指定了end
对齐方式。在右侧展示内容。justify
属性只能用在el-row
组件上。
右侧内容又分为两列。展示负责人
以及下拉框。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <!-- 组织架构--头部布局 --> <el-card> <el-row> <el-col :span="20"> <!--这里调整了宽度--> <span>xx教育集团</span> </el-col> <el-col :span="4"><!--这里调整了宽度--> <el-row justify ="end"> <el-col :span="12">负责人</el-col> <el-col :span="12"> <el-dropdown> <span class="el-dropdown-link"> 操作 <el-icon class="el-icon--right"> <ArrowDown /> </el-icon> </span> <!-- 下拉菜单 --> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>添加子部门</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </el-col> </el-row> </el-col> </el-row> </el-card>
这里在操作
旁边使用了ArrowDown
图标。
这里需要安装图标组件
1 npm install @element-plus/icons-vue
然后在main.js
文件中进行图标的注册
。(参考文档:’https://element-plus.gitee.io/zh-CN/component/icon.html ‘)
1 2 3 4 5 6 7 8 9 import UI from "@/components/index" ;import * as ElementPlusIconsVue from '@element-plus/icons-vue' ; const app =createApp (App );for ( const [key,component] of Object .entries (ElementPlusIconsVue ) ){ app.component (key,component); } app.use (store).use (router).use (UI ).mount ('#app' )
在上面的代码中使用到了dropdown
组件,官方地址如下:
1 https://element-plus.gitee.io/zh-CN/component/dropdown.html#%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95
3、树形组件基本使用 通过前面的介绍,我们知道在展示组织结构信息时需要用到树形组件,这里我们先来看一下树形组件的基本使用
1 https://element-plus.gitee.io/zh-CN/component/tree.html
修改views/departments/index.vue
文件中的代码
1 2 3 4 </el-row> <!-- 添加树形组件 --> <el-tree :data="data" :props="defaultProps" /> </el-card>
在原有的el-row
组件下面添加了el-tree
这个组件
data
属性表示该树形组件所展示的数据。
数据的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script> import { reactive } from "vue" ;export default { setup ( ) { const data = reactive ([ { label : "hello" , children : [ { label : "hello1" , }, ], }, ]); return { data, }; }, }; </script>
这里定义了响应式对象data
.
该对象的默认值是一个数组,数组中存储了一个对象,包含了label
(表示树形结构中展示的文本节点),以及children
属性(该属性也是一个数组,表示子节点)。
返回到浏览器中查看效果。
但是如果将上面的数据修改成如下的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 const data = reactive ([ { title : "hello" , child : [ { title : "hello1" , }, ], }, ]); return { data, };
这里的将原来的label
修改成了title
,将原来的children
修改成了child
,返回到浏览器中,发现不展示对应的树形结构了。
如果想展示需要定义props
属性的值defaultProps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 setup ( ) { const data = reactive ([ { title : "hello" , child : [ { title : "hello1" , }, ], }, ]); const defaultProps = { children : "child" , label : "title" , }; return { data, defaultProps, }; },
上面定义了defaultProps
对象,该对象中添加了label
属性和children
属性。
label
属性的取值为title
,表示展示的文本节点来自title
children
属性的取值为child
,表示子节点来自child
。
返回到浏览器中进行测试,发现树形结构可以展示。
注意:defaultProps
对象中的label
属性默认值是label
,children
属性默认值是children
.
所以我们也可以不在树形组件中添加props
属性并且指定其值为defaultProps
。
只需要在创建数据结构的时候,使用label
和children
就可以了。
4、树形组件构建组织架构 在上一小节中,我们了解了树形组件的基本使用。
但是结构不是我们想要的,这里会使用到树形组件中的自定义节点内容,地址如下:
1 https://element-plus.gitee.io/zh-CN/component/tree.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E8%8A%82%E7%82%B9%E5%86%85%E5%AE%B9
树形结构如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!-- 添加树形组件 --> <!--这里的data关联的数据是departs,同时添加default-expand-all属性,让树形默认全部展开--> <el-tree :data="departs" :props="defaultProps" default-expand-all> <template #default="{ node, data }"> <!--解构出传递给作用域插槽中的数据,data是每个节点的数据对象---> <el-row style="height: 40px; width: 100%" align="middle"><!--这里展示的结构与头部类似,所以将头部中使用的el-row直接拷贝到当前的树形组件中,然后进行调整。同时这里设置了el-row的高度与宽度 以及垂直居中---> <el-col :span="20"> <span>{{ data.name }}</span> </el-col> <el-col :span="4"> <el-row justify="end"> <el-col :span="12">{{ data.manager }}</el-col> <el-col :span="12"> <el-dropdown> <span class="el-dropdown-link"> 操作 <el-icon class="el-icon--right"> <ArrowDown /> </el-icon> </span> <!-- 下拉菜单 --> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>添加子部门</el-dropdown-item> <el-dropdown-item>编辑部门</el-dropdown-item> <el-dropdown-item>删除部门</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </el-col> </el-row> </el-col> </el-row> </template> </el-tree>
数据结构调整成如下的形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export default { setup ( ) { const departs = reactive ([ { name : "总裁办" , manager : "曹操" , children : [{ name : "董事会" , manager : "曹丕" }], }, { name : "行政部" , manager : "刘备" }, { name : "人事部" , manager : "孙权" }, ]); const defaultProps = { children : "children" , label : "name" , }; return { departs, defaultProps, }; }, };
返回到浏览器中进行测试
5、将树形结构单独封装成组件 通过上一小节,我们发现整个组织架构的顶部与具体内容展示的结构是一样的 ,所以这里可以单独的抽离出一个组件。
在src/views/departments
目录下面创建一个components
目录,在该目录下面创建tree-tools.vue
组件。
该组件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <template> <el-row style="height: 40px; width: 100%" align="middle"> <el-col :span="20"> <span>{{ treeNode.name }}</span> </el-col> <el-col :span="4"> <el-row justify="end"> <el-col :span="12">{{ treeNode.manager }}</el-col> <el-col :span="12"> <el-dropdown> <span class="el-dropdown-link"> 操作 <el-icon class="el-icon--right"> <ArrowDown /> </el-icon> </span> <!-- 下拉菜单 --> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>添加子部门</el-dropdown-item> <el-dropdown-item>编辑部门</el-dropdown-item> <el-dropdown-item>删除部门</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </el-col> </el-row> </el-col> </el-row> </template> <script> // 该组件需要对外开放属性 外部需要提供一个对象 对象里需要有name manager export default { name: "TreeTools", props: { // 定义一个props属性 treeNode: { type: Object, // 对象类型 required: true, // 要求对方使用您的组件的时候 必须传treeNode属性 如果不传 就会报错 }, }, }; </script>
这里我们将el-row
组件中的内容拷贝过来了。
同时定义了props
接收传递过来的数据。
下面看一下该组件的使用。
在**src/views/departments/index.vue
**进行代码的简化
1 2 3 4 5 6 import { reactive, ref } from "vue" ;import TreeTools from "./components/tree-tools.vue" ;export default { components : { TreeTools , },
导入TreeTools
组件,并且完成注册。
1 2 3 4 5 6 <!-- 添加树形组件 --> <el-tree :data="departs" :props="defaultProps" default-expand-all> <template #default="{ node, data }"> <TreeTools :treeNode="data"></TreeTools> </template> </el-tree>
在el-tree
中使用了TreeTools
组件,并且传递了具体的数据。
下面,我们来看一下组织架构
中顶部的改造,顶部也使用TreeTools
这个组件。
只不过顶部需要传递公司的信息。
在views/deparments/index.vue
中修改代码
1 2 3 4 5 setup ( ) { const company=ref ({ name :"xxx教育集团" , manager :"负责人" })
在setup
入口函数中定义了company
对象,该对象中存储了公司名称
1 2 3 4 5 return { departs, defaultProps, company };
将company
返回到模板中
1 2 3 4 5 <div class="app-container"> <!-- 组织架构--头部布局 --> <el-card> <TreeTools :treeNode="company" :isRoot="true" ></TreeTools> <!-- 添加树形组件 -->
这里将company
作为treeNode
的属性值,传递到TreeTools
这个组件中。
这里同时给isRoot
属性值为true
原因是:在上下两个位置都使用了下拉框组件,但是放置在最上层的组件是不需要显示 **删除部门
和 编辑部门
**的。所以下面在使用TreeTools
组件的时候没有传递isRoot
属性,没有传递,默认值是false
所以,增加一个新的属性 **isRoot(是否根节点)
**进行控制。
下面在TreeTools
组件中添加该属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export default { name : "TreeTools" , props : { treeNode : { type : Object , required : true , }, isRoot :{ type :Boolean , default :false } }, };
在上面的代码中,在props
中定义了isRoot
接收传递过来的值,然后在模板中进行判断,如下所示
1 2 3 4 5 6 7 8 <!-- 下拉菜单 --> <template #dropdown> <el-dropdown-menu> <el-dropdown-item>添加子部门</el-dropdown-item> <el-dropdown-item v-if="!isRoot">编辑部门</el-dropdown-item> <el-dropdown-item v-if="!isRoot">删除部门</el-dropdown-item> </el-dropdown-menu> </template>
返回到浏览器中进行测试。
通过封装,在src/views/departments/index.vue
文件中的代码看上去更加紧凑,简洁,这就是封装的魅力
6、部门模型设计 通过前面的学习,我们将组织架构的整体页面结构已经设计出来了,在这一小节中,我们需要在服务端设计部门的模型。
这里需要注意的一点就是先把数据库中T_UserInfos
这个表中的测试数据删除,否则无法建立数据表之间的关系。
具体的模型设计如下
在 Cms.Entity
项目中创建Department
类、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Department : BaseEntity <long >{ public string ? DepartmentName { get ; set ; } public string ? DepartmentCode { get ; set ; } public string ? DepartmentDescription { get ; set ; } public long ParentId { get ; set ; } public string ? City { get ; set ; } public string ? Manager { get ; set ; } public long ManagerId { get ; set ; } public List<UserInfo> UserInfos { get ; set ; }=new List<UserInfo>(); }
同时修改UserInfo.cs
实体类中的代码,如下所示:
1 2 3 4 5 6 7 8 9 public int Gender { get ; set ;} public string ? PhotoUrl { get ; set ;} public Department? Department { get ; set ; }
这里添加了一个Department
属性。
下面定义DepartmentConfig.cs
类,完成相应的配置,该类的代码如所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class DepartmentConfig : IEntityTypeConfiguration <Department > { public void Configure (EntityTypeBuilder<Department> builder ) { builder.ToTable("T_Departments" ); builder.Property(x => x.DepartmentName).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.DepartmentCode).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.DepartmentDescription).IsRequired(); builder.Property(x => x.City).HasMaxLength(16 ).IsRequired(); builder.Property(x => x.Manager).HasMaxLength(16 ).IsRequired(); } }
下面修改UserInfoConfig.cs
类中的代码,该类中的代码如下所示:
1 2 3 builder.Property(x => x.PhotoUrl).HasMaxLength(100 ); builder.HasOne<Department>(d=>d.Department).WithMany(u=>u.UserInfos).IsRequired();
修改MyDbContext
1 2 3 4 public class MyDbContext :DbContext { public DbSet<UserInfo>UserInfos { get ; set ; } public DbSet<Department> DepartmentInfos { get ; set ; }
下面完成数据的迁移操作。
1 2 Add-Migration CreateDepartament Update-database
7、获取部门信息接口设计 在这一小节中,我们根据前面创建的部门的模型来获取对应的部门信息。
1 2 3 4 5 6 namespace Cms.IRepository { public interface IDepartmentRepository :IBaseRepository <Department > { } }
在上面的代码中定义了IDepartmentRepository
这个数据仓储接口。
下面看一下具体的实现,如下所示:
1 2 3 4 5 6 7 8 namespace Cms.Repository { public class DepartmentRepository : BaseRepository <Department >, IDepartmentRepository { public DepartmentRepository (MyDbContext context ) : base (context ) { } } }
下面,实现服务的接口。
1 2 3 4 5 6 7 namespace Cms.IService { public interface IDepartmentService :IBaseService <Department > { } }
实现具体的服务类
1 2 3 4 5 6 7 8 9 10 11 12 namespace Cms.Service { public class DepartmentService :BaseService <Department >,IDepartmentService { private readonly IDepartmentRepository _departmentRepository; public DepartmentService (IDepartmentRepository departmentRepository ) { base .repository = departmentRepository; _departmentRepository = departmentRepository; } } }
创建具体的控制器DepartmentsController
,该控制器中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 namespace Cms.WebApi.Controllers { [Route("api/[controller]" ) ] [ApiController ] public class DepartmentsController : ControllerBase { private readonly IDepartmentService _departmentsService; public DepartmentsController (IDepartmentService departmentService ) { this ._departmentsService = departmentService; } [HttpGet ] [Authorize ] public IActionResult GetDepartments () { var departs = _departmentsService.LoadEntities(d=>d.DelFlag==Convert.ToBoolean(DelFlagEnum.Normal)); if (departs.Count() > 0 ) { return Ok(new ApiResult<IEnumerable<Department>>() { Success = true , Message = "获取部门成功" , Data = departs }); } else { return NotFound("没有部门信息" ); } } } }
8、前端获取组织架构数据 在这一小节中,需要构建请求的api
接口,向服务端发送请求获取组织架构的数据。
在src/api
目录下面创建departments.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 import request from "@/utils/request" ;export function getDepartments ( ) { return request ({ url : "/departments" , }); }
返回到src/views/departments/index.vue
组件中调用上面的方法来发送请求获取组织架构的数据。
1 2 3 import { onMounted, reactive,ref } from "vue" ;import TreeTools from './components/tree-tools.vue' import {getDepartments} from "@/api/departments"
这里导入了getDepartments
这个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 setup ( ) { const company=ref ({ name :"xxx教育集团" , manager :"负责人" }) const departs = reactive ([]); onMounted (()=> { loadDepartments (); }) const loadDepartments =async ( )=>{ const result =await getDepartments (); console .log ('result=' ,result); } const defaultProps = { children : "children" , label : "name" , };
这里我们将departs
数组清空,然后在onMounted
这个钩子函数中调用了loadDepartments
这个方法。
在这方法中又调用了getDepartments
方法发送请求,获取部门数据。
当然,这里也需要导入onMounted
这个钩子函数。
1 import { onMounted, reactive,ref } from "vue" ;
在loadDepartments
方法中同时打印了result
中的数据,通过查看浏览器的控制台,发现所有的组织架构的数据存储在一个数组中,而我们直接将这个数组给departs
,然后交给树形结构
是不合适的,因为该数据不是树形结构的数据。
所以在下一小节中,我们需要将数组中的数据转换成树形结构.
9、将数组转换成树形结构 在src/utils
目录下面创建index.js
文件,该文件的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function tranListToTreeData (list, rootValue ) { var arr = []; list.forEach ((item ) => { if (item.parentId === rootValue) { const children = tranListToTreeData (list, item.id ); if (children.length ) { item.children = children; } arr.push (item); } }); return arr; }
下面返回到views/departments/index.vue
组件中调用上面的递归方法。
1 2 import {getDepartments} from "@/api/departments" import { tranListToTreeData } from "@/utils/index" ;
首先导入tranListToTreeData
方法
1 2 3 4 5 6 7 8 9 const loadDepartments =async ( )=>{ const result =await getDepartments (); departs.push (...tranListToTreeData (result,0 )); } const defaultProps = { children : "children" , label : "departmentName" , };
这里在loadDepartments
方法中调用了tranListToTreeData
方法,传递的第一个参数是从服务端返回的的组织架构数组数据,第二个参数是0表示根节点,
1 2 3 4 const company=ref ({ departmentName :"xxx教育集团" , manager :"负责人" })
同时还要修改一下TreeTools.vue
这个组件中的代码
1 <span>{{ treeNode.departmentName }}</span> // 将这里修改成departmentName
返回到浏览器中进行测试。
在加载数据的时候,如果数据还没有加载完,可以展示loading
效果。
1 https://element-plus.gitee.io/zh-CN/component/loading.html
1 2 const departs = reactive ([]); const loading = ref (true );
1 2 3 4 5 const loadDepartments =async ( )=>{ const result =await getDepartments (); departs.push (...tranListToTreeData (result,0 )); loading.value = false ; }
这里在获取到数据以后loading
的值设置为false
,表示关闭loading
效果。
1 2 3 4 5 6 return { departs, defaultProps, company, loading, };
将loading
返回
1 <el-tree v-loading="loading" :data="departs" :props="defaultProps" default-expand-all>
这里给el-tree
添加了v-loading
属性,取值为loading
.
返回到浏览器端进行测试。(可以调整网速进行测试)
10、删除部门信息接口设计 在DepartmentsController
控制器中添加如下方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 [HttpDelete("{id}" ) ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> DeleteDepartments ([FromRoute]int id ) { var depart = await _departmentsService.LoadEntities(d => d.Id == id).FirstOrDefaultAsync(); if (depart == null ) { return NotFound("没有找到要删除的部门" ); } depart!.DelFlag =Convert.ToBoolean(DelFlagEnum.logicDelete); await _departmentsService.UpdateEntityAsync(depart); return Ok(new ApiResult<Department>() {Success=true ,Message="删除成功" ,Data=null }); }
11、删除部门功能–前端实现 这里先封装删除部门的接口api
在src/api/departments.js
文件中添加如下方法:
1 2 3 4 5 6 7 8 9 export function delDepartments (id ) { return request ({ url : `/departments/${id} ` , method : 'delete' }) }
问题:在什么情况下调用以上方法,发送请求删除部门信息呢?
这里需要修改views/departments/components/tree-tools.vue
组件中的代码,这里给下拉框中的删除部门
这一项注册单击事件。
1 https://element-plus.gitee.io/zh-CN/component/dropdown.html#%E6%8C%87%E4%BB%A4%E4%BA%8B%E4%BB%B6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <el-dropdown @command="operateDepts"> <!--注册了command事件,该事件触发以后,会调用operateDepts方法--> <span class="el-dropdown-link"> 操作 <el-icon class="el-icon--right"> <ArrowDown /> </el-icon> </span> <!-- 下拉菜单 --> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="add">添加子部门</el-dropdown-item><!--注册了command--> <el-dropdown-item v-if="!isRoot" command="edit">编辑部门</el-dropdown-item> <el-dropdown-item v-if="!isRoot" command="del">删除部门</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown>
在上面的代码中,给el-dropdown-item
组件添加了command
属性,并且给el-dropdown
组件注册了command
事件。当选择下拉框中的某一项的时候会触发该事件,这时候会调用operateDepts
函数。
下面实现该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 setup ( ){ const operateDepts = (type ) => { if (type === "add" ) { } else if (type === "edit" ) { } else { alert ("删除" ); } }; return { operateDepts, }; }
在上面的代码中实现了operateDepts
方法,该方法的参数type
存储的就是所单击的下拉框中对应项的command
属性的值。
这里做了相应的判断。
返回到浏览器中进行测试,当单击了下拉框中的删除部门
这一项的时候,就会弹出一个警示框。
12、删除部门功能实现–前端实现2 在这一小节中,将完成部门的真正删除操作。
修改views/departments/components/tree-tools.vue
组件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { delDepartments } from '@/api/departments' ; setup (props,{emit} ){ const operateDepts = (type ) => { if (type === "add" ) { } else if (type === "edit" ) { } else { ElMessageBox .confirm (`确定要删除${props.treeNode.departmentName} 部门?` ) .then (() => { return delDepartments (props.treeNode .id ); }) .then (() => { ElMessage ({ message : "删除部门成功" , type : "success" , }); emit ("delDepts" ); }); } }; return { operateDepts, }; }
在上面的代码中,先通过ElMessageBox.confirm方法给出了删除的提示
。在提示的信息中,需要展示要删除的部门的名称,所以这里通过props.treeNode.departmentName
获取要删除的部门名称。props
是通过setup
入口函数第一个参数获取到的。
通过文档知道confirm
方法返回的是Promise
对象。当单击确定
按钮的时候,会执行then
方法,在该方法的中调用了delDepartments
方法发送请求,更加部门编号删除部门信息,同时该方法返回的也是一个Promise
对象,所以后面可以继续添加then
方法,构成链式调用。
在该then
方法中,处理删除成功的情况。给出删除成功的提示。并且触发一个自定义事件delDepts
这里的emit
方法是从setup
入口函数的第二个参数解构出来的。
这里有两个问题:
第一个问题:删除失败的处理?
第二个问题:为什么触发自定义事件?
关于第一个问题,删除失败的处理,已经在响应拦截中处理了。
第二个问题:当删除成功以后,除了给出相应的删除成功的提示以外,还要让树形组件中展示最新的部门信息。
而树形组件是在父组件中的,所以这里需要触发自定义事件。
返回到父组件,也就是views/delDepartments/index.vue
这个文件中修改代码
1 2 3 4 5 <el-tree v-loading="loading" :data="departs" :props="defaultProps" default-expand-all> <template #default="{ node, data }"> <!--解构出传递给作用域插槽中的数据,data是每个节点的数据对象---> <TreeTools :treeNode="data" @delDepts="loadDepartments"></TreeTools> </template> </el-tree>
在上面的代码中,给TreeTools
组件,添加了delDepts
这个自定义事件,当该事件触发以后,会调用loadDepartments
方法重新发送请求,获取最新的部门信息数据。
1 2 3 4 5 6 const loadDepartments =async ( )=>{ const result =await getDepartments (); departs.length = 0 ; departs.push (...tranListToTreeData (result,0 )); loading.value = false ; }
在上面的代码中,先将departs
数组中的原有数据清空,在添加新的数据,否则数组中会有原有的数据。
1 2 3 4 5 6 7 return { departs, defaultProps, company, loading, loadDepartments };
注意这里也需要将loadDepartments
方法返回。
返回到浏览器中进行测试。
13、新增部门–展示弹出层 新增部门,需要弹出一个层,也就是弹出一个窗口,在这个窗口中展示表单。
下面在src/views/departments/components
目录下面创建add-dept.vue
组件,该组件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <template> <!-- 新增部门的弹层 --> <el-dialog title="新增部门" v-model="dialogVisible" :close-on-click-modal="false" > <!-- 表单组件 el-form label-width设置label的宽度 --> <!-- 匿名插槽 --> <el-form label-width="120px"> <el-form-item label="部门名称"> <el-input style="width: 80%" placeholder="1-50个字符" /> </el-form-item> <el-form-item label="部门编码"> <el-input style="width: 80%" placeholder="1-50个字符" /> </el-form-item> <el-form-item label="部门负责人"> <el-select style="width: 80%" placeholder="请选择" /> </el-form-item> <el-form-item label="部门介绍"> <el-input style="width: 80%" placeholder="1-300个字符" type="textarea" :rows="3" /> </el-form-item> </el-form> <!-- el-dialog有专门放置底部操作栏的 插槽 具名插槽 --> <template #footer> <el-row type="flex" justify="center"> <!-- 列被分为24 --> <el-col :span="6"> <el-button type="primary" size="small">确定</el-button> <el-button size="small">取消</el-button> </el-col> </el-row> </template> </el-dialog> </template> <script> import { ref } from "vue"; export default { name: "AddDept", setup() { const dialogVisible = ref(true); // 展示窗口 return { dialogVisible, }; }, }; </script> <style scoped></style>
返回到views/departments/index.vue
这个组件中使用上面的添加部门组件,看一下窗口的效果。
1 2 3 4 5 6 import AddDept from './components/add-dept.vue' export default { components :{ TreeTools , AddDept },
导入AddDept
这个组件,完成AddDept
组件的注册。
1 2 3 </el-tree> </el-card> <AddDept></AddDept>
在模板中进行渲染。
返回到浏览器中查看效果。
14、新增部门–控制弹出层显示与隐藏 下面先修改views/departments/components/add-dept.vue
组件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script> import { ref } from "vue" ; export default { name : "AddDept" , props :{ showDialog :{ type :Boolean , default :false } }, setup ( ) { }, }; </script>
这里我们会接收父组件传递过来的showDialog
属性,通过该属性控制添加窗口的显示与隐藏。
1 2 3 4 5 6 <!-- 新增部门的弹层 --> <el-dialog title="新增部门" v-model="showDialog" <!--这里指定了showDialog--> :close-on-click-modal="false" >
这里的v-model
属性的取值就是showDialog
返回到父组件views/departments/index.vue
1 2 </el-card> <AddDept :showDialog="showDialog"></AddDept>
这里给AddDept
这个子组件传递的属性就是showDialog
,下面需要进行定义
1 2 const loading = ref (true ); const showDialog = ref (false );
这里的showDialog
默认值是false
.也就是添加窗口不展示
1 2 3 4 5 6 7 8 return { departs, defaultProps, company, loading, loadDepartments, showDialog };
这里也将showDialog
这个对象返回到了模版中,这样在模版中才会使用。
保存代码的时候,会出现如下的错误:
1 2 3 VueCompilerError: v-model cannot be used on a prop, because local prop bindings are not writable. Use a v-bind binding combined with a v-on listener that emits update:x event instead.
Vue
在编译的时候出错了,这里不能使用v-model
来绑定props
所接收到的值。因为props
接收到的值是只读的,而v-model
是双向绑定,所以会涉及到数据的更改操作。
所以这里,我们在修改一下add-dept.vue
组件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { ref,watch } from "vue" ; export default { name : "AddDept" , props :{ showDialog :{ type :Boolean , default :false } }, setup (props ) { const visible = ref (false ); visible.value = props.ShowDialog ; watch (()=> props.showDialog ,(newValue,oldValue )=> { visible.value = newValue; }) return { visible } }, };
在上面的代码中,在setup
这个入口函数中,通过参数props
接收到父组件传递过来的showDialog
属性的值,然后将接收到的值,赋值给了变量visible
.(这里通过watch
来监视props.showDialog
值的变化)
最后,返回visible
的值。
这里需要修改一下模版中的代码,如下所示:
1 2 3 4 5 6 <!-- 新增部门的弹层 --> <el-dialog title="新增部门" v-model="visible" <!--绑定了visible--> :close-on-click-modal="false" >
在上面的代码中,我们可以看到v-model
绑定了visible
.
保存程序,返回到浏览器中,可以看到新增部门的窗口没有展示,也就是说默认情况下新增部门的窗口是隐藏的。
而当单击添加子部门
的时候需要展示窗口。
所以下面需要修改src/views/departments/tree-tools.vue
文件中的代码
1 2 3 4 5 6 setup (props,{emit} ){ const operateDepts = (type ) => { if (type === "add" ) { emit ('addDepts' ,props.treeNode ); } else if (type === "edit" ) {
添加子部门的时候,这里触发了自定义事件addDepts
,同时传递了数据,数据就是我们当前所单击的部门,也就是说,是在当前所单击的部门下面添加子部门。
在返回到父组件index.vue
1 2 3 <!-- 组织架构--头部布局 --> <el-card> <TreeTools :treeNode="company" :isRoot="true" @addDepts="addDepts" ></TreeTools>
上面是给头部区域所使用的TreeTools
组件添加了自定义的事件@addDepts
1 2 3 <template #default="{ node, data }"> <!--解构出传递给作用域插槽中的数据,data是每个节点的数据对象---> <TreeTools :treeNode="data" @delDepts="loadDepartments" @addDepts="addDepts"></TreeTools> </template>
上面是对template
标签中所使用的TreeTools
组件添加了 @addDepts="addDepts"
事件。
通过上面的代码可以看到,头部和下面的TreeTools
组件中都添加了addDepts
事件,因为这两块区域都有添加子部门的功能。
自定义事件触发以后,调用的是addDepts
方法,下面实现该方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const addDepts = (nodeValue ) => { showDialog.value = true ; node.value = nodeValue; console .log ('nodeValue = ' ,node.value ); }; return { departs, defaultProps, company, loading, loadDepartments, showDialog, addDepts };
1 2 const showDialog = ref (false ); const node = ref (null );
注意:这里我们单击了添加子部门
后展示窗口,当单击窗口右上角的叉号图标时可以关闭窗口,但是这时候再单击添加子部门
无法弹出添加部门的窗口,原因是这里没有实现真正的对showDialog
属性的控制。
因为:
当我们单击添加子部门
的时候,会执行 emit('addDepts',props.treeNode);
这行代码,从而触发父组件中对应的addDepts
事件,从而调用对应的addDepts
方法,从而 showDialog.value = true
,这时候showDialog
值发生了变化,由于showDialog
是响应对象,从而重新渲染模版,这时候,就会将``showDialog中的新值传递给
AddDept组件,在该组件中通过
watch监视到了
showDialog`值的变化,从而将添加窗口弹出来了。
但是,当我们添加窗口右上角的查号图标,将窗口关闭以后,再次单击添加子部门
的时候,不会在弹出窗口。
当单击叉号图标关闭窗口的时候,并没有改变showDialog
的值,它的值其实一直是true
.
所以,这里我们需要在单击叉号图标的时候,改变showDialog
的值。
在add-dept.vue
这个组件中,给dialog
窗口添加了close
事件,该事件会在单击叉号图标的时候被触发。
当该事件触发以后,会调用closeDialog
方法。
1 2 3 4 5 6 7 <!-- 新增部门的弹层 --> <el-dialog title="新增部门" v-model="visible" :close-on-click-modal="false" @close="closeDialog" >
closeDialog
方法的实现如下所示:
1 2 3 4 5 6 7 8 const closeDialog =( )=>{ emit ('closeAddDialog' ,false ); } return { visible, closeDialog }
在closeDialog
方法的内部也是触发了自定义的事件closeAddDialog
.并传递了参数false
返回到父组件
1 2 3 </el-card> <AddDept :showDialog="showDialog" @closeAddDialog="closeAddDialog"></AddDept> </div>
这里指定了自定义事件closeAddDialog
,该事件触发以后会调用closeAddDialog
方法,该方法的实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const closeAddDialog =(state )=>{ showDialog.value = state; } return { departs, defaultProps, company, loading, loadDepartments, showDialog, addDepts, closeAddDialog };
在closeAddDialog
方法中,根据传递过来的state
参数值,修改showDialog
的值。
15、新增部门–表单基本校验实现 修改views/departments/components/add-dept.vue
组件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 setup (props,{emit} ) { const visible = ref (false ); const formData = reactive ({ departmentName : "" , departmentCode : "" , manager : "" , departmentDescription : "" , }); const formRules = reactive ({ departmentName : [ { required : true , message : "部门名称不能为空" , trigger : "blur" }, { min : 1 , max : 50 , message : "部门名称要求1-50个字符" , trigger : "blur" }, ], departmentCode : [ { required : true , message : "部门编码不能为空" , trigger : "blur" }, { min : 1 , max : 50 , message : "部门编码要求1-50个字符" , trigger : "blur" }, ], manager : [ { required : true , message : "部门负责人不能为空" , trigger : "blur" }, ], departmentDescription : [ { required : true , message : "部门介绍不能为空" , trigger : "blur" }, { trigger : "blur" , min : 1 , max : 300 , message : "部门介绍要求1-50个字符" , }, ], });
在上面的代码中定义了formData
这个表单数据对象。
同时定义了formRules
这个校验规则对象。
并且将两项内容返回。
1 2 3 4 5 6 return { visible, closeDialog, formData, formRules }
下面查看模版中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <el-form label-width="120px" :model="formData" :rules="formRules"> <el-form-item label="部门名称" prop="departmentName"> <el-input style="width: 80%" placeholder="1-50个字符" v-model="formData.departmentName" /> </el-form-item> <el-form-item label="部门编码" prop = "departmentCode"> <el-input style="width: 80%" placeholder="1-50个字符" v-model="formData.departmentCode" /> </el-form-item> <el-form-item label="部门负责人" prop="manager"> <el-select style="width: 80%" placeholder="请选择" v-model="formData.manager" /> </el-form-item> <el-form-item label="部门介绍" prop="departmentDescription"> <el-input style="width: 80%" placeholder="1-300个字符" type="textarea" :rows="3" v-model="formData.departmentDescription" /> </el-form-item> </el-form>
在上面的模板代码中,给el-form
组件添加了model
属性绑定了表单数据对象formData
,同时添加了rules
属性绑定了formRules
这个表单校验规则对象。
为了完成表单的校验,下面给每个el-form-item
添加了prop
属性。
同时给每个表单元素
通过v-model
双向绑定了表单数据对象中的相应属性。
返回到浏览器中进行测试。
16、新增部门–自定义校验 部门名称(departmentName
):必填 1-50个字符 / 同级部门中禁止出现重复部门
部门编码(departmentCode
):必填 1-50个字符 / 部门编码在整个模块中都不允许重复
关于部门名称的基本校验(必填 1-50个字符 )和部门编码基本校验(必填 1-50个字符 )已经完成了,下面再来看一下关于部门名称和部门编码中其它的校验要求。
这些要求需要通过自定义校验来实现。
这里我们先来看一下关于部门名称的校验,校验规则的要求:同级部门中禁止出现重复部门。
例如:在上海实业部,这个部门下面已经有一个A
部门了,现在就不能在在上海实业部下面添加A
部门了。
下面修改add-dept.vue
这个组件中的代码。
在该组件中,首先应该接受传递过来的当前所单击的部门的信息。
1 2 3 4 5 6 7 8 9 10 11 12 export default { name : "AddDept" , props :{ showDialog :{ type :Boolean , default :false }, treeNode :{ type :Object , default :null } },
这里在props
中定义了treeNode
属性接收父组件中传递过来的当前所单击的部门的信息,在当前所单击的部门下面查找对应的子部门,然后看看与现在新添加的部门是否有名称上的冲突。
问题:在哪传递treeNode
?
在父组件中,返回到index.vue
组件中传递所单击的部门的信息。
1 2 </el-card> <AddDept :showDialog="showDialog" @closeAddDialog="closeAddDialog" :treeNode="node"></AddDept>
这里的treeNode
的数据来自node
node
中已经保存了所单击的部门的信息,如下所示:
就在当前父组件index.vue
中的addDepts
方法中,已经将所单击的部门信息保存到了node
中。
当然,这里需要将node
返回,这样在模板中才可以使用。
1 2 3 4 5 6 7 8 9 10 11 return { departs, defaultProps, company, loading, loadDepartments, showDialog, addDepts, closeAddDialog, node };
现在在返回到add-dept.vue
组件中,在该组件中接收了传递过来的所单击的部门的信息,下面就是要查询一下在该单击的部门下的所有的子部门。看一下是否与新添加的部门名称有重复的。
1 2 3 4 5 6 7 8 9 const formRules = reactive ({ departmentName : [ { required : true , message : "部门名称不能为空" , trigger : "blur" }, { min : 1 , max : 50 , message : "部门名称要求1-50个字符" , trigger : "blur" }, { trigger :"blur" , validator :checkNameRepeat } ],
在上面的代码中,首先指定了自定义校验,添加了validator
属性,该属性的取值是checkNameRepeat
下面实现checkNameRepeat
方法,该方法的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const checkNameRepeat =async (rule,value,callback )=>{ const result = await getDepartments (); var isRepeat = result.filter ((item )=> item.parentId ===props.treeNode .id ).some ((item )=> item.departmentName ===value); isRepeat?callback (`同级部门下已经有${value} 的部门了` ):callback (); } const formRules = reactive ({ departmentName : [ { required : true , message : "部门名称不能为空" , trigger : "blur" }, { min : 1 , max : 50 , message : "部门名称要求1-50个字符" , trigger : "blur" }, { trigger :"blur" , validator :checkNameRepeat } ],
在所定义的formRules
校验规则对象上面实现了checkNameRepeat
方法。
在上面的代码中,需要调用getDepartments
方法发送请求获取最新的架构数据数据,所以这里需要将该方法进行导入
1 2 import { ref,watch,reactive } from "vue" ;import {getDepartments}from '@/api/departments'
返回到浏览器中进行测试。
下面实现关于部门编码规则的校验
校验的规则要求是:部门编码在整个模块中都不允许重复
思路上是一样的。
1 2 3 4 5 6 7 8 const checkCodeRepeat =async (rule,value,callback )=>{ const result = await getDepartments (); const isRepeat = result.some ((item )=> item.departmentCode ==value&&value) isRepeat?callback (new Error (`组织架构中已经有部门使用${value} 编码了` )):callback () }
这里是从所有的组织架构数据中进行查找,看一下是否有部门编码相同。
1 2 3 4 5 6 7 8 departmentCode : [ { required : true , message : "部门编码不能为空" , trigger : "blur" }, { min : 1 , max : 50 , message : "部门编码要求1-50个字符" , trigger : "blur" }, { trigger :"blur" , validator :checkCodeRepeat } ],
在departmentCode
中添加了自定义的校验,完成了对checkCodeRepeat
方法的调用。
下面返回到浏览器中进行测试。
17、新增部门–自定义校验问题 在上一小节中已经实现了自定义的校验,
但是这里有一个问题。
当单击顶部的下拉框,添加子部门
的时候,输入了重名的部门名称并没有给出提示。
返回到views/departments/index.vue
组件中
在头部布局中也使用了TreeTools
组件
1 2 3 <!-- 组织架构--头部布局 --> <el-card> <TreeTools :treeNode="company" :isRoot="true" @addDepts="addDepts" ></TreeTools>
这里的treeNode
属性的值是company
,company
的定义如下所示:
1 2 3 4 const company=ref ({ departmentName :"xxx教育集团" , manager :"负责人" })
以上对象中没有添加id
对应的tree-tools.vue
组件中接收的
1 2 3 4 5 6 props : { treeNode : { type : Object , required : true , },
treeNode
中没有id
1 2 3 4 const operateDepts = (type ) => { if (type === "add" ) { emit ('addDepts' ,props.treeNode );
当单击添加子部门
的时候,触发以上的addDepts
自定义事件,同时传递了treeNode
这个数据。
当父组件中,在父组件index.vue
中,会调用addDepts
方法,该方法的实现如下所示:
1 2 3 4 5 const addDepts = (nodeValue ) => { showDialog.value = true ; node.value = nodeValue; console .log ('nodeValue = ' ,node.value ); };
把传递过来的值有给了node
而最终node
传递给了AddDept
组件
1 <AddDept :showDialog="showDialog" @closeAddDialog="closeAddDialog" :treeNode="node"></AddDept>
在该组件中,接收treeNode
中也就没有id
怎样修改呢?
把views/departments/index.vue
中给company
对象添加一个id
属性,并且取值是空字符串就可以了,如下所示:
1 2 3 4 5 6 const company=ref ({ departmentName :"xxx教育集团" , manager :"负责人" , id :0 })
为什么传递是0?
原因是:当传递到了add-dept.vue
这个组件以后
1 2 3 4 5 6 7 8 9 10 props :{ showDialog :{ type :Boolean , default :false }, treeNode :{ type :Object , default :null } },
treeNode
中有id
,并且是0
1 2 var isRepeat = result.filter ((item )=> item.parentId ===props.treeNode .id ).some ((item )=> item.departmentName ===value); isRepeat?callback (`同级部门下已经有${value} 的部门了` ):callback ();
18、新增部门–填充下拉框 服务端接口设计
1 2 3 4 5 6 7 [HttpGet("simple" ) ] [Authorize ] public IActionResult GetSimpleUser () { var userSimpleList = _userInfoService.LoadEntities(u=>true ); return Ok(new ApiResult<List<UserInfo>>() { Success=true ,Message="获取用户信息成功" ,Data=userSimpleList.Select(u=> new UserInfo() {Id=u.Id,UserName=u.UserName }).ToList() }); }
这里只查询了Id,UserName
两个属性的值,因为这里我们填充下拉框,只需要使用到这两组数据。
前端实现:
这里首先构建一个api
接口来获取员工的信息,
由于是获取员工的信息,所以我们在src/api
目录下面创建employees.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 import request from "@/utils/request" ;export function getEmployeeSimple ( ) { return request ({ url : "/users/simple" , }); }
返回到views/departments/components/add-dept.vue
文件中进行代码的修改
1 2 3 import { ref,watch,reactive } from "vue" ;import {getDepartments}from '@/api/departments' import { getEmployeeSimple } from "@/api/employees" ;
在上面的代码中,导入了ref
和getEmployeeSimple
这个方法
1 2 const options =ref ([]);
定义了一个options
数组,存储获取到的员工的基本信息
1 2 3 4 5 6 7 8 9 10 11 12 13 const getEmployeeSimplInfo = async ( )=>{ const result = await getEmployeeSimple () options.value = result; } return { visible, closeDialog, formData, formRules, getEmployeeSimplInfo, options }
这里定义了getEmployeeSimpleInfo
方法,在该方法中调用了getEmployeeSimple
方法发送请求获取员工的基本信息。
将获取到的员工的基本信息存储到了options
数组中。
最后,将getEmployeeSimpleInfo
方法和options
数组返回到模板中。
在模板中调用getEmployeeSimpleInfo
方法。
下面修改模板中的代码,如下所示:
1 2 3 4 5 6 <el-form-item label="部门负责人" prop="manager"> <el-select style="width: 80%" placeholder="请选择" v-model="formData.manager" @focus="getEmployeeSimplInfo" > <el-option v-for="item in options" :key="item.id" :label="item.userName" :value="item.userName+':'+item.id"><!----------注意:这里是把负责人的名称与编号拼接在一起,发送给服务端。---------------> </el-option> </el-select> <!---注意:el-option要在el-select里面---> </el-form-item>
在上面的代码中,给el-select
组件添加了@focs
事件,当用鼠标点击下拉框的时候,下拉框会获取到焦点,从而会触发focus
事件。该事件触发以后会调用getEmployeeSimpleInfo
方法,获取基本的员工数据。
获取到的员工数据会在options
数组中,所以下面开始对该数组进行遍历。
取出来的用户信息放到了el-option
中。label
表示展示的内容,value
表示选中的对应的值。
返回到浏览器中进行测试。
19、新增部门–完成部门添加 前端代码实现
下面要实现的功能就是当单击确定
按钮额时候完成部门的添加。
关于新增部门的api
方法定义在src/api/departments.js
文件中对应的addDepartments
方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 export function addDepartments (data ) { return request ({ url : "/departments" , method : "post" , data, }); }
下面修改src/views/departments/components/add-dept.vue
文件中的代码
在添加部门前,也要判断表单是否通过校验
1 <el-form label-width="120px" :model="formData" :rules="formRules" ref="formRef">
所以这里给el-form
添加了ref
属性,关联的是formRef
1 <el-button type="primary" size="small" @click="addOk(formRef)">确定</el-button>
给确定
按钮注册了单击事件,事件触发以后会执行addOk
方法,将表单传递到该方法中
在setup
入口函数中定义formRef
1 2 const options =ref ([]); const formRef = ref (null );
下面实现addOk
方法。
1 2 3 4 5 6 7 8 9 10 11 const addOk = (formEl ) => { formEl.validate (async (valid, fields) => { if (valid) { await addDepartments ({ ...formData, parentId : props.treeNode .id }); emit ("addDepts" ); } }); };
在上面的代码中实现了addOK
方法完成部门的添加,该方法的参数就是传递过来的表单的实例
接收传递过来的表单的实例,调用validate
方法进行校验,如果valid
参数的值为true
表示校验通过。然后调用addDepartments
方法发送请求。
注意:这里有一个问题:
添加部门,除了要获取表单中用户输入的部门的信息以外,还需要指定所添加的部门是属于哪个部门的子部门。
所以这里我们需要给所添加的部门指定parentId
属性,该属性的取值就是所单击的当前部门的id
(这里将formData
对象中存储的部门信息解构与parentId
进行了合并)
1 import { getDepartments, addDepartments } from "@/api/departments" ;
这里需要导入addDepartments
方法。
当添加成功以后,触发自定义的事件addDepts
,告诉父组件重新加载树形组件中部门的信息。
下面将formRef
和addOk
返回
1 2 3 4 5 6 7 8 9 10 return { visible, closeDialog, formData, formRules, getEmployeeSimplInfo, options, formRef, addOk }
最后,返回到父组件中,指定自定义事件
views/departments/index.vue
组件中
1 <AddDept :showDialog="showDialog" @closeAddDialog="closeAddDialog" :treeNode="node" @addDepts="loadDepartments"></AddDept>
这里指定了addDepts
这个自定义事件,当该事件触发以后会重新调用loadDepartments
方法,发送请求重新加载部门信息数据。
服务端接口实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [HttpPost ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task<IActionResult> AddDepartment ([FromBody]Department department ) { string [] strs = department.Manager!.Split(":" ); department.Manager = strs[0 ]; department.ManagerId = Convert.ToInt32(strs[1 ]); department.City = "北京" ; department.CreateTime = DateTime.Now; department.UpdateTime = DateTime.Now; department.DelFlag =Convert.ToBoolean(DelFlagEnum.Normal); await _departmentsService.InsertEntityAsync(department); return Ok(new ApiResult<Department>() { Success = true , Message = "添加成功" , Data = department }); }
启动项目进行测试。
20、新增部门–关闭新增弹层 当新增完部门的信息以后,我们要将当前弹出的窗口关闭掉。
关闭窗口其实我们前面也已经实现了。
在add-dept.vue
这个组件对应的addOk
方法中,我们也要触发closeAddDialog
事件就可以了。
1 2 3 4 5 6 7 8 9 10 11 const addOk = (formEl ) => { formEl.validate (async (valid, fields) => { if (valid) { await addDepartments ({ ...formData, parentId : props.treeNode .id }); emit ("addDepts" ); emit ("closeAddDialog" ,false ); } });
21、新增部门–重置状态 这里有一个问题,在弹出的添加部门
窗口中,我们没有在表单中输入任何数据,直接点击添加
在,这时候会出现错误的提示。
这时候,单击取消按钮,将窗口关闭,然后再次单击
添加子部门,把窗口弹出来,发现错误提示信息还存在,所以这里需要在单击
取消`按钮关闭窗口前,先将这些错误的提示状态清除掉。
1 <el-button size="small" @click="closeDialog(formRef)">取消</el-button>
在调用btnCancel
方法的时候,将formRef
这个表单的实例传递到该方法中。
1 2 3 4 5 6 7 8 9 const closeDialog =(formEl )=>{ emit ('closeAddDialog' ,false ); if (!formEl) return ; formEl.resetFields (); }
这里,调用了resetFields
方法,重置了对应的状态。
返回到浏览器中进行测试。
当单击窗口右上角的叉号
图标的时候,也需要将窗口关闭掉。
1 2 3 4 5 6 <el-dialog title="新增部门" v-model="visible" :close-on-click-modal="false" @close="closeDialog" >
22、编辑部门–弹出编辑窗口 编辑部门功能实际上和新增窗体采用的是一个组件,只不过我们需要将新增场景变成编辑场景
当单击操作--编辑部门
的时候,记录所单击的节点,然后弹出窗口,把数据填充到表单中。
下面修改views/departments/components/tree-tools.vue
组件中的代码
1 2 3 4 5 6 7 8 setup (props,{emit} ){ const operateDepts = (type ) => { if (type === "add" ) { emit ('addDepts' ,props.treeNode ); } else if (type === "edit" ) { emit ("editDepts" , props.treeNode );
这里触发了editDepts
事件,传递的是所点击的部门的数据。
返回到父组件departments/index.vue
组件中
1 2 <TreeTools :treeNode="data" @delDepts="loadDepartments" @addDepts="addDepts" @editDepts="editDepts"></TreeTools> </template>
这里是给第二个,也就是el-tree
中的TreeTools
中注册自定义事件editDepts
,当该事件触发以后,会调用editDepts
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const editDepts = (node ) => { showDialog.value = true ; node.value = node; }; return { departs, defaultProps, company, loading, loadDepartments, showDialog, addDepts, closeAddDialog, node, editDepts };
实现了editDepts
方法,将editDepts
方法返回,返回到浏览器中进行测试
23、编辑部门–展示编辑部门信息 前端会将要编辑的部门编号传递到服务端,服务端接收到该编号后,查询出要编辑的部门信息,然后返回到前端。
这里我们先来实现服务端的接口。
服务端接口实现
1 2 3 4 5 6 7 8 9 10 11 [HttpGet("{id}" ) ] [Authorize ] public async Task<IActionResult> GetDepartmentById ([FromRoute]int id ) { var depart = await _departmentsService.LoadEntities(d => d.Id == id).FirstOrDefaultAsync(); if (depart == null ) { return BadRequest("没有找到部门信息" ); } return Ok(new ApiResult<Department>() { Success=true , Message ="获取到部门信息" , Data = depart }); }
前端实现
现在已经展示出来了编辑部门的窗口
接来下把要编辑的部门信息填充到表单中。
这里先构建一个api
接口,请求服务端,获取要编辑的部门信息。
修改src/api/departments.js
文件中的代码,增加如下的方法s
1 2 3 4 5 6 7 8 9 export function getDepartDetail (id ) { return request ({ url : `/departments/${id} ` , method :"get" }); }
下面返回到views/departments/components/add-dept.vue
组件中进行代码的修改
我们知道在该组件中定义了formData
这个响应式对象,而该对象已经与表单元素进行了双向数据绑定,这里只要将服务端返回的要修改的部门数据交给formData
,对应的表单中就会展示要编辑的部门信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const getDepartDetailInfo = async (id ) => { const result = await getDepartDetail (id); formData.departmentCode = result.departmentCode ; formData.departmentDescription = result.departmentDescription ; formData.manager = result.manager ; formData.departmentName = result.departmentName ; }; return { visible, closeDialog, formData, formRules, getEmployeeSimplInfo, options, formRef, addOk, getDepartDetailInfo }
这里单独的定义了一个getDepartDetailInfo
方法,在该方法中调用了getDepartDetail
方法,发送请求获取部门信息,交给formData
对象中的各个属性。
并且返回
1 import {getDepartments,addDepartments,getDepartDetail}from '@/api/departments'
这里导入了 getDepartDetail
方法。
问题:getDepartDetailInfo
方法在哪被调用?
在父组件中进行调用(弹出编辑窗口的时候就应该调用该方法,获取要编辑的部门信息,然后填充到表单中)
views/departments/index.vue
1 <AddDept :showDialog="showDialog" @closeAddDialog="closeAddDialog" :treeNode="node" @addDepts="loadDepartments" ref="addDept"></AddDept>
给AddDept
添加了一个ref
属性,取值为addDept
,也就是关联了一个AddDept
对象
在setup
入口函数中,定义addDept
1 2 3 const showDialog = ref (false ); const node = ref (null ); const addDept = ref (null );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const editDepts = (node ) => { showDialog.value = true ; node.value = node; addDept.value .getDepartDetailInfo (node.id ); }; return { departs, defaultProps, company, loading, loadDepartments, showDialog, addDepts, closeAddDialog, node, editDepts, addDept };
在editDepts
中通过addDept
这个ref
对象找到AddDept
这个实例对象,然后调用内部的getDepartDetailInfo方法,传递的是所点击的要编辑的部门的
编号。
最后补充一个知识点细节问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 let obj = reactive ({ name : 'zhangsan' , age : '18' }) obj = { name : 'lisi' age : '' } obj['name' ] = 'lisi' ; obj['age' ] = '' ; let obj = reactive ({ data : { name : 'zhangsan' , age : '18' } }) obj.data = { name : 'lisi' age : '' }
24、编辑部门–切换标题 现在编辑与新增使用一个窗口,对应的标题也需要进行切换。
怎样进行切换呢?
下面修改add-dept.vue
这个组件中的代码:
1 2 3 4 5 6 7 const formData = reactive ({ departmentName : "" , departmentCode : "" , manager : "" , departmentDescription : "" , id :"" });
在formData
对象中又添加了一个id
属性,表示部门的编号
1 2 3 4 5 6 7 8 9 const getDepartDetailInfo = async (id ) => { const result = await getDepartDetail (id); formData.departmentCode = result.departmentCode ; formData.departmentDescription = result.departmentDescription ; formData.manager = result.manager ; formData.departmentName = result.departmentName ; formData.id = result.id ; };
在getDepartDetailInfo
方法中获取了部门详情数据,这里肯定有部门的编号,将其赋值给formData
对象中的id
属性。
这就说明了,在编辑的时候该id
属性是有值的,而新增的时候是没有值的。
所以当该id
属性的 值发生了变化以后,就可以进行判断是编辑还是添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const showTitle = computed (() => { return formData.id ? "编辑部门" : "新增子部门" ; }); return { visible, closeDialog, formData, formRules, getEmployeeSimplInfo, options, formRef, addOk, getDepartDetailInfo, showTitle }
这里我们添加了一个计算属性,根据id
属性的值进行判断。
当然也需要将showTitle
进行返回。
1 import { ref,watch,reactive,computed } from "vue" ;
这里也需要从vue
中导入computed
.
1 2 3 4 5 6 7 <!-- 新增部门的弹层 --> <el-dialog :title ="showTitle" v-model ="visible" :close-on-click-modal ="false" @close ="closeDialog" >
修改了el-dialog
中的title
属性的值,现在取值是showTitle
.
返回到浏览器中进行测试。
在测试的时候,发现了一个问题,
首先单击添加子部门
,展示的窗口中显示的标题是添加子部门
,然后关闭窗口,再点击编辑部门
,在弹出的窗口中展示的标题是编辑部门
。
关闭窗口,再单击添加子部门
,弹出的窗口的标题还是编辑部门
。
原因是:当单击编辑部门
弹出窗口后,进行了关闭,会执行如下代码
1 2 3 4 5 6 7 const closeDialog =(formEl )=>{ emit ('closeAddDialog' ,false ); if (!formEl) return ; formEl.resetFields (); }
这里调用了formEl.resetFields
方法,只是将与表单绑定的formData
对象中的属性清除了,没有将id
进行清除。
1 2 3 4 5 6 7 8 const closeDialog =(formEl )=>{ emit ('closeAddDialog' ,false ); if (!formEl) return ; formEl.resetFields (); formData.id ="" ; }
这里将formData
中的id
值清空。
返回到浏览器中进行测试。
25、编辑部门–完成编辑 前端实现
当单击确定
按钮的时候,要区分是添加部门信息还是编辑部门的信息
这里先增加一个更新部门的api
接口在api/departments.js
文件中添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 export function updateDepartments (data ) { return request ({ url : `/departments/${data.id} ` , method : "put" , data, }); }
在调用以上方法的时候,会传递过来要更新的部门数据,数据中会有编号。
下面返回到add-dept.vue
组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const addOk = (formEl ) => { formEl.validate (async (valid, fields) => { if (valid) { if (formData.id ){ await updateDepartments (formData); }else { await addDepartments ({ ...formData, parentId : props.treeNode .id }); } emit ("addDepts" ); emit ("closeAddDialog" ,false ); } }); };
我们知道,在单击确定
按钮的时候,会调用addOk
方法。在该方法中判断formData.id
是否有值,如果有值调用updateDepartments
方法进行数据的更新。
当然更新成功也需要重新加载数据,关闭窗口。
1 import {getDepartments,addDepartments,getDepartDetail,updateDepartments}from '@/api/departments'
这里导入了updateDepartments
方法。返回到浏览器中进行测试。
在测试的时候发现了问题,进行编辑的时候,直接单击确定
按钮,发现给出了部门名称与部门编码的
错误。
为什么会给出这个错误的提示呢?因为这里是更新,而且是直接单击了确定
按钮,没有更改部门名称和部门编码
所以服务端已经存储了相同名称的部门名称和部门编码。所以才会出现这样的错误。
所以说,在编辑的时候关于业务规则的校验是不能与添加的时候一样的。
继续修改add-dept.vue
组件中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 const checkNameRepeat =async (rule,value,callback )=>{ const result = await getDepartments (); let isRepeat =false ; if (formData.id ){ isRepeat = result .filter ( (item ) => item.parentId === formData.parentId && item.id !== formData.id ) .some ((item ) => item.departmentName === value); }else { isRepeat = result.filter ((item )=> item.parentId ===props.treeNode .id ).some ((item )=> item.departmentName ===value); } isRepeat?callback (`同级部门下已经有${value} 的部门了` ):callback (); }; const checkCodeRepeat =async (rule,value,callback )=>{ const result = await getDepartments (); let isRepeat =false ; if (formData.id ){ isRepeat = result.some ( (item ) => item.id !== formData.id && item.departmentCode === value && value ); }else { isRepeat = result.some ((item )=> item.departmentCode ==value&&value) } isRepeat?callback (new Error (`组织架构中已经有部门使用${value} 编码了` )):callback () }
在上面的判断中使用了parentId
,所以需要在formData
中去定义parentId
这个属性。
1 2 3 4 5 6 7 8 const formData = reactive ({ departmentName : "" , departmentCode : "" , manager : "" , departmentDescription : "" , id :"" , parentId :"" });
同时在getDepartDetailInfo
方法中给parentId
属性赋值。
1 2 3 4 5 6 7 8 9 10 const getDepartDetailInfo = async (id ) => { const result = await getDepartDetail (id); formData.departmentCode = result.departmentCode ; formData.departmentDescription = result.departmentDescription ; formData.manager = result.manager ; formData.departmentName = result.departmentName ; formData.parentId =result.parentId ; formData.id = id; };
在编辑的时候会调用getDepartDetailInfo
方法,在该方法中给parentId
赋值
返回到浏览器中进行测试。
这里进行部门编辑的时候,可以先不输入任何的数据,然后单击确定
按钮看一下
然后在输入新的内容,进行测试,看一下效果。
服务端实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [HttpPut ("{id}" )] [UnitOfWork (new Type [] { typeof (MyDbContext ) })] public async Task <IActionResult > UpdateDepartment ([FromRoute ]int id, [FromBody ]Department department) { var dept = await _departmentsService.LoadEntities (d => d.Id ==id).FirstOrDefaultAsync (); if (dept == null ) { return BadRequest ("没有找到要更新的部门" ); } string[] strs = department.Manager !.Split (":" ); dept.DepartmentDescription = department.DepartmentDescription ; dept.DepartmentName = department.DepartmentName ; dept.DepartmentCode = department.DepartmentCode ; dept.Manager = strs[0 ]; dept.ManagerId = Convert .ToInt32 (strs[1 ]); dept.UpdateTime = DateTime .Now ; return Ok (new ApiResult <Department >() { Success = true , Message = "获取到部门信息" , Data = dept }); }
由于在UpdateDepartment
这个方法中,没有使用automapper
自动映射,只能自己手动进行映射。
六、员工管理模块 1、创建通用工具栏组件 在后续的业务开发中,经常会用到一个类似下图的工具栏,作为公共组件,进行一下封装
在src/components
目录下面创建PageTools
文件夹,在该文件夹下面创建index.vue
组件,该组件中的基本布局代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <template> <el-card class="page-tools"> <el-row type="flex" justify="space-between" align="middle"> <el-col> <div v-if="showBefore" class="before"> <!--如果showBefore为true才展示该div中的内容--> <el-icon><InfoFilled /></el-icon> <!-- 定义前面得插槽 --> <slot name="before" /> </div> </el-col> <el-col> <el-row type="flex" justify="end"> <!-- 定义后面的插槽 --> <slot name="after" /> </el-row> </el-col> </el-row> </el-card> </template> <script> export default { name: "PageTools", props: { showBefore: { type: Boolean, default: false, }, }, }; </script> <style lang="scss"> .page-tools { margin: 10px 0; .before { line-height: 34px; i { margin-right: 5px; color: #409eff; } display: inline-block; padding: 0px 10px; border-radius: 3px; border: 1px solid rgba(145, 213, 255, 1); background: rgba(230, 247, 255, 1); } } </style>
下面进行测试。
在views/dashboard/index.vue
这个组件中进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> 后台布局 <PageTools :showBefore="true"> <!--指定showBefore属性为true,表示展示左侧插槽内容--> <template v-slot:before> <!--指定before这个插槽展示的内容--> <span>Hello</span> </template> <template v-slot:after><!--指定after这个插槽展示的内容--> <el-button type="primary">添加</el-button> </template> </PageTools> </template> <script> import PageTools from "@/components/PageTools"; // 导入`PageTools`组件 export default{ name:'dashboard', components: { PageTools, // 完成`PageTools`组件的注册 }, setup(){ } } </script>
返回到浏览器中进行测试。
访问首页查看效果
2、完成公共组件全局注册 在上一小节中创建的PageTools
这个组件是公共组件,在其他的页面中使用该组件的时候,都需要导入与注册。
比较麻烦,这里可以完成全局的注册。
修改src/components/index.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 import SvgIcon from "@/components/SvgIcon" ;import PageTools from "@/components/PageTools" ;export default { install (app ) { app.component (SvgIcon .name , SvgIcon ); app.component (PageTools .name , PageTools ); }, };
这里完成了PageTools
组件的全局注册。
返回到views/dashboard/index.vue
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> 后台布局 <PageTools :showBefore="true"> <!--指定showBefore属性为true,表示展示左侧插槽内容--> <template v-slot:before> <!--指定before这个插槽展示的内容--> <span>Hello</span> </template> <template v-slot:after><!--指定after这个插槽展示的内容--> <el-button type="primary">添加</el-button> </template> </PageTools> </template> <script> export default{ name:'dashboard', setup(){ } } </script>
这里不需要在导入PageTools
组件和进行注册,直接使用
返回到浏览器中查看效果。
3、员工列表页面基本布局 在这一小节中,先将员工列表的基本布局设计出来。
在views/employees/index.vue
这个组件中添加如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <PageTools :showBefore="true"> <!--使用了PageTools这个公共组件--> <template #before >共166条记录</template> <!--注意插槽在vue3中的应用---> <template #after > <el-button size="small" type="primary">新增员工</el-button> </template> </PageTools> <!-- 放置表格和分页 --> <el-card> <el-table border> <el-table-column label="序号" sortable="" /><!--sortable表示可以进行排序---> <el-table-column label="姓名" sortable="" /> <el-table-column label="手机号" sortable="" /> <el-table-column label="部门" sortable="" /> <el-table-column label="入职时间" sortable="" /> <el-table-column label="操作" sortable="" fixed="right" width="280"> <template #default> <el-button type="text" size="small">查看</el-button> <el-button type="text" size="small">转正</el-button> <el-button type="text" size="small">调岗</el-button> <el-button type="text" size="small">离职</el-button> <el-button type="text" size="small">编辑</el-button> <el-button type="text" size="small">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <el-row type="flex" justify="center" align="middle" style="height: 60px" > <el-pagination layout="prev, pager, next" :total="10" :page-size="2" /> </el-row> </el-card>
在app-container
这个div
内部添加如上内容。
返回到浏览器中查看效果
4、员工列表数据请求和分页加载 4.1 服务端接口设计 第一:首先构建搜索条件。
在 Cms.Entity
项目中创建一个Search
文件夹,在该文件夹中创建UserInfoSearch.cs
,代码如下所示:
1 2 3 4 5 6 7 8 9 namespace Cms.Entity.Search { public class UserInfoSearch :BaseSearch { public string ? UserName { get ; set ; } public string ? UserPhone { get ; set ; } } }
同时在Search
文件夹中创建BaseSearch.cs
,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 namespace Cms.Entity.Search { public class BaseSearch { public int PageIndex { get ;set ; } public int PageSize { get ; set ; } public int TotalCount { get ; set ; } public bool Order { get ; set ; } } }
第二:定义获取员工信息的服务接口与具体实现,如下所示:
1 2 3 4 5 6 7 8 namespace Cms.IService { public interface IUserInfoService :IBaseService <UserInfo > { IQueryable<UserInfo> LoadSearchEntities (UserInfoSearch userInfoSearch, bool delFlag ) ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public IQueryable<UserInfo> LoadSearchEntities (UserInfoSearch userInfoSearch, bool delFlag ) { var temp = _userInfoRepository.LoadEntities(u=>u.DelFlag==delFlag); if (!string .IsNullOrWhiteSpace(userInfoSearch.UserName)) { temp = temp.Where<UserInfo>(u => u.UserName!.Contains(userInfoSearch.UserName)); } if (!string .IsNullOrWhiteSpace(userInfoSearch.UserPhone)) { temp = temp.Where<UserInfo>(u => u.UserPhone!.Contains(userInfoSearch.UserPhone)); } userInfoSearch.TotalCount = temp.Count(); if (userInfoSearch.Order) { return temp.OrderBy<UserInfo, long >(u => u.Id).Skip<UserInfo>((userInfoSearch.PageIndex - 1 ) * userInfoSearch.PageSize).Take<UserInfo>(userInfoSearch.PageSize); } else { return temp.OrderByDescending<UserInfo, long >(u => u.Id).Skip<UserInfo>((userInfoSearch.PageIndex - 1 ) * userInfoSearch.PageSize).Take<UserInfo>(userInfoSearch.PageSize); } }
第三:控制器(UsersController.cs
)中代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [HttpGet("pages" ) ] [Authorize ] public IActionResult GetUsers ([FromQuery] UserParams userParams ) { int totalCount = 0 ; UserInfoSearch userInfoSearch = new UserInfoSearch() { UserName = userParams.UserName, UserPhone = userParams.UserPhone, Order = userParams.Order, PageIndex = userParams.PageIndex, PageSize = userParams.PageSize, TotalCount =totalCount }; var users = _userInfoService.LoadSearchEntities(userInfoSearch, Convert.ToBoolean(DelFlagEnum.Normal)); if (userInfoSearch.TotalCount <= 0 ) { return NotFound("没有用户信息" ); } var usersDepments = users.Include(u => u.Department).Select(u => new {Id=u.Id,UserName=u.UserName, UserPhone=u.UserPhone, CreateTime=u.CreateTime, departmentName=u.Department!.DepartmentName }); return Ok(new ApiResult<object >() {Success=true ,Message="获取用户信息成功" ,Data =new { rows= usersDepments,total=userInfoSearch.TotalCount } }); }
上面GetUsers
方法接收到的参数类型是UserParams
类型,在WebApi
项目中创建ResourceParams
文件夹,在该文件夹中创建UserParams.cs
,代码如下所示:
1 2 3 4 5 6 7 8 namespace Cms.WebApi.ResourceParams { public class UserParams :BaseParams { public string ? UserName { get ; set ; } public string ? UserPhone { get ; set ; } } }
BaseParams.cs
中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 namespace Cms.WebApi.ResourceParams { public class BaseParams { public int PageIndex { get ; set ; } public int PageSize { get ; set ; } public bool Order { get ; set ; } } }
注意:这里不包含totalCount
4.2前端实现 这里先定义获取员工列表数据的接口
在src/api/employees.js
文件中进行定义
1 2 3 4 5 6 7 8 9 export function getEmployeeList (params ) { return request ({ url : "/users/pages" , params, }); }
下面修改views/employees/index.vue
这个组件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <script> import { ref,onMounted} from 'vue' import { getEmployeeList } from "@/api/employees" ; export default { name : "Employees" , setup ( ){ const loading = ref (false ); const list = ref ([]); const page = ref ({ PageIndex : 1 , PageSize : 2 , total :0 }); onMounted (() => { loadEmployeeList (); }); const loadEmployeeList = async ( ) => { loading.value = true ; const { rows, total } = await getEmployeeList (page.value ); page.value .total = total; list.value = rows; loading.value = false ; }; return { loading, list, page, }; } } </script>
下面修改模板中的代码
1 2 3 4 5 6 7 <div class="app-container"> <PageTools :showBefore="true"> <!--使用了PageTools这个公共组件--> <template #before>共{{ page.total }}条记录</template> <!--注意插槽在vue3中的应用---> <template #after > <el-button size="small" type="primary">新增员工</el-button> </template> </PageTools>
这里展示了page.total
属性中存储的总的记录数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <el-card v-loading="loading"> <el-table border :data="list"> <el-table-column label="序号" type="index" /><!--sortable表示可以进行排序---> <el-table-column label="姓名" prop="userName" /> <el-table-column label="手机号" prop="userPhone" /> <el-table-column label="部门" prop="departmentName"/> <el-table-column label="入职时间" prop="createTime" /> <el-table-column label="操作" fixed="right" width="280"> <template #default> <el-button size="small">查看</el-button> <el-button size="small">转正</el-button> <el-button size="small">调岗</el-button> <el-button size="small">离职</el-button> <el-button size="small">编辑</el-button> <el-button size="small">删除</el-button> </template> </el-table-column> </el-table>
给el-card
组件添加了v-loading
属性。
同时给el-table
添加了data
属性关联了list
,也就是指定了表格要展示的数据源。
下面给序号列
指定了type=index
给其它的列指定了prop
属性,指定要展示的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!-- 分页组件 --> <el-row type="flex" justify="center" align="middle" style="height: 60px" > <el-pagination layout="prev, pager, next" :total="page.total" :page-size="page.PageSize" v-model:currentPage="page.PageIndex" @current-change="handleCurrentChange" /> </el-row>
这里给el-pagination
这个分页组件指定了total
和page-size
属性的值
还要实现分页的功能,也就是实现页码切换的功能。
这里给分页
组件绑定了currentPage
属性,表示当前页码值。取值是page
对象的page
属性。
同时指定了current-change
事件,当进行页码切换的时候会触发该事件,执行handleCurrentChange
这个方法,下面实现该方法:
1 2 3 4 5 6 7 8 9 10 11 const handleCurrentChange =(value )=>{ page.value .PageIndex = value; loadEmployeeList (); } return { loading, list, page, handleCurrentChange }; }
这里将切换后得到的页码值赋值给了page
对象中的page
属性,然后调用loadEmployeeList
方法重新加载员工数据。
最后将handleCurrentChange
方法返回。
下面返回到浏览器中进行测试。
5、数据格式处理 这里是对展示的日期格式进行处理。
1 <el-table-column label="入职时间" prop="createTime" :formatter="formatTimeOfEntry" />
这里也是给入职时间
这一列中添加了formatter
属性,该属性的值是formatTimeOfEntry
方法。
该方法的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const formatTimeOfEntry = (row, column, cellValue, index ) => { const date = new Date (cellValue); return ( date.getFullYear () + "-" + (date.getMonth () + 1 ) + "-" + date.getDate () ); }; return { loading, list, page, handleCurrentChange, formatTimeOfEntry };
这里对日期时间做了简单的处理
返回到浏览器中查看效果。
6、新建员工弹出层组件 在views/employees
目录下面创建components
目录,在该目录下面创建add-employee.vue
组件,该组件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <template> <el-dialog title="新增员工" v-model="visible"> <!-- 表单 --> <el-form label-width="120px"> <el-form-item label="姓名"> <el-input style="width: 50%" placeholder="请输入姓名" /> </el-form-item> <el-form-item label="手机"> <el-input style="width: 50%" placeholder="请输入手机号" /> </el-form-item> <el-form-item label="密码"> <el-input style="width: 50%" placeholder="请输入密码" /> </el-form-item> <el-form-item label="入职时间"> <el-date-picker style="width: 50%" placeholder="请选择入职时间" /> </el-form-item> <el-form-item label="邮箱"> <el-input style="width: 50%" placeholder="请输入邮箱" /> </el-form-item> <el-form-item label="部门"> <el-input style="width: 50%" placeholder="请选择部门" /> </el-form-item> <el-form-item label="转正时间"> <el-date-picker style="width: 50%" placeholder="请选择转正时间" /> </el-form-item> <el-form-item label="性别"> <el-radio-group class="ml-4"> <el-radio label="1" size="large">男</el-radio> <el-radio label="0" size="large">女</el-radio> </el-radio-group> </el-form-item> </el-form> <!-- footer插槽 --> <template #footer> <el-row type="flex" justify="center"> <el-col :span="6"> <el-button size="small">取消</el-button> <el-button type="primary" size="small">确定</el-button> </el-col> </el-row> </template> </el-dialog> </template> <script> import {ref} from 'vue' export default { name: "AddEmployee", props: { showDialog: { type: Boolean, default: true, // 这里先将默认值修改为`true`,查看效果,后面在将其修改为false }, }, setup(props){ const visible =ref(false); visible.value = props.showDialog; return { visible } } }; </script>
以上通过showDialog
属性接收传递过来的值,决定弹出层的展示与隐藏。
下面返回到employees/index.vue
组件中使用上面创建出来的添加员工组件。
1 2 3 4 5 6 import AddEmployee from "./components/add-employee.vue" ;export default { name : "Employees" , components :{ AddEmployee },
导入AddEmployee
组件,并且完成注册。
在模板中使用
1 2 3 4 5 </el-row> </el-card> <AddEmployee></AddEmployee> </div> </div>
返回到浏览器中进行测试。
7、控制弹出层显示 当单击页面右上角的新增员工
按钮的时候,将添加员工的弹出层展示出来。
employees/index.vue
组件中修改代码:
1 2 3 4 onMounted (() => { loadEmployeeList (); }); const showAddDialog = ref (false );
定义了showAddDialog
这个状态
1 2 3 4 5 6 7 8 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog };
将showAddDialog
返回
1 <el-button size="small" type="primary" @click="showAddDialog=true">新增员工</el-button>
这里给新增员工
的按钮注册点击事件,事件触发以后将showAddDialog
设置为true
同时传递到添加员工的子组件中。
1 <AddEmployee :showDialog="showAddDialog"></AddEmployee>
当然,AddEmployee
这个组件中的代码还需要修改一下
1 2 3 4 5 6 7 8 9 10 11 setup (props ){ const visible =ref (false ); watch (()=> props.showDialog ,(newValue,oldValue )=> { visible.value = newValue; }) return { visible } }
当然,这里需要导入watch
1 import {ref,watch} from 'vue'
返回到浏览器中进行测试。
8、新增员工表单校验 在这一小节中,将要完成的是新增员工
的表单校验。
修改views/employees/components/add-employee.vue
组件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 setup (props ){ const visible =ref (false ); const formData = ref ({ UserName :"" , UserPhone :"" , UserPassword :"" , UserEmail :'' , Gender :"" , DepartmentName :'' , CreateTime :'' , UpdateTime :'' }) const rules = reactive ({ UserName : [ { required : true , message : "用户姓名不能为空" , trigger : "blur" }, { min : 1 , max : 16 , message : "用户姓名为1-16位" , }, ], UserPhone : [ { required : true , message : "手机号不能为空" , trigger : "blur" }, { pattern : /^1[3-9]\d{9}$/ , message : "手机号格式不正确" , trigger : "blur" , }, ], UserPassword : [ { required : true , message : "密码不能为空" , trigger : "blur" }, ], UserEmail : [ { required : true , message : "邮箱不能为空" , trigger : "blur" }, ], DepartmentName : [ { required : true , message : "部门不能为空" , trigger : "change" }, ], CreateTime : [{ required : true , message : "入职时间" , trigger : "blur" }], UpdateTime : [{ required : true , message : "转正时间" , trigger : "blur" }], }); return { visible, formData, rules }
在上面的代码中定义了formData
对象,该对象中的属性都是与表单进行了双向的绑定。
这些属性都是通过查看接口文档获取到的。
模板中表单中添加相应的属性,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <!-- 表单 --> <el-form label-width="120px" :model="formData" :rules="rules"> <el-form-item label="姓名" prop="UserName"> <el-input style="width: 50%" placeholder="请输入姓名" v-model="formData.UserName" /> </el-form-item> <el-form-item label="手机" prop="UserPhone"> <el-input style="width: 50%" placeholder="请输入手机号" v-model="formData.UserPhone" /> </el-form-item> <el-form-item label="密码" prop="UserPassword"> <el-input style="width: 50%" placeholder="请输入密码" v-model="formData.UserPassword" /> </el-form-item> <el-form-item label="入职时间" prop="CreateTime"> <el-date-picker style="width: 50%" placeholder="请选择入职时间" v-model="formData.CreateTime" /> </el-form-item> <el-form-item label="邮箱" prop="UserEmail"> <el-input style="width: 50%" placeholder="请输入邮箱" v-model="formData.UserEmail"/> </el-form-item> <el-form-item label="部门" prop="DepartmentName"> <el-input style="width: 50%" placeholder="请选择部门" v-model="formData.DepartmentName" /> </el-form-item> <el-form-item label="转正时间" prop="UpdateTime"> <el-date-picker style="width: 50%" placeholder="请选择转正时间" v-model="formData.UpdateTime" /> </el-form-item> <el-form-item label="性别"> <el-radio-group class="ml-4" v-model="formData.Gender"> <el-radio label="0" size="large">男</el-radio> <el-radio label="1" size="large">女</el-radio> </el-radio-group> </el-form-item> </el-form>
这里先给el-form
组件添加了model,rules
同时给el-form-item
组件添加prop
属性完成校验。
给表单项
添加v-model
属性完成双向绑定。
返回到浏览器中查看效果。
9、加载部门数据 在新增员工的表单中有一项是部门
员工的部门是从树形部门中选择一个部门
下面看一下怎样获取部门数据,将其转换成一个树形结构,在下拉框中进行展示。
下面修改views/employees/components/add-employee.vue
组件中的代码,如下所示:
1 2 3 import {ref,watch,reactive} from 'vue' import { getDepartments } from "@/api/departments" ; import { tranListToTreeData } from "@/utils" ;
1 2 3 4 const treeData = ref ([]); const showTree = ref (false ); const loading = ref (false ); const visible =ref (false );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const getDepartmentsInfo = async ( ) => { showTree.value = true ; loading.value = true ; const depts = await getDepartments (); treeData.value = tranListToTreeData (depts,0 ); loading.value = false ; }; return { visible, formData, rules, getDepartmentsInfo, treeData, showTree, loading, }
定义getDepartmentsInfo
方法,获取部门数据,并且组织成树形结构。
并且将相关的内容返回。
下面修改模板中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 <el-form-item label="部门" prop="DepartmentName"> <el-input style="width: 50%" placeholder="请选择部门" v-model="formData.DepartmentName" @focus="getDepartmentsInfo" /><!--当文本框失去焦点后调用getDepartmentsInfo方法。---> </el-form-item> <!-- 放置一个el-tree组件 --> <el-tree v-if="showTree" v-loading="loading" :data="treeData" :default-expand-all="true" :props="{ label: 'departmentName' }" />
在部门的el-form-item
这个组件下面添加一个树形组件。当showTree
属性为true
的时候进行展示,通过data
指定数据源,default-expand-all
属性的值为true
表示展示整个树形结构。同时通过props
中的label
属性指定树形结构中展示的内容是来自name
属性。
问题:什么时候调用getDepartmentsInfo
方法来获取部门数据?
当el-input
这个选择部门
的文本框获取到了焦点后来获取部门数据,并且将其转换成树形结构,填充到树形组件中。
所以要给该el-input
这个组件添加@focus
事件。
返回到浏览器中进行测试,查看效果。
10、选择部门 接下来要实现的功能是当单击了部门名称以后,将其填充到文本框中,
修改views/employees/components/add-employee.vue
组件中的代码:
1 2 3 4 5 6 7 8 9 <!-- 放置一个el-tree组件 --> <el-tree v-if="showTree" v-loading="loading" :data="treeData" :default-expand-all="true" :props="{ label: 'departmentName' }" @node-click="selectNode" />
给el-tree
这个组件中添加了node-click
事件,当单击了树形结构中的节点的时候,会触发该事件。
调用selectNode
这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const selectNode = (node ) => { formData.value .DepartmentName = node.departmentName ; showTree.value = false ; }; return { visible, formData, rules, getDepartmentsInfo, treeData, showTree, loading, selectNode }
这里传递给selectNode
方法的参数node
中存储了所单击的部门的信息。
将其赋值给formData
对象中的DepartmentName
属性,而该属性已经与部门的文本框进行了绑定,所以所单击的部门的名称会展示到文本框中。
并将树形结构隐藏隐藏掉。
最后将selectNode
方法返回。
返回到浏览器中进行测试。
11、完成新增员工 11.1 前端实现 在src/api/employees.js
文件中添加请求的方法,如下所示:
1 2 3 4 5 6 7 8 9 10 export function addEmployee (data ) { return request ({ method : 'post' , url : '/users' , data }) }
修改views/employees/components/add-employee.vue
组件中的代码
1 2 import { tranListToTreeData } from "@/utils" ; import { addEmployee } from "@/api/employees" ;
这里将addEmployee
导入进来。
1 2 const visible =ref (false ); const formRef = ref (null );
创建formRef
对象与表单进行关联。
1 2 3 4 5 6 7 8 9 const btnOk = (formEl ) => { formEl.validate (async (valid, fields) => { if (valid) { await addEmployee (formData.value ); emit ("addEmployee" ); emit ("changedialog" , false ); } });
创建btnOk
方法,当单击确定
按钮的时候,会调用该方法,同时获取表单中的数据,传递到addEmployee
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 return { visible, formData, rules, getDepartmentsInfo, treeData, showTree, loading, selectNode, btnOk, formRef }
这里将btnOk
和formRef
返回。
1 2 <!-- 表单 --> <el-form label-width="120px" :model="formData" :rules="rules" ref="formRef">
这里将formRef
对象与el-form
表单关联在一起。
1 2 3 4 5 6 7 8 9 <!-- footer插槽 --> <template #footer> <el-row type="flex" justify="center"> <el-col :span="6"> <el-button size="small">取消</el-button> <el-button type="primary" size="small" @click="btnOk(formRef)">确定</el-button> </el-col> </el-row> </template>
这里给确定
按钮添加了click
事件,事件触发以后调用btnOk
方法,传递了formRef
对象。
通过校验以后,在btnOk
方法的内部调用了addEmployee
方法,发送请求,完成员工信息的添加。
然后触发了以下两个事件
1 2 emit ("addEmployee" ); emit ("changedialog" , false );
1 setup (props, { emit } ) {
这里通过emits
声明了两个事件,同时在setup
入口函数中,在第二个参数中解构出了emit
下面返回到父组件index.vue
中。
1 <AddEmployee :showDialog="showAddDialog" @addEmployee="loadEmployeeList" @changedialog="updatedialog"></AddEmployee>
给AddEmployee组件添加事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const updatedialog = (value ) => { showAddDialog.value = value; }; return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList };
在updatedialog
方法中关闭了窗口。同时将该方法和loadEmployeeList
方法返回。
下面是关于取消
按钮
返回到add-employee.vue
组件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const btnCanel = (formEl ) => { formData.value = {}; formEl.resetFields (); emit ("changedialog" , false ); }; return { visible, formData, rules, getDepartmentsInfo, treeData, showTree, loading, selectNode, btnOk, formRef, btnCanel } }
这里,创建了取消的方法btnCanel
,将该方法返回
1 2 3 <template> <el-dialog title="新增员工" v-model="visible" @close="btnCanel(formRef)"> <!-- 表单 -->
给el-dialog
组件添加了@close
事件,该事件触发说明了单击了窗口右上角的叉号图标。调用btnCanel
方法。
1 <el-button size="small" @click="btnCanel(formRef)">取消</el-button>
这里给取消
按钮也添加了单击事件,事件触发以后调用btnCanel
方法。
以上是前端代码的实现。
11.2 服务端接口实现 在UsersController.cs
控制器中,实现新增员工的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [HttpPost ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task <IActionResult> AddUser([FromBody]UserInfoDto userInfoDto) { var departMent = await this ._departmentService.LoadEntities(d => d.DepartmentName == userInfoDto.DepartmentName).FirstOrDefaultAsync(); UserInfo userInfo = new UserInfo() { PhotoUrl= "/images/f.jpg" , UserName = userInfoDto.UserName, UserPhone = userInfoDto.UserPhone, CreateTime = userInfoDto.CreateTime, DelFlag = Convert.ToBoolean(DelFlagEnum.Normal), Gender = userInfoDto.Gender, UpdateTime = userInfoDto.UpdateTime, UserPassword = userInfoDto.UserPassword, UserEmail = userInfoDto.UserEmail, Department = departMent, }; await _userInfoService.InsertEntityAsync(userInfo); return Ok(new ApiResult<UserInfoDto>() {Success=true ,Message="添加用户成功" ,Data= userInfoDto }); }
接收数据,需要定义UserInfoDto
来实现,原因是:原有的UserInfo
这个实体类中没有DepartmentName
这个部门名称的属性,而我们自己定义UserInfoDto
这个数据传输对象,可以添加该属性。
在上面的代码中,我们没有使用自动映射AutoMapper
,是我们自己实现了映射,代码稍微有点麻烦,但是大家要理解原理。
在WebApi
项目中创建的models
目录下面创建UserInfoDto.cs
这个数据传输对象,该对象中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 namespace Cms.WebApi.models { public class UserInfoDto { public string ? UserName { get ; set ; } public string ? UserPassword { get ; set ; } public string ? UserEmail { get ; set ; } public string ? UserPhone { get ; set ; } public int Gender { get ; set ; } public string ? PhotoUrl { get ; set ; } public long Id { get ; set ; } public DateTime CreateTime { get ; set ; } public DateTime UpdateTime { get ; set ; } public bool DelFlag { get ; set ; } public string ? DepartmentName { get ; set ; } }
启动项目进行测试。
1 2 3 4 5 6 7 8 private IUserInfoService _userInfoService;private IDepartmentService _departmentService;public UsersController (IUserInfoService userInfoService,IDepartmentService departmentService ){ this ._userInfoService = userInfoService; this ._departmentService = departmentService; }
这里还需要完成departmentService
注入的操作
七、角色管理模块 什么是RBAC
RBAC
(全称是Role-Based Access Control
)基于角色的权限访问控制,在RBAC
中,权限与角色相关联,用户通过成为合适的角色的成员从而得到这些角色的权限,这样就极大的简化了权限的管理。
RBAC
是目前公认的解决大型企业的统一资源访问控制的有效方法。
1、创建角色模型 在Cms.Entity
类库项目中创建RoleInfo.cs
,代码如下所示:
1 2 3 4 5 6 7 8 9 10 namespace Cms.Entity { public class RoleInfo :BaseEntity <long > { public string ? RoleName { get ; set ; } public string ? Remark { get ; set ;} public List<UserInfo> UserInfos { get ; set ; }=new List<UserInfo>(); } }
用户与角色是多对多的关系。
修改UserInfo.cs
实体类中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 public string ? PhotoUrl { get ; set ;} public Department? Department { get ; set ; } public List<RoleInfo> RoleInfos { get ; set ; } = new List<RoleInfo>();
在上面的代码中,指定了一个用户有多个角色。
创建RoleInfoConfig.cs
,对应的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 namespace Cms.Entity { public class RoleInfoConfig : IEntityTypeConfiguration <RoleInfo > { public void Configure (EntityTypeBuilder<RoleInfo> builder ) { builder.ToTable("T_RoleInfos" ); builder.Property(x => x.RoleName).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.Remark).IsRequired(); builder.HasMany<UserInfo>(s => s.UserInfos).WithMany(s => s.RoleInfos).UsingEntity(a => a.ToTable("T_UserInfos_RoleInfos" )); } } }
下面修改MyDbContext.cs
中的代码,如下所示:
1 2 3 4 5 6 public class MyDbContext :DbContext { public DbSet<UserInfo>UserInfos { get ; set ; } public DbSet<Department> DepartmentInfos { get ; set ; } public DbSet<RoleInfo> RoleInfos { get ; set ; }
在上面的代码中,创建了RoleInfos
这个DbSet
.
下面进行数据迁移的操作
在[程序包管理器控制台]
中,选择Cms.EntityFrameWorkCore
执行如下命令,完成数据的迁移操作。
1 2 Add-Migration CreateRoleInfo Update-database
2、前端角色列表布局实现 在Views
目录下面创建roles
文件夹,在该文件夹中创建index.vue
组件,该组件中的基本代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <template> <div class="dashboard-container"> <div class="app-container"> <el-card> <!-- 新增角色按钮 --> <el-row style="height: 60px"> <el-button size="small" type="primary">新增角色</el-button> </el-row> <!-- 表格 --> <el-table :border="true"> <el-table-column label="序号" width="120" /> <el-table-column label="角色名称" width="240" /> <el-table-column label="描述" /> <el-table-column label="操作"> <el-button size="small" type="success">分配权限</el-button> <el-button size="small" type="primary">编辑</el-button> <el-button size="small" type="danger">删除</el-button> </el-table-column> </el-table> <!-- 分页组件 --> <el-row type="flex" justify="center" style="height: 60px"> <!-- 分页组件 --> <el-pagination layout="prev,pager,next" :total="100" /> </el-row> </el-card> </div> </div> </template>
以上是角色列表的基本布局。
下面定义对应的前端角色列表组件对应的路由规则。
在router/modules
目录下面创建roleInfo.js
,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import Layout from "@/layout" ;export default { path : "/roles" , name : "roles" , component : Layout , children : [ { path : "" , name :'rolesChild' , component : () => import ("@/views/roles" ), meta : { title : "角色管理" , icon :"setting" }, }, ], };
对应的router
目录下的index.js
文件中的代码,如下所示:
1 2 3 import employeesRouter from './modules/employees' import departmentsRouter from './modules/departments' import roleRouter from './modules/roleInfo'
指定动态路由规则:
1 2 3 4 5 6 export const asyncRoutes =[ employeesRouter, departmentsRouter, roleRouter ];
返回浏览器,查看前端展示效果。
3、角色列表接口定义 下面先创建数据仓库接口
定义IRoleInfoService.cs
,代码如下所示:
1 2 3 4 5 6 7 8 namespace Cms.IRepository { public interface IRoleInfoRepository :IBaseRepository <RoleInfo > { } }
数据仓储的具体实现,如下所示:
1 2 3 4 5 6 7 namespace Cms.Repository { public class RoleInfoRepository :BaseRepository <RoleInfo >,IRoleInfoRepository { public RoleInfoRepository (MyDbContext context ) : base (context ) { } } }
定义服务的接口
1 2 3 4 5 6 namespace Cms.IService { public interface IRoleInfoService :IBaseService <RoleInfo > { } }
服务的具体实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 namespace Cms.Service { public class RoleInfoService :BaseService <RoleInfo >,IRoleInfoService { private readonly IRoleInfoRepository _roleInfoRepository; public RoleInfoService (IRoleInfoRepository roleInfoRepository ) { base .repository = roleInfoRepository; _roleInfoRepository = roleInfoRepository; } } }
构建搜索条件,在Cms.Entity
项目中的Search
文件夹中创建RoleSearch.cs
,代码如下所示:
1 2 3 4 5 6 7 8 namespace Cms.Entity.Search { public class RoleSearch :BaseSearch { public string ? RoleName { get ; set ; } public string ? Remark { get ; set ; } } }
定义服务层中,关于获取角色列表的接口
1 2 3 4 5 6 7 8 namespace Cms.IService { public interface IRoleInfoService :IBaseService <RoleInfo > { IQueryable<RoleInfo> LoadSearchEntities (RoleSearch roleInfoSearch, bool delFlag ) ; } }
具体实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 namespace Cms.Service { public class RoleInfoService :BaseService <RoleInfo >,IRoleInfoService { private readonly IRoleInfoRepository _roleInfoRepository; public RoleInfoService (IRoleInfoRepository roleInfoRepository ) { base .repository = roleInfoRepository; _roleInfoRepository = roleInfoRepository; } public IQueryable<RoleInfo> LoadSearchEntities (RoleSearch roleInfoSearch, bool delFlag ) { var temp = _roleInfoRepository.LoadEntities(r => r.DelFlag == delFlag); if (!string .IsNullOrWhiteSpace(roleInfoSearch.RoleName)) { temp = temp.Where<RoleInfo>(r=>r.RoleName!.Contains(roleInfoSearch.RoleName)); } if (!string .IsNullOrWhiteSpace(roleInfoSearch.Remark)) { temp = temp.Where<RoleInfo>(r=>r.Remark!.Contains(roleInfoSearch.Remark)); } roleInfoSearch.TotalCount = temp.Count(); if (roleInfoSearch.Order) { return temp.OrderBy<RoleInfo, long >(u => u.Id).Skip<RoleInfo>((roleInfoSearch.PageIndex - 1 ) * roleInfoSearch.PageSize).Take<RoleInfo>(roleInfoSearch.PageSize); } else { return temp.OrderByDescending<RoleInfo, long >(u => u.Id).Skip<RoleInfo>((roleInfoSearch.PageIndex - 1 ) * roleInfoSearch.PageSize).Take<RoleInfo>(roleInfoSearch.PageSize); } } } }
构建参数
在WebApi
项目中的ResourceParams
文件夹中创建RoleParams.cs
,代码如下所示:
1 2 3 4 5 6 7 8 namespace Cms.WebApi.ResourceParams { public class RoleParams :BaseParams { public string ? RoleName { get ;set ; } public string ? Remark { get ; set ; } } }
创建RoleInfoController.cs
控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 namespace Cms.WebApi.Controllers { [Route("api/[controller]" ) ] [ApiController ] public class RolesController : ControllerBase { private readonly IRoleInfoService _roleInfoService; public RolesController (IRoleInfoService roleInfoService ) { this ._roleInfoService = roleInfoService; } [HttpGet ] [Authorize ] public IActionResult GetRoles ([FromQuery]RoleParams roleParams ) { int totalCount = 0 ; RoleSearch roleSearch = new RoleSearch() { Order = roleParams.Order, PageIndex = roleParams.PageIndex, PageSize = roleParams.PageSize, Remark = roleParams.Remark, RoleName = roleParams.RoleName, TotalCount = totalCount }; var roles = _roleInfoService.LoadSearchEntities(roleSearch, Convert.ToBoolean(DelFlagEnum.Normal)); if (roles.Count() <= 0 ) { return BadRequest("没有角色信息" ); } return Ok(new ApiResult<object >() { Success = true , Message = "获取角色成功" , Data = new { rows = roles, total = roleSearch.TotalCount } }); } } }
以上是接口的具体实现。
4、前端角色信息展示 在这一小节中,我们要实现的是展示角色信息
在src/api/
目录下面创建role.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 import request from "@/utils/request" ;export function getRoleList (params ){ return request ({ method :'get' , url :'/roles' , params }) }
params是查询参数,里面需要携带分页信息
下面修改view/roles/index.vue
组件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <script> import {reactive, ref,onMounted, toRefs} from 'vue' import { getRoleList } from '@/api/role' ; export default { name :'Role' , setup ( ){ const list = ref ([]); const page =reactive ({ params :{ PageIndex :1 , PageSize :2 , total :0 } }) onMounted (()=> { getRoleListInfo (); }) const getRoleListInfo = async ( )=>{ const {total,rows} = await getRoleList (page.params ); page.params .total = total; list.value = rows; } return { list, ...toRefs (page) } } } </script>
在上面的代码中,导入getRoleList
.
同时定义list
集合存储服务端返回的角色数据。
定义page
对象,对象中的params
也是一个对象,其中的PageIndex
和PageSize
分别表示当前页码和每页显示的记录数。
total
:表示总的记录数。
在onMounted
这个钩子函数中调用了getRoleListInfo
方法,在该方法调用了getRoleList
方法,发送请求。
服务端返回的数据中包含了total
和rows
.
total
:表示总的记录数
rows
: 表示获取到的角色数据。
下面把数据展示到表格中。
1 2 3 4 return { list, ...toRefs (page) }
在返回的数据中,将page
进行了解构,为了保证解构出的内容还是响应式的,这里通过toRefs
函数进行了转换。
下面在表格中展示上面所返回的list
中的数据,如下所示:
1 2 3 4 5 6 7 8 9 10 11 <!-- 表格 --> <el-table :border="true" :data="list"> <el-table-column label="序号" width="120" type ="index" /> <el-table-column label="角色名称" width="240" prop ="roleName" /> <el-table-column label="描述" prop="remark" /> <el-table-column label="操作"> <el-button size="small" type="success">分配权限</el-button> <el-button size="small" type="primary">编辑</el-button> <el-button size="small" type="danger">删除</el-button> </el-table-column> </el-table>
这里给el-table
组件添加data
属性,绑定list
,也就是指定了数据源
同时给第一列添加了type='index'
表示第一列展示序号
然后给其它列通过prop
属性指定要展示的数据的属性.
返回到浏览器中查看效果
下面处理分页的问题:
1 2 3 4 5 6 7 8 9 10 11 <!-- 分页组件 --> <el-row type="flex" justify="center" style="height: 60px"> <!-- 分页组件 --> <el-pagination layout="prev,pager,next" :total="params.total" :page-size="params.PageSize" v-model:currentPage="params.PageIndex" @current-change="handleCurrentChange" /> </el-row>
这里给el-pagination
组件指定了total,page-size
,同时指定了currentPage
表示当前页。这里需要注意指定该属性的时候是通过v-model
进行了绑定。
并且指定了current-change
事件,当进行页码切换的时候,会触发该事件。
从而会执行handleCurrentChange
这个处理函数。该函数的实现如下所示:
1 2 3 4 5 6 7 8 9 const handleCurrentChange = (value ) => { page.params .page = value; getRoleListInfo (); }; return { list, ...toRefs (page), handleCurrentChange }
接收到的value
参数中存储的就是当前切换的页码。
然后再次调用了getRoleListInfo
方法,根据新的页码值重新发送请求,获取最新的数据。
将handleCurrentChange
函数也返回了。
1 import { ref, reactive, onMounted, toRefs } from "vue" ;
这里也需要导入toRefs
函数
返回到浏览器中进行测试。
5、为用户分配角色 关于角色的添加,编辑与删除,大家可以自己实现。
5.1 获取用户已经具有角色接口实现 在UsersController.cs
控制器中添加如下方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [HttpGet("{id}/roles" ) ] [Authorize ] public async Task<IActionResult> GetUserRoles ([FromRoute]int id ) { var userInfo =await _userInfoService.LoadEntities(u=>u.Id == id).FirstOrDefaultAsync(); if (userInfo == null ) { return BadRequest("没有查询到对应的用户" ); } var userAllRoles = _userInfoService.LoadEntities(u => u.Id == id).Select(r=>r.RoleInfos); var userRoleIdList = userAllRoles.Select(u=>u.Select(r=>r.Id) var roleInfoList = _roleInfoService.LoadEntities(r=>r.DelFlag==Convert.ToBoolean(DelFlagEnum.Normal)).ToList(); return Ok(new ApiResult<object > {Success=true ,Message="获取成功" , Data=new { userRoles=userRoleIdList,allRoles=roleInfoList } }); }
5.2 展示用户具有的角色-前端实现 在views/employees/index.vue
这个组件中,添加窗口,如下所示:
1 2 3 4 5 6 7 <AddEmployee :showDialog="showAddDialog" @addEmployee="loadEmployeeList" @changedialog="updatedialog"></AddEmployee> </div> <!--弹出分配角色的窗口--> <el-dialog v-model="showRoleDialog" title='为用户分配角色'> hello </el-dialog>
这里我们添加了一个el-dialog
组件,用来展示为用户分配角色的窗口,当然,这里也可以单独的封装成一个组件也是可以的。
通过v-model
的值showRoleDialog
,来控制窗口的弹出与因此。
1 2 3 setup ( ){ const loading = ref (false ); const showRoleDialog =ref (false );
在上面的代码中声明了showRoleDialog
,同时也需要将其返回
1 2 3 4 5 6 7 8 9 10 11 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList, showRoleDialog, }
下面在表格中添加一个分配角色的按钮,如下所示:
1 2 3 <template #default="scope"> <el-button size="small">查看</el-button> <el-button type="primary" size="small" @click="btnRole(scope.row)">分配角色</el-button>
这里给分配角色
的按钮注册了click
事件,事件触发以后,调用btnRole
这个函数,同时将scope.row
也就是要分配角色的用户信息传递该函数中,btnRole
函数的定义如下所示;
1 2 3 4 5 6 7 const btnRole = async (row )=>{ showRoleDialog.value = true ; const {userRoles,allRoles } = await getUserRoleList (row.id ); console .log ("userRoles=" ,userRoles); console .log ("allRoles=" ,allRoles); }
通过上面的代码,我们可以看到,这里调用了getUserRoleList
这个方法,并且传递了用户的编号。
api/employees.js
文件中定义该方法,如下所示:
1 2 3 4 5 export function getUserRoleList (id ){ return request (`/users/${id} /roles` ); }
在views/employees/index.vue
这个组件中,导入上面定义的getUserRoleList
这个方法
1 import { getEmployeeList,getUserRoleList } from "@/api/employees" ;
最后将btnRole
这个方法返回
1 2 3 4 5 6 7 8 9 10 11 12 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList, showRoleDialog, btnRole };
返回到浏览器中进行测试,在控制台中查看服务端返回的结果。
现在已经,获取到了所有的角色,以及用户已经具有的角色,下面要进行展示。
这里我们会给每个角色名称前面添加一个复选框,如果当前分配角色的用户以前已经具有了该角色,则让该复选框选中。
这里我们使用的是复选框CheckBox
的多选框组来进行构建:
1 https://element-plus.gitee.io/zh-CN/component/checkbox.html#%E5%A4%9A%E9%80%89%E6%A1%86%E7%BB%84
下面我们先将获取到的角色数据,赋值给一个响应对象
1 2 3 4 5 6 const showRoleDialog =ref (false ); const roles=reactive ({ userRoles :[], allRoles :[] })
1 2 3 4 5 6 7 8 const btnRole = async (row )=>{ showRoleDialog.value = true ; const {userRoles,allRoles } = await getUserRoleList (row.id ); roles.allRoles = allRoles; roles.userRoles = userRoles; }
在btnRole
方法中,我们将数据分别赋值给了roles
这个响应式对象中的allRoles
属性和userRoles
属性。
最后,需要将roles
这个对象返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList, showRoleDialog, btnRole, ...toRefs (roles), };
下面改造dialog
窗口中的内容,如下所示:
1 2 3 4 5 6 <el-dialog v-model="showRoleDialog" :title="`为用户分配角色`"> <el-checkbox-group v-model="roleIdList"> <el-checkbox v-for="item in allRoles" :label="item.id" :key="item.id">{{ item.roleName }}</el-checkbox> </el-checkbox-group> </el-dialog>
在el-dialog
窗口中使用了el-checkbox-group
,并且为其添加了v-model
属性进行双向绑定,这里绑定了roleIdList
。
作用是:当我们选中了某些复选框后,选中的复选框的内容,会保存到roleIdList
中。
下面在对el-checkbox
进行循环,这里展示的是角色的名称,同时label
属性的值是角色的编号,也就是说,我们选中了复选框后,要获取选中的复选框的值,而这些值就是角色的编号,当前选中的角色编号会自动的保存到roleIdList
中。
1 2 3 const loading = ref (false ); const roleIdList = ref ([]); const showRoleDialog =ref (false );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList, showRoleDialog, btnRole, ...toRefs (roles), username, roleIdList };
上面将roleIdList
这个数组返回了。
返回到浏览器中查看效果。
下面要实现的就是将用户已经具有的角色选中
这个需求实现起来比较简单
1 <el-checkbox-group v-model="roleIdList">
前面,我们给el-checkbox-group
添加了v-model
这个属性,实现了双向数据绑定,也就是说我们给roleIdList
赋值,对应的复选框也会选中。
1 2 3 4 5 6 7 8 9 10 const btnRole = async (row )=>{ username.value =row.userName ; const {userRoles,allRoles } = await getUserRoleList (row.id ); roles.allRoles = allRoles; roles.userRoles = userRoles; roleIdList.value = roles.userRoles [0 ]; showRoleDialog.value = true ; }
返回到浏览器中进行查看。
5.3 完成角色分配 5.3.1 前端实现 首先在窗口中增加“确定与取消”按钮。
并且给“确定”按钮注册单击事件,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 <el-dialog v-model="showRoleDialog" :title="`为用户分配角色`"> <el-checkbox-group v-model="roleIdList"> <el-checkbox v-for="item in allRoles" :label="item.id" :key="item.id">{{ item.roleName }}</el-checkbox> </el-checkbox-group> <!-- ----------------确定取消按钮 --> <el-row type="flex" justify="center"> <el-col :span="6"> <el-button type="primary" size="small" @click="btnRoleOk">确定</el-button> <el-button size="small" @click="showRoleDialog=false">取消</el-button> </el-col> </el-row> </el-dialog>
当单击确定
按钮的时候,会执行btnRoleOk
这个函数,在该函数中会发送请求。
所以这里我们还需要创建一个发送请求的方法。
在src/api/employees.js
文件中,添加如下方法
1 2 3 4 5 6 7 8 9 export function setUserRole (id,data ){ return request ({ method :'post' , url : `/users/${id} /roles/${data} ` , }) }
参数id
,表示的是用户的编号,data
就是要分配的角色的编号。
这里发送的是post
请求,并且是通过url
地址的方式传递参数。
返回到views/employee/index.vue
这个组件中继续修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 const btnRoleOk =async ( )=>{ const ids=roleIdList.value .length >0 ?roleIdList.value .join (',' ):'0' await setUserRole (userId.value ,ids); ElMessage ({ message :"分配角色成功" , type :'success' }) }
上面的代码实现了btnRoleOk
函数,在这函数中首先从roleIdList
中获取所选中的角色的编号,roleIdList
前面已经绑定了复选框
1 <el-checkbox-group v-model="roleIdList">
roleIdList
是一个数组,当选中了复选框后,所选中的复选框的角色编号会存储在该数组中,
在向服务端发送的时候,将该数组中存储的角色编号都转换成了字符串,然后使用逗号进行分割。
如果,在分配角色的时候,没有选择一个角色,也就是没有选择一个复选框,但是单击了确定按钮,这里会向服务端发送一个0,表示取消用户的角色(以前用户是有角色的,但是这里取消了选择)
问题是:在调用setUserRole
方法的时候,需要传递用户的编号,用户的编号怎样获取呢?
1 2 const showRoleDialog =ref (false ); const userId=ref (0 );
这里定义了userId
,用来存储要分配角色的用户的编号。
1 2 3 4 5 const btnRole = async (row )=>{ showRoleDialog.value = true ; userId.value = row.id ; const {userRoles,allRoles } = await getUserRoleList (row.id );
我们知道,当单击用户列表中的分配角色
按钮的时候,会首先调用btnRole
方法,在这方法中,传递过来的参数row
中存储了要分配角色的用户信息,将其编号存储到了userId
中。
所以,在btnRoleOk
方法中,就可以通过userId
来获取用户的编号。最后通过ElMessage
组件给出相应的提示。
当然,这里需要将setUserRole
方法导入进来。
1 import { getEmployeeList,getUserRoleList,setUserRole } from "@/api/employees" ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList, showRoleDialog, btnRole, ...toRefs (roles), roleIdList, btnRoleOk };
最后,返回btnRoleOk
函数
5.3.2服务端接口实现 第一步:在给某个用户分配角色的时候,我们需要获取该用户以前具有的角色,所以这里我们需要定义一个数据访问的仓储接口。
修改Cms.IRepository
项目中的IRoleInfoRepository
接口
1 2 3 4 5 6 7 namespace Cms.IRepository { public interface IRoleInfoRepository :IBaseRepository <RoleInfo > { Task<List<RoleInfo>> GetUserRoles(long userId); } }
在IRoleInfoRepository
接口中添加了GetUserRoles
方法,该方法的作用就是根据用户的编号,查询用户具有的角色信息。
第二:实现GetUserRoles
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace Cms.Repository { public class RoleInfoRepository :BaseRepository <RoleInfo >,IRoleInfoRepository { public RoleInfoRepository (MyDbContext context ) : base (context ) { } public async Task<List<RoleInfo>> GetUserRoles(long userId) { var userInfo = await ctx.UserInfos.Include(r => r.RoleInfos).Where(u => u.Id == userId).FirstOrDefaultAsync(); return userInfo!.RoleInfos.ToList(); } } }
在上面的代码中,我们通过父类 中注册的DbContext
对象ctx
来查询用户具有的角色信息,这里使用了include
来进行关联的查询。
最后返回的就是用户以前具有的角色信息,这里将其存储到了list
集合中返回。
第三:定义实现分配角色的服务接口
1 2 3 4 5 6 7 8 9 namespace Cms.IService { public interface IUserInfoService :IBaseService <UserInfo > { IQueryable<UserInfo> LoadSearchEntities (UserInfoSearch userInfoSearch, bool delFlag ) ; Task<bool > SetUserRoles (UserInfo userInfo, List<long > roleIdList ) ; } }
在上面的代码中,声明了SetUserRoles
方法,用来实现给某个用户分配角色。roleIdList
中存储的就是要分配的角色的编号。
第四:实现角色的分配业务
在UserInfoService.cs
服务类中实现了SetUserRoles
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public async Task< bool > SetUserRoles(UserInfo userInfo, List<long > strIds){ await _roleInfoRepository.GetUserRoles(userInfo.Id); userInfo.RoleInfos.Clear(); if (strIds.Count == 1 && strIds[0 ] == 0 ) { return true ; } foreach (var roleId in strIds) { var roleInfo = await _roleInfoRepository.LoadEntities(r => r.Id == roleId).FirstOrDefaultAsync(); userInfo!.RoleInfos.Add(roleInfo!); } return true ; }
在上面的代码中,根据传递过来的用户编号,调用roleInfoRepository
数据仓储中的GetUserRoles
方法,来获取该用户以前具有的角色信息。
在对用户进行分配角色的时候,我们是先将用户以前具有的角色删除。这里通过Clear
方法做了删除的标记。
然后,判断一下传递过来的角色编号数量是否是1,并且值是0,如果该条件成立,有两种可能
第一:什么角色也没有选,直接单击了确定按钮(所有的复选框都没有选中)
第二:把用户以前具有的角色都取消了,然后单击了确定按钮。
不管以上那种情况,这里我们都做了删除标记,即使第一种情况,进行删除操作也不会出错。
下面根据传递过来的角色编号,获取到每个角色信息,添加到userInfo.RoleInfos
这个集合中。
第五:控制器实现
在UsersController
控制器中添加SetUserRoles
方法,注意指定的路由规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [HttpPost("{id}/roles/{roleIds}" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] [Authorize ] public async Task<IActionResult> SetUserRoles ([FromRoute]int id, [FromRoute] string roleIds ) { var userInfo = await _userInfoService.LoadEntities(u => u.Id == id).FirstOrDefaultAsync(); if (userInfo == null ) { return BadRequest("没有查询到对应的用户" ); } List<long > list = new List<long >(); var strIds = roleIds.Split(',' ); foreach (var rId in strIds) { list.Add(Convert.ToInt32(rId)); } await _userInfoService.SetUserRoles(userInfo,list); return Ok(new ApiResult<object > { Success = true , Message = "获取成功" }); }
启动项目进行测试。
八、权限管理模块 现在已经为用户分配了角色,接下来需要做的就是给角色分配权限,这样用户就有相应的权限了。
在应用系统中,权限是以什么样的形式展现出来呢?对菜单的访问,页面上按钮的可见性,后端接口的控制,都要进行充分考虑.
前端菜单:根据是否有请求菜单权限进行动态加载
前端按钮(功能权限):根据是否具有次权限进行按钮的显示与隐藏的控制
后端:前端发送请求到后端的接口,有必要对接口的访问进行权限的验证。
针对以上的需求,在设计中可以将菜单,按钮,后端Api
请求作为资源,这样就构成了基于RBAC
的另外一种授权模型(用户-角色-权限-资源)
针对以上权限模型,其中权限究竟是属于菜单,按钮,还是API
的权限呢?那就需要在设计权限实体模型(数据库权限表)的时候添加类型加以区分,例如权限类型1是菜单,2是功能(按钮),3是api
1、创建权限模型 1.1 基础模型创建
权限模型与角色模型是多对多的关系
权限模型与菜单权限,功能权限,Api
权限都是一对一的关心,这里将不同的权限拆分到不同的模型(数据表)中的目的是为了方便扩展。
在Cms.Entity
类库项目中先创建权限模型PermissionInfo.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 namespace Cms.Entity { public class PermissionInfo :BaseEntity <long > { public string ? PermissionName { get ; set ; } public int PermissionType { get ; set ; } public string ? PermissionCode { get ; set ; } public string ? PermissionDescription { get ; set ;} public int ParentId { get ; set ; } } }
Api权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace Cms.Entity { public class PermissionApi :BaseEntity <long > { public string ? ApiUrl { get ; set ; } public string ? ApiMethod { get ; set ; } } }
菜单权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace Cms.Entity { public class PermissionMenu :BaseEntity <long > { public string ? MenuIcon { get ; set ; } public string ? MenunOrder { get ; set ; } } }
功能权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace Cms.Entity { public class PermissionPoint :BaseEntity <long > { public string ? PointClass { get ; set ; } public string ? PointIcon { get ; set ;} public int PointStatus { get ; set ;} } }
1.2 配置权限模型关系 先配置权限模型与角色模型之间的关系
在PermissionInfo.cs
这个模型中添加了RoleInfos
,表示一个权限有多个角色
1 2 3 4 5 6 7 public string ? ParentId { get ; set ; }public List<RoleInfo> RoleInfos { get ; set ; } = new List<RoleInfo>();
在RoleInfo.cs
中添加了PermissionInfos
,表示一个角色有多个权限
1 2 3 4 5 6 7 8 public class RoleInfo :BaseEntity <long > { public string ? RoleName { get ; set ; } public string ? Remark { get ; set ;} public List<UserInfo> UserInfos { get ; set ; }=new List<UserInfo>(); public List<PermissionInfo>PermissionInfos { get ; set ; } =new List<PermissionInfo>(); }
下面创建PermissionInfoConfig.cs
配置类
代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 public class PermissionInfoConfig : IEntityTypeConfiguration <PermissionInfo > { public void Configure (EntityTypeBuilder<PermissionInfo> builder ) { builder.ToTable("T_PermissionInfos" ); builder.Property(x => x.PermissionName).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.PermissionCode).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.PermissionDescription).HasMaxLength(500 ).IsRequired(); builder.HasMany<RoleInfo>(s => s.RoleInfos).WithMany(s => s.PermissionInfos).UsingEntity(a => a.ToTable("T_RoleInfos_Permissions" )); } }
在MyDbContext
中添加PermissionInfos
这个DbSet
1 2 3 4 5 6 7 public class MyDbContext :DbContext { public DbSet<UserInfo>UserInfos { get ; set ; } public DbSet<Department> DepartmentInfos { get ; set ; } public DbSet<RoleInfo> RoleInfos { get ; set ; } public DbSet<PermissionInfo> PermissionInfos { get ; set ; }
以上是角色模型与权限模型关系的配置。
下面就是权限模型与菜单权限模型,功能权限模型,Api
权限模型的配置。
继续修改PermissionInfo.cs
模型中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public int ParentId { get ; set ; } public PermissionApi? PermissionApi { get ; set ; } public PermissionMenu? PermissionMenu { get ; set ; } public PermissionPoint? PermissionPoint { get ; set ; } public List<RoleInfo> RoleInfos { get ; set ; } = new List<RoleInfo>();
在上面的代码中添加了 Api权限,菜单权限,功能权限
修改PermissionApi.cs
模型中的代码
1 2 3 4 5 6 7 8 public string ? ApiMethod { get ; set ; } public PermissionInfo? PermissionInfo { get ; set ; } public long PermissionId { get ; set ; }
PermissionPoint.cs
模型中的代码
1 2 3 4 5 6 7 8 public string ? PointStatus { get ; set ;} public PermissionInfo? PermissionInfo { get ; set ; } public long PermissionId { get ; set ; }
PermissionMenu.cs
模型中的代码
1 2 3 4 5 6 7 private string ? MenunOrder { get ; set ; } public PermissionInfo? PermissionInfo { get ; set ; } public long PermissionId { get ; set ; }
下面还需要配置PermissionInfoConfig.cs
模型,与上面三个模型一对一的关心(当然,在以上三个模型中进行配置也可以)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void Configure (EntityTypeBuilder<PermissionInfo> builder ) { builder.ToTable("T_PermissionInfos" ); builder.Property(x => x.PermissionName).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.PermissionCode).HasMaxLength(20 ).IsRequired(); builder.Property(x => x.PermissionDescription).HasMaxLength(500 ).IsRequired(); builder.HasMany<RoleInfo>(s => s.RoleInfos).WithMany(s => s.PermissionInfos).UsingEntity(a => a.ToTable("T_RoleInfos_Permissions" )); builder.HasOne<PermissionApi>(p => p.PermissionApi).WithOne(p => p.PermissionInfo).HasForeignKey<PermissionApi>(d => d.PermissionId); builder.HasOne<PermissionMenu>(p => p.PermissionMenu).WithOne(p => p.PermissionInfo).HasForeignKey<PermissionMenu>(d => d.PermissionId); builder.HasOne<PermissionPoint>(p => p.PermissionPoint).WithOne(p => p.PermissionInfo).HasForeignKey<PermissionPoint>(d => d.PermissionId); }
下面创建PermissionApiConfig.cs
1 2 3 4 5 6 7 8 9 public class PermissionApiConfig : IEntityTypeConfiguration <PermissionApi > { public void Configure (EntityTypeBuilder<PermissionApi> builder ) { builder.ToTable("T_PermissionApis" ); builder.Property(x => x.ApiUrl).HasMaxLength(200 ).IsRequired(); builder.Property(x => x.ApiMethod).HasMaxLength(20 ).IsRequired(); } }
下面创建PermissionMenuConfig.cs
1 2 3 4 5 6 7 8 9 public class PermissionMenuConfig : IEntityTypeConfiguration <PermissionMenu > { public void Configure (EntityTypeBuilder<PermissionMenu> builder ) { builder.ToTable("T_PermissionMenus" ); builder.Property(x => x.MenuIcon).HasMaxLength(200 ); builder.Property(x => x.MenunOrder).HasMaxLength(20 ); } }
下面创建PermissionPointConfig.cs
1 2 3 4 5 6 7 8 9 public class PermissionPointConfig : IEntityTypeConfiguration <PermissionPoint > { public void Configure (EntityTypeBuilder<PermissionPoint> builder ) { builder.ToTable("T_PermissionPoints" ); builder.Property(x => x.PointClass).HasMaxLength(200 ); builder.Property(x => x.PointIcon).HasMaxLength(20 ); } }
下面还需要修改MyDbContext.cs
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 public class MyDbContext :DbContext { public DbSet<UserInfo>UserInfos { get ; set ; } public DbSet<Department> DepartmentInfos { get ; set ; } public DbSet<RoleInfo> RoleInfos { get ; set ; } public DbSet<PermissionInfo> PermissionInfos { get ; set ; } public DbSet<PermissionApi> PermissionApiInfos { get ; set ; } public DbSet<PermissionMenu> PermissionMenuInfos { get ; set ; } public DbSet<PermissionPoint> PermissionPointInfos { get ; set ; }
最后进行数据的迁移
在[程序包管理器控制台]
中一定要选择[Cms.EntityFrameworkCore]
这个项目
1 2 Add-Migration AddPermission Update-database
2、添加权限 2.1 添加权限接口创建 第一:先实现所有的数据仓储
定义权限数据仓储接口,如下所示:
1 2 3 4 5 6 namespace Cms.IRepository { public interface IPermissionInfoRepository :IBaseRepository <PermissionInfo > { } }
定义权限数据仓储实现类,如下所示:
1 2 3 4 5 6 7 8 namespace Cms.Repository { public class PermissionInfoRepository :BaseRepository <PermissionInfo >,IPermissionInfoRepository { public PermissionInfoRepository (MyDbContext context ) : base (context ) { } } }
定义菜单权限
数据仓储接口,如下所示:
1 2 3 4 5 6 namespace Cms.IRepository { public interface IPermissionMenuRepository :IBaseRepository <PermissionMenu > { } }
菜单权限
数据仓储具体实现,如下所示:
1 2 3 4 5 6 7 8 namespace Cms.Repository { public class PermissionMenuRepository :BaseRepository <PermissionMenu >,IPermissionMenuRepository { public PermissionMenuRepository (MyDbContext context ) : base (context ) { } } }
Api
权限数据仓储接口
1 2 3 4 5 6 7 8 namespace Cms.IRepository { public interface IPermissionApiRepository :IBaseRepository <PermissionApi > { } }
Api
权限数据仓储具体实现,如下所示:
1 2 3 4 5 6 7 8 namespace Cms.Repository { public class PermissionApiRepository :BaseRepository <PermissionApi >, IPermissionApiRepository { public PermissionApiRepository (MyDbContext context ) : base (context ) { } } }
功能
权限数据仓储接口实现,如下所示:
1 2 3 4 5 6 7 namespace Cms.IRepository { public interface IPermissionPointRepository :IBaseRepository <PermissionPoint > { } }
功能权限
数据仓储具体实现
1 2 3 4 5 6 7 8 namespace Cms.Repository { public class PermissionPointRepository :BaseRepository <PermissionPoint >, IPermissionPointRepository { public PermissionPointRepository (MyDbContext context ) : base (context ) { } } }
第二:权限服务创建
实现权限服务的接口
1 2 3 4 5 6 namespace Cms.IService { public interface IPermissionInfoService :IBaseService <PermissionInfo > { } }
下面是权限服务的具体实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 namespace Cms.Service { public class PermissionInfoService :BaseService <PermissionInfo >, IPermissionInfoService { private readonly IPermissionInfoRepository _permissionInfoRepository; public PermissionInfoService (IPermissionInfoRepository permissionInfoRepository ) { base .repository = permissionInfoRepository; _permissionInfoRepository = permissionInfoRepository; } } }
针对其他权限的服务,如果需要再进行创建。
下面来实现具体的业务
在保存权限信息的时候,我们需要对权限的类别进行判断,所以这里需要再创建一个枚举类型。
在Cms.Entity
项目的Enum
目录下面创建PermissionTypeEnum.cs
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 namespace Cms.Entity.Enum { public enum PermissionTypeEnum { PermissionMenu =1 , PermissionPoint =2 , PermissionApi =3 } }
下面还需要定义一个保存权限信息的接口,所以需要修改IPermissionInfoService.cs
这个业务接口中的代码,需要添加如下方法:
1 2 3 4 5 6 7 8 namespace Cms.IService { public interface IPermissionInfoService :IBaseService <PermissionInfo > { Task<bool > AddPermission (PermissionDto permissionDto ) ; } }
这里我们需要一个PermissionDto
类来接收前端传递过来的权限信息,在Cms.Entity
这个类库项目中创建Permission
文件夹,在该文件夹中创建PermissionDto.cs
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 namespace Cms.Entity.Permission { public class PermissionDto :BaseEntity <long > { public string ? PermissionName { get ; set ; } public int PermissionType { get ; set ; } public string ? PermissionCode { get ; set ; } public string ? PermissionDescription { get ; set ; } public int ParentId { get ; set ; } public string ? ApiUrl { get ; set ; } public string ? ApiMethod { get ; set ; } public string ? MenuIcon { get ; set ; } public string ? MenunOrder { get ; set ; } public string ? PointClass { get ; set ; } public string ? PointIcon { get ; set ; } public int ? PointStatus { get ; set ; } }
这个类中定义的属性都是需要保存的权限信息。
下面我们要在具体的业务类PermissionInfoService.cs
中实现前面接口所以定义的AddPermission
这个方法,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 public class PermissionInfoService :BaseService <PermissionInfo >, IPermissionInfoService { private readonly IPermissionInfoRepository _permissionInfoRepository; private readonly IPermissionMenuRepository _permissionMenuRepository; private readonly IPermissionApiRepository _permissionApiRepository; private readonly IPermissionPointRepository _permissionPointRepository; public PermissionInfoService (IPermissionInfoRepository permissionInfoRepository, IPermissionMenuRepository permissionMenuRepository, IPermissionApiRepository permissionApiRepository, IPermissionPointRepository permissionPointRepository ) { base .repository = permissionInfoRepository; _permissionInfoRepository = permissionInfoRepository; _permissionMenuRepository = permissionMenuRepository; _permissionApiRepository = permissionApiRepository; _permissionPointRepository = permissionPointRepository; } public Task<bool > AddPermission (PermissionDto permissionDto ) { PermissionInfo permissionInfo = new PermissionInfo(); permissionInfo.ParentId = permissionDto.ParentId; permissionInfo.PermissionCode = permissionDto.PermissionCode; permissionInfo.PermissionName = permissionDto.PermissionName; permissionInfo.PermissionType=permissionDto.PermissionType; permissionInfo.PermissionDescription=permissionDto.PermissionDescription; permissionInfo.CreateTime=DateTime.Now; permissionInfo.UpdateTime=DateTime.Now; permissionInfo.DelFlag=Convert.ToBoolean(DelFlagEnum.Normal); _permissionInfoRepository.InsertEntityAsync(permissionInfo); bool result = false ; switch (permissionInfo.PermissionType) { case (int )PermissionTypeEnum.PermissionMenu: PermissionMenu permissionMenu = new PermissionMenu(); permissionMenu.MenuIcon = permissionDto.MenuIcon; permissionMenu.MenunOrder = permissionDto.MenunOrder; permissionMenu.CreateTime = DateTime.Now; permissionMenu.UpdateTime = DateTime.Now; permissionMenu.DelFlag = Convert.ToBoolean(DelFlagEnum.Normal); permissionMenu.PermissionInfo = permissionInfo; this ._permissionMenuRepository.InsertEntityAsync(permissionMenu); result = true ; break ; case (int )PermissionTypeEnum.PermissionPoint: PermissionPoint point = new PermissionPoint(); point.PointClass=permissionDto.PointClass; point.PointIcon=permissionDto.PointIcon; point.PointStatus=permissionDto.PointStatus; point.CreateTime = DateTime.Now; point.UpdateTime = DateTime.Now; point.DelFlag = Convert.ToBoolean(DelFlagEnum.Normal); point.PermissionInfo = permissionInfo; this ._permissionPointRepository.InsertEntityAsync(point); result = true ; break ; case (int )PermissionTypeEnum.PermissionApi: PermissionApi permissionApi = new PermissionApi(); permissionApi.ApiMethod = permissionDto.ApiMethod; permissionApi.ApiUrl = permissionDto.ApiUrl; permissionApi.CreateTime = DateTime.Now; permissionApi.UpdateTime = DateTime.Now; permissionApi.DelFlag = Convert.ToBoolean(DelFlagEnum.Normal); permissionApi.PermissionInfo = permissionInfo; this ._permissionApiRepository.InsertEntityAsync(permissionApi); result = true ; break ; default : break ; } return Task.FromResult(result); } }
在以上的操作中,我们没有使用AutoMapper
进行自动映射,这里是进行了手动的映射,相对来讲比较麻烦一下,但是需要大家理解整个原理。
下面实现具体的控制。
创建PermissionsController.cs
控制器,该控制器中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [Route("api/[controller]" ) ] [ApiController ] public class PermissionsController : ControllerBase { private readonly IPermissionInfoService _permissionInfoService; public PermissionsController (IPermissionInfoService permissionInfoService ) { this ._permissionInfoService = permissionInfoService; } [HttpPost ] [Authorize ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] public async Task <IActionResult > AddPermissions ([FromBody]PermissionDto permissionDto ) { await this ._permissionInfoService.AddPermission(permissionDto); return Ok(new ApiResult<object >() {Message="添加权限成功" ,Success=true }); } }
下面返回到浏览器中,在swagger/index.html
页面中测试该添加权限的接口。
3、权限列表 3.1 权限列表接口创建 这里我们先来构建权限列表展示的接口,后面再来考虑前端的设计。
这里也需要对权限的信息进行搜索,所以在Cms.Entity
类库项目中的Search
文件夹中创建PermissionSearch.cs
类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 namespace Cms.Entity.Search { public class PermissionSearch :BaseSearch { public string ? PermissionName { get ; set ; } public string ? PermissionDescription { get ;set ; } } }
下面在权限业务的接口中定义获取权限列表的方法,如下所示:
1 2 3 4 5 6 7 8 9 10 namespace Cms.IService { public interface IPermissionInfoService :IBaseService <PermissionInfo > { Task<bool > AddPermission (PermissionDto permissionDto ) ; IQueryable<PermissionInfo> LoadSearchEntities (PermissionSearch permissionSearch, bool delFlag ) ; } }
下面要实现上面定义的LoadSearchEntities
这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public IQueryable<PermissionInfo> LoadSearchEntities (PermissionSearch permissionSearch, bool delFlag ) { var temp = _permissionInfoRepository.LoadEntities(u => u.DelFlag == delFlag); if (!string .IsNullOrWhiteSpace(permissionSearch.PermissionName)) { temp = temp.Where<PermissionInfo>(u => u.PermissionName!.Contains(permissionSearch.PermissionName)); } if (!string .IsNullOrWhiteSpace(permissionSearch.PermissionDescription)) { temp = temp.Where<PermissionInfo>(u => u.PermissionDescription!.Contains(permissionSearch.PermissionDescription)); } permissionSearch.TotalCount = temp.Count(); if (permissionSearch.Order) { return temp.OrderBy<PermissionInfo, long >(u => u.Id).Skip<PermissionInfo>((permissionSearch.PageIndex - 1 ) * permissionSearch.PageSize).Take<PermissionInfo>(permissionSearch.PageSize); } else { return temp.OrderByDescending<PermissionInfo, long >(u => u.Id).Skip<PermissionInfo>((permissionSearch.PageIndex - 1 ) * permissionSearch.PageSize).Take<PermissionInfo>(permissionSearch.PageSize); } }
这里,需要进行搜索,所以在Cms.Entity
这个类库项目中的Search
文件夹中,创建PermissionSearch.cs
类,该类的代码如下所示:
1 2 3 4 5 6 7 8 9 namespace Cms.Entity.Search { public class PermissionSearch :BaseSearch { public string ? PermissionName { get ; set ; } public string ? PermissionDescription { get ;set ; } } }
下面需要控制器PermissionsController.cs
中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [HttpGet ] public IActionResult GetPermissions ([FromQuery] PermissionParams permissionParams ) { int totalCount = 0 ; PermissionSearch permissionSearch = new PermissionSearch() { PermissionName = permissionParams.PermissionName, PermissionDescription = permissionParams.PermissionDescription, Order = permissionParams.Order, PageIndex = permissionParams.PageIndex, PageSize = permissionParams.PageSize, TotalCount = totalCount }; var permissionsList = _permissionInfoService.LoadSearchEntities(permissionSearch, Convert.ToBoolean(DelFlagEnum.Normal)); if (permissionSearch.TotalCount <= 0 ) { return NotFound("没有权限信息" ); } var permissions = permissionsList.Select(u => new { Id = u.Id, PermissionName = u.PermissionName, PermissionCode = u.PermissionCode, CreateTime = u.CreateTime, PermissionDescription= u.PermissionDescription,ParentId=u.ParentId, PermissionType=u.PermissionType }); return Ok(new ApiResult<object >() { Success = true , Message = "获取权限信息成功" , Data = new { rows = permissions, total = permissionSearch.TotalCount } }); }
这里需要在WebApi
项目中ResourceParams
文件夹中创建PermissionParams.cs
类来来接收搜索和分页的参数
1 2 3 4 5 6 7 8 namespace Cms.WebApi.ResourceParams { public class PermissionParams :BaseParams { public string ? PermissionName { get ; set ; } public string ? PermissionDescription { get ; set ; } } }
返回到浏览器中对该接口进行测试。
注意:前端展示的时候,只需要展示菜单权限和功能权限的信息就可以了,不需要展示Api
的权限,所以在服务的LoadSearchEntities
方法中,我们进行过滤的时候,可以在添加如下的过滤条件。
1 2 3 4 public IQueryable<PermissionInfo> LoadSearchEntities (PermissionSearch permissionSearch, bool delFlag ) { var temp = _permissionInfoRepository.LoadEntities(u => u.DelFlag == delFlag&&(u.PermissionType==Convert.ToInt32(PermissionTypeEnum.PermissionMenu) ||u.PermissionType==Convert.ToInt32(PermissionTypeEnum.PermissionPoint)));
3.2 权限列表前端展示 定义路由规则
在router/modules
目录下面创建permission.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import Layout from "@/layout" ;export default { path : "/permissions" , name : "permissions" , component : Layout , children : [ { path : "" , name :'permissionsChild' , component : () => import ("@/views/permissions" ), meta : { title : "权限管理" , icon :"lock" }, }, ], };
在router/index.js
文件中,添加以上的路由规则对象。
1 2 import roleRouter from './modules/roleInfo' import permissionRouter from './modules/permission'
1 2 3 4 5 6 7 export const asyncRoutes =[ employeesRouter, departmentsRouter, roleRouter, permissionRouter ];
下面在views
目录下面创建permissions
文件夹,在该文件夹中创建index.vue
组件,
该组件的初步代码如下:
1 2 3 <template> 权限列表 </template>
返回到浏览器中进行查看。
对index.vue
这个组件,做一个简单的布局,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <template> <div class="dashboard-container"> <div class="app-container"> <!-- 靠右的按钮 ,这里使用了前面封装的公共组件PageTools--> <PageTools> <template #after > <!--使用插槽--> <el-button type="primary" size="small" >添加权限</el-button> </template> </PageTools> <!-- 表格 --> <el-table border> <el-table-column align="center" label="权限名称"></el-table-column> <el-table-column align="center" label="权限标识"></el-table-column> <el-table-column align="center" label="权限描述"></el-table-column> <el-table-column align="center" label="操作"> <template #default="scope"> <el-button>添加</el-button> <el-button>编辑</el-button> <el-button>删除</el-button> </template> </el-table-column> </el-table> </div> </div> </template>
创建请求的方法。
在src/api
目录下面创建permission.js
文件,添加如下代码
1 2 3 4 5 6 7 import request from '@/utils/request' export function getPermissionList (params ){ return request ({ url :'/permissions' , params }); }
修改views/permissions/index.vue
组件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <script> import {onMounted, ref} from 'vue' import {getPermissionList} from '@/api/permission' export default { name :'Permission' , setup ( ){ const list=ref ([]); const loading = ref (false ); const page = ref ({ PageIndex : 1 , PageSize : 12 , total :0 }); onMounted (()=> { loadPermissionsList (); }) const loadPermissionsList =async ( )=>{ loading.value = true ; const { rows, total } = await getPermissionList (page.value ); list.value = rows; page.value .total = total; loading.value = false ; } const handleCurrentChange =(value )=>{ page.value .PageIndex = value; loadPermissionsList (); } return { list, page, handleCurrentChange } } } </script>
继续修改模版中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <!-- 表格 --> <el-table border :data="list"> <!--把返回的list赋值给data属性,作为表格的数据源--> <!--通过prop属性指定要渲染的属性名--> <el-table-column align="center" label="权限名称" prop="permissionName"></el-table-column> <el-table-column align="center" label="权限标识" prop="permissionCode"></el-table-column> <el-table-column align="center" label="权限描述" prop="permissionDescription"></el-table-column> <el-table-column align="center" label="操作"> <template #default="scope"> <el-button size="small">添加</el-button> <el-button size="small">编辑</el-button> <el-button size="small">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <el-row type="flex" justify="center" align="middle" style="height: 60px" > <el-pagination layout="prev, pager, next" :total="page.total" :page-size="page.PageSize" v-model:currentPage="page.PageIndex" @current-change="handleCurrentChange" /> </el-row>
在上面的代码中,给表格指定了数据源,同时也添加了分页组件。
返回到浏览器中查看效果。
虽然,在表格中展示了权限的信息,但是,数据的展示没有呈现出树形结构。
我们知道,权限是有父子关系的,所以再进行展示的时候,最好是通过树形方式来进行展示。
在表格中怎样呈现出树形的结构呢?
参考文档:
1 https://element-plus.gitee.io/zh-CN/component/table.html#%E6%A0%91%E5%BD%A2%E6%95%B0%E6%8D%AE%E4%B8%8E%E6%87%92%E5%8A%A0%E8%BD%BD
通过文档,我们可以看到在使用树形表格的时候,需要注意两点
第一:服务端返回的数据如果包含了children
属性,则会被当作树形数据,而我们这里服务端返回的数据中没有children
,所以需要前端进行转换
第二:就是table
表格中必须添加row-key
属性,该属性的值是唯一的。
1 2 <!-- 表格 --> <el-table border :data="list" row-key="id">
这里添加了row-key
属性,取值是id
1 2 import {getPermissionList} from '@/api/permission' import {tranListToTreeData} from '@/utils/index'
1 2 3 4 5 6 7 const loadPermissionsList =async ( )=>{ loading.value = true ; const { rows, total } = await getPermissionList (page.value ); list.value =tranListToTreeData (rows,0 ); page.value .total = total; loading.value = false ; }
返回到浏览器中进行查看。
在这里,权限的展示,我们要求只展示两层,所以在第二层中,就不需要再出现添加
按钮了。
修改index.vue
组件,
1 2 <template #default="scope"> <el-button size="small" v-if="scope.row.permissionType===1">添加</el-button>
这里,我们添加了一个判断,就是判断返回的permissionType
的值是否是1,如果是1,表示的就是菜单权限,所以才会出现添加
按钮.
也就是说,功能权限不需要再添加对应的子权限了。
而功能权限实际上菜单权限的子权限。
4、创建添加权限的窗口 4.1 基础表单创建 修改views/Permission/index.vue
这个组件中修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <!-- 新增窗口 --> <el-dialog title="新增权限" v-model="showAddDialog"> <el-form label-width="120px"> <el-form-item label="名称"> <el-input v-model="formData.PermissionName" style="width: 90%;" /> </el-form-item> <el-form-item label="标识"> <el-input v-model="formData.PermissionCode" style="width: 90%;" /> </el-form-item> <el-form-item label="描述"> <el-input v-model="formData.PermissionDescription" style="width: 90%;" /> </el-form-item> </el-form> <template #footer> <el-row type="flex" justify="center"> <el-col :span="6"> <el-button size="small">确定</el-button> <el-button type="primary" size="small" @click="showAddDialog=false">取消</el-button> </el-col> </el-row> </template> </el-dialog> </div> </template>
这里创建一个dialog
窗口,然后添加了v-model
属性,该属性的值是showAddDialog
,通过shoAddDialog
控制窗口的显示与隐藏,
在dialog
窗口中添加了el-form
表单,表单元素通过v-model
双向绑定了formData
响应式对象中的相关属性。
在setup
入口函数中定义formData,showAddDialog
1 2 3 4 5 6 7 8 9 const showAddDialog=ref (true ); const formData=reactive ({ PermissionName :"" , PermissionCode :"" , PermissionDescription :'' , PermissionType :1 , ParentId :0 , })
同时将其返回
1 2 3 4 5 6 7 return { list, page, handleCurrentChange, formData, showAddDialog }
返回到浏览器中进行查看。
4.2 完成权限的添加 第一步:给添加权限
的按钮注册单击事件。
我们知道,权限列表页面的顶部有一个添加权限的按钮,单击该按钮,添加的是菜单权限
而单击每一行中的添加权限
的按钮,添加的权限是功能权限(按钮权限)
所以,单击不同按钮的时候,在所调用的方法中要进行判断。
1 2 3 4 5 <PageTools> <template #after > <el-button type="primary" size="small" @click="addPermission(1,0)" >添加权限</el-button> </template> </PageTools>
以上是顶部按钮,在调用addPermission
方法的时候,传递的参数是1,表示将要添加菜单权限。(在服务端我们规定了菜单权限对应的类别就是1),同时这里添加的是根权限,所以第二个参数是0。
1 2 <template #default="scope"> <el-button size="small" @click="addPermission(2,scope.row.id)" v-if="scope.row.permissionType===1">添加</el-button>
每行中的添加
按钮触发后,调用的addPermission
方法,传递的参数是2,表示添加的是功能权限。同时第二个参数,表示的是父id
,
也就是当前所添加的权限是哪个权限的子权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const addPermission =(type,pid )=>{ showAddDialog.value = true ; formData.PermissionType = type; formData.ParentId = pid; } return { list, page, handleCurrentChange, formData, showAddDialog, addPermission }
以上是addPermission
方法的初步定义。(该方法的作用:将窗口弹出,同时记录下权限的类型与父编号)
同时这里还需要注意的一点就是 :给el-form
标签,添加model
属性,绑定formData
1 2 3 <!-- 新增窗口 --> <el-dialog title="新增权限" v-model="showAddDialog"> <el-form label-width="120px" :model="formData">
关于表单校验,大家可以自己完善。
当单击了新增权限
这个窗口中的确定
按钮的时候,才会真正的发送请求,完成权限的添加。
给【确定】按钮注册单击事件
1 2 <el-col :span="6"> <el-button size="small" @click="btnOk">确定</el-button>
下面看一下btnOk
方法的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import {getPermissionList,addPermissionInfo} from '@/api/permission' const btnOk =async ( )=>{ await addPermissionInfo (formData); ElMessage .success ("权限添加成功" ); showAddDialog.value = false ; loadPermissionsList (); } return { list, page, handleCurrentChange, formData, showAddDialog, addPermission, btnOk }
在src/api/permission.js
文件中添加addPermissionInfo
方法,如下所示:
1 2 3 4 5 6 7 export function addPermissionInfo (data ){ return request ({ method :'post' , url :'/permissions' , data }) }
返回到浏览器端进行测试。
当然,这里大家可以把新增权限前面做的稍微详细点,我们知道,当添加菜单权限的时候 ,可以添加MenuIcon
等值,而添加功能权限的时候,可以添加PointClass
等值。
所以在添加的时候,可以根据不同的类型进行判断,展示成不同的输入框。
5、为角色分配权限 5.1 展示分配权限的窗口 在角色列表组件中,添加如下布局代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 </el-card> <!-- 给角色分配权限的窗口 --> <el-dialog title="分配权限" v-model="showPermDialog"> <!--showPermDialog控制窗口的显示与隐藏--> <!--permData就是要展示的数据--> <!--defaultProps:定义显示的字段的名称以及子属性显示的字段的名称--> <el-tree :data="permData" :props="defaultProps" default-expand-all></el-tree> <template #footer> <el-row type="flex" justify="center"> <el-col :span="6"> <el-button type="primary" size="small">确定</el-button> <el-button size="small" @click="showPermDialog=false">取消</el-button> </el-col> </el-row> </template> </el-dialog> </div>
1 2 3 4 5 const list = ref ([]); const showPermDialog=ref (false ); const permData=ref ([]); const defaultProps={};
1 2 <el-table-column label="操作"> <el-button size="small" type="success" @click="setPermission">分配权限</el-button>
当单击分配权限
按钮的时候,调用setPermission
方法,在该方法中需要将以上的窗口弹出
1 2 3 4 5 6 7 8 9 10 11 12 const setPermission =( )=>{ showPermDialog.value =true ; } return { list, ...toRefs (page), handleCurrentChange, showPermDialog, permData, setPermission, defaultProps }
下面要做的就是发送请求,获取所有的权限数据,将其展示到树形结构中。
在ap/permission.js
文件中定义如下方法:
1 2 3 4 5 export function getAllPermissions ( ){ return request ({ url :'/permissions/per' }) }
以上代码是发送请求,获取所有的权限。
返回到views/roles/index.vue
这个组件中,继续修改代码:
1 2 import {getAllPermissions} from '@/api/permission' import {tranListToTreeData} from '@/utils/index'
在上面的代码中,导入了getAllPermissions
这个方法和tranListToTreeData
方法,这个方法的作用就是将数据转换成树形结构。
1 2 3 4 5 6 const setPermission =async ( )=>{ showPermDialog.value =true ; const {rows}= await getAllPermissions (); permData.value =tranListToTreeData (rows,0 ); }
在上面的代码中,我们调用getAllPermissions
方法,获取所有的权限数据,然后传递tranListToTreeData
方法进行转换,第二个参数,表示的是根节点的编号是0.
转换好的数据赋值给permData
1 <el-tree :data="permData" :props="defaultProps" default-expand-all></el-tree>
我们知道permData
已经与el-tree
组件的data
属性关联,data
属性表示的就是要展示的数据,
但是还需要注意props
属性,该属性关联的是defaultProps
,
1 2 3 4 5 6 const permData=ref ([]); const defaultProps={ label :"permissionName" , children :'children' };
以上完善了defaultProps
对象,label
属性的取值是permissionName
,表示在树形结构中展示的就是权限的名称。
children
属性的取值是children
,表示构建树形结构,就是通过children
来完成的。
当然,这里我们调用getAllPermissions
发送的获取所有权限请求对应的接口,在服务端还没有创建,服务端以前创建的是分页展示。
而这里我们需要获取到所有的权限数据,而不需要分页的内容。
在PermissionsController.cs
控制器中添加如下方法
1 2 3 4 5 6 7 8 9 10 11 12 [HttpGet("per" ) ] public IActionResult GetAllPermissions () { var permissionsList= _permissionInfoService.LoadEntities(p => true ); if (permissionsList.Count() <= 0 ) { return NotFound("没有权限信息" ); } var permissions = permissionsList.Select(u => new { Id = u.Id, PermissionName = u.PermissionName, PermissionCode = u.PermissionCode, CreateTime = u.CreateTime, PermissionDescription = u.PermissionDescription, ParentId = u.ParentId, PermissionType = u.PermissionType }); return Ok(new ApiResult<object >() { Success = true , Message = "获取权限信息成功" , Data = new { rows = permissions} }); }
返回到浏览器中进行测试。
5.2 为角色分配权限前端处理1 当单击分配角色按钮,在弹出的窗口中可以以树形结构展示权限信息了。
这里,我们还需要在每个权限名称前面添加复选框,方便进行选择。
参考文档:
1 https://element-plus.gitee.io/zh-CN/component/tree.html#%E5%8F%AF%E9%80%89%E6%8B%A9
1 2 3 <!-- 给角色分配权限的窗口 --> <el-dialog title="分配权限" v-model="showPermDialog"> <el-tree :data="permData" show-checkbox check-strictly :props="defaultProps" default-expand-all></el-tree>
这里修改了views/roles/index.vue
这个组件中添加的el-tree
组件。
给改组件添加了show-checkbox
表示在每个角色名称前面添加复选框
check-strictly
:如果表示选择了某个根权限,不会选择它下面的子权限。
下面我们要处理的就是,当弹出以上窗口的时候,应该将要分配权限的角色,以前具有的权限选中。
1 2 3 4 <el-table-column label="描述" prop="remark" /> <el-table-column label="操作"> <template #default="scope"><!--添加了默认插槽---> <el-button size="small" type="success" @click="setPermission(scope.row.id)">分配权限</el-button>
当单击分配权限
按钮的时候,会调用前面我们所创建的setPermission
这个方法,同时会将要分配权限的角色的编号传递到该方法中。
1 2 3 4 5 6 const setPermission =async (id )=>{ showPermDialog.value =true ; const {rows}= await getAllPermissions (); permData.value =tranListToTreeData (rows,0 ); roleId.value =id; }
在setPermission
方法中,我们将传递过来的角色的编号赋值给了一个响应对象roleId
进行了保存。
1 2 3 const permData=ref ([]); const roleId = ref (0 );
所以在上面的代码中,我们创建了roleId
这个响应对象。
下面我们要做的就是把角色编号
发送到服务端,服务端根据该角色编号,查询一下该角色以前具有哪些权限。
5.3 实现获取角色已经具有权限接口实现 在PermissionsController.cs
控制器中添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [HttpGet("{id}/roles" ) ] public async Task<IActionResult> GetPermissionsByRoleId ([FromRoute]int id ) { var roleInfo = await _roleInfoService.LoadEntities(r => r.Id == id).FirstOrDefaultAsync(); if (roleInfo == null ) { return NotFound("没有找到对应的角色" ); } var rolesAllPermissions = _roleInfoService.LoadEntities(r => r.Id == id).Select(r => r.PermissionInfos); var permIdList = rolesAllPermissions.Select(u => u.Select(r => r.Id)).ToList(); return Ok(new ApiResult<object >() { Success = true , Data = permIdList }); }
5.4 角色分配权限前端处理2 在上一小节中,我们已经创建好了获取角色已经具有的权限对应的接口。
在前端的api/permission.js
文件中应该创建一个方法请求该接口。
1 2 3 4 5 6 7 export function getRolePermissionIdList (id ){ return request ({ url :`/permissions/${id} /roles` }) }
返回到views/roles/index.vue
这个组件中
1 import {getAllPermissions,getRolePermissionIdList} from '@/api/permission'
1 2 3 4 5 6 7 8 9 10 const setPermission =async (id )=>{ showPermDialog.value =true ; const {rows}= await getAllPermissions (); permData.value =tranListToTreeData (rows,0 ); roleId.value =id; const result = await getRolePermissionIdList (id) selectCheck.value =result[0 ]; }
在setPermission
方法中,调用了我们刚刚封装好的getRolePermissionIdList
方法,传递了角色的编号,发送请求,获取了当前这个角色已经具有的权限信息。当然这里服务端返回的都是权限的编号。
而且,返回的是一个二维数组,二维数组中的第一个元素也是一个数组,里面存储的都是当前角色已经具有的权限编号。
然后赋值给了响应对象selectCheck
1 2 const roleId = ref (0 ); const selectCheck =ref ([]);
这里我们定义了selectCheck
这个响应对象,并且将其返回
1 2 3 4 5 6 7 8 9 10 return { list, ...toRefs (page), handleCurrentChange, showPermDialog, permData, setPermission, defaultProps, selectCheck }
下面需要给树形组件添加属性。
1 2 3 4 5 6 7 8 <!-- 给角色分配权限的窗口 --> <el-dialog title ="分配权限" v-model ="showPermDialog" > <el-tree :data ="permData" show-checkbox check-strictly :props ="defaultProps" // 添加了default-checked-keys 和node-key default-expand-all :default-checked-keys ="selectCheck" node-key ="id" > </el-tree >
在上面的el-tree
这个组件中,添加了default-checked-keys
属性,该属性关联的值就是selectCheck
, 也就是,树形中的复选框要想选中,需要根据selectCheck
这个数组来确定。问题是,需要根据selectCheck
这个数组中存储的id
值来选中对应的复选框,所以这里又添加了node-key
这个属性,取值是id
,来作为唯一的标识。
返回到浏览器中进行测试(可以现在数据库中输入数据,方便进行测试)
5.5 完成角色权限的分配 下面我们首先要获取的就是所选中的权限的编号。
1 2 3 4 5 6 <el-tree :data="permData" show-checkbox check-strictly :props="defaultProps" ref="permTree" default-expand-all :default-checked-keys="selectCheck" node-key="id"> </el-tree>
这里,给el-tree
这个组件,添加了ref
属性,关联了permTree
.
1 2 const permTree =ref (null );
当单击分配权限
窗口中的确定
按钮以后,会触发click
事件。
1 <el-button type="primary" size="small" @click="setPermissionOk">确定</el-button>
调用setPermissionOk
方法。在该方法中,会通过permTree
获取到el-tree
组件。
然后获取到所选中的权限的编号。
1 2 3 4 const setPermissionOk =( )=>{ console .log ("keys=" ,permTree.value .getCheckedKeys ()); }
最终是调用了el-tree
组件中的getCheckedKeys
方法获取了所选中的权限编号。
最后返回
1 2 3 4 5 6 7 8 9 10 11 12 return { list, ...toRefs (page), handleCurrentChange, showPermDialog, permData, setPermission, defaultProps, selectCheck, permTree, setPermissionOk }
可以返回到浏览器中,看一下控制台中的打印。
5.5.2 服务端接口实现 第一:在Cms.IRepository
类库项目中的IPermissionInfoRepository
接口中声明一个用来获取角色已有权限的方法,如下所示:
1 2 3 4 5 6 7 8 namespace Cms.IRepository { public interface IPermissionInfoRepository :IBaseRepository <PermissionInfo > { Task<List<PermissionInfo>> GetRolePermissions(long roleId); } }
第二:在Cms.Repository
类库项目中的PermissionInfoRepository.cs
数据仓储中实现上面的方法,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 public class PermissionInfoRepository :BaseRepository <PermissionInfo >,IPermissionInfoRepository { public PermissionInfoRepository (MyDbContext context ) : base (context ) { } public async Task<List<PermissionInfo>> GetRolePermissions(long roleId) { var roleInfo = await ctx.RoleInfos.Include(r => r.PermissionInfos).Where(u => u.Id == roleId).FirstOrDefaultAsync(); return roleInfo!.PermissionInfos.ToList(); } }
第三:在IPermissionInfoService.cs
这个服务接口中声明一个用来完成给角色分配权限的方法,如下所示:
1 2 3 4 5 6 7 public interface IPermissionInfoService :IBaseService <PermissionInfo > { Task<bool > AddPermission (PermissionDto permissionDto ) ; IQueryable<PermissionInfo> LoadSearchEntities (PermissionSearch permissionSearch, bool delFlag ) ; Task<bool > SetRolePermission (RoleInfo roleInfo, List<long > permissionIdList ) ; }
第四:在PermissionInfoService.cs
中实现上面所声明的SetRolePermission
方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public async Task<bool > SetRolePermission (RoleInfo roleInfo, List<long > permissionIdList ) { await _permissionInfoRepository.GetRolePermissions(roleInfo.Id); roleInfo.PermissionInfos.Clear(); if (permissionIdList.Count == 1 && permissionIdList[0 ] == 0 ) { return true ; } foreach (var pId in permissionIdList) { var permissionInfo =await _permissionInfoRepository.LoadEntities(p => p.Id == pId).FirstOrDefaultAsync(); roleInfo.PermissionInfos.Add(permissionInfo!); } return true ; }
第五:PermissionsController.cs
控制器中的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [HttpPost("{id}/roles/{permissionId}" ) ] [UnitOfWork(new Type[ ] { typeof (MyDbContext) })] [Authorize ] public async Task<IActionResult> SetRolePermissions ([FromRoute]int id, [FromRoute]string permissionId ) { var roleInfo = await _roleInfoService.LoadEntities(r=>r.Id==id).FirstOrDefaultAsync(); if (roleInfo == null ) { return NotFound("没有找到对应的角色信息" ); } List<long > list = new List<long >(); var strIds = permissionId.Split(',' ); foreach (var pId in strIds) { list.Add(Convert.ToInt32(pId)); } await _permissionInfoService.SetRolePermission(roleInfo,list); return Ok(new ApiResult<object > { Success = true , Message = "获取成功" }); }
5.5.3 前端实现 在src/api/permission.js
文件中,添加如下方法:
1 2 3 4 5 6 7 export function setRolePermissions (id,data ){ return request ({ method :'post' , url :`/permissions/${id} /roles/${data} ` }) }
返回到Views/roles/index.vue
组件,继续修改代码,如下所示:
1 import {getAllPermissions,getRolePermissionIdList,setRolePermissions} from '@/api/permission'
在上面的代码中,导入setRolePermissions
方法
修改setPermissionOk
方法中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const setPermissionOk =async ( )=>{ const ids = permTree.value .getCheckedKeys ().length >0 ? permTree.value .getCheckedKeys ().join (',' ):"0" ; await setRolePermissions (roleId.value ,ids); ElMessage ({ message :"分配权限成功" , type :'success' }) showPermDialog.value = false ; selectCheck.value =[] }
返回到浏览器中进行测试。
九、权限过滤 1、前端权限实现思路 在权限管理页面中,我们设置了一个标识,这个标识可以和我们的路由模块进行关联,也就是说,如果用户拥有了这个标识,那么用户就可以拥有这个路由模块,如果没有这个标识,就不能访问路由模块。
怎样动态的添加路由呢?
在vue-router
中提供了一个叫做addRoutes
的方法,这个方法的含义是:动态添加路由规则
。
思路如下:
用户登录—-获取标识—标识和路由进行关联,筛选出具有权限的路由—通过addRoutes
添加。
2、通过Vuex
管理权限模块 在src/store/modules
目录下面创建一个permission.js
文件,该文件的初步代码
1 2 3 4 5 6 7 8 9 10 const state = {}const mutations = {}const actions = {};export default { namespaced :true , state, mutations, actions, }
这个Vuex
模块,专门就是用来权限路由的。
当然,需要把该模块添加到Vuex
的容器中。修改src/store/index.js
文件中,对代码进行修改,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { createStore } from 'vuex' import user from './modules/user' import getters from "./getters" ;import permission from './modules/permission' ; export default createStore ({ modules : { user, permission }, getters })
返回到permisson
模块中,继续修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import {constantRoutes} from '@/router' const state = { routes :constantRoutes } const mutations = {}const actions = {};export default { namespaced :true , state, mutations, actions, }
在state
这个状态对象中添加了一个属性routes
,表示的是路由表,当前用户所拥有的所有路由都会存储在该数组中。
而我们也知道,用户默认是拥有静态路由的,例如:登录,首页等。所以这里导入进来,然后赋值给routes
属性。
当然,这里我们把静态路由的名字修改成了constantRoutes
,所以在router/index.js
文件中,修改一下静态路由的名字,如下:
1 2 3 4 5 6 export const constantRoutes = [ { path : '/' , redirect :'/dashboard' , component : Layout , children :[
上面把名字修改成了constantRoutes
,同时,在创建路由对象router
的时候也需要修改一下
1 2 3 4 const router = createRouter ({ history : createWebHashHistory (), routes :[...constantRoutes,...asyncRoutes] })
下面继续修改store/modules/permission.js
模块中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import {constantRoutes} from '@/router' const state = { routes :constantRoutes } const mutations = { setRoutes (state,newRoutes ){ state.routes =[...constantRoutes,...newRoutes] } } const actions = {};export default { namespaced :true , state, mutations, actions, }
如果我们想修改state
中的routes
这个状态,需要再mutations
中创建一个方法来完成修改。
这里我们创建了setRoutes
方法:该方法的作用就是修改routes
这个状态值。
每次用户登录以后,我们都是将静态路由和该用户拥有的动态路由合并以后,重新赋值给rotues
这个状态。
这里需要注意的一点,就是如下写法,在当前的场景中不符合业务的要求:
1 state.routes =[...state.routes ,...newRoutes]
在以上的写法中,是将新的路由规则,与state.routes
中存储的原有的路由规则进行合并。
这样会导致出现一种情况:一个用户登录以后,含有了静态路由和动态路由,这时候他退出了,另外一个用户登录以后,就有可能获取到第一个用户的动态路由(静态路由大家都是一样的,而且是在一台电脑中进行登录的,并且浏览器没有关闭。)。
3、Vuex
筛选权限路由 3.1 获取用户具有的权限编码–接口实现 在前面我们讲解过,当用户登录以后,我们要获取权限的标识,然后与路由进行关联。
怎样获取标识呢?
在src/permission.js
这个文件中,我们前面实现了前置的路由守卫,当用户登录成功以后,我们又执行了如下代码:
1 2 3 if (!store.state .user .userInfo .Id ){ await store.dispatch ("user/getUserInfo" ); }
这里执行了store/modules/user.js
中的actions
中的getUserInfo
方法来获取用户的信息。而这时候,服务端是可以将该用户具有的权限标识返回过来的。
当然,目前服务端接口中,还没有返回该用户具有的权限标识,所以这里我们需要完善一下该接口。
修改IUserInfoService.cs
服务端接口中的代码,如下所示:
1 2 3 4 5 6 7 public interface IUserInfoService :IBaseService <UserInfo > { IQueryable<UserInfo> LoadSearchEntities (UserInfoSearch userInfoSearch, bool delFlag ) ; Task<bool > SetUserRoles (UserInfo userInfo, List<long > roleIdList ) ; Task<Dictionary<string , List<string >>> GetUserPermissionCodes(long userId); }
在上面的代码中,声明了GetUserPermissionCodes
方法,用来获取用户具有的权限编码,该方法返回的数据类型是Dictionary
字典类型、key
是字符串,值是一个list
集合。
定义字典集合的原因是,最终返回的用户具有的权限编码的格式是:
1 2 3 4 5 6 7 8 9 10 11 "menus": [ "UserManager", "DepartmentManager", "RoleManager" ], "points": [ "UserAdd", "DepartmentAdd", "RoleAdd" ], "apis": []
下面返回到UserInfoService.cs
这个服务类中,实现上面接口中所声明的GetUserPermissionCodes
方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public async Task<Dictionary<string ,List<string >>> GetUserPermissionCodes(long userId) { var userRoles = await _roleInfoRepository.GetUserRoles(userId); List<string > menus = new List<string >(); List<string > points = new List<string >(); List<string > apis = new List<string >(); foreach (var role in userRoles) { var rolePermissions = await _permissionInfoRepository.GetRolePermissions(role.Id); foreach (var permission in rolePermissions) { if (permission.PermissionType == (int )PermissionTypeEnum.PermissionMenu) { menus.Add(permission.PermissionCode!); }else if (permission.PermissionType == (int )PermissionTypeEnum.PermissionPoint) { points.Add(permission.PermissionCode!); } else { apis.Add(permission.PermissionCode!); } } } Dictionary<string ,List<string >>dict =new Dictionary<string ,List<string >>(); dict.Add("menus" , menus); dict.Add("points" , points); dict.Add("apis" ,apis); return dict; }
1 2 3 4 5 6 7 8 9 10 private readonly IUserInfoRepository _userInfoRepository; private readonly IRoleInfoRepository _roleInfoRepository; private readonly IPermissionInfoRepository _permissionInfoRepository; public UserInfoService (IUserInfoRepository userInfoRepository,IRoleInfoRepository roleInfoRepository,IPermissionInfoRepository permissionInfoRepository ) { base .repository = userInfoRepository; _userInfoRepository = userInfoRepository; _roleInfoRepository = roleInfoRepository; _permissionInfoRepository = permissionInfoRepository; }
下面看一下UsersController.cs
控制器的具体实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [HttpGet ] public async Task<IActionResult> GetUserInfo ([FromQuery]string userPhone ) { if (string .IsNullOrWhiteSpace(userPhone)) { return BadRequest("手机号不能为空" ); } var user = await _userInfoService.LoadEntities(u => u.UserPhone == userPhone).FirstOrDefaultAsync(); if (user == null ) { return BadRequest("没有查找到对应用户" ); } var dirct = await _userInfoService.GetUserPermissionCodes(user.Id); return Ok(new ApiResult<object >() { Success = true , Message = "获取用户成功" , Data = new { user = new {Id=user.Id,UserName = user.UserName,UserPhone=user.UserPhone,PhotoUrl=user.PhotoUrl}, roles = dirct } }); }
下面可以进行接口的测试。
当然,前面我们讲过,是在前端项目的store/modules/user.js
文件中发送的请求,请求以上接口的,现在该接口中返回的数据内容和格式发生了变化,所以我们也需要对前端做进一步的修改处理。
1 2 3 4 5 6 7 8 async getUserInfo (context ){ const userPhone =localStorage .getItem ("userPhone" ); const result = await getUserInfo (userPhone); context.commit ("setUserInfo" ,result); return result; },
我们知道,在以上的actions
中所定义的getUserInfo
这个方法中,向服务端发送了请求,并且传递了的数据是手机号,服务端返回的数据通过commit
方法,提交到了mutations
中,来根据服务端返回的数据修改state
的状态,下面我们来看一下再mutations
中定义的setUserInfo
这个方法,
1 2 3 4 setUserInfo (state,payload ){ const {user,roles } =payload state.userInfo = user; },
在setUserInfo
这个Mutations
方法中,把服务端返回的数据进行解构,然后把用户数据重新赋值给了state
状态中的userInfo
属性。
通过浏览器查看前端页面,可以看到页面中可以照常展示用户名和用户的头像。
3.2 前端路由筛选——- 我们现在已经能够从服务端获取到权限的编码了,下面我们要做的就是把服务端返回的这些权限的编码与路由规则进行绑定。
怎样绑定呢?就是将动态路由中的name
属性与权限编码一致就可以了。
例如,员工管理的权限编码是UserManager
,而当前登录用户有该权限编码,说明当前登录用户具有员工管理的权限,同时就应该能够访问员工管理
菜单,怎样能够访问呢?就是让员工管理对应的路由规则的name
属性值也是UserManager
就可以了。
这里,我们需要进行路由的筛选,筛选出与这些服务端返回的权限的标识一样的name
属性值相同的动态路由。
在哪里筛选呢?—
这里,我们需要在store/modules/permission.js
文件中进行筛选,这里定义一个actions
方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const actions = { filterRoutes (context,menus ){ const routes=[]; menus.forEach (key => { routes.push (...asyncRoutes.filter (item => item.name ===key)); }) context.commit ('setRoutes' ,routes); return routes; } };
注意:这里需要导入asyncRoutes
这个动态路由
1 2 import {asyncRoutes, constantRoutes} from '@/router'
4、在权限拦截处调用筛选权限的Action
在上一小节中,我们已经在Vuex
中定义了一个action
的方法叫做filterRoutes
,用来进行路由的筛选。
问题是:在哪调用filterRoutes
这个方法呢?
我们知道filterRoutes
这个方法的第二个参数,是当前登录用户具有的菜单权限,也就是对应的菜单权限的标识。
所以说,我们肯定是在获取了用户的信息以后调用filterRoutes
这个方法的。
所以这里修改src/permission.js
文件中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 router.beforeEach (async (to,from ,next)=>{ NProgress .start (); if (store.getters .token !=null && Object .keys (store.getters .token ).length ) { if (to.path === "/login" ) { next ("/" ); } else { if (!store.state .user .userInfo .Id ){ const {roles} =await store.dispatch ("user/getUserInfo" ); const routes = await store.dispatch ('permission/filterRoutes' ,roles.menus ); console .log ('routes===' ,routes); } next ();
我们返回到浏览器中,刷新页面,可以看到当前打印的routes
的值是空值。
原因是:
我们定义的动态路由规则的name
属性的值没有修改。
所以下面我们修改一下
修改router/departments.js
文件,该文件中定义的是部门管理的动态路由规则
1 2 3 4 5 import Layout from "@/layout" ;export default { path : "/departments" , name : "DepartmentManager" ,
router/employees.js
1 2 3 4 5 import Layout from "@/layout" ;export default { path : "/employees" , name : "UserManager" ,
router/permission.js
1 2 3 4 5 import Layout from "@/layout" ;export default { path : "/permissions" , name : "PermissionsManager" ,
router/roleInfo.js
1 2 3 4 5 import Layout from "@/layout" ;export default { path : "/roles" , name : "RoleManager" ,
当然,在系统中
也要把这些菜单权限都添加到系统中,然后给某个角色
分配好,在把该角色分配给某个用户。
当用户登录以后,可以看到浏览器的控制台中打印了routes
的内容。
在上面的src/permission.js
文件中,我们已经能够获取到筛选出来的动态路由了
。
下面要做的就是,把获取到的动态路由添加到路由表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 if (!store.state .user .userInfo .Id ){ const {roles} =await store.dispatch ("user/getUserInfo" ); const routes = await store.dispatch ('permission/filterRoutes' ,roles.menus ); routes.forEach ((route )=> { router.addRoute (route); }) }
5、静态路由与动态路由解除合并 在上一小节中,我们调用了addRoute
这个方法,进行了动态路由的添加,这样我们原来临时添加的动态路由就需要删除掉。
这里我们需要修改src/router/index.js
文件中的代码,
1 2 3 4 const router = createRouter ({ history : createWebHashHistory (), routes :[...constantRoutes,...asyncRoutes] })
在上面的代码中,我们将静态路由与动态路由进行了临时的合并。现在要把动态路由删除掉。
如下所示:
1 2 3 4 5 const router = createRouter ({ history : createWebHashHistory (), routes :[...constantRoutes] })
这里只有静态的路由,没有动态路由了。
返回到浏览器中进行刷新,发现左侧菜单只有首页了,即使我们当前登录的用户zhangsan
有其他的菜单权限,但是在左侧也没有进行展示。
原因是什么呢?
我们首先来看一下左侧菜单的构建
左侧菜单的构建是在src/layout/index.vue
这个组件中,
1 2 3 <template v-for="route in routes" > <el-menu-item v-if="!route.hidden && route.children" :index="route.path" > <el-icon><SvgIcon :iconClass="route.children[0].meta.icon"
我们可以看到,这里通过循环的routes
,来构建菜单。
routes
的定义如下
1 2 3 routes = computed (()=> { return router.options .routes ; })
现在的问题就是,前面我们通过调用addRoute
方法动态添加的路由,这里并不会进行响应式的变化。
所以说,在通过调用了addRoute
方法动态添加了路由以后,左侧菜单没有展示相应的内容。
怎样进行处理呢?
我们前面也讲过,当我们执行src/store/modules/permission.js
文件中的filterRoutes
方法进行路由过滤的时候,我们将过滤出来的动态路由保存到了state
中的routes
这个状态中了。保存到该状态中的目的就是给左侧菜单使用的。
问题是在src/layout/index.vue
中怎样获取这个状态数据呢?
我们可以在src/store/getters.js
文件中建立对应的快捷访问的方式:
1 2 3 4 5 6 7 8 const getters = { token : (state ) => state.user .token , name :(state )=> state.user .userInfo .userName , photoUrl :(state )=> state.user .userInfo .photoUrl , routes :state => state.permission .routes }; export default getters;
上面代码建立了针对routes
这个状态的快捷访问的方式。
下面返回到src/layout/index.vue
组件中继续修改代码:
1 2 3 4 routes = computed (()=> { return store.getters .routes ; })
这里是从store
仓库的getters
中的routes
来获取动态的路由。
返回到浏览器中进行刷新。可以看到对应的效果
这样就完成了菜单权限的过滤。
这里可以登录具有不同角色的用户进行测试。
当然,在测试的时候,可以给不同不同的角色分配相同的权限,例如给经理和组长两个角色都分配员工管理的权限
然后给王五这个用户都分配经理和组长两个角色。
当以王五这个账号进行登录的时候,发现左侧菜单中显示了两个员工管理
菜单。
造成这种情况的问题是:服务端在根据角色获取权限的标识的时候没有进行去重的操作。
所以在UserInfoService.cs
这个服务类中的GetUserPermissionCodes
方法中,
1 2 3 4 Dictionary<string ,List<string >>dict =new Dictionary<string ,List<string >>(); dict.Add("menus" , menus.Distinct().ToList()); dict.Add("points" , points.Distinct().ToList()); dict.Add("apis" ,apis.Distinct().ToList());
对相关的数据进行去重的操作。
重新返回到浏览器中进行测试。
当然,现在遇到了另外一个问题,就是刷新浏览器的时候,会出现404的情况,关于这个问题我们后面再来解决。
6、退出登录重置路由权限与404问题 6.1 退出登录重置路由 在解决404问题之前,我们先来看一下关于退出登录后重置路由权限的问题。
我们知道用户登录的时候,会执行前端项目中的src/permission.js
文件中的如下代码:
1 2 3 4 5 routes.forEach ((route )=> { router.addRoute (route); })
也就是调用addRoute
这个方法动态的添加路由,但是问题时,用户退出登录的时候,没有移除所添加的动态路由。
下面首先修改router/index.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 export function resetRouter ( ){ const newRouter = createRouter ({ history : createWebHashHistory (), routes :[...constantRoutes]} ); router.matcher = newRouter.matcher ; } export default router
这里我们创建了一个resetRouter
方法,在该方法中通过createRouter
方法又重新创建了一个路由对象,然后将新路由对象中的信息重新赋值给了原有router
路由对象。完成了路由信息的重置操作。
问题:在哪调用该resetRouter
方法呢?
在退出登录的时候调用
下面修改store/modules/user.js
文件中的代码:
1 2 3 4 5 6 7 8 9 10 logout (context ){ context.commit ("removeToken" ); context.commit ("removeUserInfo" ); localStorage .removeItem ("userPhone" ); resetRouter ();
在user.js
这个模块中,我们实现了logout
这个actions
方法,该方法在单击退出
按钮的时候,会被调用。
所以在这logout
方法中完成对resetRouter
方法的调用。
重置了路由的信息以后,还有一个问题:
这里还需要将store/permission.js
模块中,用来保存路由信息的routes这个state状态还需要清空.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 logout (context ){ context.commit ("removeToken" ); context.commit ("removeUserInfo" ); localStorage .removeItem ("userPhone" ); resetRouter (); context.commit ('permission/setRoutes' ,[],{root :true }); }
在调用完了resetRouter
方法以后,需要调用permission
这个模块中的setRoutes
这个mutations
方法,完成routes
这个state
状态数据的清空操作。
6.2 解决404问题 在前面我们也提到过,当登录成功以后,刷新浏览器会出现404的问题。在这一小节中,我们来看一下怎样解决404的问题。
我们知道关于404,是我们在路由中进行了如下的配置。
查看router/index.js
文件
1 2 3 4 { path : "/:pathMatch(.*)*" , redirect : "/404" , },
如果没有其他的路由规则进行匹配,会跳转到/404
.
但是,问题是,以上配置必须放在所有路由规则的最后。
我们看到,以上路由规则并没有放在其他路由规则的最后,后面还有一个about
的路由规则,当然about
这个路由规则这里我们使用不到,可以删除。
但是删除以后,单击了某个菜单项以后,重新刷新浏览器还是不行。
原因是:这里我们添加的以上404的路由规则是在整个路由规则的最后吗?
不是的,因为以上添加的404路由规则是在静态路由规则的最后,但是不是在动态路由规则的最后。
所以这里我们需要修改src/permission.js
文件中的代码,如下所示:
1 2 3 4 5 6 7 8 9 routes.forEach ((route )=> { router.addRoute (route); }) router.addRoute ( { path : "/:pathMatch(.*)*" , component : () => import ("@/views/404.vue" ), });
在上面的代码中,我们是将其他的动态路由添加完毕以后,再去添加404
对应的路由规则。
这时候,我们需要把router/index.js
文件中定义的404
路由规则注释掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if (!store.state .user .userInfo .id ){ const {roles} =await store.dispatch ("user/getUserInfo" ); const routes = await store.dispatch ('permission/filterRoutes' ,roles.menus ); routes.forEach ((route )=> { router.addRoute (route); }) router.addRoute ( { path : "/:pathMatch(.*)*" , component : () => import ("@/views/404.vue" ), }); next (to.path ); }else { next (); }
在上面的代码中,我们修改了判断条件,原来是对Id
进行判断的,但是这里应该是id
,注意大小写。
同时,在添加了404的路由规则以后,直接通过next(to.path)
跳转到对应的页面,否则(这里添加了else
)直接放行,也就是说有用户数据了直接放行,不需要在重新请求服务端获取用户数据。
这里将其逻辑结构做了一定的调整。
7、功能权限判断 在服务端返回的roles
数据中,有一个points
属性,该属性中存储的标识,表示的就是登录用户具有的功能权限,也就是按钮权限。
下面我们看一下怎样使用points
属性中的数据来进行功能权限的判断。
这里我们先来修改store/modules/user.js
文件中的代码:
1 2 3 4 5 6 setUserInfo (state,payload ){ const {user,roles } =payload state.userInfo = user; state.userPermission =roles; },
在setUserInfo
这个mutations
方法中,我们已经将服务端返回的user
和roles
数据给解构出来了。
我们知道roles
中存储了对应的points
,这里将roles
直接赋值给了userPermission
这个状态属性。
1 2 3 4 5 6 state ( ) { return { token : getTokenInfo (), userInfo :{}, userPermission :{} };
然后在src
目录下面创建checkPermission
文件夹,在该文件夹中创建index.js
文件,该文件中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 import store from '@/store' export function checkPermission (key ){ const {userPermission} = store.state .user ; if (userPermission.points &&userPermission.points .length >0 ){ const result = userPermission.points .some (item => item === key); return result; } return false ; }
这里创建了checkPermission
方法,当调用该方法的时候,会传递一个参数key
,表示的就是登录用户所具有的功能权限的标识。
然后看一下是否在userPermission
状态中的points
数组中存在,如果存在返回true
,否则返回的是false
.
下面我们就可以在组件中调用checkPermission
方法来进行功能权限的校验。
例如,我们在views/employees/index.vue
这个组件中进行测试
1 2 import AddEmployee from "./components/add-employee.vue" ; import {checkPermission } from '@/checkPermission/index'
这里导入了checkPermission
这个方法,
当然,也需要返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 return { loading, list, page, handleCurrentChange, formatTimeOfEntry, showAddDialog, updatedialog, loadEmployeeList, showRoleDialog, btnRole, ...toRefs (roles), roleIdList, btnRoleOk, checkPermission };
1 2 3 <template #after > <el-button size ="small" type ="primary" @click ="showAddDialog=true" :disabled ="!checkPermission('UserAdd')" > 新增员工</el-button > </template>
这里在模版中,找到新增员工
这个按钮,给其添加了disabled
属性(这里由于使用了elementplus
组件,所以要查看一下文档不同组件禁用的属性是哪一个),该属性的取值如果是true
会禁用按钮,所以这里该属性的值就是checkPermission
方法返回的值,当然需要取反。
同时在调用checkPermission
方法的时候,需要传递UserAdd
这个标识,具有这个标识表示的就是登录的用户具有添加员工的权限。
UserAdd
这个标识,可以从服务端返回的数据中查看到
首先以管理员登录,然后给某个用户分配角色,在给改角色分配指定的员工管理
这个菜单权限,然后在分配添加员工
这个功能权限,最后以该用户进行登录,查看(后面再禁用添加员工
这个功能权限,在让该用户进行登录来查看一下效果)。
后面可以在其他所有的组件中,给对应的按钮指定checkPermission
方法,当然,在调用该方法的时候,注意一定要传递正确的功能权限的标识。
8、api
权限校验 前面我们已经实现了菜单权限,功能权限的过滤,在这一小节中,我们来看一下api
权限的校验。
这里我们列举一个场景:
例如:用户添加,假如某个登录用户没有这个权限,对应的该按钮是不可用,或者是隐藏的。
但是该用户如果知道了,用户添加的访问接口地址,是不是就可以直接访问了,所以这里我们也需要对api
接口进行权限的校验。
怎样对api
接口的权限进行校验呢?
这里需要使用到Filter
过滤器。
我们可以实现ActionFilter
,这样就可以保证,在请求服务端的接口,执行具体的api
方法之前,先执行该改过滤器,在该改过滤器中,判断用户是否具有访问api
接口的权限。
在Cms.WebApi
这个项目中的Filters
文件夹中,创建ApiPermissionCheckFilter.cs
这个类,该类中的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using Cms.IService;using Microsoft.AspNetCore.Mvc.Filters;using System.IdentityModel.Tokens.Jwt;namespace Cms.WebApi.Filters { public class ApiPermissionCheckFilter : IAsyncActionFilter { private readonly IUserInfoService _userInfoService; public ApiPermissionCheckFilter (IUserInfoService userInfoService ) { this ._userInfoService = userInfoService; } public async Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next ) { var authorization = context.HttpContext.Request.Headers["Authorization" ]; if (authorization.Count>0 ) { var token = authorization[0 ]!.ToString().Split("Bearer " ); var tokenInfo = new JwtSecurityTokenHandler().ReadJwtToken(token[1 ]); var info = tokenInfo.Claims.FirstOrDefault(); var userInfo = _userInfoService.LoadEntities(u=>u.Id.ToString()==info!.Value).include().toList(); userInfo.RoleInfos var url = context.HttpContext.Request.Path; var method = context.HttpContext.Request.Method; } await next(); } } }
在上面的代码中,我们没有实现完整的代码,但是实现思路已经理清楚了,大家可以自己实现代码。
其实代码我们前面也已经讲过。
当然,我们还需要注入该Filter
.
修改Program.cs
文件中的代码,如下所示:
1 2 3 4 5 6 builder.Services.Configure<MvcOptions>(c => { c.Filters.Add<ApiPermissionCheckFilter>(); c.Filters.Add<UnitOfWorkFilter>();
关于api
权限的存储问题,在数据库中我们单独的创建了一张表来存储api
权限。
问题:怎样向该表中添加api
权限呢?
我们在前端没有提供添加api
权限的表单,当然,这里可以提供。
或者是,我们可以将api
权限(api
地址)写到一个Excel
表格中,最后读取该Excel
文件,批量进行导入。
.net core
读取Excel
文件,参考:EPPlus
开源组件,
同时还要注意:一些api
接口的权限是所有用户都具有的,例如:登录,所以需要进行判断。