山东淄博网站建设的公司,做防水施工 上什么网站找,个人备案的网站做企业内容,外贸新手怎样用谷歌找客户项目实战第十五记 写在前面1.后端接口实现1.1 用户表添加角色字段1.2 角色表增加唯一标识字段1.3 UserDTO1.4 UserServiceImpl1.5 MenuServiceImpl 2. 前端实现2.1 User.vue2.2 动态菜单设计2.2.1 Login.vue2.2.2 Aside.vue 2.3 动态路由设计2.3.1 菜单表新增字段page_path2.3.… 项目实战第十五记 写在前面1.后端接口实现1.1 用户表添加角色字段1.2 角色表增加唯一标识字段1.3 UserDTO1.4 UserServiceImpl1.5 MenuServiceImpl 2. 前端实现2.1 User.vue2.2 动态菜单设计2.2.1 Login.vue2.2.2 Aside.vue 2.3 动态路由设计2.3.1 菜单表新增字段page_path2.3.2 路由设计2.3.3 登录界面Login.vue设置路由 总结写在最后 写在前面
动态菜单设计动态路由设计
1.后端接口实现
1.1 用户表添加角色字段
CREATE TABLE sys_user (id int(11) NOT NULL AUTO_INCREMENT COMMENT id,username varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 用户名,password varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 密码,nickname varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 昵称,email varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 邮箱,phone varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 电话,address varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 地址,create_time timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间,avatar_url varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT https://himg.bdimg.com/sys/portraitn/item/public.1.23bd3c8c.ANoeKxl_gef9fnrikdXOYA COMMENT 头像,role varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 角色,deleted tinyint(4) DEFAULT 0 COMMENT 逻辑删除0未删除1删除,PRIMARY KEY (id) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci ROW_FORMATDYNAMIC
1.2 角色表增加唯一标识字段
CREATE TABLE sys_role (id int(11) NOT NULL AUTO_INCREMENT COMMENT id,role_key varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 唯一标识,name varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 名称,description varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 描述,deleted tinyint(1) DEFAULT 0 COMMENT 是否删除,PRIMARY KEY (id) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci ROW_FORMATDYNAMIC
1.3 UserDTO
Data
public class UserDTO {private String username;private String password;private String nickname;private String avatarUrl;private String token;private String role;private ListMenu menus;
}1.4 UserServiceImpl
package com.ppj.service.impl;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ppj.constants.Constants;
import com.ppj.entity.Menu;
import com.ppj.entity.Role;
import com.ppj.entity.RoleMenu;
import com.ppj.entity.User;
import com.ppj.entity.dto.UserDTO;
import com.ppj.exception.ServiceException;
import com.ppj.mapper.MenuMapper;
import com.ppj.mapper.RoleMapper;
import com.ppj.mapper.RoleMenuMapper;
import com.ppj.mapper.UserMapper;
import com.ppj.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ppj.utils.TokenUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import javax.sql.rowset.serial.SerialException;
import java.util.ArrayList;
import java.util.List;/*** p* 服务实现类* /p** author ppj* since 2024-04-20*/
Service
public class UserServiceImpl extends ServiceImplUserMapper, User implements IUserService {private static final Log LOG Log.get();Autowiredprivate RoleMapper roleMapper;Autowiredprivate RoleMenuMapper roleMenuMapper;Autowiredprivate MenuServiceImpl menuService;Overridepublic UserDTO login(UserDTO userDTO) {if(StrUtil.isBlank(userDTO.getUsername()) || StrUtil.isBlank(userDTO.getPassword())){return null;}User user getUserInfo(userDTO);if(user ! null){String token TokenUtils.genToken(user.getId().toString(), userDTO.getPassword());userDTO.setToken(token);// 把user相应的值传递给userDTOBeanUtil.copyProperties(user,userDTO,true);ListMenu roleMenus getRoleMenus(user.getRole());userDTO.setMenus(roleMenus);return userDTO;}else{ // 数据库查不到throw new ServiceException(Constants.CODE_600,用户名或密码错误);}}Overridepublic Boolean register(UserDTO userDTO) {if(StrUtil.isBlank(userDTO.getUsername()) || StrUtil.isBlank(userDTO.getPassword())){return false;}User user getUserInfo(userDTO);if(user null){User newUser new User();// 值传递BeanUtil.copyProperties(userDTO,newUser,true);// 保存至数据库save(newUser);}else{throw new ServiceException(Constants.CODE_600,用户已存在);}return true;}public User getUserInfo(UserDTO userDTO){QueryWrapperUser queryWrapper new QueryWrapper();queryWrapper.eq(username,userDTO.getUsername()).eq(password,userDTO.getPassword());User user;try {// 可能查到多条记录后台报异常写个异常类主动捕获异常user getOne(queryWrapper);}catch (Exception e){ // 可能查出多个符合条件的记录LOG.error(e);throw new ServiceException(Constants.CODE_500,系统错误);}return user;}/*** 根据角色名称获取菜单列表* param roleName* return*/public ListMenu getRoleMenus(String roleName){Integer roleId roleMapper.getRoleIdByName(roleName);ListInteger menuIds roleMenuMapper.getMenuIdsByRoleId(roleId);// 查询系统中所有菜单,树结构ListMenu menus menuService.findMenus();// new一个最后筛选完成之后的listArrayListMenu roleMenus new ArrayList();for (Menu menu : menus) {if(menuIds.contains(menu.getId())){roleMenus.add(menu);}ListMenu children menu.getChildren();// 移除children中不在menuIds集合中的元素if (children ! null) {children.removeIf(child - !menuIds.contains(child.getId()));}}return roleMenus;}}
1.5 MenuServiceImpl
将写在MenuController中的方法提取到service中更符合现实中的开发 如下图所示controller不含业务代码
package com.ppj.service.impl;import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ppj.entity.Menu;
import com.ppj.mapper.MenuMapper;
import com.ppj.service.IMenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;import java.util.List;
import java.util.stream.Collectors;/*** p* 服务实现类* /p** author ppj* since 2024-05-29*/
Service
public class MenuServiceImpl extends ServiceImplMenuMapper, Menu implements IMenuService {public ListMenu findMenus(String name) {QueryWrapperMenu queryWrapper new QueryWrapper();if(StrUtil.isNotBlank(name)){queryWrapper.like(name,name);}ListMenu list list(queryWrapper);//找出一级菜单ListMenu parentNodes list.stream().filter(menu - menu.getPid() null).collect(Collectors.toList());//找出一级菜单的子菜单for (Menu menu : parentNodes) {menu.setChildren(list.stream().filter(m - menu.getId().equals(m.getPid())).collect(Collectors.toList()));}return parentNodes;}}2. 前端实现
2.1 User.vue
主要是表格多添加role字段添加框增加角色选择框
templatedivdiv stylemargin: 10px 0el-input stylewidth: 200px placeholder请输入名称 suffix-iconel-icon-search v-modelusername/el-inputel-input stylewidth: 200px placeholder请输入地址 suffix-iconel-icon-position classml-5 v-modeladdress/el-inputel-button classml-5 typeprimary clickgetList搜索/el-buttonel-button iconel-icon-refresh sizemini clickresetQuery重置/el-button/divdiv stylemargin: 10px 0el-button typeprimary clickhandleAdd新增 i classel-icon-circle-plus-outline/i/el-buttonel-button typewarning plain iconel-icon-edit sizemini :disabledsingle clickhandleUpdate修改/el-buttonel-button typedanger :disabledmultiple clickhandleDelete删除 i classel-icon-remove-outline/i/el-button!-- el-upload actionhttp://localhost:9000/user/import :show-file-listfalse acceptxlsx :on-successhandleImport styledisplay: inline-block--
!-- el-button typeprimary classml-5导入 i classel-icon-bottom/i/el-button--
!-- /el-upload--el-button typesuccess clickhandleImport classml-5导入 i classel-icon-bottom/i/el-buttonel-button typewarning clickhandleExport classml-5导出 i classel-icon-top/i/el-button/divel-table v-loadingloading :datatableData border stripe :header-cell-class-nameheaderBg selection-changehandleSelectionChangeel-table-column typeselection width55/el-table-columnel-table-column propid label序号 width140/el-table-columnel-table-column propusername label用户名 width140/el-table-columnel-table-column propnickname label昵称 width140/el-table-columnel-table-column proprole label角色 width140template v-slotscopeel-tag{{ scope.row.role}}/el-tag/template/el-table-columnel-table-column propemail label邮箱 width200/el-table-columnel-table-column propaddress label地址 width140/el-table-columnel-table-column propcreateTime label创建时间 width140/el-table-columnel-table-column label操作 aligncentertemplate v-slotscopeel-button typesuccess clickhandleUpdate(scope.row)编辑 i classel-icon-edit/i/el-buttonel-button typedanger clickhandleDelete(scope.row)删除 i classel-icon-remove-outline/i/el-button/template/el-table-column/el-tablediv stylepadding: 10px 0el-paginationclasspagesize-changehandleSizeChangecurrent-changehandleCurrentChange:page-sizes[5, 10]:page-sizepageSizelayouttotal, sizes, prev, pager, next, jumper:totaltotal/el-pagination/div!-- 用户信息 --el-dialog title用户信息 :visible.syncdialogFormVisible width30% el-form label-width80px sizesmallel-form-item label用户名el-input v-modelform.username autocompleteoff/el-input/el-form-itemel-form-item label昵称el-input v-modelform.nickname autocompleteoff/el-input/el-form-itemel-form-item label角色 el-select v-modelform.role placeholder请选择 stylewidth: 100%el-optionv-foritem in roles:keyitem.name:labelitem.name:valueitem.roleKey/el-option/el-select/el-form-itemel-form-item label邮箱el-input v-modelform.email autocompleteoff/el-input/el-form-itemel-form-item label电话el-input v-modelform.phone autocompleteoff/el-input/el-form-itemel-form-item label地址el-input v-modelform.address autocompleteoff/el-input/el-form-item/el-formdiv slotfooter classdialog-footerel-button clickdialogFormVisible false取 消/el-buttonel-button typeprimary clicksave确 定/el-button/div/el-dialog!-- 用户导入对话框 --el-dialog :titleupload.title :visible.syncupload.open width400pxel-uploadrefupload:limit1accept.xlsx, .xls:actionupload.url:disabledupload.isUploading:on-progresshandleFileUploadProgress:on-successhandleFileSuccess:auto-uploadfalsedragi classel-icon-upload/idiv classel-upload__text将文件拖到此处或em点击上传/em/divdiv classel-upload__tip slottipel-link typeinfo stylefont-size: 16px;color:green clickimportTemplate下载模板/el-link/divdiv classel-upload__tip stylecolor: red slottip提示仅允许导入“xls”或“xlsx”格式文件/div/el-uploaddiv slotfooter classdialog-footerel-button typeprimary clicksubmitFileForm确 定/el-buttonel-button clickupload.open false取 消/el-button/div/el-dialog/div/templatescript
export default {name: User,data(){return {tableData: [],pageSize: 5,total: 0,pageNum: 1,username: ,address: ,collapseBtnClass: el-icon-s-fold,isCollapse: false,sideWidth: 200,logoTextShow: true,headerBg: headerBg,dialogFormVisible: false,form: {},// 遮罩层loading: true,// 选中数组ids: [],// 非单个禁用single: true,// 非多个禁用multiple: true,//用户导入参数upload: {//是否显示弹出层用户导入open: false,//弹出层标题用户导入title: ,//是否禁用上传// isUploading: false,//是否更新已经存在的用户数据//updateSupport: 0,//设置上传的请求头部//headers: ,//上传的地址url: http://localhost:9000/user/import,},roles: [],}},created() {this.getList();},methods: {getList(){this.loading true;this.request.get(/user/page,{params: {pageNum: this.pageNum,pageSize: this.pageSize,username: this.username,address: this.address}}).then(res {if(res.code 200){this.tableData res.data.records;this.total res.data.total;this.loading false;}else{this.$message.error(res.msg)}})this.request.get(/role).then(res {if(res.code 200){this.roles res.data;}else{this.$message.error(res.msg)}})},handleSizeChange(val) {this.pageSize val;},handleCurrentChange(val) {this.pageNum val;this.getList();},// 多选框选中数据handleSelectionChange(selection) {this.ids selection.map(item item.id);this.single selection.length ! 1;this.multiple !selection.length;},// 重置按钮resetQuery(){this.username undefined;this.address undefined;this.getList();},// 新增handleAdd(){this.dialogFormVisible true;this.form {};},save(){this.request.post(/user,this.form).then(res {if(res.code 200 || res.code 200){this.$message.success(操作成功)}else {this.$message.error(操作失败)}this.dialogFormVisible false;this.getList();})},// 修改handleUpdate(row){// 表单置空this.reset();// 重新查询数据const userId row.id || this.ids;this.request.get(/user/userId).then(response {this.form response.data;this.dialogFormVisible true;});},reset(){this.form.username undefined;this.form.nickname undefined;this.form.email undefined;this.form.phone undefined;this.form.address undefined;},// 删除handleDelete(row){let _this this;const userIds row.id || this.ids;this.$confirm(是否确认删除用户编号为 userIds 的数据项, 删除用户, {confirmButtonText: 确定,cancelButtonText: 取消,type: warning}).then(() {_this.request.delete(/user/userIds).then(res{if(res.code 200 || res.code 200){_this.$message.success(删除成功)}else {_this.$message.error(删除失败)}this.getList();})}).catch(() {});},// 导出handleExport(){window.open(http://localhost:9000/user/export);this.$message.success(导出成功);},// handleImport(){// this.$message.success(导入成功)// this.getList()// },handleImport(){this.upload.title 用户导入this.upload.open true},importTemplate(){this.$message.success(正在下载模版);window.open(http://localhost:9000/user/download)},//文件上传处理handleFileUploadProgress(event,file,fileList){//this.upload.isUploading true;this.loading true;},//文件上传成功处理handleFileSuccess(response,file,fileList){this.loading false;this.upload.open false;// this.upload.isUploading false;this.$refs.upload.clearFiles();this.$message.success(导入成功);this.getList()},//提交上传文件submitFileForm(){this.$refs.upload.submit();}}
}
/scriptstyle scoped/style2.2 动态菜单设计
2.2.1 Login.vue
在登录成功的时候将菜单存到浏览器中
templatediv classwrapperdiv stylemargin: 200px auto; background-color: #fff; width: 350px; height: 300px; padding: 20px; border-radius: 10pxdiv stylemargin: 20px 0; text-align: center; font-size: 24pxb登 录/b/divel-form :modeluser :rulesrules refuserFormel-form-item propusernameel-input sizemedium stylemargin: 10px 0 prefix-iconel-icon-user v-modeluser.username/el-input/el-form-itemel-form-item proppasswordel-input sizemedium stylemargin: 10px 0 prefix-iconel-icon-lock show-password v-modeluser.password/el-input/el-form-itemel-form-item stylemargin: 10px 0; text-align: rightel-button typeprimary sizesmall autocompleteoff clicklogin登录/el-buttonel-button typewarning sizesmall autocompleteoff click$router.push(/register)注册/el-button/el-form-item/el-form/div/div
/templatescript
export default {name: Login,data() {return {user: {},rules: {username: [{ required: true, message: 请输入用户名, trigger: blur },{ min: 3, max: 10, message: 长度在 3 到 5 个字符, trigger: blur }],password: [{ required: true, message: 请输入密码, trigger: blur },{ min: 1, max: 20, message: 长度在 1 到 20 个字符, trigger: blur }],}}},methods: {login() {this.$refs[userForm].validate((valid) {if (valid) { // 表单校验合法this.request.post(/user/login, this.user).then(res {if(res.code 200 || res.code 200) {localStorage.setItem(loginUser,JSON.stringify(res.data));localStorage.setItem(menus,JSON.stringify(res.data.menus));this.$router.push(/)this.$message.success(登录成功)} else {this.$message.error(res.msg)}})} else {return false;}});}}
}
/scriptstyle
.wrapper {height: 100vh;background-image: linear-gradient(to bottom right, #FC466B , #3F5EFB);overflow: hidden;
}
/style2.2.2 Aside.vue
侧边栏动态显示菜单
templateel-menu :default-openeds[1, 3] stylemin-height: 100%; overflow-x: hiddenbackground-colorrgb(48, 65, 86)text-color#fffactive-text-color#ffd04b:collapse-transitionfalse:collapseisCollapserouterdiv styleheight: 60px; line-height: 60px; text-align: centerimg src../assets/logo.png alt stylewidth: 20px; position: relative; top: 5px; right: 5pxb stylecolor: white v-showlogoTextShow后台管理系统/b/divdiv v-foritem in menus :keyitem.id!-- 一级菜单 --div v-ifitem.pathel-menu-item :indexitem.pathtemplate slottitlei :classitem.icon/ispan slottitle{{ item.name }}/span/template/el-menu-item/div!-- 二级菜单 --div v-elseel-submenu :indexitem.idtemplate slottitlei :classitem.icon/ispan slottitle{{ item.name }}/span/templatediv v-forsubItem in item.children :keysubItem.idel-menu-item :indexsubItem.pathtemplate slottitlei :classsubItem.icon/ispan slottitle{{ subItem.name }}/span/template/el-menu-item/div/el-submenu/div/div/el-menu
/templatescript
export default {name: Aside,props: {isCollapse: Boolean,logoTextShow: Boolean},data() {return {menus: localStorage.getItem(menus) ? JSON.parse(localStorage.getItem(menus)) : []}},}
/scriptstyle scoped/style2.3 动态路由设计
为什么设置动态路由这是因为没有其他页面权限的用户也是可以访问其他页面
如下图所示
安其拉是普通用户只有主页的菜单权限自己设置的角色所拥有的菜单
2.3.1 菜单表新增字段page_path
对应每个页面组件名称
CREATE TABLE sys_menu (id int(11) NOT NULL AUTO_INCREMENT COMMENT id,name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 名称,path varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 路径,page_path varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 页面路径,pid int(11) DEFAULT NULL COMMENT 父级id,icon varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 图标,description varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 描述,deleted tinyint(1) DEFAULT 0 COMMENT 逻辑删除,PRIMARY KEY (id) USING BTREE
) ENGINEInnoDB AUTO_INCREMENT12 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci ROW_FORMATDYNAMIC
2.3.2 路由设计
import Vue from vue
import VueRouter from vue-router
import Manage from ../views/Manage.vue
import store from /store;Vue.use(VueRouter)
//定义一个路由对象数组
const routes [{path: /login,name: 登录,component: () import(../views/Login.vue)},{path: /register,name: 注册,component: () import(../views/Register.vue)}]//使用路由对象数组创建路由实例供main.js引用
const router new VueRouter({mode: history,base: process.env.BASE_URL,routes
})// 注意刷新页面会导致页面路由重置
export const setRoutes () {const storeMenus localStorage.getItem(menus);if (storeMenus) {// 获取当前的路由对象名称数组const currentRouteNames router.getRoutes().map(v v.name)if (!currentRouteNames.includes(Manage)) {// 拼装动态路由const manageRoute { path: /, name: Manage, component: () import(../views/Manage.vue), redirect: /home, children: [{ path: person, name: 个人信息, component: () import(../views/Person.vue)},// { path: password, name: 修改密码, component: () import(../views/Password.vue)}] }const menus JSON.parse(storeMenus)menus.forEach(item {if (item.path) { // 当且仅当path不为空的时候才去设置路由let itemMenu { path: item.path.replace(/, ), name: item.name, component: () import(../views/ item.pagePath .vue),meta: { title: item.name }}manageRoute.children.push(itemMenu)} else if(item.children.length) {item.children.forEach(item {if (item.path) {let itemMenu { path: item.path.replace(/, ), name: item.name, component: () import(../views/ item.pagePath .vue),meta: { title: item.name }}manageRoute.children.push(itemMenu)}})}})// 动态添加到现在的路由对象中去router.addRoute(manageRoute)}}
}// 重置我就再set一次路由
setRoutes()// 路由守卫
// router.beforeEach((to, from, next) {
// localStorage.setItem(currentPathName,to.name); // 设置当前的路由名称为了在Header组件中去使用
// store.commit(setPath) // 触发store的数据更新
// next() // 放行路由
// })export default router
2.3.3 登录界面Login.vue设置路由
改动登录页面在成功登录时设置路由
// 导入
import {setRoutes} from /router;// 当登录成功时同时设置路由
localStorage.setItem(user,JSON.stringify(res.data)); //存储用户信息到浏览器
localStorage.setItem(menus,JSON.stringify(res.data.menus)); //存储菜单到浏览器
setRoutes();
this.$router.push(/);
this.$message.success(登录成功);总结
本篇主要讲解动态菜单和动态路由的设计与实现
写在最后
如果此文对您有所帮助请帅戈靓女们务必不要吝啬你们的Zan感谢不懂的可以在评论区评论有空会及时回复。 文章会一直更新