一、element-ui实现下拉菜单路由跳转总结
结构:
<el-menu>
// 一级菜单 (el-menu-item)
<el-menu-item index="/dashboard">
<template slot="title">
<i class="iconfont icon-shouye"></i>
<span slot="title">后台首页</span>
</template>
</el-menu-item>
// 二级菜单 (el-submenu > el-menu-item)
<el-submenu index="/product">
<template slot="title">
<i class="iconfont icon-shangpin"></i>
<span>商品管理</span>
</template>
<el-menu-item index="/product/proList">商品列表</el-menu-item>
<el-menu-item index="/product/proAdd">商品添加</el-menu-item>
<el-menu-item index="/product/proCate">商品分类</el-menu-item>
</el-submenu>
</el-menu>
el-menu 标签上的相关属性配置:
default-active="/"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
background-color="#545c64"
text-color="#fff"
active-text-color="#007acc"
在配置了路由js文件的路由规则后,想要实现对各菜单标签点击进行路由跳转的效果,则还需要一些属性配置:
1. <el-menu-item 标签上的index属性:
在上述 <el-menu中若加上 :router="true"属性,则可以让index产生路由跳转效果
官方解释::router表示是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转
2. default-active实现路由与标签高亮双向绑定;
此时,若果在刷新页面或者在地址栏直接输入路由地址,菜单导航的标签并不会高亮;
解决: 利用$route属性,结合default-active属性:
:default-active="$route.path"
二、项目目录结构相关(src前端资源):
-assets 主要是资源文件
-css
-fonts
-images
-components 组件
-layout 布局文件:通常来说是根据一个项目中的大体布局特点,抽离出主要布局,用“根文件”布 局,并将子组件抽离出来放在components文件夹中
-components
根文件
-router
index.js
-utils 工具类文件夹:放置一些功能文件
validate.js
-views 放置一个项目中的各个页面,一个模块即一个文件夹,一个模块下有多个子页面,即多级路由
其他页面组件1,
其他页面组件2,
其他页面组件3
...
App.vue
main.js
三、/deep/样式深度作用域elementui修改默认样式
举例:
<el-submenu index="/product">
<template slot="title">
<i class="iconfont icon-shangpin"></i>
<span>商品管理</span>
</template>
<el-menu-item index="/product/proList">商品列表</el-menu-item>
<el-menu-item index="/product/proAdd">商品添加</el-menu-item>
<el-menu-item index="/product/proCate">商品分类</el-menu-item>
</el-submenu>
问题:当想要修改el-submenu的背景色时,发现无论如何修改都不起作用;
解决:在浏览器元素审查发现,el-submenu的标签下有编译生成一个el-submenu_title的类,应该是template标签便已生成,将slot中的值作为后缀加上el-submenu作为前缀;
然而,直接对el-submenu_title类直接加样式也是不起作用的,要用到/deep/深度作用,如下:
.el-submenu /deep/ .el-submenu__title {
background-color: #304156 !important;
}
深度向下查找到el-submenu__title并作用
四、表单确认密码功能实现:
说明:(1)确认密码框失焦验证是否一致;(2)新密码框在失焦后同样要触发确认密码选框一致性验证规则
实现:
// 规则配置
rules: {
prePwd: [{ validator: validatePwd, trigger: 'blur' }],
pwd: [{ validator: validatePwd2(this), trigger: 'blur' }],
checkPwd: [{ validator: validatePwd2Check(this), trigger: 'blur' }]
}
这里validator验证函数本该是直接将函数体赋值(如validatePwd),但是,确认密码框的配置规则需要获取其他input框的值,即需要在验证函数里操作dom,又因为,我们模块化编程,将验证规则函数抽离到了utils工具类中,所以这里需要通过形参,进行多页面通信,即将this当做形参传入validate.js文件中;
// 输入新密码
export var validatePwd2 = obj => (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'))
} else if (!pwdReg.test(value)) {
callback(new Error('密码必须为6-20位字符'))
} else {
if (obj.modifyForm.checkPass !== '') {
console.log(obj.$refs.modifyForm)
obj.$refs.modifyForm.validateField('checkPwd')
}
callback()
}
}
而这里需要return一个函数,因为在rules配置中,validator需要联系一个函数体,若果不return,他不会自执行;
// 确认密码验证
export var validatePwd2Check = obj => (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (!pwdReg.test(value)) {
callback(new Error('密码必须为6-20位字符'))
} else if (value !== obj.modifyForm.pwd) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
五、面包屑导航
主要思路:(1)路由监听时机:路由地址改变监听(watch监听路由地址)、页面刷新监听(生命周期在created时)
**问题一:**如何监听路由地址改变,在watch中当然需要监听一个值,这里就需要通过route.path,如:
watch: {
'$route.path'() {
this.getRoute()
}
}
**问题二:**在监听到路由改变后,并且可以通过$route里的matched属性获得当前路由的先前匹配路由,如/product/proList
获得这个数组,便可以实现面包屑的层级效果;
**问题三:**在上一难点基础上,会发现没有任何一个(route&router)路由属性可以获得中文路由,这就需要我们自定义设置,在路由route文件夹的index.js路由配置文件里设置,例(依然/product/proList为例):
{
path: '/product/proList',
name: 'ProList',
meta: { name: '商品列表' },
component: () =>
import(
/* webpackChunkName: "proList" */ '../views/Product/ProList.vue'
)
},
注意到有一个meta属性的配置,设置后,访问/product/proList路由,打印$route观察变化
meta属性多了name,所以这时便可以把name拿到并渲染
**问题四:**当点击面包屑导航的标签(与当前路由相同)时会出现路由冗余的报错:ncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location
**解决:**在路由配置js文件里加上:
// 路由导航冗余报错(路由重复)
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
六、全局注册模块使用(结合插槽):
背景:同layout布局文件夹一样,在局部模块页面中,也会有通用布局,同样我们可以将其抽离,如:
抽离到/components/Panel_mainContent文件夹下,简单布局如下:
<template>
<el-card>
<div class="pageTitle">
<slot name="title"></slot>
</div>
<div class="pageContent">
<slot name="content"></slot>
</div>
</el-card>
</template>
显然,要用到这个模板,我们只需要在每一个页面中import导入并注册components属性 , 然后利用插槽:
<template>
<Panel>
<template #title></template>
// 或者可以直接<h3 slot="title"></h3>
<template #content></template>
</Panel>
</template>
script>
import Panel from ''
export default{
components:{
Panel
}
}
</script>
但是这种方法需要每个页面都要引入注册,过于繁琐,所以用到全局注册:
只需要新建一个components.js文件用于全局组件注册即可:
// components.js
import vue from 'vue'
import Panel from '../components/panel_mainContent/Index.vue'
vue.component('Panel', Panel)
然后在所有模块中都可以直接使用
七、封装项目的axios和promise远程异步请求
封装1:在每一个需要发送异步请求的页面一定都会使用axios的get,post方法,而且还有可能会用到多层回调(回调地狱),所以这个封装一是要将get和post的方法封装复用,二是要解决回调地狱,用axios异步,
export default {
get(url, params) {
return new Promise((resolve, reject) => {
axios
.get(url, { params })
.then(res => {
resolve(res.data)
})
.catch(err => {
reject(err)
})
})
},
post(url, params) {
return new Promise((resolve, reject) => {
axios
.post(url, qs.stringify(params))
.then(res => {
resolve(res.data)
})
.catch(err => {
reject(err)
})
})
}
}
**注意:**这里用promise里面封装axios,原本,axios本身就会返回一个promise对象,返回这个axios即可,但是由于axios无法返回错误error数据,所以我们就要利用promise的reject函数参数,所以new一个promise包住axios,返回这个promise对象即可;
**封装2:**封装接口,我们在项目中会发现有很多后端接口,同时这些接口地址前面ip及端口相同,只有后面地址或者请求方式不同,所以可以集中封装(src根目录新建user.js文件主要用来对用户相关接口封装),如:
import request from '@/utils/request.js'
// 登录接口
export function Login(data) {
return request.post('/users/checkLogin', data)
}
// 添加账号接口
export function addUser(data) {
return request.post('/users/add', data)
}
**调用接口函数:**在页面中进行远端异步请求,就直接调用api中暴露的接口封装方法即可
let data = await Login({
account: this.loginForm.uname,
password: this.loginForm.pwd
})
八、axios一些配置:
1、配置地址头:
axios.defaults.baseURL = 'http://127.0.0.1:5000'
2、注册请求拦截(token):
在访问除登录以外的大部分接口时,当然需要查看访客是否登录,在以前会用到cookie验证用户登录信息,但在这里我们使用更方便有效安全的token,方法即是在请求时拦截并验证请求头携带的token,配置在request.js中:
axios.interceptors.request.use(config=>{
let token = localstorage.getItem('token')
if(token){
config.headers.Authorization = token
}
return config
})
九、elementUI分页删除选项小bug
bug:在删除最后一页的最后唯一一项时,虽然页脚选页数是跳到上一页,但是页面为空;正常情况下,应该向前进一页,并且将此页及页码一起去掉;
**分析:**是currentPage当前页参数没变,导致当前页还停留在此页;
**解决:**每一次删除函数执行完都会执行一次getUserLi()函数以获取一次数据库中的总条数(total),在这时,就要进行判断==>if(删除的是当前页仅剩的唯一一条数据 && currentPage>1)==>currentPage–
代码:
let data = await deleteCur({ id })
if (data.code === 0) {
this.$message({
type: 'success',
message: '删除成功!'
})
await this.getUsersLi()
// 解决删除后造成的分页bug
if (
this.total === this.pageSize * (this.currentPage - 1) &&
this.currentPage > 1
// 判断当前页是否删空,并且最好排除第一页
) {
this.currentPage -= 1
await this.getUsersLi()
}
} else {
this.$message({
type: 'error',
message: '删除失败!'
})
}
注意:
解决中遇到另一bug:在调用删除后更新表单数据的 getUserLi 方法时没有加 await 进行同步操作,以至于后面 this.total 值没有更新,是因为受到 getUserLi 函数里的异步请求影响,暂时搁置而先执行同步导致;
所以,在以后凡是调用方法前先确定方法中是否存在异步操作;
十、请求拦截(token)
对于没有登陆的用户当然没有权限访问除登录页外的管理系统其他页面,不能访问的实质也是不能向服务器发起除登录外其他接口的任何请求,所以我们就要在请求发送的过程中就对每一次请求发起拦截并检查是否携带正确token,有则放行,没有即拦截
// 路由守卫拦截
router.beforeEach((to, from, next) => {
let token = storage.get('token') || storage.get('token', 'session')
if (token) {
next()
} else {
if (to.path === '/login') {
next()
} else {
next('/login')
}
}
})
// 请求拦截
axios.interceptors.request.use(config => {
const token = local.get('token')
// 除了登录之外的请求,都必须携带token
if (token) {
config.headers.Authorization = token
}
return config
})
十一、路由权限设计
假设项目中存在超级管理员和普通管理员两种角色,故要为两种管理员设计两种路由规则,对于普通管理员则要屏蔽一些路由和页面;
流程:
步骤一:
配置每一个路由地址的可访问者身份,例:
meta: { name: '订单管理', role: ['super', 'normal'], icon: 'icon-icon-' },
① 若是从一级路由到二级、三级路由均没有访问限制的地址,可不设置 **“role”**属性;② 若是只有部分子路由有访问限制的,在一级路由上设置 role: [‘super’, ‘normal’] ,受限制路由则设置一项即可,不受限制则不设置 “role”;
**注意:建议将路由配置文件中的routes:[]**抽离出来再导入,方便路由规则修改
步骤二:
既然要根据用户角色确定权限,那么首先我们得 获取用户权限 ,在登陆成功时,服务器返回一个role拿到即可,当然要和token一般存到本地
步骤三:
获得了role,这时就要根据步骤一中设置好的role字段,遍历routes规则数组,过滤掉屏蔽路由;
而这里的难点在于有children子路由,深层级遍历筛选,所以解决这种层级不深的用递归实现即可:
步骤三:
通过router.addRoutes([])方法将路由加到路由规则里,注意:此方法必须用[]包裹参数;
步骤四:
不同的角色的不同路由规则已经动态配置好,但是首页的导航菜单也要相应地屏蔽及显示,所以这里要操作dom,所以事先将每个角色的路由规则存入本地内存;
以便首页根据本地内存的menu字段路由信息,动态渲染导航菜单;
<!-- 二级菜单 -->
<template v-for="item in menuList">
<el-menu-item v-if="!item.children" :index="item.path" :key="item.name">
<i :class="'iconfont ' + item.meta.icon"></i>
<template slot="title">
<span>{{ item.meta.name }}</span>
</template>
</el-menu-item>
<el-submenu v-else :index="item.path" :key="item.name" :ref="item.path">
<template slot="title">
<i :class="'iconfont ' + item.meta.icon"></i>
<span>{{ item.meta.name }}</span>
</template>
<el-menu-item
v-for="itemC in item.children"
:key="itemC.name"
:index="itemC.path"
>{{ itemC.meta.name }}</el-menu-item
>
</el-submenu>
步骤五:
有些像个人中心或是订单详情类没有在导航菜单上有标签的页面,也会被渲染到菜单上,所以,在路由规则的这些特殊页面上加上hidden之类的boolean属性,进行再次筛选,将筛选好的数组执行步骤四的存入menu字段本地内存中;
getSyncRoutes()
// 根据不同登录角色,来确定权限
export function getSyncRoutes() {
let role = storage.get('role')
let finalRoutes = confirmRoutes(syncRoutes.routes, role) // 筛选好的而最终路由配置
routes[0].children = finalRoutes
router.addRoutes([routes[0]])
let arr = visiblePage(finalRoutes)
storage.set('menu', JSON.stringify(arr))
}
function confirmRoutes(arr, role) {
return arr.filter(item => {
// 数组过滤返回新数组,新数组即新的路由规则
if (hasPermission(item, role)) {
if (item.children) {
item.children = confirmRoutes(item.children, role)
}
return true
} else {
return false
}
})
}
function hasPermission(obj, role) {
if (obj.meta && obj.meta.role) {
return obj.meta.role.includes(role)
} else {
return true
}
}
// 导航菜单中只显示能够访问的页面标签
function visiblePage(arr) {
return arr.filter(item => {
if (!item.hidden) {
if (item.children) {
item.children = visiblePage(item.children)
}
return true
} else {
return false
}
})
}
十二、nprogress
请求发送后的进度条效果:
npm下包到依赖,然后在请求响应时设置拦截,通过nprogress.start()和nprogress.done()来实现即可;
结合elementUI的全局设置message提示框,因为在每一次请求响应都会调用this.$message,代码太冗余:
axios.interceptors.request.use(config => {
NProgress.start()
return config
})
// 注册响应拦截器
axios.interceptors.response.use(res => {
NProgress.done()
// 所有的响应都会先走这里
// 成功和失败的提示
let { code, msg } = res.data
if (code || msg) {
if (code === 0 || code === '00') {
Message.success(msg)
} else {
Message.error(msg)
}
}
return res
})