Home页的制作主要是菜单栏的动态加载,根据不同人的角色加载不同的导航栏。
(一)Vuex的作用
由于在用户登录后,无论是跳转到哪一个页面,都需要菜单导航栏,因此我们可以说,这个用户在登录之后需要一直使用同一个菜单导航栏,所以菜单导航栏需要做成一个全局且是共享并且是单例的组件。因此我们可以将这个组件存在Vuex中的store中,实现这个组件成为单例,全局,且共享的组件。
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
routes: []
},
mutations: {
initRoutes(state, data) {
state.routes = data;
}
},
actions: {
}
}
)
1)、其中state中定义的变量,如routes就是菜单栏中的路径,该变量就是全局、共享且单例。
2)、mutations用于更新state中的变量,代码中的mutations就是先初始化菜单导航栏的路由,然后把返回的路由赋值给states.routes。(使用下面的store.commit(‘initRoutes’, fmtRoutes); 进行调用)
3)、要是这个组件(store)真正能被用起来,需要在main.js的Vue中进行注册,具体代码如下:
import store from './store'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
(二)router的前置导航守卫vue-router
在微人事(一)登录模块中有介绍到router,其中router定义了部分的路由routes,而这次的菜单导航栏我们需要动态地往router添加更多地routes。并且是在页面跳转之前就加载好了。因此需要使用前置导航守卫vue-router来实现我们的需求。
由于该项目的vue-router是写在main.js中,因此是属于全局导航守卫,因为调用的router.beforeEach()方法,因此称为是前置的导航守卫。每次当页面有刷新或者是点击链接跳转到别的页面的时候,路由router都会发生变化,此时就会调用router.beforeEach()方法,而初始化菜单导航栏的方法(initMenu())放在了该方法中,因此每次自动调用router.beforeEach()方法的时候都会自动调用initMenu()方法,从而达到加载菜单导航栏的效果。
main.js
router.beforeEach((to, from, next) => {
if(to.path == '/'){ //如果访问的路径是首页,则直接跳转
next();
}else{
//否则,如果用户已登录,那么需要先执行初始化菜单导航栏,之后跳转到访问的Url
//若用户未登录,则会被后端的SpringSecurity弹回登录页
//(之后权限管理部分会将),此时浏览器显示的是登陆后需要重定向的url,
//待登录后,直接跳转到该url
if(window.sessionStorage.getItem("user")){
initMenu(router,store);
next();
}else{
next("?redirect="+to.path);
}
}
})
注意: router.beforeEach()需要放在Vue注册router之前,不然当你按下F5刷新页面的时候,该方法不会再次自己调用,具体的原因我没有去细究,问了公司的前端,他也表示不懂,需要去看源码才知道。
(三)初始化菜单导航栏—initMenu()
menus.js
import {getRequest} from "./api";
export const initMenu = (router, store) => {
//因为每次路由跳转的时候全局前置路由导航守卫都会调用该方法
//但是如果不是初次加载的话,store里面是有菜单导航栏的,因此不用每次都去调用
//后端接口去请求菜单导航栏,所以如果store不为空的时候直接返回。
if (store.state.routes.length > 0) {
return;
}
//但是,如果store为空的话,就需要去请求后端接口,根据当前登录的用户的角色去加载
//相应的菜单导航栏,用户信息存在session,因此不用特地传参到该接口中
//得到菜单导航栏的信息后,这些信息都是存在数据库中,只是一些字符串,因此需要做一些格式化处理
getRequest("/system/config/menu").then(data => {
if (data) {
//格式化返回的菜单栏导航项
let fmtRoutes = formatRoutes(data);
//把格式化后的菜单导航项添加到路由对象中
router.addRoutes(fmtRoutes);
//更新store中的state中的routes,该方法会在底层调用store中的
//mutation中的方法,而mutation中包含了更新state的方法。
store.commit('initRoutes', fmtRoutes);
}
})
}
//格式菜单导航项的核心方法:
export const formatRoutes = (routes) => {
//1.定义了存储格式化后的菜单导航项的容器
let fmRoutes = [];
//2。遍历从数据库获取到的未格式化的导航项
//这里就类似于Java的for(String str : String[])
routes.forEach(router => {
let {
path,
component,
name,
meta,
iconCls,
children
} = router;
//这里的children是router.children
//如果router.children不为空且它是个数组,说明这个菜单项中还有子菜单项
//因此递归调用格式化子菜单项。
if (children && children instanceof Array) {
children = formatRoutes(children);
}
//格式化菜单导航栏的核心
//每一项就是一个fmRouter,最后调用component方法,将传入的组件名
//和项目中真实的vue文件进行绑定
let fmRouter = {
path: path,
name: name,
iconCls: iconCls,
meta: meta,
children: children,
component(resolve) {
if (component.startsWith("Home")) {
require(['../views/' + component + '.vue'], resolve);
} else if (component.startsWith("Emp")) {
require(['../views/emp/' + component + '.vue'], resolve);
} else if (component.startsWith("Per")) {
require(['../views/per/' + component + '.vue'], resolve);
} else if (component.startsWith("Sal")) {
require(['../views/sal/' + component + '.vue'], resolve);
} else if (component.startsWith("Sta")) {
require(['../views/sta/' + component + '.vue'], resolve);
} else if (component.startsWith("Sys")) {
require(['../views/sys/' + component + '.vue'], resolve);
}
}
}
//最后,把格式化好后的每一项菜单导航栏放入容器中
fmRoutes.push(fmRouter);
})
//把容器返回出去
return fmRoutes;
}
(四)页面展示 Home.vue
<template>
<div>
<el-container>
<el-header class="homeHeader">
<div class="title">微人事</div>
<!--下拉抽屉中定义了一个@command方法,可以根据抽屉项不同的command调用不同的方法逻辑-->
<el-dropdown class="userInfo" @command="commandHandler">
<span class="el-dropdown-link">
{{ user.name }}<i><img :src="user.userface"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="userInfo">个人中心</el-dropdown-item>
<el-dropdown-item command="setting">设置</el-dropdown-item>
<el-dropdown-item command="logout" divided>注销登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-header>
<el-container>
<el-aside width="200px">
<div>
<!--菜单栏:设置了动态的路由router和样式:每次只打开一个菜单导航项 -->
<el-menu router unique-opened>
<!-- 遍历router中的routes,v-if="!item.hidden" 表示有一些被隐藏的route不被展示,这个属性是我们自定义的-->
<el-submenu :index="index+''" v-for="(item,index) in routes" v-if="!item.hidden" :key="index">
<template slot="title">
<i style="color: #409eff;margin-right: 5px" :class="item.iconCls"></i>
<span>{{item.name}}</span>
</template>
<el-menu-item-group>
<!--遍历子菜单项-->
<el-menu-item :index="children.path" v-for="(children,indexj) in item.children" :key="indexj">{{children.name}}</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</el-aside>
<el-main>
<!--定义在页面上的面包屑导航,只有两层,并且没有做成动态的,其实这里是比较瑕疵的-->
<el-breadcrumb separator-class="el-icon-arrow-right" v-if="this.$router.currentRoute.path!='/home'">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<!--当前路径的名称-->
<el-breadcrumb-item>{{this.$router.currentRoute.name}}</el-breadcrumb-item>
</el-breadcrumb>
<!--如果是首页的话,就显示这一行字-->
<div class="homeWelcome" v-if="this.$router.currentRoute.path=='/home'">欢迎来到微人事!!!</div>
<router-view/>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
name: "Home",
data() {
return {
user: JSON.parse(window.sessionStorage.getItem("user"))
}
//主要的作用是:由于每次页面刷新,虽然全局导航守卫每次都会加载一遍,但是没有能够
//展示到页面的方法,而computed中的方法每次刷新页面都会调用,所以routes()就是把路由调用到页面显示的桥梁
},computed:{
routes(){
return this.$store.state.routes;
}
},
methods: {
//command方法(目前只写了退出逻辑)
//1.弹出警示框,提示是否需要注释登录
commandHandler(cmd) {
if (cmd == 'logout') {
this.$confirm('此操作将注销登录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//点击确定后,调用后端的logout接口,
//从session中删除用户信息,销毁菜单导航栏,跳转到首页
this.getRequest('/logout');
window.sessionStorage.removeItem("user");
this.$store.commit('initRoutes',[]);
this.$router.replace('/');
}).catch(() => {
//点击取消后,只有弹出提示框,不做其他操作
this.$message({
type: 'info',
message: '已取消删除'
});
});
}
}
}
}
</script>
<style>
.homeHeader {
background-color: #409eff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 15px;
box-sizing: border-box;
}
.homeHeader .title {
font-size: 30px;
font-family: 华文行楷;
color: white;
}
.homeHeader .userInfo {
cursor: pointer;
}
.el-dropdown-link img {
width: 48px;
height: 48px;
/* 让头像框变圆*/
border-radius: 24px;
margin-left: 8px;
}
.el-dropdown-link {
display: flex;
align-items: center;
}
.homeWelcome{
text-align: center;
font-size: 30px;
font-family: 华文行楷;
color: #409eff;
padding-top: 50px;
}
</style>
后端:主要调用的接口有两个 /system/config/menu 和 /logout 接口
(一)、/system/config/menu
后端的逻辑其实很简单,就是单纯的Controller–>Service–>Mapper
Controller就是调用的MenuService的getMenuById()方法,得到一个菜单项集合而已,无它。
SystemConfigController.java
@RestController
@RequestMapping("/system/config")
public class SystemConfigController {
@Autowired
private MenuService menuService;
@GetMapping("/menu")
public List<Menu> getMenusByHrId(){
return menuService.getMenusByHrId();
}
}
主要的业务逻辑在Service层,之前有朋友问我,想这些业务逻辑比较简单的,可不可以不要写Service层,因为感觉Service层的东西,Controller层也能做,虽然省略后,确实也能把功能实现了,但是我们做一个系统或者做一个其他什么东西,都得考虑他的扩展性,如果把所有的业务逻辑都写在的Controller层里面,那么之后会由于Controller层的耦合度过高,扩展会变得很困难。因此就算业务逻辑简单,也要遵守MVC模式去写。
Controller层主要负责页面跳转(前后端不分离的情况)或者向前端提供JSON串(前后端分离的情况),或者接收前端传来的数据。
Service层主要负责对Model层传来的数据做一些业务上的处理,或者对Controller层传来的数据做业务处理,然后传给Model层。
Model层就是对数据库进行CRUD(增[create]查[retrieve]改[update]删[delete])了。
MenuService.java
@Service
public class MenuService {
@Autowired
private MenuMapper menuMapper;
public List<Menu> getMenusByHrId() {
return menuMapper.getMenusByHrId(((Hr)SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal()).getId());
}
}
在微人事(一)登录模块中说过,我们集成了Spring Security用于用户信息和权限的管理,因此在后端,Spring Security的SecurityContextHolder对象中保存了用户的信息,就类似于前端的sessionStorage(在保存用户信息的功能上类似),因此我们能从SecurityContextHolder中拿到用户的ID,然后调用MenuMapper的getMenusById方法去数据库中查找对应的菜单导航栏。
MenuMapper.java
public interface MenuMapper {
List<Menu> getMenusByHrId(Integer hrid);
}
无它,根据用户ID查数据库而已。
(二)/logout
退出登录用的也是Spring Security自带的logout方法,之后自定义了成功退出的处理器(logoutSuccessHandler()方法)
主要的逻辑:
1.logout():底层会清除掉用户的session
2.logoutSuccessHandler():在响应请求中写入“注销成功”的JSON串,发给前端。
前端调用了api.js(微人事(一)登录模块中有介绍)中的getRequest()方法,底层调用了axios,在axios的拦截器中包含了Message组件,因此在退出登录后,在登录页上会有消息提示框提示“注销成功”。
SecurityConfig.java
.logout()//退出登录
.logoutSuccessHandler(new LogoutSuccessHandler() {//退出登录成功的处理器
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功")));
out.flush();
out.close();
}
})
至此,Home页制作完成。