项目前端学习笔记目录
B站云E办Vue+SpringBoot前后端分离项目——搭建vue.js项目
B站云E办Vue+SpringBoot前后端分离项目——首页菜单功能
项目后端学习笔记目录
B站云E办Vue+SpringBoot前后端分离项目——MVC三层架构搭建后台项目
一、首页设计
上一篇B站云E办Vue+SpringBoot前后端分离项目——搭建vue.js项目介绍了项目背景、前端vue项目结构搭建及登录模块的设计与实现。在这一篇我们将进入首页的开发中,主要实现了目录的动态加载功能。这里的动态加载是指浏览器可以根据登录用户的用户名信息判断其拥有的菜单角色,动态加载菜单。
数据菜单由路由模式从后端获取,实现自动展示相应的菜单。为了实现下图所示的层级结构,在代码里创建相应的目录,内容可以先是简单的一行文字,先把骨架搭建起来,后续进行内容的填充。在实现这个页面时后端的接口设计实在有点复杂,在后续的更新中会加入后端的逻辑一并进行。为了更有逻辑和层次的写博客,后端的接口实现部分将分开写。边写前端边搞后端,还时不时一堆bug,脑袋大。
1.代码目录结构
首先先把菜单的骨架搭建起来,新建以下若干页面,具体实现待后面继续补充。
views/emp基本资料 新建 EmpBasic.vue EmpAdv.vue
views/per 员工资料 新建 PerEmp.vu PerEc.vue PerTrain.vue PerSalary.vue PerMv.vue
views/sal 工资账套 新增SalSob.vue SalSobcfg.vue SalTable.vue SalMonth.vue SalSearch.vue
views/sta 综合信息统计 新增StaAll.vue StaScore.vue StaPers.vue StaRecord.vue
views/sys 系统管理 新增 SysBasic.vue SysConfig.vue SysLog.vue SysAdmin.vue SysData.vue SysInit.vue
2.后端请求菜单接口返回信息
我们设计的菜单是根据用户信息加载的路由信息,即不同用户可能有不同的菜单权限。接口返回的菜单信息如下。通过children表示子菜单,子菜单中的parentId与父菜单的id相等时表示一个确定的父子菜单关系。如下的关系表示有一个层级菜单“员工资料/基本资料”。
component表示组件所在的文件名称。
3.封装菜单请求工具src/utils/menus.js
为了方便管理菜单数据,使用vuex的全局状态管理,将菜单数据存于store.state中。如果store.state.routes有数据,则直接使用。否则初始化路由菜单,通过getRequest('/system/config/menu')方法从后端获取路由数据,按照层次关系拆分。
import {getRequest} from "@/utils/api";
// 菜单请求工具类
// router 路由; store Vuex
export const initMenu = (router, store) => {
// 如果有数据,初始化路由菜单
if (store.state.routes.length > 0) {
return;
}
getRequest('/system/config/menu').then(data => {
// 如果数据存在 格式化路由
if (data) {
// 格式化好路由
let fmtRoutes = formatRoutes(data)
// 添加到 router
router.addRoutes(fmtRoutes)
// 将数据存入 Vuex
store.commit('initRoutes',fmtRoutes)
// 连接 WebSocket
store.dispatch('connect')
}
})
}
export const formatRoutes = (routes) => {
let fmtRoutes = []
routes.forEach(router => {
let {
path,
component,
name,
iconCls,
children
} = router;
// 如果有 children 并且类型是数组
if (children && children instanceof Array) {
// 递归
children = formatRoutes(children)
}
// 单独对某一个路由格式化 component
let fmRouter = {
path: path,
name: name,
iconCls: iconCls,
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);
}
}
}
fmtRoutes.push(fmRouter)
})
return fmtRoutes
}
如何根据接口中的component字段找到对应的代码路径呢?
通过对接口对象中的component字段分类查找,例如component以Home开头,源代码在src/views/Home.vue中。
if (component.startsWith('Home')) {
require(['@/views/' + component + '.vue'], resolve);
}
initMenu方法将路由数据存于store中,如果store中有数据则无需初始化,否则,初始化。
什么时候调用?每一个页面都需要调用初始化菜单方法。我们可以放在路由拦截器里,每次访问路由都要执行一次。接下来设计路由拦截器
4.添加路由拦截器——更新main.js
使用 router.beforeEach 注册一个全局前置守卫。(to, from, next)三个参数:to 要去的路由; from 来自哪里的路由 ; next() 放行。用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行。
用户首次登陆时,将当前用户信息保存在sessionStorage的user中,每次路由切换时获取用户的登陆信息。
// 使用 router.beforeEach 注册一个全局前置守卫
router.beforeEach((to, from, next) => {
// to 要去的路由; from 来自哪里的路由 ; next() 放行
// 用户登录成功时,把 token 存入 sessionStorage,如果携带 token,初始化菜单,放行
if (window.sessionStorage.getItem('tokenStr')) {
initMenu(router, store)
// 如果用户不存在
if (!window.sessionStorage.getItem('user')
) {
// 判断用户信息是否存在
return getRequest('/admin/info').then(resp => {
if (resp) {
// 存入用户信息,转字符串,存入 sessionStorage
window.sessionStorage.setItem('user', JSON.stringify(resp))
// 同步用户信息 编辑用户
store.commit('INIT_ADMIN',resp)
next();
}
})
}
next();
} else {
if (to.path === '/') {
next()
} else {
next('/?redirect=' + to.path)
}
}
})
5.配置store/index.js
通过vuex进行路由状态管理
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// 导入 Vuex
const store = new Vuex.Store({
state: {
routes: []
},
mutations: { // 与 state 同步执行;可以改变 state 对应的值的方法
// 初始化路由 菜单
initRoutes(state, data) {
state.routes = data
},
},
// 异步执行
actions: {
}
})
export default store;
在Main.js中引入store
import store from './store'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
6.首页功能及样式实现
导航菜单功能实现使用element-ui自带的container布局容器。选择界面样式如下:
相关组件介绍
el-container 外层容器
el-header 顶栏容器
el-aside 侧边栏容器 el-menu导航区域
el-main 主要区域容器
el-footer底栏容器
在el-menu导航里添加router属性实现菜单路由的动态渲染
Home.vue整体上实现了首页左侧菜单的获取和展示,右上角的个人中心的设置。从store.state中获取当前菜单信息、当前用户的登陆信息.
在el-menu导航里添加router属性实现菜单路由的动态渲染;首页导航菜单使用element-ui的NavMenu导航菜单控件。使用属性unique-opened:保证每次点击菜单只有一个菜单的展开。使用router属性,在激活导航时以 index 作为 path 进行路由跳转。
通过el-dropdown的@command点击菜单项触发的事件回调方法绑定el-dropdown-item中的command,实现注销登陆和进入个人中心功能。element的MessageBox弹框实现注销登陆提示弹框。退出登陆后清除vuex中的菜单信息。
使用el-breadcrumb面包屑控件实现显示当前页面的路径,快速返回之前的任意页面功能。对于非首页的页面v-if="this.$router.currentRoute.path!=='/home'"显示层级:首先/当前页。
对于首页v-if="this.$router.currentRoute.path==='/home'",显示欢迎字体。
7.Home.vue文件-首页
<template>
<div>
<el-container>
<el-header class="homeHeader">
<div class="title">云办公</div>
<!-- 1-1 添加在线聊天入口 -->
<div>
<el-button type="text" icon="el-icon-bell" size="normal"
style="margin-right: 8px;color: black;" @click="goChar"></el-button>
<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">注销登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-container>
<el-aside width="200px">
<!-- 1、添加 router -->
<el-menu router unique-opened>
<!-- 2、循环整个路由组件,不展示 hidden: true 的路由组件 -->
<el-submenu :index="index +''" v-for="(item,index) in routes"
:key="index" v-if="!item.hidden">
<template slot="title"><i :class="item.iconCls" style="color: black;margin-right: 5px"></i>
<span>{{ item.name }}</span>
</template>
<!-- 3、循环遍历子路由 -->
<el-menu-item :index="children.path"
v-for="(children,index) in item.children" :key="index">{{ children.name }}
</el-menu-item>
</el-submenu>
</el-menu>
</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 class="homeRouterView"/>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
// 获取用户信息,将字符串转对象
// user: JSON.parse(window.sessionStorage.getItem('user'))
}
},
computed: {
// 从 vuex 获取 routes
routes() {
return this.$store.state.routes
},
user() {
return this.$store.state.currentAdmin
}
},
methods: {
// 1-2 进入在线聊天页面
goChar() {
this.$router.push('/chat')
},
// 注销登录
commandHandler(command) {
if (command === 'logout') {
// 弹框提示用户是否要删除
this.$confirm('此操作将注销登录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 注销登录
this.postRequest('/logout')
// 清空用户信息
window.sessionStorage.removeItem('tokenStr')
window.sessionStorage.removeItem('user')
// 路由替换到登录页面
// this.$router.replace('/')
// 清空菜单信息;在src/utils/menus.js 中初始化菜单信息
this.$store.commit('initRoutes', [])
this.$router.replace('/')
}).catch(() => {
this.$message({
type: 'info',
message: '已取消注销登录'
});
});
}
if (command === 'userinfo') {
this.$router.push('/userinfo')
}
}
}
}
</script>
<style scoped>
.homeHeader {
background: #3e9ef5;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-sizing: border-box;
}
.homeHeader .title {
font-size: 30px;
/*font-family: 微软雅黑;*/
font-family: 华文楷体;
color: white;
}
.homeHeader .userInfo {
cursor: pointer;
}
.el-dropdown-link img {
width: 48px;
height: 48px;
border-radius: 50%;
margin-left: 8px;
}
.homeWelcome {
text-align: center;
font-size: 30px;
font-family: 华文楷体;
color: #409ef4;
padding-top: 50px;
}
.homeRouterView {
margin-top: 10px;
}
</style>
8.更新路由router/index.js
忽略router/index.js的hidden:true的
/home路由从首页获取
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Login',
component: Login,
hidden: true // 不会被循环遍历出来
}
]
const router = new VueRouter({
routes
})
export default router
9.index.html消除边距
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>yeb-front</title>
</head>
<body style="margin:0px;padding:0px">
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>