1. 简介
本项目是根据江南一点雨编写书籍《Spring Boot+Vue全栈开发实战》编写。此篇博客重点讲解一个前后端分离项目中前端模块开发,关于后端模块设计以及实现请参考博主另一篇博客:前后端分离项目中后端的实现
2. 技术架构
本项目采用当下流行的前后端分离的方式开发,后端使用 Spring Boot 开发,前端使用 Vue+ ementUI 来构建 SPA SPA 是指 Single-Page Application, ll!P 单页面应用, SPA 应用通过动 态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之 间切换打断用户体验,使应用程序更像 个桌面应用程序。在 SPA 中,所有的 HTML JavaScript css 都通过单个页面的加载来检索,或者根据用户操作动态装载适当的资源并添加到页面。在 SPA 中,前端将通过 Ajax 与后端通信。对于开发者而 SPA 直观的感受就是项目开发完成后, 只有 HTML 页面,所有页面的跳转都通过路由进行导航。前后端分离的另 个好处是 个后 端可以对应多个前端,由于后端只负责提供数据,前后端的交互都是通过 JSON 数据完成的,因此 后端开发成功后,前端可以是 PC 端页面,也可以是 Android iOS 以及微信小程序等
2.1Vue简介
Vue (读音/vju : ,类似于 view)是 套用于构建用户界面的渐进式框架。与其他大 型框架不同的是, Vue 被设计为可以自底向上逐层应用 Vue 的核心库只关 主视图层, 不仅易 于上手,还使于与第 方库或既有项目整合 方面,当与现代化的工具链以及 各种支持类库结合使用时, Vue 完全能够为复杂的单页应用提供驱动 一-Vue 官网
2.2 Element简介
Vue桌面端组件库非常多,比较流行的有 Element Vux iView mint ui muse ui 等,本项 目采用 eme 作为前端页面组件库 。要说设计,这些 UI 异都不是很大,基本上都是 Ma erial Design 风格的。 用法,强烈建议初学者通读官方文档学习(地址为 ttp: //element cn.eleme.io/#/zh-CN/component )。
3.项目构建
3.1前端项目构建
项目使用webpack构建。首先需要确保本地安装了NodeJS。 控制台或终端输入:
npm install -g vue-cli
vue init webpack vuehr
cd vuehr
npm run dev
其中在执行初始化后需要填写一些信息如下:
? Project name vuehr
? Project description A vue project
? Author xie-super <2507062649@qq.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm
vue-cli · Generated "vuehr".
具体含义如下:
- 项目名称
- 项目描述
- 项目作者
- Vue 项目构建:运行+编译还是仅运行。
- 是否安装 ue-rout
- 是否使用 ESLint
- 是否使用单元测试。
- 是否适用 Nightwatch e2e 测试
- 是否在项目创建成功后自动执行“npm install ,,安装依赖,若选择否,则在第 行命令执行之前执行“npm install” 当“ pm run dev 命令执行之后,在浏览器中输入 http://localhost:8080 ,显示页面如图:
3.2后端项目构建
开发工具IDEA
构建springboot工程
这里依赖,配置什么的就不再给出了,本文最后会给出项目链接,有兴趣的小伙伴可以根据链接下载。
4.登录模块
4.1 后端接口实现
编写 Hr类,实现 UserDetails接口
public class Hr implements UserDetails {
private Integer id;
private String name;
private String phone;
private String telephone;
private String address;
private Boolean enabled;
private String username;
private String password;
private String userface;
private String remark;
private List<Role> roles;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Hr hr = (Hr) o;
return Objects.equals(username, hr.username);
}
@Override
public int hashCode() {
return Objects.hash(username);
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone == null ? null : telephone.trim();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address == null ? null : address.trim();
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getUserface() {
return userface;
}
public void setUserface(String userface) {
this.userface = userface == null ? null : userface.trim();
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark == null ? null : remark.trim();
}
}
代码解释:
- 自定义类继承自 UserDetai ,并实现该接口中相关的方法 前端用户在登录成功后 ,需要获取当前登录用户的信息,对于 些敏感信息不必返回,使用 @Jsonlgnore 注解即可。
- 对于 isAccountNonExpired isAccountNonLocked isCredentialsNonExpired ,由于 Hr 表并未 设计相关字段,因此这里直接返回 true, sEnabled 法则根据实际情况返回。
- roles 属性中存储了当前用户的所有角色信息,在 getAuthorities 方法中,将这些角色转换为List < GrantedAuthority > 的实例返回.
编写HrService 实例用来查询用户:
@Service
public class HrService implements UserDetailsService {
@Autowired
HrMapper hrMapper;
@Autowired
HrRoleMapper hrRoleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Hr hr = hrMapper.loadUserByUsername(username);
if (hr == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
return hr;
}
}
自定义 HrService 实现UserDetailsService 接口,并实现该接口中的 loadUserByUsemame 方法, loadUserByUsemame 方法是根据用户名查询用户的所有信息,包括用 的角色,如果没有 到相 关用户,就抛 semameNotF oundException 常,表示用户不存在 ,如果查到了,就直接返回, prin Security 框架完成密码的 对操作。 接下来需要实现动态配置权限,因此还需要提供 ilterlnvocationSecurityMetadataSource AccessDecisionManager 的实例 FilterlnvocationSecurityMetadataSource 代码如下:
4.2 前端实现
- 前端UI使用Element网络请求使用Axios,先安装两者依赖
cnpm i element-ui -S
cnpm i axios -S
- main.js引入Element
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
引入 Element之后就可以在项目直接使用相关组件了。
对于网络请求在每次请求都需要判断异常情况,然后提示用户,例如请求否成功失败的原因等,考虑这些判断基本上都使用重复的代码,因此可以将网络请求封装,做成Vue的插件方便使用。由于封装的代码较长,这里就不贴出了, 在main.js导入封装的方法,,然后配置为Vue的prototy ,代码如下:
import {getRequest} from './utils/api'
import {postRequest} from './utils/api'
import {deleteRequest} from './utils/api'
import {putRequest} from './utils/api'
Vue.prototype.getRequest = getRequest;
Vue.prototype.postRequest = postRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.putRequest = putRequest;
配置完成后 接下来对于任何需要使用网络请求的地址,都可以使用 this.XXX 执行一个网络请求 例如要执行登录请求,就可以通过 this. postRequest(url, param) 执行。
- 登录页面
<template>
<el-form :rules="rules" class="login-container" label-position="left"
label-width="0px" v-loading="loading">
<h3 class="login_title">系统登录</h3>
<el-form-item prop="account">
<el-input type="text" v-model="loginForm.username" auto-complete="off" placeholder="账号"></el-input>
</el-form-item>
<el-form-item prop="checkPass">
<el-input type="password" v-model="loginForm.password" auto-complete="off" placeholder="密码"></el-input>
</el-form-item>
<el-checkbox class="login_remember" v-model="checked" label-position="left">记住密码</el-checkbox>
<el-form-item style="width: 100%">
<el-button type="primary" @click.native.prevent="submitClick" style="width: 100%">登录</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default{
data(){
return {
rules: {
account: [{required: true, message: '请输入用户名', trigger: 'blur'}],
checkPass: [{required: true, message: '请输入密码', trigger: 'blur'}]
},
checked: true,
loginForm: {
username: 'admin',
password: '123'
},
loading: false
}
},
methods: {
submitClick: function () {
var _this = this;
this.loading = true;
this.postRequest('/login', {
username: this.loginForm.username,
password: this.loginForm.password
}).then(resp=> {
_this.loading = false;
if (resp && resp.status == 200) {
var data = resp.data;
_this.$store.commit('login', data.msg);
var path = _this.$route.query.redirect;
_this.$router.replace({path: path == '/' || path == undefined ? '/home' : path});
}
});
}
}
}
</script>
<style>
.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 35px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login_title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.login_remember {
margin: 0px 0px 35px 0px;
text-align: left;
}
</style>
代码解释
- 系统登录使用 Element 中的 el-form 来实现 同时使用了 Element 标签提供的校验规则
- 当用户击“登录”按钮时,通过 this.postRequest 方法发起登录请求,登录成功后,将登录的用户信息保存到 store 中,同时跳转到 Home 页,或者某个重定向页面
4.配置路由
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login,
hidden:true
}
]
})
- 配置请求转发
最后 ,由于端项目和后端项目在不同的端口下启动,网络请求无法直接发送到后端,需要配置请求转发。下面介绍配置方式。 修改 config 录下的 index.js 文件,修改 proxyTable ,代码如下
proxyTable: {
'/': {
target: 'http://localhost:8082',
changeOrigin: true,
pathRewrite: {
'^/': ''
}
}
}
proxyTable 是 vue-cli 脚手架在开发模式下,为我们提供的一个跨域的代理中转服务器服务.基于 (http-proxy-middleware插件).
此处的8082就是后端端口号
对于proxyTable简述,请参考另一篇文章[vue前端服务器代理,proxyTable简述] (www.cnblogs.com/wasbg/p/126…)
5.动态加载用户菜单的实现
什么意思呢,本项目因为有不同的用户,所以每个用户主页所展示功能菜单也是有区别的。如下:
系统管理员主界面:
操作员界面:
其中菜单中的数据是存储在数据库中的menu表中,后端获取到数据
后端RESTful API设计如下:
/config/sysmenu
- request 无
- response success
[
{
"id": 2,
"path": "/home",
"component": "Home",
"name": "员工资料",
"iconCls": "fa fa-user-circle-o",
"children": [
{
"id": null,
"path": "/emp/basic",
"component": "EmpBasic",
"name": "基本资料",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/emp/adv",
"component": "EmpAdv",
"name": "高级资料",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": 3,
"path": "/home",
"component": "Home",
"name": "人事管理",
"iconCls": "fa fa-address-card-o",
"children": [
{
"id": null,
"path": "/per/emp",
"component": "PerEmp",
"name": "员工资料",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/per/ec",
"component": "PerEc",
"name": "员工奖惩",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/per/train",
"component": "PerTrain",
"name": "员工培训",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/per/salary",
"component": "PerSalary",
"name": "员工调薪",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/per/mv",
"component": "PerMv",
"name": "员工调动",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": 4,
"path": "/home",
"component": "Home",
"name": "薪资管理",
"iconCls": "fa fa-money",
"children": [
{
"id": null,
"path": "/sal/sob",
"component": "SalSob",
"name": "工资账套管理",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sal/sobcfg",
"component": "SalSobCfg",
"name": "员工账套设置",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sal/table",
"component": "SalTable",
"name": "工资表管理",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sal/month",
"component": "SalMonth",
"name": "月末处理",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sal/search",
"component": "SalSearch",
"name": "工资表查询",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": 5,
"path": "/home",
"component": "Home",
"name": "统计管理",
"iconCls": "fa fa-bar-chart",
"children": [
{
"id": null,
"path": "/sta/all",
"component": "StaAll",
"name": "综合信息统计",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sta/score",
"component": "StaScore",
"name": "员工积分统计",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sta/pers",
"component": "StaPers",
"name": "人事信息统计",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sta/record",
"component": "StaRecord",
"name": "人事记录统计",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": 6,
"path": "/home",
"component": "Home",
"name": "系统管理",
"iconCls": "fa fa-windows",
"children": [
{
"id": null,
"path": "/sys/basic",
"component": "SysBasic",
"name": "基础信息设置",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sys/cfg",
"component": "SysCfg",
"name": "系统管理",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sys/log",
"component": "SysLog",
"name": "操作日志管理",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sys/hr",
"component": "SysHr",
"name": "操作员管理",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sys/data",
"component": "SysData",
"name": "备份恢复数据库",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/sys/init",
"component": "SysInit",
"name": "初始化数据库",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
]
后端返回了菜单数据,前端请求该接口获取菜单数据,这里的步骤很简单, 主要分两步
-
将服务端返回的 JSON 动态添加到当前路由中
-
将服务端返回的 JSON 数据保存到store中 ,然后各个 Vue 页面根据 store 中的数据来渲染菜单。
这个模块的实现小伙伴们需要掌握一些Vuex以及Vue Rooter的基本知识
这里涉及的第一个问题是请求时机,即何时去请求菜单数据。如果直接在登录成功之后请求菜单资源,那么在请求到JSON数据之后,将其保存在 store中,以便下一次使用。但是这样会有一个问题:假如用户登录成功之后,单击Home页的某一个按钮,进入某一个子页面中,然后按一下F5键进行刷新,这个时候就会出现空白页面,因为按F5键刷新之后store中的数据就没了,而我们又只在登录成功的时候请求了一次菜单资源。要解决这个问题,有两种方案:方案一,不要将菜单资源保存到store中,而是保存到localStorage中,这样即使按F5键刷新之后数据还在;方案二,直接在每一个页面的mounted方法中都加载一次菜单资源。由于菜单资源是非常敏感的,因此不建议将其保存到本地,故舍弃方案一,但是方案二的工作量有点大,而且也不易维护,这里可以使用路由中的导航守卫来简 方案二的工作量。 下面介绍具体实现步骤。首先在 store 中创建一 routes 数组,这是 空数组,代码如下:
export default new Vuex.Store({
state: {
routes: []
},
mutations: {
initMenu(state, menus){
state.routes = menus;
}
});
开启全局守卫:
router.beforeEach((to, from, next)=> {
if (to.name == 'Login') {
next();
return;
}
var name = store.state.user.name;
if (name == '未登录') {
if (to.meta.requireAuth || to.name == null) {
next({path: '/', query: {redirect: to.path}})
} else {
next();
}
} else {
initMenu(router, store);
next();
}
}
)
代码解释:
- 这里使用router.beforeEach 配置了一个全局前置守卫。
- 首先判断目标页面是不是Login,若是Login 页面,则直接通过,因为Login 页面不需要菜单数据。
- 接下来获取store中保存的当前登录的用户数据,若获取到的用户名为“未登录”,则表示用户尚未登录,在用户尚未登录的情况下,如果要跳转到某一个页面,就需要判断该页面是否要求登录后才能访问,若要求了,则直接跳转到登录页面,并配置redirect参数。若用户已经登录,则先执行initMenu方法初始化菜单数据,然后通过next();进入下一个页面。
初始化菜单的操作如下:
export const initMenu = (router, store)=> {
if (store.state.routes.length > 0) {
return;
}
getRequest("/config/sysmenu").then(resp=> {
if (resp && resp.status == 200) {
var fmtRoutes = formatRoutes(resp.data);
router.addRoutes(fmtRoutes);
store.commit('initMenu', fmtRoutes);
}
})
}
export const formatRoutes = (routes)=> {
let fmRoutes = [];
routes.forEach(router=> {
let {
path,
component,
name,
meta,
iconCls,
children
} = router;
if (children && children instanceof Array) {
children = formatRoutes(children);
}
let fmRouter = {
path: path,
component(resolve){
if (component.startsWith("Home")) {
require(['../components/' + component + '.vue'], resolve)
} else if (component.startsWith("Emp")) {
require(['../components/emp/' + component + '.vue'], resolve)
} else if (component.startsWith("Per")) {
require(['../components/personnel/' + component + '.vue'], resolve)
} else if (component.startsWith("Sal")) {
require(['../components/salary/' + component + '.vue'], resolve)
} else if (component.startsWith("Sta")) {
require(['../components/statistics/' + component + '.vue'], resolve)
} else if (component.startsWith("Sys")) {
require(['../components/system/' + component + '.vue'], resolve)
}
},
name: name,
iconCls: iconCls,
meta: meta,
children: children
};
fmRoutes.push(fmRouter);
})
return fmRoutes;
}
代码解释:
- 在初始化菜单中,首先判断store 中的数据是否存在,如果存在,则说明这次跳转是正常的跳 转,而不是用户按F5键或者直接在地址栏输入某个地址进入的,这时直接返回,不必执行 菜单初始化。
- 若store中不存在菜单数据,则需要初始化菜单数据,通过getRequest("/config/sysmenu")方法获得菜单JSON数据之后,首先通过formnatRoutes方法将服务器返回的JSON转为router 需要的格式,这里主要是转component,因为服务端返回的component是一-个字符串,而router中需要的却是一一个组件,因此我们在formatRoutes方法中根据服务端返回的component动态加载需要的组件即可。
- 数据格式准备成功之后,一方面将数据存到store中,另一方面利用路由中的addRoutes方法将之动态添加到路由中。加载到路由数据之后,接下来就是菜单渲染了。菜单渲染操作在Home.vue组件中完成, 前端部分代码:
<template>
<div>
<el-container class="home-container">
<el-header class="home-header">
<span class="home_title">微人事</span>
<div style="display: flex;align-items: center;margin-right: 7px">
<el-dropdown @command="handleCommand">
<span class="el-dropdown-link home_userinfo" style="display: flex;align-items: center">
{{user.name}}
<i><img v-if="user.userface!=''" :src="user.userface"
style="width: 40px;height: 40px;margin-right: 5px;margin-left: 5px;border-radius: 40px"/></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>设置</el-dropdown-item>
<el-dropdown-item command="logout" divided>注销</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-header>
<el-container>
<el-aside width="180px" class="home-aside">
<div style="display: flex;justify-content: flex-start;width: 180px;text-align: left;">
<el-menu style="background: #ececec;width: 180px;" unique-opened router>
<template v-for="(item,index) in this.routes" v-if="!item.hidden">
<el-submenu :key="index" :index="index+''">
<template slot="title">
<i :class="item.iconCls" style="color: #20a0ff;width: 14px;"></i>
<span slot="title">{{item.name}}</span>
</template>
<el-menu-item width="180px"
style="padding-left: 30px;padding-right:0px;margin-left: 0px;width: 170px;text-align: left"
v-for="child in item.children"
:index="child.path"
:key="child.path">{{child.name}}
</el-menu-item>
</el-submenu>
</template>
</el-menu>
</div>
</el-aside>
<el-container>
<el-main>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item v-text="this.$router.currentRoute.name"></el-breadcrumb-item>
</el-breadcrumb>
<keep-alive>
<router-view v-if="this.$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!this.$route.meta.keepAlive"></router-view>
</el-main>
</el-container>
</el-container>
</el-container>
</div>
</template>
<script>
export default{
mounted: function () {
// this.devMsg();
},
methods: {
devMsg(){
this.$alert('为了确保所有的小伙伴都能看到完整的数据演示,数据库只开放了查询权限和部分字段的更新权限,其他权限都不具备,完整权限的演示需要大家在自己本地部署后,换一个正常的数据库用户后即可查看,这点请大家悉知!', '友情提示', {
confirmButtonText: '确定',
callback: action => {
this.$notify({
title: '重要重要!',
type: 'warning',
message: '小伙伴们需要注意的是,目前只有权限管理模块完工了,因此这个项目中你无法看到所有的功能,除了权限管理相关的模块。权限管理相关的模块主要有两个,分别是 [系统管理->基础信息设置->权限组] 可以管理角色和资源的关系, [系统管理->操作员管理] 可以管理用户和角色的关系。',
duration: 0
});
}
});
},
handleCommand(cmd){
var _this = this;
if (cmd == 'logout') {
this.$confirm('注销登录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
_this.getRequest("/logout");
_this.$store.commit('logout');
_this.$router.replace({path: '/'});
}).catch(() => {
_this.$message({
type: 'info',
message: '取消'
});
});
}
}
},
data(){
return {}
},
computed: {
user(){
return this.$store.state.user;
},
routes(){
return this.$store.state.routes
}
}
}
</script>
<style>
.home-container {
height: 100%;
position: absolute;
top: 0px;
left: 0px;
width: 100%;
}
.home-header {
background-color: #20a0ff;
color: #333;
text-align: center;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: content-box;
padding: 0px;
}
.home-aside {
background-color: #ECECEC;
}
.home-main {
background-color: #fff;
color: #000;
text-align: center;
margin: 0px;
padding: 0px;;
}
.home_title {
color: #fff;
font-size: 22px;
display: inline;
margin-left: 8px;
}
.home_userinfo {
color: #fff;
cursor: pointer;
}
.home_userinfoContainer {
display: inline;
margin-right: 20px;
}
.el-submenu .el-menu-item {
width: 180px;
min-width: 175px;
}
</style>