安全框架springSecurity+Jwt+Vue-1(vue环境搭建、动态路由、动态标签页)

一、安装vue环境,并新建Vue项目

①:安装node.js

官网(https://nodejs.org/zh-cn/)

2.安装完成之后检查下版本信息:

在这里插入图片描述

②:创建vue项目

1.接下来,我们安装vue的环境

# 安装淘宝npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# vue-cli 安装依赖包
cnpm install --g vue-cli
# 打开vue的可视化管理工具界面
vue ui

2.创建spring_security_vue项目 运行vue ui

在这里插入图片描述

3. 会为我们打开一个http://localhost:8001/dashboard的页面:

在这里插入图片描述

4.我们将在这个页面完成我们的前端Vue项目的新建。然后切换到【创建】,注意创建的目录最好是和你运行vue ui同一级。这样方便管理和切换

在这里插入图片描述

5.然后点击按钮【在此创建新项目】下一步中,项目文件夹中输入项目名称“sping_security_vue”

在这里插入图片描述

6.点击下一步,选择【手动】,再点击下一步,如图点击按钮,勾选上路由Router、状态管理Vuex,去掉js的校验。

在这里插入图片描述
在这里插入图片描述

7.下一步中,也选上【Use history mode for router】,点击创建项目,然后弹窗中选择按钮【创建项目,不保存预设】,就进入项目创建啦

稍等片刻之后,项目就初始化完成了。上面的步骤中,我们创建了一个vue项目,并且安装了Router、Vuex。这样我们后面就可以直接使用。

Router: WebApp的链接路径管理系统,简单就是建立起url和页面之间的映射关系
Vuex: 一个专为 Vue.js 应用程序开发的状态管理模式,简单来说就是为了方便数据的操作而建立的一个临时” 前端数据库“,用于各个组件间共享和检测数据变化。

ok,我们使用IDEA导入项目,看看创建好的项目长啥样子:
在这里插入图片描述

③:启动项目

1.然后我们在IDEA窗口的底部打开Terminal命令行窗口,输入yarn run serve
运行vue项目,我们就可以通过http://localhost:8080/打开我们的项目了。

在这里插入图片描述

2.效果如下,Hello Vue!

在这里插入图片描述

④:安装element-ui

接下来我们引入element-ui组件(https://element.eleme.cn),这样我们就可以获得好看的vue组件,开发好看的后台管理系统的界面啦。

在这里插入图片描述

1.命令安装

# 安装element-ui
yarn add element-ui --save

在这里插入图片描述

2.然后我们打开项目src目录下的main.js,引入element-ui依赖。

import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)

⑤: 安装axios、qs、mockjs

  • axios:一个基于 promise 的 HTTP 库,类ajax
  • qs:查询参数序列化和解析库
  • mockjs:为我们生成随机数据的工具库

1. 安装axios

接下来,我们来安装axios(http://www.axios-js.com/),axios是一个基于 promise 的 HTTP 库,这样我们进行前后端对接的时候,使用这个工具可以提高我们的开发效率。

1.安装命令

 yarn add axios --save

2.在main.js中全局引入axios

import axios from 'axios'
Vue.prototype.$axios = axios //

2.安装qs

我们安装一个qs,什么是qs?qs是一个流行的查询参数序列化和解析库。可以将一个普通的object序列化成一个查询字符串,或者反过来将一个查询字符串解析成一个object,帮助我们查询字符串解析和序列化字符串。

1.安装命令

 yarn add qs --save

3.安装mockjs

因为后台我们现在还没有搭建,无法与前端完成数据交互,因此我们这里需要mock数据,因此我们引入mockjs(http://mockjs.com/),方便后续我们提供api返回数据

1.安装命令

 yarn add mockjs --save-dev

2.然后我们在src目录下新建mock.js文件,用于编写随机数据的api,然后我们需要在main.js中引入这个文件

  • src/main.js
require("./mock") //引入mock数据,关闭则注释该行

后面我们mackjs会自动为我们拦截ajax,并自动匹配路径返回数据!

二、页面路由

Router:WebApp的链接路径管理系统,简单就是建立起url和页面之间的映射关系
所以我们要打开页面然后开发页面,我们需要先配置路由,然后再开发,这样我们可以试试看到效果。项目中,src\router\index.js就是用来配置路由的。

1.我们在views文件夹下定义几个页面:

  • Login.vue(登录页面)
  • Index.vue(首页)

2.配置url与vue页面的映射关系src\router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
import Index from "@/views/Index";Vue.use(VueRouter)const routes = [{path: '/',name: 'login',component: Login},{path: '/index',name: 'index',component: Index},
]const router = new VueRouter({mode: 'history',base: process.env.BASE_URL,routes
})export default router

3.运行yarn run serve打开http://localhost:8082/login查看效果

在这里插入图片描述

三、登陆界面开发

一开始的时候为了页面风格的统一,我们采用了Element Ui的组件库,所以这里我们就直接去element的官网。所以先找到Loyout布局然后再弄表单,然后我们涉及到的后台交互有2个:

  • 获取登录验证码
  • 提交登录表单完成登录

因为后台系统我们暂时还没有开发,所以这里我们需要自己mock数据完成交互。前面我们已经引入了mockjs,所以我们到mock.js文件中开发我们的api。

①:登录交互过程

1.交互流程

1.我们梳理一下交互流程:

  1. 浏览器打开登录页面

  2. 动态加载登录验证码,因为这是前后端分离的项目,我们不再使用session进行交互,所以后端我打算禁用session,那么验证码的验证就是问题了,所以后端设计上我打算生成验证码同时生成一个随机码,随机码作为key,验证码为value保存到redis中,然后把随机码和验证码图片的Base64字符串码发送到前端

  3. 前端提交用户名、密码、验证码还有随机码

  4. 后台验证验证码是否匹配以及密码是否正确
    在这里插入图片描述
    ok,这样我们就知道mock应该弄成什么样的api了。

2. mock.js定义需要的api

2.mock.js - 获取登录验证码

// 引入mock
let Mock = require('mockjs');
// 获取Mock.random对象
// 参考:https://github.com/nuysoft/Mock/wiki/Mock.Random
let random = Mock.Random;
let Result = {code: 200,msg: '操作成功!',data: null
}
/*** Mock.mock( url, post/get , function(options));* url 表示需要拦截的 URL,* post/get 需要拦截的 Ajax 请求类型** 用于生成响应数据的函数*/Mock.mock('/captcha', 'post', ()=>{Result.data = {randomCode: random.string(32), // 获取一个32位的随机字符串captchaImg: random.dataImage('120x40', 'p7n5w') // //生成验证码为11111的base64图片编码}return Result;
})

mock生成数据还算简单,一般都是利用Mock.Random对象来生成一些随机数据,具体的用法可以参考https://github.com/nuysoft/Mock/wiki/Mock.Random。然后Result是为了统一返回结果,因为后台设计的时候,前后端交互,一般都有固定的返回格式,所以就有了Result。

3.mock.js - 登录接口

/*登录接口*/// 因为mock 不认识/login?username=xxx, 所以用了正则表达式
Mock.mock(RegExp('/login*'),'post',(config)=>{// 这里无法在header添加authorization,直接跳过console.log("mock----------------login")return Result
})

3.开发登录页面

1.Login.vue登录页面

<template><el-row type="flex" class="row-bg" justify="center"><el-col class="el-col"><h3 style="color: white; font-weight: bold; font-size: 21px; margin: 0 0 20px 0;padding: 0">Spring security安全框架</h3><el-form :model="form" :rules="rules" ref="ruleForm" class="demo-ruleForm"><el-form-item prop="username" style="width: 18rem;"><el-input prefix-icon="el-icon-user" placeholder="用户名" v-model="form.username"></el-input></el-form-item><el-form-item prop="password" style="width: 18rem;"><el-input prefix-icon="el-icon-lock" show-password placeholder="密码" v-model="form.password"></el-input></el-form-item><el-form-item prop="code" style="width: 18rem;"><el-input prefix-icon="el-icon-picture-outline" v-model="form.code" placeholder="验证码":show-password="true" style="width: 10.8rem; float: left;" maxlength="5"></el-input><el-image class="captchaImg" :src="captchaImg" style="width: 6.7rem; float: left;"></el-image></el-form-item><el-form-item><el-button type="primary" style="width: 18rem;" @click="submitForm('ruleForm')">登录</el-button></el-form-item></el-form></el-col></el-row>
</template><script>
export default {name: "Login",data() {return {form: {username: null, // 用户名password: null, // 密码code: null, // 验证码randomCode: null, // 随机码},captchaImg: null, //图片rules: {username: [{required: true, message: '请输入用户名', trigger: 'blur'},],password: [{required: true, message: '请输入密码', trigger: 'blur'},{min: 6, message: '密码长度至少 6 个字符', trigger: 'blur'}],code: [{required: true, message: '请输入验证码', trigger: 'blur'},{min: 5, max: 5, message: '验证码长度为 5 个字符', trigger: 'blur'}],}}},mounted() {this.getCaptchaImg();},methods: {// 获取验证码和随机码getCaptchaImg() {this.$axios.post('/captcha').then((res) => {if (res.data.code == 200){this.form.randomCode = res.data.data.randomCode;this.captchaImg = res.data.data.captchaImg;}else {this.$message.error("验证码获取失败!")}})},// 登录toLogin() {this.$axios.post('/login', this.form).then((res) => {if (res.data.code == 200){// todo 登录成功const jwt = res.headers['authorization']this.$store.commit('SET_TOKEN', jwt)this.$router.push('/index')}else {this.$message.error(res.data.msg)}})},submitForm(formName) {this.$refs[formName].validate((valid) => {if (valid) {this.toLogin();} else {console.log('error submit!!');return false;}});},}
}
</script><style scoped>
.row-bg {background-image: url("/public/img/login_bk2.jpg");background-size: cover;background-repeat: no-repeat;/*background-color: #fafafa;*/height: 100vh;opacity: 0.9;filter: none;
}.el-col {width: 22rem;margin: auto;/* 半透明黑色背景 */background-color: rgba(0, 0, 0, 0.30) !important;padding: 1rem 1.5rem 1rem 1.5rem;border-radius: 0.6rem;box-shadow: 0 0 10.8rem 0.2rem rgba(0, 0, 0, 0.1);
}.demo-ruleForm {display: flex;justify-content: center;align-items: center;flex-direction: column;margin-bottom: -10px;
}.captchaImg {float: left;margin-left: 8px;border-radius: 4px;
}
</style>

2.效果

在这里插入图片描述

②:token的状态同步

再讲一下,submitForm方法中,提交表单之后做了几个动作,从Header中获取用户的authorization,也就是含有用户登录信息的jwt,然后提交到store中进行状态管理。

this.$store.commit(“SET_TOKEN”, jwt) 表示调用store中的SET_TOKEN方法,所以我们需要在store中编写方法

1.src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {token: null,},getters: {},mutations: {SET_TOKEN(state, token) {state.token = token;localStorage.setItem('token', token)}},actions: {},modules: {}
})

在这里插入图片描述
这样登录之后获取到的jwt就可以存储到应用的store以及localStorage中,方便使用直接从localStorage中获取即可! 这样用户登录成功之后就会跳转到/index页面this.$router.push(“/index”)

③:定义全局axios拦截器

这里有个问题,那么如果登录失败,我们是需要弹窗显示错误的,比如验证码错误,用户名或密码不正确等。不仅仅是这个登录接口,所有的接口调用都会有这个情况,所以我们想做个拦截器,对返回的结果进行分析,如果是异常就直接弹窗显示错误,这样我们就省得每个接口都写一遍了。

1.在src目录下创建一个文件axios.js(与main.js同级),定义axios的拦截:

// 引入所需的库和模块
import axios from "axios";
import router from "@/router"; // 假设这是指向路由模块的路径
import Element from "element-ui";// 设置所有 Axios 请求的基础 URL
// axios.defaults.baseURL = "https://localhost:19005";// 创建一个具有自定义设置的 Axios 实例
let request = axios.create({timeout: 5000, // 设置请求的超时时间为5000毫秒headers: {'Content-Type': 'application/json;charset=utf-8' // 设置请求数据的内容类型为 JSON}
});// 在发送请求之前拦截请求
request.interceptors.request.use(config => {// 使用本地存储中的令牌设置请求的 'Authorization' 头部config.headers['Authorization'] = localStorage.getItem('token');return config;
});// 在处理响应之前拦截响应
request.interceptors.response.use(response => {// 从响应中提取数据let res = response.data;// 检查响应代码是否为200(成功)if (res.code === 200) {return response; // 如果成功,则返回响应} else {// 如果响应代码不是200,则使用 Element UI 显示错误消息Element.Message.error(res.msg ? res.msg : '系统异常');return Promise.reject(res.msg); // 使用错误消息拒绝 Promise}
}, error => {console.log('error', error);// 处理特定的错误情况if (error.code === 401) {router.push('/login'); // 如果错误代码是401(未经授权),则重定向到登录页面}console.log(error.message);// 使用 Element UI 显示错误消息,持续时间为3000毫秒Element.Message.error(error.message, { duration: 3000 });return Promise.reject(error.message); // 使用错误消息拒绝 Promise
});// 将配置好的 Axios 实例导出,以在应用程序的其他部分中使用
export default request;

前置拦截,其实可以统一为所有需要权限的请求装配上header的token信息,后置拦截中,判断status.code和error.response.status,如果是401未登录没权限的就调到登录页面,其他的就直接弹窗显示错误。

2.再main.js中导入自己创建axios.js

import axios from "@/axios";Vue.prototype.$axios = axios

在这里插入图片描述

这样axios每次请求都会被前置拦截器和后置拦截器拦截了。

3.在mock.js中修改登录的接口

/*登录接口*/// 因为mock 不认识/login?username=xxx, 所以用了正则表达式
Mock.mock(RegExp('/login*'),'post',(config)=>{// 这里无法在header添加authorization,直接跳过Result.code = 400;Result.msg = '验证码错误!';return Result
})

4.登录异常弹窗效果如下:

  • 我们发现登录时 确实有验证码错误的弹出 但是同时界面会出现一个遮罩层提示Uncaught runtime errors
    在这里插入图片描述

  • 解决方法

5.打开vue.config.js

    devServer:{// 解决页面弹出红色报错遮罩层client: {//将overlay设置为false即可overlay: false}}

在这里插入图片描述

6.重新测试登录 正常

在这里插入图片描述

四、后台管理界面开发

ok,登录界面我们已经开发完毕,并且我们已经能够进入管理系统的首页了,接下来我们就来开发首页的页面。

一般来说,管理系统的页面我们都是头部是一个简单的信息展示系统名称和登录用户信息,然后中间的左边是菜单导航栏,右边是内容,对应到elementui的组件中,我们可以找到这个Container 布局容器用于布局,方便快速搭建页面的基本结构。

而我们采用这个布局:

在这里插入图片描述

而这个页面,一般来说Header和Aside都是不会变化的,只有Main部分会跟着链接变化而变化,所以我们可以提炼公共部分出来,放在Home.vue中,然后Main部分放在Index.vue中,

那么问题来了,我们如何才能做到点击左边的Aside,然后局部刷新Main中的内容呢?在Vue中,我们可以通过嵌套路由(子路由)的形式。也就是我们需要重新定义路由,一级路由是Home.vue,Index.vue是作为Home.vue页面的子路由,然后Home.vue中我们通过来展示Index.vue的内容即可。

1.创建 src/views/Home.vue

2.在router中,我们这样修改:

const routes = [{path: '/login',name: 'login',component: Login},{path: '/',name: 'home',redirect: '/index',component: Home,children: [{path: '/index',name: 'index',meta: {title: '首页'},component: Index}]},
]

可以看到原本的Index已经作为了Home的children,所以在链接到/index的时候我们会展示父级Home的内容,然后再显示Index内容。

3.src/views/Home.vue

<template><div id="home"><el-container><el-aside width="200px">菜单栏</el-aside><el-container><el-header><strong>Spring Security安全框架</strong><div class="header-right"><el-avatar size="medium" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar><el-dropdown><span class="el-dropdown-link">Admin<i class="el-icon-arrow-down el-icon--right"></i></span><el-dropdown-menu slot="dropdown"><el-dropdown-item divided>个人中心</el-dropdown-item><el-dropdown-item divided>退出</el-dropdown-item></el-dropdown-menu></el-dropdown><el-link href="https://mp.csdn.net/mp_blog/manage/article?spm=1011.2124.3001.5298">CSDN笔记</el-link><el-link href="https://gitee.com/">Gitee仓库</el-link></div></el-header><el-main><router-view/></el-main></el-container></el-container></div>
</template><script>
export default {name: "Home"
}
</script><style lang="less" scoped>
.el-container {margin: 0;padding: 0;height: 100vh;.header-right {width: 260px;float: right;display: flex;justify-content: space-around;align-items: center;font-weight: bold;}
}.el-header, .el-footer {background-color: #B3C0D1;color: #333;text-align: center;line-height: 60px;
}.el-aside {background-color: #D3DCE6;color: #333;text-align: center;line-height: 200px;
}.el-main {background-color: #E9EEF3;color: #333;text-align: center;line-height: 160px;
}body > .el-container {margin-bottom: 40px;
}.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {line-height: 260px;
}.el-container:nth-child(7) .el-aside {line-height: 320px;
}.el-dropdown-link {cursor: pointer;color: #409EFF;
}
.el-icon-arrow-down {font-size: 12px;
}
</style>

4.src/views/Index.vue

<template><div><el-carousel :interval="4000" type="card" indicator-position="outside"><el-carousel-item v-for="url in urls" :key="url"><el-image :src="url"></el-image></el-carousel-item></el-carousel></div>
</template><script>
export default {name: "Index",data() {return {urls: ['https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg','https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg','https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg','https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg','https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg','https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg','https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg']}}
}
</script><style lang="less" scoped>
.el-carousel__item h3 {color: #475669;font-size: 14px;opacity: 0.75;line-height: 200px;margin: 0;
}.el-carousel__item:nth-child(2n) {background-color: #99a9bf;
}.el-carousel__item:nth-child(2n+1) {background-color: #d3dce6;
}
</style>

5.总体下来效果如下:

在这里插入图片描述

有点感觉了,然后左边的菜单栏我们也弄下,我们找到NavMenu 导航菜单组件,然后加到Home.vue中,因为考虑到后面我们需要做动态菜单,所以我想单独这个页面出来,因此我新建了个SideMenu.vue

6.SideMenu.vue

<template><el-menuclass="el-menu-vertical-demo"background-color="#545c64"text-color="#fff"active-text-color="#ffd04b"><router-link to="/index"><el-menu-item index="Index"><template slot="title"><i class="el-icon-s-home"></i><span slot="title">首页</span></template></el-menu-item></router-link><el-submenu index="1"><template slot="title"><i class="el-icon-s-operation"></i><span>系统管理</span></template><el-menu-item index="1-1"><template slot="title"><i class="el-icon-s-custom"></i><span slot="title">用户管理</span></template></el-menu-item><el-menu-item index="1-2"><template slot="title"><i class="el-icon-rank"></i><span slot="title">角色管理</span></template></el-menu-item><el-menu-item index="1-3"><template slot="title"><i class="el-icon-menu"></i><span slot="title">菜单管理</span></template></el-menu-item></el-submenu><el-submenu index="2"><template slot="title"><i class="el-icon-s-tools"></i><span>系统工具</span></template><el-menu-item index="2-2"><template slot="title"><i class="el-icon-s-order"></i><span slot="title">数字字典</span></template></el-menu-item></el-submenu></el-menu>
</template><script>
export default {name: "SideMenu"
}
</script><style lang="less" scoped>
.el-menu-vertical-demo{height: 100%;
}
</style>

SideMenu.vue作为一个组件添加到Home.vue中,我们首选需要导入,然后声明compoents,然后才能使用标签

7.在Home.vue中代码如下

<template><div id="home"><el-container><el-aside width="200px"><SideMenu></SideMenu></el-aside><el-container>....</el-container></el-container></div>
</template><script>
import SideMenu from "@/views/SideMenu";
export default {name: "Home",components: {SideMenu}
}
</script>

在这里插入图片描述

8.最后效果如下:

在这里插入图片描述

我们先来新建几个页面,先在views下新建文件夹sys,然后再新建vue页面,具体看下面,这样我们就能把链接和页面可以连接起来。

  • src\views\sys
    • Dict.vue 数字字典
    • Menu.vue 菜单管理
    • Role.vue 角色管理
    • User.vue 用户管理
      在这里插入图片描述

虽然建立了页面,但是因为我们没有在router中注册链接与组件的关系,所以我们现在打开链接还是打开不了页面的。下面我们就要动态联系起来。

五、用户登录信息展示

管理界面的右上角的用户信息现在是写死的,因为我们现在已经登录成功,所以我们可以通过接口去请求获取到当前的用户信息了,这样我们就可以动态显示用户的信息,这个接口比较简单,然后退出登录的链接也一起完成,就请求接口同时把浏览器中的缓存删除就退出了哈。

1.src\views\Home.vue

<template><div id="home"><el-container><el-aside width="200px"><SideMenu></SideMenu></el-aside><el-container><el-header><strong>Spring Security安全框架</strong><div class="header-right"><el-avatar size="medium" :src="form.avatar"></el-avatar><el-dropdown><span class="el-dropdown-link">{{ form.username }}<i class="el-icon-arrow-down el-icon--right"></i></span><el-dropdown-menu slot="dropdown"><el-dropdown-item divided>个人中心</el-dropdown-item><el-dropdown-item @click.native="logout" divided>退出</el-dropdown-item></el-dropdown-menu></el-dropdown><el-link href="https://mp.csdn.net/mp_blog/manage/article?spm=1011.2124.3001.5298">CSDN笔记</el-link><el-link href="https://gitee.com/">Gitee仓库</el-link></div></el-header><el-main><router-view/></el-main></el-container></el-container></div>
</template><script>
import SideMenu from "@/views/SideMenu";
import {getUserInfo, logout} from "@/api/login";export default {name: "Home",components: {SideMenu},data() {return {form: {id: null,username: null, // 用户名avatar: null, // 头像}}},mounted() {this.getUserInfo();},methods: {getUserInfo(){getUserInfo().then(res =>{Object.assign(this.form, res.data.data);})},logout(){logout().then(res =>{console.log(res.data.data)this.$store.commit('RESET_STATE')this.$router.push('/login')})}},
}
</script><style lang="less" scoped>
.el-container {margin: 0;padding: 0;height: 100vh;.header-right {width: 260px;float: right;display: flex;justify-content: space-around;align-items: center;font-weight: bold;}
}.el-header, .el-footer {background-color: #B3C0D1;color: #333;text-align: center;line-height: 60px;
}.el-aside {background-color: #D3DCE6;color: #333;text-align: center;line-height: 200px;
}.el-main {background-color: #E9EEF3;color: #333;text-align: center;line-height: 160px;
}body > .el-container {margin-bottom: 40px;
}.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {line-height: 260px;
}.el-container:nth-child(7) .el-aside {line-height: 320px;
}.el-dropdown-link {cursor: pointer;color: #409EFF;
}.el-icon-arrow-down {font-size: 12px;
}</style>

2.由于我们将请求接口提取到js中了 所以在src下创建一个api文件夹

在这里插入图片描述

  • login.js
import axios from "@/axios";// 获取验证码和随机码
export function getCaptchaImg(data) {return axios({url: '/captcha',method: 'post',data: data})
}// 登录
export function toLogin(data) {return axios({url: '/login',method: 'post',data: data})
}// 获取用户信息
export function getUserInfo(data) {return axios({url: '/userInfo',method: 'get',params: data})
}// 登出
export function logout(data) {return axios({url: '/logout',method: 'post',data: data})
}

3.src/store/index.js

        RESET_STATE(state, token) {state.token = null;localStorage.clear();sessionStorage.clear();},

在这里插入图片描述

4.src/mock.js

/**获取用户信息*/Mock.mock(RegExp('/userInfo'),'get',(config)=>{// 这里无法在header添加authorization,直接跳过Result.data = {id: random.string(3), // 获取一个3位的随机字符串username:'Admin',avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.QiENtPtG3CIjC6yr0P-bMQHaFj?w=252&h=188&c=7&r=0&o=5&pid=1.7'}return Result
})/**登出*/Mock.mock(RegExp('/logout'),'post',(config)=>{return Result
})

5.效果

在这里插入图片描述

六、动态菜单栏开发

①:动态菜单

上面代码中,左侧的菜单栏的数据是写死的,在实际场景中我们不可能这样做,因为菜单是需要根据登录用户的权限动态显示菜单的,也就是用户看到的菜单栏可能是不一样的,这些数据需要去后端访问获取。

首先我们先把写死的数据简化成一个json数组数据,然后for循环展示出来,代码如下

1./src/views/inc/SideMenu.vue

<template><el-menuclass="el-menu-vertical-demo"background-color="#545c64"text-color="#fff"active-text-color="#ffd04b"><router-link to="/index"><el-menu-item index="Index"><template slot="title"><i class="el-icon-s-home"></i><span slot="title">首页</span></template></el-menu-item></router-link><el-submenu :index="menu.name" v-for="menu in menuList" :key="menu.id"><template slot="title"><i :class="menu.icon"></i><span>{{ menu.title }}</span></template><router-link :to="item.path" v-for="item in menu.children" :key="item.id"><el-menu-item :index="item.name"><template slot="title"><i :class="item.icon"></i><span slot="title">{{ item.title }}</span></template></el-menu-item></router-link></el-submenu></el-menu>
</template><script>
export default {// 导航菜单name: "SideMenu",data() {return {}},computed: {menuList: {get() {return this.$store.state.menus.menuList}}}}
</script><style lang="less" scoped>
.el-menu-vertical-demo {height: 100%;
}
</style>

在这里插入图片描述

可以看到,用for循环显示数据,那么这样变动菜单栏时候只需要修改menuList即可。效果和之前的完全一样。 menuList的数据一般我们是要请求后端的,所以这里我们定义一个mock接口,因为是动态菜单,一般我们也要考虑到权限问题,所以我们请求数据的时候一般除了动态菜单,还要权限的数据,比如菜单的添加、删除是否有权限,是否能显示该按钮等,有了权限数据我们就定动态决定是否展示这些按钮了。

2.src/mock.js

/**获取用户菜单以及权限接口*/Mock.mock('/sys/menuAndAuth','get',(config)=>{let menu = [{id:1,name: 'SysManga',title: '系统管理',icon: 'el-icon-s-operation',component: '',path: '',children: [{id:2,name: 'SysUser',title: '用户管理',icon: 'el-icon-s-custom',path: '/sys/user',component: 'sys/User',children: []},{id:3,name: 'SysRole',title: '角色管理',icon: 'el-icon-rank',path: '/sys/role',component: 'sys/Role',children: []},{id:4,name: 'SysMenu',title: '菜单管理',icon: 'el-icon-menu',path: '/sys/menu',component: 'sys/Menu',children: []}]},{id:5,name: 'SysTools',title: '系统工具',icon: 'el-icon-s-tools',path: '',component: '',children: [{id:6,name: 'SysDict',title: '数字字典',icon: 'el-icon-s-order',path: '/sys/dict',component: 'sys/Dict',children: []},]}]let  auth = ['sys:user:list', "sys:user:save", "sys:user:delete"]Result.data = {menus: menu,auths:auth}return Result
})

综上,我们把加载菜单数据这个动作放在router.js中。Router有个前缀拦截,就是在路由到页面之前我们可以做一些判断或者加载数据。

②:动态路由

1.创建src/store/modules/menus.js 模块来共享菜单相关的全局变量

在这里插入图片描述

2.在src/store/index.js中引刚刚创建的menus.js

import menus from "@/store/modules/menus";modules: {menus}

在这里插入图片描述

3.src/store/modules/menus.js中添加全局共享变量

import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)export default{state: {hasRoutes: false, // 是否为第一次加载路由menuList: [],authList:[],},getters: {},mutations: {// 设置菜单列表SET_MENU_LIST(state, menuList) {state.menuList = menuList;},// 设置权限列表SET_AUTH_LIST(state, authList) {state.authList = authList;},// 设置路由已经加载过SET_HAS_ROUTES(state, hasRoutes) {state.hasRoutes = hasRoutes;},},actions: {}
}

4.src/router/index.js加载菜单数据

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
import Index from "@/views/Index";
import Home from "@/views/Home";
import store from "@/store";
import {getUserMenuAndAuth} from "@/api/login";Vue.use(VueRouter)const routes = [{path: '/login',name: 'login',component: Login},{path: '/',name: 'home',redirect: '/index',component: Home,children: [{path: '/index',name: 'Index',meta: {title: '首页'},component: Index},]},
]const router = new VueRouter({mode: 'history',base: process.env.BASE_URL,routes
})router.beforeEach((to, from, next) => {// 获取到是否为第一个加载路由let hasRoutes = store.state.menus.hasRoutes;// 获取tokenlet token = localStorage.getItem('token');// 如果访问的是登录页面,直接放行if (to.path === '/login') next()// 如果token为空 没有登录 跳转到登录页面if (!token) next({path: '/login'})// 如果不是第一次动态加载路由(已经登录 并且加载过路由) 无需再次加载 直接放行if (hasRoutes) next();// 能够执行到这里(代表 已经 登录 并且是第一次加载路由)// 获取用户菜单以及权限接口(发送请求)getUserMenuAndAuth().then(res => {console.log('获取用户菜单以及权限接口', res.data.data);// 拿到用户菜单store.commit('SET_MENU_LIST', res.data.data.menus)// 拿到用户权限store.commit('SET_AUTH_LIST', res.data.data.auths)// 动态绑定路由// 获取当前的路由配置let newRoutes = router.options.routes;// 置空之前的动态配置newRoutes[1].children = []console.log('newRoutes前', newRoutes)res.data.data.menus.forEach(menu => {// 判断是否有子菜单 有子菜单转成路由if (menu.children) {menu.children.forEach(e => {// 转成路由let router = menuToRouter(e);// 把路由添加到路由管理器  因为要添加到home路由下的children中 所有newRoutes[1].childrenif (router) newRoutes[1].children.push(router)})}})// 将新生成的路由逐个添加到现有路由配置中newRoutes.forEach(route => {router.addRoute(route);});console.log('newRoutes后',newRoutes)// 设置路由是否已经加载过hasRoutes = true;store.commit('SET_HAS_ROUTES', hasRoutes)next({path: to.path});})
})// 导航转成路由
function menuToRouter(menu) {// 如果 component为空 无需转换if (!menu.component) return nulllet route = {name: menu.name,path: menu.path,meta: {icon: menu.icon,title: menu.title},};route.component = () => import ('@/views/' + menu.component + '.vue')return route
}export default router

可以看到,我们通过menuToRoute就是把menu(菜单)数据转换成路由对象,然后router.addRoute(route)动态添加路由对象。 同时上面的menu对象中,有个menu.component,这个就是连接对应的组件,我们需要添加上去,比如说**/sys/user**链接对应到 component(sys/User)

这样我们才能绑定添加到路由。所以我会修改mock中的nav的数据成这样:

在这里插入图片描述

同时上面router中我们还通过判断是否登录页面,是否有token等判断提前判断是否能加载菜单,同时还做了个开关hasRoute来动态判断是否已经加载过菜单。

还需要在store中定义几个方法用于存储数据,我们定义一个menu模块

这样我们菜单的数据就可以加载了,然后再SideMenu.vue中直接获取store中的menuList数据即可显示菜单出来了。

5.最后效果如下

七、 动态标签页开发

我看别的后台管理系统都有这个,效果是这样的:

在这里插入图片描述

element-ui中寻了一圈,发现Tab标签页组件挺符合我们要求的,可以动态增减标签页。

理想的动作是这样的:

  1. 当我们点击导航菜单,上方会添加一个对应的标签,注意不能重复添加,发现已存在标签直接切换到这标签即可
  2. 删除当前标签的时候会自动切换到前一个标签页
  3. 点击标签页的时候会调整到对应的内容页中
    综合Vue的思想,我们可以这样设计:在Store中统一存储:1、当前标签Tab,2、已存在的标签Tab列表,然后页面从Store中获取列表显示,并切换到当前Tab即可。删除时候我们循环当前Tab列表,剔除Tab,并切换到指定Tab。

我们先和左侧菜单一样单独定义一个组件Tabs.vue放在views/文件夹内:

1.src/views/Tabs.vue

<template><el-tabs v-model="editableTabsValue" closable type="card" @tab-remove="removeTab" @tab-click="clickTab"><el-tab-pane v-for="item in editableTabs":key="item.name":label="item.title":name="item.name"></el-tab-pane></el-tabs>
</template><script>
export default {name: "Tabs",data() {return {};},computed: {editableTabs: {get() {return this.$store.state.menus.editableTabs},set(val) {this.$store.state.menus.editableTabs = val}},editableTabsValue: {get() {return this.$store.state.menus.editableTabsValue},set(val) {this.$store.state.menus.editableTabsValue = val}},},methods: {removeTab(tabName) {let tabs = this.editableTabs;let tabValue = this.editableTabsValue;// 如果 关闭的时首页直接返回if (tabValue === 'Index') return// 如果关闭的是当前页面 则寻找下一个页面做为当前页if (tabName === tabValue) {tabs.forEach((tab, index) => {if (tab.name === tabValue) {// 找下一个 或者前一个页面let nextTab = tabs[index + 1] || tabs[index - 1];if (nextTab) tabValue = nextTab.name;}})}// 替换 标签名this.editableTabsValue = tabValue;// 过滤出除了关闭的标签this.editableTabs = tabs.filter(tab => tab.name !== tabName)this.$router.push({name: tabValue})},clickTab(tab) {this.$router.push({name: tab.name})}}
}
</script><style scoped>
</style>

上面代码中,computed表示当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值。这样我们就可以实时监测Tabs标签的动态变化实时显示(相当于实时get、set)。其他clickTab、removeTab的逻辑其实也还算简单,特别是removeTab注意考虑多种情况就可以。 然后我们来到store中的menu.js,我们添加 editableTabsValue和editableTabs,然后把首页作为默认显示的页面。

2.src/store/modules/menus.js

import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)export default {state: {hasRoutes: false, // 是否为第一次加载路由menuList: [],authList: [],editableTabsValue: 'Index',editableTabs: [{title: '首页',name: 'Index',}]},getters: {},mutations: {// 设置菜单列表SET_MENU_LIST(state, menuList) {state.menuList = menuList;},// 设置权限列表SET_AUTH_LIST(state, authList) {state.authList = authList;},// 设置路由已经加载过SET_HAS_ROUTES(state, hasRoutes) {state.hasRoutes = hasRoutes;},ADD_TAB(state, tab) {// 查看要添加的标签是否已经存在let index = state.editableTabs.findIndex(e => e.name === tab.name);console.log(tab.name)// 没有找打 不存在 则添加if (index === -1) {state.editableTabs.push({title: tab.title,name: tab.name,})}// 把标签名字改为刚添加的名字state.editableTabsValue = tab.name;},RESET_TAB_STATUS(state) {state.menuList = [];state.authList = [];state.hasRoutes = false;state.editableTabsValue = 'Index';state.editableTabs = [{title: '首页',name: 'Index',}]}},actions: {}
}

ok,然后再Home.vue中引入我们Tabs.vue这个组件,添加代码的地方比较零散,所以我就写重要代码出来就好,自行添加到指定的地方哈。

3.src/views/Home.vue

  • 只需引入即可
    在这里插入图片描述

  • 退出登录时要重置标签的状态
    在这里插入图片描述

  • 注释掉居中的样式
    在这里插入图片描述

好了完成了第一步了,现在我们需要点击菜单导航,然后再tabs列表中添加tab标签页,那么我们来到SideMenu.vue,我们给el-menu-item每个菜单都添加一个点击事件:

4.src/views/inc/SideMenu.vue

<template><el-menu:default-active="this.$store.state.menus.editableTabsValue"class="el-menu-vertical-demo"background-color="#545c64"text-color="#fff"active-text-color="#ffd04b"><router-link to="/index"><el-menu-item index="Index" @click="addTab({name: 'Index', title: '首页'})"><template slot="title"><i class="el-icon-s-home"></i><span slot="title">首页</span></template></el-menu-item></router-link><el-submenu :index="menu.name" v-for="menu in menuList" :key="menu.id"><template slot="title"><i :class="menu.icon"></i><span>{{ menu.title }}</span></template><router-link :to="item.path" v-for="item in menu.children" :key="item.id"><el-menu-item :index="item.name" @click="addTab(item)"><template slot="title"><i :class="item.icon"></i><span slot="title">{{ item.title }}</span></template></el-menu-item></router-link></el-submenu></el-menu>
</template><script>
export default {// 导航菜单name: "SideMenu",data() {return {}},computed: {menuList: {get() {return this.$store.state.menus.menuList}}},methods: {addTab(tab){this.$store.commit('ADD_TAB', tab)}},}
</script><style lang="less" scoped>
.el-menu-vertical-demo {height: 100%;
}
</style>

添加tab标签的时候注意需要激活指定当前标签,也就是设置editableTabsValue。然后我们也添加了setActiveTab方法,方便其他地方指定激活某个标签。

但是当我们刷新浏览器、或者直接通过输入链接打开页面时候就不会自动帮我们根据链接回显激活Tab。

刷新浏览器之后链接/sys/users不变,内容不变,但是Tab却不见了,所以我们需要修补一下,当用户是直接通过输入链接形式打开页面的时候我们也能根据链接自动添加激活指定的tab。那么在哪里添加这个回显的方法呢?router中?其实可以,只不过我们需要做判断,因为每次点击导航都会触发router。有没有更简便的方法?有的!因为刷新或者打开页面都是一次性的行为,所以我们可以在更高层的App.vue中做这个回显动作,具体如下:

5.src\App.vue

<template><div id="app"><router-view/></div>
</template>
<script>
export default {name: 'App',watch: {$route(to, from) {if (to.path !== '/login') {let object = {name: to.name,title: to.meta.title}this.$store.commit('ADD_TAB', object)}}}
}
</script>

上面代码可以看到,除了login页面,其他页面都会触发addTabs方法,这样我们就可以添加tab和激活tab了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/196846.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何零基础自学AI人工智能

随着人工智能&#xff08;AI&#xff09;的快速发展&#xff0c;越来越多的有志之士被其强大的潜力所吸引&#xff0c;希望投身其中。然而&#xff0c;对于许多零基础的人来说&#xff0c;如何入门AI成了一个难题。本文将为你提供一份详尽的自学AI人工智能的攻略&#xff0c;帮…

SpringCloud微服务:Ribbon负载均衡

目录 负载均衡策略&#xff1a; 负载均衡的两种方式&#xff1a; 饥饿加载 1. Ribbon负载均衡规则 规则接口是IRule 默认实现是ZoneAvoidanceRule&#xff0c;根据zone选择服务列表&#xff0c;然后轮询 2&#xff0e;负载均衡自定义方式 代码方式:配置灵活&#xff0c;但修…

OpenCV C++ 图像 批处理 (批量调整尺寸、批量重命名)

文章目录 图像 批处理(调整尺寸、重命名)图像 批处理(调整尺寸、重命名) 拿着棋盘格,对着相机变换不同的方角度,采集十张以上(以10~20张为宜);或者棋盘格放到桌上,拿着相机从不同角度一通拍摄。 以棋盘格,第一个内焦点为坐标原点,便于计算世界坐标系下三维坐标; …

Pycharm之配置python虚拟环境

最近给身边的人写了脚本&#xff0c;在自己电脑可以正常运行。分享给我身边的人&#xff0c;却运行不起来&#xff0c;然后把报错的截图给我看了&#xff0c;所以难道不会利用pycharm搭建虚拟的环境&#xff1f;记录一下配置的过程。 第一步&#xff1a;右键要打开的python的代…

利用jquery对HTML中的名字进行替代

想法&#xff1a;将网页中经常要修改的名字放在一个以jquery编写的js文件中&#xff0c;如果需要修改名字&#xff0c;直接修改js文件中的名字即可。 新建name_07.html文件&#xff0c;写入下面的代码&#xff1a; <!DOCTYPE html> <html> <head><meta …

mp4视频批量截取!!!

mp4视频批量截取&#xff01;&#xff01;&#xff01; 问题&#xff1a;如果我们想截取一个mp4视频中的多个片段&#xff0c;一个一个截会很麻烦&#xff01; 可以将想要截取的开始时间和结束时间保存到 excel表 中&#xff0c;进行批量截取。 1、对一个视频&#xff0c;记…

给openlab搭建web网站

1.作业的要求 2.访问www.openlab.com网站 2.1先准备好相关的包和关闭防火墙等操作 mount /dev/sr0 /mnt/ //先挂载 yum install httpd -y //下载htppd systemctl stop firewalld //关闭防火墙 setenforce 02.2然后开始配置文件和仓库 这一步比较关键,之前改了接口…

【原创】java+swing+mysql鲜花购物商城设计与实现

前言&#xff1a; 本文主要介绍了鲜花购物商城的设计与实现。首先&#xff0c;通过市场需求&#xff0c;我们确定了鲜花商场的功能&#xff0c;通常的商城一般都是B/S架构&#xff0c;然而我们今天要用javaswing去开发一个C/S架构的鲜花商城&#xff0c;利用开发技术和工具&am…

C#WPF用户控件及自定义控件实例

本文演示C#WPF自定义控件实例 用户控件(UserControl)和自定义控件(CustomControl)都是对UI控件的一种封装方式,目的都是实现封装后控件的重用。 只不过各自封装的实现方式和使用的场景上存在差异。 1 基于UserControl 创建 创建控件最简单一个方法就是基于UserControl …

【观察】OpenHarmony:技术先进“创新局”,持续创新“谋新篇”

毫无疑问&#xff0c;开源作为今天整个软件产业的创新“原动力”&#xff0c;目前在软件产业发展中的重要性愈加凸显。根据Linux基金会的统计&#xff0c;现在全球软件产业中有70%以上的代码来源于开源软件。 从这个角度来看&#xff0c;开源技术已逐渐成为推动企业数字化转型和…

MATLAB中zticks函数用法

目录 语法 说明 示例 指定 z 轴刻度值和标签 指定非均匀 z 轴刻度值 以 2 为增量递增 z 轴刻度值 将 z 轴刻度值设置回默认值 指定特定坐标区的 z 轴刻度值 删除 z 轴刻度线 zticks函数的功能是设置或查询 z 轴刻度值。 语法 zticks(ticks) zt zticks zticks(auto)…

土地利用强度(LUI)综合指数

土地利用强度的概念可以解释为某一时间特定区域内人类活动对土地利用强度的干扰程度[1]&#xff0c;其不仅反映不同土地利用类型本身的自然属性&#xff0c;也体现了人类利用土地的深度和广度&#xff0c;进而揭示在人类社会系统干扰下土地资源自然综合体自然平衡的保持状态[2]…

jbase打印导出实现

上一篇实现了虚拟M层&#xff0c;这篇基于虚拟M实现打印导出。 首先对接打印层 using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using System.Xml;namesp…

为什么Transformer模型中使用Layer Normalization(Layer Norm)而不是Batch Normalization(BN)

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

通过 Canal 将 MySQL 数据实时同步到 Easysearch

Canal 是阿里巴巴集团提供的一个开源产品&#xff0c;能够通过解析数据库的增量日志&#xff0c;提供增量数据的订阅和消费功能。使用 Canal 模拟成 MySQL 的 Slave&#xff0c;实时接收 MySQL 的增量数据 binlog&#xff0c;然后通过 RESTful API 将数据写入到 Easysearch 中。…

Diagrams——制作短小精悍的流程图

今天为大家分享的是一款轻量级的流程图绘制软件——Diagrams。 以特定的图形符号加上说明&#xff0c;表示算法的图&#xff0c;称为流程图或框图。流程图是流经一个系统的信息流、观点流或部件流的图形代表。我们常用流程图来说明某一过程。 流程图使用一些标准符号代表某些类…

Vue 路由缓存 防止路由切换数据丢失 路由的生命周期

在切换路由的时候&#xff0c;如果写好了一丢数据在去切换路由在回到写好的数据的路由去将会丢失&#xff0c;这时可以使用路由缓存技术进行保存&#xff0c;这样两个界面来回换数据也不会丢失 在 < router-view >展示的内容都不会被销毁&#xff0c;路由来回切换数据也…

(c语言进阶)内存函数

一.memcpy(void* dest,void* src,int num) &#xff0c;操作单位为字节&#xff0c;完成复制且粘贴字符串 1.应用 #include <stdio.h> #include<string.h> int main() {int arr1[] { 1,2,3,4,5,6,7,8,9,10 };int arr2[20] { 0 };memcpy(arr2, arr1, 20);//从…

中移链共识机制介绍

01 为什么需要共识 共识是对某事达成的共同看法&#xff0c;它是区块链的灵魂&#xff0c;对确保区块链的完整性和安全性起着至关重要的作用。在传统的集中式系统中&#xff0c;单个实体或一组实体有权验证和记录交易。然而&#xff0c;区块链中的一个核心概念是去中心化&…

gittee启动器

前言 很多小伙伴反馈不是使用gitee&#xff0c;不会寻找好的项目&#xff0c;在拿到一个项目不知道从哪里入手。 鼠鼠我呀就是宠粉&#xff0c;中嘞&#xff0c;老乡。整&#xff01;&#xff01;&#xff01; git的基本指令 在使用gitee的时候呢&#xff0c;我们只需要记住…