当前位置: 首页 > 工具软件 > HomeCenter > 使用案例 >

微人事(二) Home页的制作

柳俊逸
2023-12-01

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页制作完成。

 类似资料: