网站自行备案,重庆建设工程安全管理协会网站,网店推广方案范文,果洛营销网站建设一、安装vue环境#xff0c;并新建Vue项目
①#xff1a;安装node.js 官网(https://nodejs.org/zh-cn/) 2.安装完成之后检查下版本信息#xff1a; ②#xff1a;创建vue项目 1.接下来#xff0c;我们安装vue的环境 # 安装淘宝npm
npm install -g cnpm --registryhttps:/…一、安装vue环境并新建Vue项目
①安装node.js 官网(https://nodejs.org/zh-cn/) 2.安装完成之后检查下版本信息 ②创建vue项目 1.接下来我们安装vue的环境 # 安装淘宝npm
npm install -g cnpm --registryhttps://registry.npm.taobao.org
# vue-cli 安装依赖包
cnpm install --g vue-cli
# 打开vue的可视化管理工具界面
vue ui2.创建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 --save2.然后我们打开项目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 库类ajaxqs查询参数序列化和解析库mockjs为我们生成随机数据的工具库
1. 安装axios
接下来我们来安装axioshttp://www.axios-js.com/axios是一个基于 promise 的 HTTP 库这样我们进行前后端对接的时候使用这个工具可以提高我们的开发效率。 1.安装命令 yarn add axios --save2.在main.js中全局引入axios import axios from axios
Vue.prototype.$axios axios //2.安装qs
我们安装一个qs什么是qsqs是一个流行的查询参数序列化和解析库。可以将一个普通的object序列化成一个查询字符串或者反过来将一个查询字符串解析成一个object,帮助我们查询字符串解析和序列化字符串。 1.安装命令 yarn add qs --save3.安装mockjs
因为后台我们现在还没有搭建无法与前端完成数据交互因此我们这里需要mock数据因此我们引入mockjshttp://mockjs.com/方便后续我们提供api返回数据 1.安装命令 yarn add mockjs --save-dev2.然后我们在src目录下新建mock.js文件用于编写随机数据的api然后我们需要在main.js中引入这个文件 src/main.js
require(./mock) //引入mock数据关闭则注释该行后面我们mackjs会自动为我们拦截ajax并自动匹配路径返回数据
二、页面路由
RouterWebApp的链接路径管理系统简单就是建立起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?usernamexxx, 所以用了正则表达式
Mock.mock(RegExp(/login*),post,(config){// 这里无法在header添加authorization直接跳过console.log(mock----------------login)return Result
})3.开发登录页面 1.Login.vue登录页面 templateel-row typeflex classrow-bg justifycenterel-col classel-colh3 stylecolor: white; font-weight: bold; font-size: 21px; margin: 0 0 20px 0;padding: 0Spring security安全框架/h3el-form :modelform :rulesrules refruleForm classdemo-ruleFormel-form-item propusername stylewidth: 18rem;el-input prefix-iconel-icon-user placeholder用户名 v-modelform.username/el-input/el-form-itemel-form-item proppassword stylewidth: 18rem;el-input prefix-iconel-icon-lock show-password placeholder密码 v-modelform.password/el-input/el-form-itemel-form-item propcode stylewidth: 18rem;el-input prefix-iconel-icon-picture-outline v-modelform.code placeholder验证码:show-passwordtrue stylewidth: 10.8rem; float: left; maxlength5/el-inputel-image classcaptchaImg :srccaptchaImg stylewidth: 6.7rem; float: left;/el-image/el-form-itemel-form-itemel-button typeprimary stylewidth: 18rem; clicksubmitForm(ruleForm)登录/el-button/el-form-item/el-form/el-col/el-row
/templatescript
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;}});},}
}
/scriptstyle 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 vuexVue.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;charsetutf-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?usernamexxx, 所以用了正则表达式
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.vueIndex.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 templatediv idhomeel-containerel-aside width200px菜单栏/el-asideel-containerel-headerstrongSpring Security安全框架/strongdiv classheader-rightel-avatar sizemedium srchttps://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png/el-avatarel-dropdownspan classel-dropdown-linkAdmini classel-icon-arrow-down el-icon--right/i/spanel-dropdown-menu slotdropdownel-dropdown-item divided个人中心/el-dropdown-itemel-dropdown-item divided退出/el-dropdown-item/el-dropdown-menu/el-dropdownel-link hrefhttps://mp.csdn.net/mp_blog/manage/article?spm1011.2124.3001.5298CSDN笔记/el-linkel-link hrefhttps://gitee.com/Gitee仓库/el-link/div/el-headerel-mainrouter-view//el-main/el-container/el-container/div
/templatescript
export default {name: Home
}
/scriptstyle langless 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 templatedivel-carousel :interval4000 typecard indicator-positionoutsideel-carousel-item v-forurl in urls :keyurlel-image :srcurl/el-image/el-carousel-item/el-carousel/div
/templatescript
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]}}
}
/scriptstyle langless 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(2n1) {background-color: #d3dce6;
}
/style 5.总体下来效果如下 有点感觉了然后左边的菜单栏我们也弄下我们找到NavMenu 导航菜单组件然后加到Home.vue中因为考虑到后面我们需要做动态菜单所以我想单独这个页面出来因此我新建了个SideMenu.vue 6.SideMenu.vue templateel-menuclassel-menu-vertical-demobackground-color#545c64text-color#fffactive-text-color#ffd04brouter-link to/indexel-menu-item indexIndextemplate slottitlei classel-icon-s-home/ispan slottitle首页/span/template/el-menu-item/router-linkel-submenu index1template slottitlei classel-icon-s-operation/ispan系统管理/span/templateel-menu-item index1-1template slottitlei classel-icon-s-custom/ispan slottitle用户管理/span/template/el-menu-itemel-menu-item index1-2template slottitlei classel-icon-rank/ispan slottitle角色管理/span/template/el-menu-itemel-menu-item index1-3template slottitlei classel-icon-menu/ispan slottitle菜单管理/span/template/el-menu-item/el-submenuel-submenu index2template slottitlei classel-icon-s-tools/ispan系统工具/span/templateel-menu-item index2-2template slottitlei classel-icon-s-order/ispan slottitle数字字典/span/template/el-menu-item/el-submenu/el-menu
/templatescript
export default {name: SideMenu
}
/scriptstyle langless scoped
.el-menu-vertical-demo{height: 100%;
}
/style
SideMenu.vue作为一个组件添加到Home.vue中我们首选需要导入然后声明compoents然后才能使用标签 7.在Home.vue中代码如下 templatediv idhomeel-containerel-aside width200pxSideMenu/SideMenu/el-asideel-container..../el-container/el-container/div
/templatescript
import SideMenu from /views/SideMenu;
export default {name: Home,components: {SideMenu}
}
/script8.最后效果如下 我们先来新建几个页面先在views下新建文件夹sys然后再新建vue页面具体看下面这样我们就能把链接和页面可以连接起来。
src\views\sys Dict.vue 数字字典Menu.vue 菜单管理Role.vue 角色管理User.vue 用户管理
虽然建立了页面但是因为我们没有在router中注册链接与组件的关系所以我们现在打开链接还是打开不了页面的。下面我们就要动态联系起来。
五、用户登录信息展示
管理界面的右上角的用户信息现在是写死的因为我们现在已经登录成功所以我们可以通过接口去请求获取到当前的用户信息了这样我们就可以动态显示用户的信息这个接口比较简单然后退出登录的链接也一起完成就请求接口同时把浏览器中的缓存删除就退出了哈。 1.src\views\Home.vue templatediv idhomeel-containerel-aside width200pxSideMenu/SideMenu/el-asideel-containerel-headerstrongSpring Security安全框架/strongdiv classheader-rightel-avatar sizemedium :srcform.avatar/el-avatarel-dropdownspan classel-dropdown-link{{ form.username }}i classel-icon-arrow-down el-icon--right/i/spanel-dropdown-menu slotdropdownel-dropdown-item divided个人中心/el-dropdown-itemel-dropdown-item click.nativelogout divided退出/el-dropdown-item/el-dropdown-menu/el-dropdownel-link hrefhttps://mp.csdn.net/mp_blog/manage/article?spm1011.2124.3001.5298CSDN笔记/el-linkel-link hrefhttps://gitee.com/Gitee仓库/el-link/div/el-headerel-mainrouter-view//el-main/el-container/el-container/div
/templatescript
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)})}},
}
/scriptstyle langless 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;
}/style2.由于我们将请求接口提取到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?w252h188c7r0o5pid1.7}return Result
})/**登出*/Mock.mock(RegExp(/logout),post,(config){return Result
})5.效果 六、动态菜单栏开发
①动态菜单
上面代码中左侧的菜单栏的数据是写死的在实际场景中我们不可能这样做因为菜单是需要根据登录用户的权限动态显示菜单的也就是用户看到的菜单栏可能是不一样的这些数据需要去后端访问获取。
首先我们先把写死的数据简化成一个json数组数据然后for循环展示出来代码如下 1./src/views/inc/SideMenu.vue templateel-menuclassel-menu-vertical-demobackground-color#545c64text-color#fffactive-text-color#ffd04brouter-link to/indexel-menu-item indexIndextemplate slottitlei classel-icon-s-home/ispan slottitle首页/span/template/el-menu-item/router-linkel-submenu :indexmenu.name v-formenu in menuList :keymenu.idtemplate slottitlei :classmenu.icon/ispan{{ menu.title }}/span/templaterouter-link :toitem.path v-foritem in menu.children :keyitem.idel-menu-item :indexitem.nametemplate slottitlei :classitem.icon/ispan slottitle{{ item.title }}/span/template/el-menu-item/router-link/el-submenu/el-menu
/templatescript
export default {// 导航菜单name: SideMenu,data() {return {}},computed: {menuList: {get() {return this.$store.state.menus.menuList}}}}
/scriptstyle langless 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 vuexVue.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、当前标签Tab2、已存在的标签Tab列表然后页面从Store中获取列表显示并切换到当前Tab即可。删除时候我们循环当前Tab列表剔除Tab并切换到指定Tab。
我们先和左侧菜单一样单独定义一个组件Tabs.vue放在views/文件夹内 1.src/views/Tabs.vue templateel-tabs v-modeleditableTabsValue closable typecard tab-removeremoveTab tab-clickclickTabel-tab-pane v-foritem in editableTabs:keyitem.name:labelitem.title:nameitem.name/el-tab-pane/el-tabs
/templatescript
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})}}
}
/scriptstyle 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 vuexVue.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 templateel-menu:default-activethis.$store.state.menus.editableTabsValueclassel-menu-vertical-demobackground-color#545c64text-color#fffactive-text-color#ffd04brouter-link to/indexel-menu-item indexIndex clickaddTab({name: Index, title: 首页})template slottitlei classel-icon-s-home/ispan slottitle首页/span/template/el-menu-item/router-linkel-submenu :indexmenu.name v-formenu in menuList :keymenu.idtemplate slottitlei :classmenu.icon/ispan{{ menu.title }}/span/templaterouter-link :toitem.path v-foritem in menu.children :keyitem.idel-menu-item :indexitem.name clickaddTab(item)template slottitlei :classitem.icon/ispan slottitle{{ item.title }}/span/template/el-menu-item/router-link/el-submenu/el-menu
/templatescript
export default {// 导航菜单name: SideMenu,data() {return {}},computed: {menuList: {get() {return this.$store.state.menus.menuList}}},methods: {addTab(tab){this.$store.commit(ADD_TAB, tab)}},}
/scriptstyle langless scoped
.el-menu-vertical-demo {height: 100%;
}
/style
添加tab标签的时候注意需要激活指定当前标签也就是设置editableTabsValue。然后我们也添加了setActiveTab方法方便其他地方指定激活某个标签。
但是当我们刷新浏览器、或者直接通过输入链接打开页面时候就不会自动帮我们根据链接回显激活Tab。
刷新浏览器之后链接/sys/users不变内容不变但是Tab却不见了所以我们需要修补一下当用户是直接通过输入链接形式打开页面的时候我们也能根据链接自动添加激活指定的tab。那么在哪里添加这个回显的方法呢router中其实可以只不过我们需要做判断因为每次点击导航都会触发router。有没有更简便的方法有的因为刷新或者打开页面都是一次性的行为所以我们可以在更高层的App.vue中做这个回显动作具体如下 5.src\App.vue templatediv idapprouter-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了。