安全框架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.mzph.cn/news/152574.shtml

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

相关文章

软件测试/测试开发/人工智能丨基于Spark的分布式造数工具:加速大规模测试数据构建

随着软件开发规模的扩大&#xff0c;测试数据的构建变得越来越复杂&#xff0c;传统的造数方法难以应对大规模数据需求。本文将介绍如何使用Apache Spark构建分布式造数工具&#xff0c;以提升测试数据构建的效率和规模。 为什么选择Spark&#xff1f; 分布式计算&#xff1a;…

easyExcel注解详情

前言11个注解字段注解 类注解基础综合示例补充颜色总结 11个注解 ExcelProperty ColumnWith 列宽 ContentFontStyle 文本字体样式 ContentLoopMerge 文本合并 ContentRowHeight 文本行高度 ContentStyle 文本样式 HeadFontStyle 标题字体样式 HeadRowHeight 标题高度 HeadStyle…

Python将原始数据集和标注文件进行数据增强(随机仿射变换),并生成随机仿射变换的数据集和标注文件

Python将原始数据集和标注文件进行数据增强&#xff08;随机仿射变换&#xff09;&#xff0c;并生成随机仿射变换的数据集和标注文件 前言前提条件相关介绍实验环境生成随机仿射变换的数据集和标注文件代码实现输出结果 前言 由于本人水平有限&#xff0c;难免出现错漏&#x…

OpenCV快速入门:图像滤波与边缘检测

文章目录 前言一、噪声种类与生成1.1 椒盐噪声1.2 高斯噪声1.3 彩色噪声 二、卷积操作2.1 卷积基本原理2.2 卷积操作代码实现 三、线性滤波3.1 均值滤波均值滤波原理均值滤波公式均值滤波代码实现 3.2 方框滤波方框滤波原理方框滤波公式方框滤波代码实现 3.3 高斯滤波高斯滤波原…

redis非关系型数据库(缓存型数据库)——中间件

【重点】redis为什么这么快&#xff1f;&#xff08;应届&#xff09; ①redis是纯内存结构&#xff0c;避免磁盘I/O的耗时 ②redis核心模块是一个单进程&#xff0c;减少线程切换和回收线程资源时间 ③redis采用的是I/O的多路复用机制&#xff08;每一个执行线路可以同时完…

npm install 下载不下来依赖解决方案

背景 最近在构建 前端自动化部署 的方案中发现了一个问题&#xff0c;就是我在npm install的时候&#xff0c;有时候成功&#xff0c;有时候不成功&#xff0c;而且什么代码也没发生更改&#xff0c;报错也就是那么几个错&#xff0c;所以在此也整理了一下遇到这种情况&#xf…

如何使用 WPF 应用程序连接 FastReport报表

随着期待已久的FastReport WPF的发布&#xff0c;您不再需要使用 FastReport .NET 来处理基于 WPF 的项目。 不久前&#xff0c;在 FastReport .NET 中使用 WPF 还相当不方便。并非一切都进展顺利&#xff1b;连接 FastReport.dll 和许多其他问题存在问题。我们重新思考了该方…

2023年中职“网络安全“—Web 渗透测试①

2023年中职"网络安全"—Web 渗透测试① Web 渗透测试任务环境说明&#xff1a;1.访问地址http://靶机IP/task1&#xff0c;分析页面内容&#xff0c;获取flag值&#xff0c;Flag格式为flag{xxx}&#xff1b;2.访问地址http://靶机IP/task2&#xff0c;访问登录页面。…

面试题c/c++--语言基础

一 、语言基础 1.1 指针 野指针&#xff1a;指针指向的位置是不可知的 悬空指针&#xff1a;指针最初指向的内存已经被释放了的一种指针 两种指针都指向无效内存空间&#xff0c; 即不安全不可控 。需要在定义指针后且在使用之前完成初始化或者使用 智能指针来避免 智能指针 智…

获取阿里云Docker镜像加速器

1、阿里云官网&#xff08;www.aliyun.com&#xff09;注册账号 2、打开“控制台首页” 控制台首页地址&#xff1a;https://home.console.aliyun.com/home/dashboard/ProductAndService 3、点击“概览->容器镜像服务 ACR” 4、打开“镜像工具->镜像加速器”页面&#x…

【grafana | clickhouse】实现展示多折线图

说明&#xff1a; 采用的是 Visualizations 的 Time series&#xff0c;使用的 clickhouse 数据源 在工作中遇到了一个需求&#xff0c;写好了代码&#xff0c;需要在grafana上展示在一个项目中所有人的&#xff0c;随时间的代码提交量变化图 目前遇到的问题&#xff1a;展示…

FFmpeg常用命令行讲解及实战一

文章目录 前言一、学习资料参考二、FFmpeg 选项1、主要选项①、主要命令选项②、举例 2、视频选项①、主要命令选项②、举例1&#xff09;提取固定帧2&#xff09;禁止输出视频3&#xff09;指定视频的纵横比 3、音频选项①、主要命令选项②、举例 4、字幕选项①、主要命令选项…

负载均衡简介

负载均衡 负载均衡&#xff08;Load Balance&#xff0c;简称 LB&#xff09;是高并发、高可用系统必不可少的关键组件&#xff0c;目标是 尽力将网络流量平均分发到多个服务器上&#xff0c;以提高系统整体的响应速度和可用性。 负载均衡的分类和OSI模型息息相关&#xff0c…

【CHI】Ordering保序

本节介绍CHI协议所包含的支持系统保序需求的机制&#xff0c;包括&#xff1a; • Multi-copy atomicity • Completion response and ordering • Completion acknowledgment • Transaction ordering 一、 Multi-copy atomicity CHI协议中所使用的memory model要求为mu…

【面试经典150 | 数学】Pow(x, n)

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;快速幂-递归方法二&#xff1a;快速幂-迭代 其他语言python3 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主…

王者荣耀游戏

游戏运行如下&#xff1a; sxt Background package sxt;import java.awt.*; //背景类 public class Background extends GameObject{public Background(GameFrame gameFrame) {super(gameFrame);}Image bg Toolkit.getDefaultToolkit().getImage("C:\\Users\\24465\\D…

5分钟教你轻松搭建Web自动化测试框架

在程序员的世界中&#xff0c;一切重复性的工作&#xff0c;都应该通过程序自动执行。「自动化测试」就是一个最好的例子。 随着互联网应用开发周期越来越短&#xff0c;迭代速度越来越快&#xff0c;只会点点点&#xff0c;不懂开发的手工测试&#xff0c;已经无法满足如今的…

3.8-镜像的发布

如果我们想将image push到docker hub里面&#xff0c;那么我们的image的名字一定要是这种格式&#xff1a;docker hub id/imageName&#xff0c;例如&#xff1a;lvdapiaoliang/hello-docker docker hub个人账户设置地址&#xff1a; 在push之前要先登录&#xff1a; docker l…

数学建模值TOPSIS法及代码

TOPSIS法 TOPSIS法简称为优劣距离解法&#xff0c;是一种常见法综合评价方法&#xff0c;其能充分利用原始数据的信息&#xff0c;其结果能精确地反映各个评价方案之间的差距。 模型介绍 上篇文章谈到的层次分析法是有局限性的。比如评价的决策层不能太多&#xff0c;太多的…

ISP--Black Level Correction(黑电平矫正)

图像的每一个像素点都是由一个光电二极管控制的&#xff0c;由二极管将电信号&#xff0c;转换为数字信号。 那么&#xff0c;我们知道了&#xff0c;图像的像素值是与电信号强度相关的。但是&#xff0c;我们得知道&#xff0c;每一个光电二极管要想工作&#xff0c;都得有一定…