安全框架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.我们梳理一下交互流程:
-
浏览器打开登录页面
-
动态加载登录验证码,因为这是前后端分离的项目,我们不再使用session进行交互,所以后端我打算禁用session,那么验证码的验证就是问题了,所以后端设计上我打算生成验证码同时生成一个随机码,随机码作为key,验证码为value保存到redis中,然后把随机码和验证码图片的Base64字符串码发送到前端
-
前端提交用户名、密码、验证码还有随机码
-
后台验证验证码是否匹配以及密码是否正确

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标签页组件挺符合我们要求的,可以动态增减标签页。
理想的动作是这样的:
- 当我们点击导航菜单,上方会添加一个对应的标签,注意不能重复添加,发现已存在标签直接切换到这标签即可
- 删除当前标签的时候会自动切换到前一个标签页
- 点击标签页的时候会调整到对应的内容页中
综合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了。
相关文章:
安全框架springSecurity+Jwt+Vue-1(vue环境搭建、动态路由、动态标签页)
一、安装vue环境,并新建Vue项目 ①:安装node.js 官网(https://nodejs.org/zh-cn/) 2.安装完成之后检查下版本信息: ②:创建vue项目 1.接下来,我们安装vue的环境 # 安装淘宝npm npm install -g cnpm --registryhttps:/…...
React整理总结(三)
1.props和state的更新 父组件重新render时,所有的子组件也会调用render()函数。shouldComponentUpdate(nextProp, nextState) shouldComponentUpdate(nextProps, nextState) {if (equal(nextProps, this.props) && equa…...
天气这么好,都外出了。顺便了解一下漏桶算法
看到标题,你想到了些什么呢? 又是一个阳光明媚的周末,大家都外出了,路上到处堵车,尤其是各桥梁、隧道入口处,很多车排队等着进入,而出口处就像一个漏桶一样,一辆车接着一辆车有序且…...
【FPGA】Verilog:实现 RS 触发器 | Flip-Flop | 使用 NOR 的 RS 触发器 | 使用 NAND 的 RS 触发器
目录 0x00 RS 触发器(RS Flip-Flop) 0x01 实现 RS 触发器 0x02 使用 NOR 的 RS 触发器 0x03 使用 NAND 的 RS 触发器 0x00 RS 触发器(RS Flip-Flop) 触发器(Flip-Flop)是一种带有时钟的二进制存储设备…...
【技术追踪】SAM(Segment Anything Model)代码解析与结构绘制之Mask Decoder
论文:Segment Anything 代码:https://github.com/facebookresearch/segment-anything 系列篇: (1)【技术追踪】SAM(Segment Anything Model)代码解析与结构绘制之Image Encoder &am…...
认识Tomcat
文章目录 什么是tomcat?tomcat的使用tomcat的下载tomcat的目录结构tomcat的启动在tomcat上部署页面通过浏览器访问部署的页面 学习servlet的原因 什么是tomcat? 盖棺定论:Tomcat是一个HTTP服务器。 我们接下来要长期学习的东西都是关于前后…...
c语言通信之串口通信
在C语言中,可以使用串口通信、网络通信等多种方式实现计算机之间的通信。其中,串口通信通常用于近距离、低速率的通信,而网络通信则适用于远距离、高速率的通信。 下面以串口通信为例,介绍在C语言中如何实现串口通信。 1.打开串…...
软考-高级-系统架构设计师教程(清华第2版)【第16章 嵌入式系统架构设计理论与实践(P555~613)-思维导图】
软考-高级-系统架构设计师教程(清华第2版)【第16章 嵌入式系统架构设计理论与实践(P555~613)-思维导图】 课本里章节里所有蓝色字体的思维导图...
2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-B卷
2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-B卷 2024年山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题-B卷A模块基础设施设置/安全加固(200分)A-1:登录安全加固(Windows, Linux)A-2&#…...
【Python数据结构与算法】——(线性结构)精选好题分享,不挂科必看系列
🌈个人主页: Aileen_0v0🔥系列专栏:<<Python数据结构与算法专栏>>💫个人格言:"没有罗马,那就自己创造罗马~" 时间复杂度大小比较 1.time complexity of algorithm A is O(n^3) while algorithm B is O(2^n). Which o…...
大数据-之LibrA数据库系统告警处理(ALM-12054 证书文件失效)
告警解释 系统在每天二十三点检查当前系统中的证书文件是否失效(即当前集群中的证书文件是否过期,或者尚未生效)。如果证书文件失效,产生该告警。 当重新导入一个正常证书,并且状态不为失效状态,该告警恢…...
Linux 之 journalctl 查看系统与 kernel 日志
目录 1. Linux 之 journalctl 查看系统与 kernel 日志 1. Linux 之 journalctl 查看系统与 kernel 日志 1 概述 日志管理工具 journalctl 是 centos7 上专有的日志管理工具, 该工具是从 message 这个文件里读取信息。Systemd 统一管理所有 Unit 的启动日志。带来的好处就是, …...
【PTA题目】7-3 冰雹猜想。 分数 10
7-3 冰雹猜想。 分数 10 全屏浏览题目 切换布局 作者 赵静静 单位 浙江工贸职业技术学院 冰雹猜想的内容是:任何一个大于1的整数n,按照n为偶数则除等2,n为奇数则乘3后再加1的规则不断变化,最终都可以变化为1。 例如ÿ…...
springBoot 配置druid多数据源 MySQL+SQLSERVER
1:pom 文件引入数据 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.0</version> </dependency>…...
二叉树的创建与遍历
目录 前言: 二叉树的概念与结构 二叉树的链式存储 二叉树的创建 二叉树的销毁 二叉树结点个数计算 二叉树叶子结点个数计算 二叉树第k层节点个数的计算 二叉树高度的计算 二叉树查找值为x的结点 二叉树的遍历 二叉树的前序遍历 二叉树的中序遍历 二叉树…...
Mysql相关操作命令合集
参考文档:2021-06-25MySQL8.0创建用户和权限控制 - 简书 mysql登陆命令: mysql -u用户名 -p密码; 若遇到复杂密码,包含特殊字符,则需要做转义(以下密码为:rootry?elyl!): mysql…...
前端开发学习 (一) 搭建Vue基础环境
一、环境搭建 1、安装nodejs #下载地址 https://nodejs.org/dist/v20.9.0/node-v20.9.0-x64.msi 2、配置环境变量 上面下载完安装包后自行安装,安装完成后安装下图操作添加环境变量 #查看版本 node --version v20.9.0# npm --version 10.1.03、配置npm加速源 np…...
二维码智慧门牌管理系统升级解决方案:查询功能大提升,让地址查找变得轻松便捷!
文章目录 前言一、支持地址名称、小区等信息进行模糊查询二、支持地图上绘制多边形、圆形、矩形进行范围查询三、高效的数据处理能力,保证查询速度四、灵活的应用场景,满足多种需求 前言 随着科技的快速发展和城市化的加速推进,传统的门牌管…...
vite+vue3+electron开发环境搭建
环境 node 18.14.2 yarn 1.22 项目创建 yarn create vite test01安装vue环境 cd test01 yarn yarn dev说明vue环境搭建成功 安装electron # 因为有的版本会报错所以指定了版本 yarn add electron26.1.0 -D安装vite-plugin-electron yarn add -D vite-plugin-electron根目…...
C#入门(9):多态介绍与代码演示
多态性是面向对象编程的一个核心概念,它允许你使用一个父类引用来指向一个子类对象。这可以使程序具有可扩展性,并且可以用来实现一些高级编程技术,如接口、事件、抽象类等。 多态相关的概念 以下是一些在C#中使用多态性的关键概念…...
7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
Python基于历史模拟方法实现投资组合风险管理的VaR与ES模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在金融市场日益复杂和波动加剧的背景下,风险管理成为金融机构和个人投资者关注的核心议题之一。VaR&…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
【堆垛策略】设计方法
堆垛策略的设计是积木堆叠系统的核心,直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法,涵盖基础规则、优化算法和容错机制: 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则: 大尺寸/重量积木在下…...
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
是不是受够了安装了oracle database之后sqlplus的简陋,无法删除无法上下翻页的苦恼。 可以安装readline和rlwrap插件的话,配置.bahs_profile后也能解决上下翻页这些,但是很多生产环境无法安装rpm包。 oracle提供了sqlcl免费许可,…...
