mirror of
https://github.com/iczer/vue-antd-admin
synced 2025-04-06 04:00:06 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
a84952577f
docs
src
components
config/default
layouts
router
store/modules
utils
@ -36,7 +36,7 @@ module.exports = {
|
||||
title: '进阶',
|
||||
collapsable: false,
|
||||
children: [
|
||||
'/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors', '/advance/skill'
|
||||
'/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -24,7 +24,7 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
### 注册路由组件
|
||||
基础路由组件包含路由基本配置和对应的视图组件,我们统一在 `/router/router.map.js` 文件中注册它们。它和正常的路由配置基本无异,相当于把完整的路由拆分成单个的路由配置进行注册,为后面的路由动态配置打好基础。
|
||||
基础路由组件包含路由基本配置和对应的视图组件,我们统一在 `/router/async/router.map.js` 文件中注册它们。它和正常的路由配置基本无异,相当于把完整的路由拆分成单个的路由配置进行注册,为后面的路由动态配置打好基础。
|
||||
一个单独的路由组件注册示例如下:
|
||||
```jsx
|
||||
registerName: { //路由组件注册名称,唯一标识
|
||||
@ -108,7 +108,7 @@ export default routerMap
|
||||
```
|
||||
:::
|
||||
### 配置基本路由
|
||||
如果没有任何路由,你的应用是无法访问的,所以我们需要在本地配置一些基本的路由,比如登录页、404、403 等。你可以在 `/router/config.async.js` 文件中配置一些本地必要的路由。如下:
|
||||
如果没有任何路由,你的应用是无法访问的,所以我们需要在本地配置一些基本的路由,比如登录页、404、403 等。你可以在 `/router/async/config.async.js` 文件中配置一些本地必要的路由。如下:
|
||||
```js
|
||||
const routesConfig = [
|
||||
'login', //匹配 router.map.js 中注册的 registerName = login 的路由
|
||||
@ -163,25 +163,25 @@ export default options
|
||||
那么我们就需要先从后端服务获取异步路由配置,后端返回的异步路由配置 `routesConfig` 是一个异步路由配置数组, 应当如下格式:
|
||||
```jsx
|
||||
[{
|
||||
router: 'root', //匹配 /router/router.map.js 中注册名 registerName = root 的路由
|
||||
router: 'root', //匹配 router.map.js 中注册名 registerName = root 的路由
|
||||
children: [ //root 路由的子路由配置
|
||||
{
|
||||
router: 'dashboard', //匹配 /router/router.map.js 中注册名 registerName = dashboard 的路由
|
||||
router: 'dashboard', //匹配 router.map.js 中注册名 registerName = dashboard 的路由
|
||||
children: ['workplace', 'analysis'], //dashboard 路由的子路由配置,依次匹配 registerName 为 workplace 和 analysis 的路由
|
||||
},
|
||||
{
|
||||
router: 'form', //匹配 /router/router.map.js 中注册名 registerName = form 的路由
|
||||
router: 'form', //匹配 router.map.js 中注册名 registerName = form 的路由
|
||||
children: [ //form 路由的子路由配置
|
||||
'basicForm', //匹配 /router/router.map.js 中注册名 registerName = basicForm 的路由
|
||||
'stepForm', //匹配 /router/router.map.js 中注册名 registerName = stepForm 的路由
|
||||
'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由
|
||||
'stepForm', //匹配 router.map.js 中注册名 registerName = stepForm 的路由
|
||||
{
|
||||
router: 'advanceForm', //匹配 /router/router.map.js 中注册名 registerName = advanceForm 的路由
|
||||
router: 'advanceForm', //匹配 router.map.js 中注册名 registerName = advanceForm 的路由
|
||||
path: 'advance' //重写 advanceForm 路由的 path 属性
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
router: 'basicForm', //匹配 /router/router.map.js 中注册名 registerName = basicForm 的路由
|
||||
router: 'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由
|
||||
name: '验权表单', //重写 basicForm 路由的 name 属性
|
||||
icon: 'file-excel', //重写 basicForm 路由的 icon 属性
|
||||
authority: 'form' //重写 basicForm 路由的 authority 属性
|
||||
@ -203,7 +203,7 @@ getRoutesConfig().then(result => {
|
||||
至此,异步路由的加载就完成了,你可以访问异步加载的路由了。
|
||||
:::tip
|
||||
上面获取异步路由的代码,在 /pages/login/Login.vue 文件中可以找到。
|
||||
loadRoutes 方法会合并 /router/config.async.js 文件中配置的基本路由。
|
||||
loadRoutes 方法会合并 /router/async/config.async.js 文件中配置的基本路由。
|
||||
:::
|
||||
:::details 点击查看 loadRoutes 的详细代码
|
||||
```js
|
||||
|
@ -1,7 +1,109 @@
|
||||
---
|
||||
title: 导航守卫
|
||||
title: 路由守卫
|
||||
lang: zn-CN
|
||||
---
|
||||
# 导航守卫
|
||||
# 路由守卫
|
||||
Vue Antd Admin 使用 vue-router 实现路由导航功能,因此可以为路由配置一些守卫。
|
||||
我们统一把导航守卫配置在 router/guards.js 文件中。
|
||||
|
||||
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
|
||||
## 前置守卫
|
||||
Vue Antd Admin 为每个前置导航守卫函数注入 to,from,next,options 四个参数:
|
||||
* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)
|
||||
* `from: Route`: 当前导航正要离开的路由对象
|
||||
* `next: Function`: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。详情查看 [Vue Router #导航守卫](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)
|
||||
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
|
||||
如下,是登录拦截导航守卫的定义
|
||||
```js
|
||||
const loginGuard = (to, from, next, options) => {
|
||||
const {message} = options
|
||||
if (!loginIgnore.includes(to) && !checkAuthorization()) {
|
||||
message.warning('登录已失效,请重新登录')
|
||||
next({path: '/login'})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 后置守卫
|
||||
你也可以定义后置导航守卫,Vue Antd Admin 为每个后置导航函数注入 to,from,options 三个参数:
|
||||
* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)
|
||||
* `from: Route`: 当前导航正要离开的路由对象
|
||||
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
|
||||
如下,是一个后置导航守卫的定义
|
||||
```js
|
||||
const afterGuard = (to, from, options) => {
|
||||
const {store, message} = options
|
||||
// 做些什么
|
||||
message.info('do something')
|
||||
}
|
||||
```
|
||||
|
||||
## 导出守卫配置
|
||||
定义好导航守卫后,只需按照类别在 guard.js 中导出即可。分为两类,`前置守卫`和`后置守卫`。如下:
|
||||
```js
|
||||
export default {
|
||||
beforeEach: [loginGuard, authorityGuard],
|
||||
afterEach: [afterGuard]
|
||||
}
|
||||
```
|
||||
|
||||
:::details 点击查看完整的导航守卫配置
|
||||
```js
|
||||
import {loginIgnore} from '@/router/index'
|
||||
import {checkAuthorization} from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 登录守卫
|
||||
* @param to
|
||||
* @param form
|
||||
* @param next
|
||||
* @param options
|
||||
*/
|
||||
const loginGuard = (to, from, next, options) => {
|
||||
const {message} = options
|
||||
if (!loginIgnore.includes(to) && !checkAuthorization()) {
|
||||
message.warning('登录已失效,请重新登录')
|
||||
next({path: '/login'})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限守卫
|
||||
* @param to
|
||||
* @param form
|
||||
* @param next
|
||||
* @param options
|
||||
*/
|
||||
const authorityGuard = (to, from, next, options) => {
|
||||
const {store, message} = options
|
||||
const permissions = store.getters['account/permissions']
|
||||
const roles = store.getters['account/roles']
|
||||
if (!hasAuthority(to, permissions, roles)) {
|
||||
message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`)
|
||||
next({path: '/403'})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后置守卫
|
||||
* @param to
|
||||
* @param form
|
||||
* @param options
|
||||
*/
|
||||
const afterGuard = (to, from, options) => {
|
||||
const {store, message} = options
|
||||
// 做些什么
|
||||
message.info('do something')
|
||||
}
|
||||
|
||||
export default {
|
||||
beforeEach: [loginGuard, authorityGuard],
|
||||
afterEach: [afterGuard]
|
||||
}
|
||||
```
|
||||
:::
|
@ -3,5 +3,129 @@ title: 拦截器配置
|
||||
lang: zn-CN
|
||||
---
|
||||
# 拦截器配置
|
||||
Vue Antd Admin 基于 aixos 封装了 http 通信功能,我们可以为 http 请求响应配置一些拦截器。拦截器统一配置在 /utils/axios-interceptors.js 文件中。
|
||||
## 请求拦截器
|
||||
你可以为每个请求拦截器配置 `onFulfilled` 或 `onRejected` 两个钩子函数。
|
||||
### onFulfilled
|
||||
我们会为 onFulfilled 钩子函数注入 config 和 options 两个参数:
|
||||
* `config: AxiosRequestConfig`: axios 请求配置,详情参考 [axios 请求配置](http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE)
|
||||
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
|
||||
|
||||
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
|
||||
### onRejected
|
||||
我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数:
|
||||
* `error: Error`: axios 请求错误对象
|
||||
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
|
||||
|
||||
如下,为一个完整的请求拦截器配置:
|
||||
```js
|
||||
const tokenCheck = {
|
||||
// 发送请求之前做些什么
|
||||
onFulfilled(config, options) {
|
||||
const {message} = options
|
||||
const {url, xsrfCookieName} = config
|
||||
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
|
||||
message.warning('认证 token 已过期,请重新登录')
|
||||
}
|
||||
return config
|
||||
},
|
||||
// 请求出错时做点什么
|
||||
onRejected(error, options) {
|
||||
const {message} = options
|
||||
message.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
## 响应拦截器
|
||||
响应拦截器也同样可以配置 `onFulfilled` 或 `onRejected` 两个钩子函数。
|
||||
### onFulfilled
|
||||
我们会为 onFulfilled 钩子函数注入 response 和 options 两个参数:
|
||||
* `response: AxiosResponse`: axios 响应对象,详情参考 [axios 响应对象](http://www.axios-js.com/zh-cn/docs/#%E5%93%8D%E5%BA%94%E7%BB%93%E6%9E%84)
|
||||
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
|
||||
|
||||
### onRejected
|
||||
我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数:
|
||||
* `error: Error`: axios 请求错误对象
|
||||
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
|
||||
|
||||
如下,为一个完整的响应拦截器配置:
|
||||
```js
|
||||
const resp401 = {
|
||||
// 响应数据之前做点什么
|
||||
onFulfilled(response, options) {
|
||||
const {message} = options
|
||||
if (response.status === 401) {
|
||||
message.error('无此接口权限')
|
||||
}
|
||||
return response
|
||||
},
|
||||
// 响应出错时做点什么
|
||||
onRejected(error, options) {
|
||||
const {message} = options
|
||||
if (response.status === 401) {
|
||||
message.error('无此接口权限')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
```
|
||||
## 导出拦截器
|
||||
定义好拦截器后,只需在 axios-interceptors.js 文件中导出即可。分为两类,`请求拦截器`和`响应拦截器`。如下:
|
||||
```js
|
||||
export default {
|
||||
request: [tokenCheck], // 请求拦截
|
||||
response: [resp401] // 响应拦截
|
||||
}
|
||||
```
|
||||
|
||||
:::details 点击查看完整的拦截器配置示例
|
||||
```js
|
||||
import Cookie from 'js-cookie'
|
||||
// 401拦截
|
||||
const resp401 = {
|
||||
onFulfilled(response, options) {
|
||||
const {message} = options
|
||||
if (response.status === 401) {
|
||||
message.error('无此接口权限')
|
||||
}
|
||||
return response
|
||||
},
|
||||
onRejected(error, options) {
|
||||
const {message} = options
|
||||
message.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
const resp403 = {
|
||||
onFulfilled(response, options) {
|
||||
const {message} = options
|
||||
if (response.status === 403) {
|
||||
message.error(`请求被拒绝`)
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
const reqCommon = {
|
||||
onFulfilled(config, options) {
|
||||
const {message} = options
|
||||
const {url, xsrfCookieName} = config
|
||||
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
|
||||
message.warning('认证 token 已过期,请重新登录')
|
||||
}
|
||||
return config
|
||||
},
|
||||
onRejected(error, options) {
|
||||
const {message} = options
|
||||
message.error(error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
request: [reqCommon], // 请求拦截
|
||||
response: [resp401, resp403] // 响应拦截
|
||||
}
|
||||
```
|
||||
:::
|
@ -3,5 +3,74 @@ title: 登录认证
|
||||
lang: zn-CN
|
||||
---
|
||||
# 登录认证
|
||||
Vue Antd Admin 使用 js-cookie.js 管理用户的 token,结合 axios 配置,可以为每个请求头加上 token 信息。
|
||||
|
||||
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
|
||||
## token名称
|
||||
后端系统通常会从请求 header 中获取用户的 token,因此我们需要配置好 token 名称,好让后端能正确的识别到用户 token。
|
||||
Vue Antd Admin 默认token 名称为 `Authorization`,你可以在 /utils/request.js 中修改它。
|
||||
```js{5}
|
||||
import axios from 'axios'
|
||||
import Cookie from 'js-cookie'
|
||||
|
||||
// 跨域认证信息 header 名
|
||||
const xsrfHeaderName = 'Authorization'
|
||||
...
|
||||
```
|
||||
## token 设置
|
||||
调用登录接口后拿到用户的 token 和 token 过期时间(如无过期时间,可忽略),并使用 /utils/request.js #setAuthorization 方法保存token。
|
||||
```js{5}
|
||||
import {setAuthorization} from '@/utils/request'
|
||||
|
||||
login(name, password).then(res => {
|
||||
const {token, expireAt} = res.data
|
||||
setAuthorization({token, expireAt: new Date(expireAt)})
|
||||
})
|
||||
```
|
||||
## token 校验
|
||||
Vue Antd Admin 默认添加了登录导航守卫,如检查到本地cookie 中不包含 token 信息,则会拦截跳转至登录页。你可以在 /router/index.js 中配置
|
||||
不需要登录拦截的路由
|
||||
```js
|
||||
// 不需要登录拦截的路由配置
|
||||
const loginIgnore = {
|
||||
names: ['404', '403'], //根据路由名称匹配
|
||||
paths: ['/login'], //根据路由fullPath匹配
|
||||
/**
|
||||
* 判断路由是否包含在该配置中
|
||||
* @param route vue-router 的 route 对象
|
||||
* @returns {boolean}
|
||||
*/
|
||||
includes(route) {
|
||||
return this.names.includes(route.name) || this.paths.includes(route.path)
|
||||
}
|
||||
}
|
||||
```
|
||||
或者在 /router/guards.js 中移出登录守卫
|
||||
```diff
|
||||
...
|
||||
export default {
|
||||
- beforeEach: [loginGuard, authorityGuard, redirectGuard],
|
||||
+ beforeEach: [authorityGuard, redirectGuard],
|
||||
afterEach: []
|
||||
}
|
||||
```
|
||||
## Api
|
||||
### setAuthorization(auth, authType)
|
||||
来源:/utils/request.js
|
||||
该方法用于保存用户 token,接收两个参数:
|
||||
* **auth**
|
||||
认证信息,包含 token、expireAt 等认证数据。
|
||||
* **authType**
|
||||
认证类型,默认为 `AUTH_TYPE.BEARER`(AUTH_TYPE.BEARER 默认会给token 加上 Bearer 识别前缀),可根据自己的认证类型自行扩展。
|
||||
|
||||
### checkAuthorization(authType)
|
||||
该方法用于校验用户 token 是否过期,接收一个参数:
|
||||
* **authType**
|
||||
认证类型,默认为 `AUTH_TYPE.BEARER`。
|
||||
|
||||
### removeAuthorization(authType)
|
||||
该方法用于移出用户本地存储的 token,接收一个参数:
|
||||
* **authType**
|
||||
认证类型,默认为 `AUTH_TYPE.BEARER`。
|
||||
:::tip
|
||||
以上 Api 均可在 /utils/request.js 文件中找到。
|
||||
:::
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-tooltip :title="title">
|
||||
<a-tooltip :title="title" :overlayStyle="{zIndex: 2001}">
|
||||
<div class="img-check-box" @click="toggle">
|
||||
<img :src="img" />
|
||||
<div v-if="sChecked" class="check-item">
|
||||
|
@ -77,7 +77,7 @@ export default {
|
||||
},
|
||||
created () {
|
||||
this.updateMenu()
|
||||
if (!this.options[0].fullPath) {
|
||||
if (this.options.length > 0 && !this.options[0].fullPath) {
|
||||
this.formatOptions(this.options, '')
|
||||
}
|
||||
// 自定义国际化配置
|
||||
@ -90,7 +90,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
options(val) {
|
||||
if (!val[0].fullPath) {
|
||||
if (val.length > 0 && !val[0].fullPath) {
|
||||
this.formatOptions(this.options, '')
|
||||
}
|
||||
},
|
||||
@ -195,18 +195,14 @@ export default {
|
||||
},
|
||||
updateMenu () {
|
||||
const menuRoutes = this.$route.matched.filter(item => item.path !== '')
|
||||
const route = menuRoutes.pop()
|
||||
this.selectedKeys = [this.getSelectedKey(route)]
|
||||
this.selectedKeys = this.getSelectedKey(this.$route)
|
||||
let openKeys = menuRoutes.map(item => item.path)
|
||||
if (!fastEqual(openKeys, this.sOpenKeys)) {
|
||||
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys
|
||||
}
|
||||
},
|
||||
getSelectedKey (route) {
|
||||
if (route.meta.invisible && route.parent) {
|
||||
return this.getSelectedKey(route.parent)
|
||||
}
|
||||
return route.path
|
||||
return route.matched.map(item => item.path)
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
|
@ -26,6 +26,7 @@
|
||||
>
|
||||
<img-checkbox :title="$t('navigate.side')" img="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" value="side"/>
|
||||
<img-checkbox :title="$t('navigate.head')" img="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" value="head"/>
|
||||
<img-checkbox :title="$t('navigate.mix')" img="https://gw.alipayobjects.com/zos/antfincdn/x8Ob%26B8cy8/LCkqqYNmvBEbokSDscrm.svg" value="mix"/>
|
||||
</img-checkbox-group>
|
||||
</setting-item>
|
||||
<setting-item>
|
||||
|
@ -12,6 +12,7 @@ module.exports = {
|
||||
title: '导航设置',
|
||||
side: '侧边导航',
|
||||
head: '顶部导航',
|
||||
mix: '混合导航',
|
||||
content: {
|
||||
title: '内容区域宽度',
|
||||
fluid: '流式',
|
||||
@ -82,6 +83,7 @@ module.exports = {
|
||||
title: 'Navigation Mode',
|
||||
side: 'Side Menu Layout',
|
||||
head: 'Top Menu Layout',
|
||||
mix: 'Mix Menu Layout',
|
||||
content: {
|
||||
title: 'Content Width',
|
||||
fluid: 'Fluid',
|
||||
|
@ -18,6 +18,7 @@ module.exports = {
|
||||
copyright: '2018 ICZER 工作室出品', //copyright
|
||||
asyncRoutes: false, //异步加载路由,true:开启,false:不开启
|
||||
showPageTitle: true, //是否显示页面标题(PageLayout 布局中的页面标题),true:显示,false:不显示
|
||||
filterMenu: true, //根据权限过滤菜单,true:过滤,false:不过滤
|
||||
animate: { //动画设置
|
||||
disabled: false, //禁用动画,true:禁用,false:启用
|
||||
name: 'bounce', //动画效果,支持的动画效果可参考 ./animate.config.js
|
||||
|
@ -3,7 +3,7 @@
|
||||
<drawer v-if="isMobile" v-model="collapsed">
|
||||
<side-menu :theme="theme.mode" :menuData="menuData" :collapsed="false" :collapsible="false" @menuSelect="onMenuSelect"/>
|
||||
</drawer>
|
||||
<side-menu :class="[fixedSideBar ? 'fixed-side' : '']" :theme="theme.mode" v-else-if="layout === 'side'" :menuData="menuData" :collapsed="collapsed" :collapsible="true" />
|
||||
<side-menu :class="[fixedSideBar ? 'fixed-side' : '']" :theme="theme.mode" v-else-if="layout === 'side' || layout === 'mix'" :menuData="sideMenuData" :collapsed="collapsed" :collapsible="true" />
|
||||
<div v-if="fixedSideBar && !isMobile" :style="`width: ${sideMenuWidth}; min-width: ${sideMenuWidth};max-width: ${sideMenuWidth};`" class="virtual-side"></div>
|
||||
<drawer v-if="!hideSetting" v-model="showSetting" placement="right">
|
||||
<div class="setting" slot="handler">
|
||||
@ -12,7 +12,7 @@
|
||||
<setting />
|
||||
</drawer>
|
||||
<a-layout class="admin-layout-main beauty-scroll">
|
||||
<admin-header :style="headerStyle" :menuData="menuData" :collapsed="collapsed" @toggleCollapse="toggleCollapse"/>
|
||||
<admin-header :style="headerStyle" :menuData="headMenuData" :collapsed="collapsed" @toggleCollapse="toggleCollapse"/>
|
||||
<a-layout-header v-if="fixedHeader"></a-layout-header>
|
||||
<a-layout-content class="admin-layout-content">
|
||||
<div :style="`min-height: ${minHeight}px; position: relative`">
|
||||
@ -32,7 +32,7 @@ import PageFooter from './footer/PageFooter'
|
||||
import Drawer from '../components/tool/Drawer'
|
||||
import SideMenu from '../components/menu/SideMenu'
|
||||
import Setting from '../components/setting/Setting'
|
||||
import {mapState, mapMutations} from 'vuex'
|
||||
import {mapState, mapMutations, mapGetters} from 'vuex'
|
||||
|
||||
const minHeight = window.innerHeight - 64 - 24 - 122
|
||||
|
||||
@ -46,30 +46,61 @@ export default {
|
||||
showSetting: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route(val) {
|
||||
this.setActivated(val)
|
||||
},
|
||||
layout() {
|
||||
this.setActivated(this.$route)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('setting', ['isMobile', 'theme', 'layout', 'footerLinks', 'copyright', 'fixedHeader', 'fixedSideBar',
|
||||
'hideSetting', 'menuData']),
|
||||
'hideSetting']),
|
||||
...mapGetters('setting', ['firstMenu', 'subMenu', 'menuData']),
|
||||
sideMenuWidth() {
|
||||
return this.collapsed ? '80px' : '256px'
|
||||
},
|
||||
headerStyle() {
|
||||
let width = (this.fixedHeader && this.layout == 'side' && !this.isMobile) ? `calc(100% - ${this.sideMenuWidth})` : '100%'
|
||||
let width = (this.fixedHeader && this.layout !== 'head' && !this.isMobile) ? `calc(100% - ${this.sideMenuWidth})` : '100%'
|
||||
let position = this.fixedHeader ? 'fixed' : 'static'
|
||||
let transition = this.fixedHeader ? 'transition: width 0.2s' : ''
|
||||
return `width: ${width}; position: ${position}; ${transition}`
|
||||
},
|
||||
headMenuData() {
|
||||
const {layout, menuData, firstMenu} = this
|
||||
return layout === 'mix' ? firstMenu : menuData
|
||||
},
|
||||
sideMenuData() {
|
||||
const {layout, menuData, subMenu} = this
|
||||
return layout === 'mix' ? subMenu : menuData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('setting', ['correctPageMinHeight']),
|
||||
...mapMutations('setting', ['correctPageMinHeight', 'setActivatedFirst']),
|
||||
toggleCollapse () {
|
||||
this.collapsed = !this.collapsed
|
||||
},
|
||||
onMenuSelect () {
|
||||
this.toggleCollapse()
|
||||
},
|
||||
setActivated(route) {
|
||||
if (this.layout === 'mix') {
|
||||
let matched = route.matched
|
||||
matched = matched.slice(0, matched.length - 1)
|
||||
const {firstMenu} = this
|
||||
for (let menu of firstMenu) {
|
||||
if (matched.findIndex(item => item.path === menu.fullPath) !== -1) {
|
||||
this.setActivatedFirst(menu.fullPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.correctPageMinHeight(minHeight - 1)
|
||||
this.setActivated(this.$route)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.correctPageMinHeight(-minHeight + 1)
|
||||
|
@ -6,12 +6,12 @@
|
||||
<h1 v-if="!isMobile">{{systemName}}</h1>
|
||||
</router-link>
|
||||
<a-divider v-if="isMobile" type="vertical" />
|
||||
<a-icon v-if="layout === 'side'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
|
||||
<div v-if="layout == 'head' && !isMobile" class="admin-header-menu">
|
||||
<i-menu class="head-menu" style="height: 64px; line-height: 64px;box-shadow: none" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
|
||||
<a-icon v-if="layout !== 'head'" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
|
||||
<div v-if="layout !== 'side' && !isMobile" class="admin-header-menu" :style="`width: ${menuWidth};`">
|
||||
<i-menu class="head-menu" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
|
||||
</div>
|
||||
<div :class="['admin-header-right', headerTheme]">
|
||||
<header-search class="header-item" />
|
||||
<header-search class="header-item" @active="val => searchActive = val" />
|
||||
<a-tooltip class="header-item" title="帮助文档" placement="bottom" >
|
||||
<a href="https://iczer.github.io/vue-antd-admin/" target="_blank">
|
||||
<a-icon type="question-circle-o" />
|
||||
@ -49,7 +49,8 @@ export default {
|
||||
{key: 'CN', name: '简体中文', alias: '简体'},
|
||||
{key: 'HK', name: '繁體中文', alias: '繁體'},
|
||||
{key: 'US', name: 'English', alias: 'English'}
|
||||
]
|
||||
],
|
||||
searchActive: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -63,6 +64,12 @@ export default {
|
||||
langAlias() {
|
||||
let lang = this.langList.find(item => item.key == this.lang)
|
||||
return lang.alias
|
||||
},
|
||||
menuWidth() {
|
||||
const {layout, searchActive} = this
|
||||
const headWidth = layout === 'head' ? '1236px' : '100%'
|
||||
const extraWidth = searchActive ? '564px' : '364px'
|
||||
return `calc(${headWidth} - ${extraWidth})`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -24,10 +24,12 @@ export default {
|
||||
methods: {
|
||||
enterSearchMode () {
|
||||
this.searchMode = true
|
||||
this.$emit('active', true)
|
||||
setTimeout(() => this.$refs.input.focus(), 300)
|
||||
},
|
||||
leaveSearchMode () {
|
||||
this.searchMode = false
|
||||
setTimeout(() => this.$emit('active', false), 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,12 @@
|
||||
box-shadow: @shadow-down;
|
||||
position: relative;
|
||||
background: @base-bg-color;
|
||||
.head-menu{
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
vertical-align: middle;
|
||||
box-shadow: none;
|
||||
}
|
||||
&.dark{
|
||||
background: @header-bg-color-dark;
|
||||
color: white;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {hasPermission, hasRole} from '@/utils/authority-utils'
|
||||
import {hasAuthority} from '@/utils/authority-utils'
|
||||
import {loginIgnore} from '@/router/index'
|
||||
import {checkAuthorization} from '@/utils/request'
|
||||
|
||||
@ -30,7 +30,7 @@ const authorityGuard = (to, from, next, options) => {
|
||||
const {store, message} = options
|
||||
const permissions = store.getters['account/permissions']
|
||||
const roles = store.getters['account/roles']
|
||||
if (!hasPermission(to, permissions) && !hasRole(to, roles)) {
|
||||
if (!hasAuthority(to, permissions, roles)) {
|
||||
message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`)
|
||||
next({path: '/403'})
|
||||
} else {
|
||||
@ -38,7 +38,30 @@ const authorityGuard = (to, from, next, options) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 混合导航模式下一级菜单跳转重定向
|
||||
* @param to
|
||||
* @param from
|
||||
* @param next
|
||||
* @param options
|
||||
* @returns {*}
|
||||
*/
|
||||
const redirectGuard = (to, from, next, options) => {
|
||||
const {store} = options
|
||||
if (store.state.setting.layout === 'mix') {
|
||||
const firstMenu = store.getters['setting/firstMenu']
|
||||
if (firstMenu.find(item => item.fullPath === to.fullPath)) {
|
||||
store.commit('setting/setActivatedFirst', to.fullPath)
|
||||
const subMenu = store.getters['setting/subMenu']
|
||||
if (subMenu.length > 0) {
|
||||
return next({path: subMenu[0].fullPath})
|
||||
}
|
||||
}
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
export default {
|
||||
beforeEach: [loginGuard, authorityGuard],
|
||||
beforeEach: [loginGuard, authorityGuard, redirectGuard],
|
||||
afterEach: []
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import {formatAuthority} from '@/utils/routerUtil'
|
||||
import {formatRoutes} from '@/utils/routerUtil'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
@ -25,7 +25,7 @@ const loginIgnore = {
|
||||
*/
|
||||
function initRouter(isAsync) {
|
||||
const options = isAsync ? require('./async/config.async').default : require('./config').default
|
||||
formatAuthority(options.routes)
|
||||
formatRoutes(options.routes)
|
||||
return new Router(options)
|
||||
}
|
||||
export {loginIgnore, initRouter}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import config from '@/config'
|
||||
import {ADMIN} from '@/config/default'
|
||||
import {formatFullPath} from '@/utils/i18n'
|
||||
import {filterMenu} from '@/utils/authority-utils'
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
@ -9,8 +12,37 @@ export default {
|
||||
palettes: ADMIN.palettes,
|
||||
pageMinHeight: 0,
|
||||
menuData: [],
|
||||
activatedFirst: undefined,
|
||||
...config,
|
||||
},
|
||||
getters: {
|
||||
menuData(state, getters, rootState) {
|
||||
if (state.filterMenu) {
|
||||
const {permissions, roles} = rootState.account
|
||||
filterMenu(state.menuData, permissions, roles)
|
||||
}
|
||||
return state.menuData
|
||||
},
|
||||
firstMenu(state) {
|
||||
const {menuData} = state
|
||||
if (!menuData[0].fullPath) {
|
||||
formatFullPath(menuData)
|
||||
}
|
||||
return menuData.map(item => {
|
||||
const menuItem = {...item}
|
||||
delete menuItem.children
|
||||
return menuItem
|
||||
})
|
||||
},
|
||||
subMenu(state) {
|
||||
const {menuData, activatedFirst} = state
|
||||
if (!menuData[0].fullPath) {
|
||||
formatFullPath(menuData)
|
||||
}
|
||||
const current = menuData.find(menu => menu.fullPath === activatedFirst)
|
||||
return current && current.children ? current.children : []
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setDevice (state, isMobile) {
|
||||
state.isMobile = isMobile
|
||||
@ -53,6 +85,9 @@ export default {
|
||||
},
|
||||
setPageWidth(state, pageWidth) {
|
||||
state.pageWidth = pageWidth
|
||||
},
|
||||
setActivatedFirst(state, activatedFirst) {
|
||||
state.activatedFirst = activatedFirst
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
/**
|
||||
* 判断是否有路由的权限
|
||||
* @param route 路由
|
||||
* @param authority 路由权限配置
|
||||
* @param permissions 用户权限集合
|
||||
* @returns {boolean|*}
|
||||
*/
|
||||
function hasPermission(route, permissions) {
|
||||
const authority = route.meta.authority || '*'
|
||||
function hasPermission(authority, permissions) {
|
||||
let required = '*'
|
||||
if (typeof authority === 'string') {
|
||||
required = authority
|
||||
@ -17,11 +16,10 @@ function hasPermission(route, permissions) {
|
||||
|
||||
/**
|
||||
* 判断是否有路由需要的角色
|
||||
* @param route 路由
|
||||
* @param authority 路由权限配置
|
||||
* @param roles 用户角色集合
|
||||
*/
|
||||
function hasRole(route, roles) {
|
||||
const authority = route.meta.authority || '*'
|
||||
function hasRole(authority, roles) {
|
||||
let required = undefined
|
||||
if (typeof authority === 'object') {
|
||||
required = authority.role
|
||||
@ -47,4 +45,38 @@ function hasAnyRole(required, roles) {
|
||||
}
|
||||
}
|
||||
|
||||
export {hasPermission, hasRole}
|
||||
/**
|
||||
* 路由权限校验
|
||||
* @param route 路由
|
||||
* @param permissions 用户权限集合
|
||||
* @param roles 用户角色集合
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasAuthority(route, permissions, roles) {
|
||||
const authorities = [...route.meta.pAuthorities, route.meta.authority]
|
||||
for (let authority of authorities) {
|
||||
if (!hasPermission(authority, permissions) && !hasRole(authority, roles)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权限配置过滤菜单数据
|
||||
* @param menuData
|
||||
* @param permissions
|
||||
* @param roles
|
||||
*/
|
||||
function filterMenu(menuData, permissions, roles) {
|
||||
menuData.forEach(menu => {
|
||||
if (menu.meta && menu.meta.invisible === undefined) {
|
||||
menu.meta.invisible = !hasAuthority(menu, permissions, roles)
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
filterMenu(menu.children, permissions, roles)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {filterMenu, hasAuthority}
|
||||
|
@ -73,5 +73,6 @@ function mergeI18nFromRoutes(i18n, routes) {
|
||||
|
||||
export {
|
||||
initI18n,
|
||||
mergeI18nFromRoutes
|
||||
mergeI18nFromRoutes,
|
||||
formatFullPath
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import routerMap from '@/router/async/router.map'
|
||||
import {mergeI18nFromRoutes} from '@/utils/i18n'
|
||||
import Router from 'vue-router'
|
||||
import deepMerge from 'deepmerge'
|
||||
|
||||
/**
|
||||
* 根据 路由配置 和 路由组件注册 解析路由
|
||||
@ -65,7 +66,7 @@ function loadRoutes({router, store, i18n}, routesConfig) {
|
||||
if (asyncRoutes) {
|
||||
if (routesConfig && routesConfig.length > 0) {
|
||||
const routes = parseRoutes(routesConfig, routerMap)
|
||||
formatAuthority(routes)
|
||||
formatRoutes(routes)
|
||||
const finalRoutes = mergeRoutes(router.options.routes, routes)
|
||||
router.options = {...router.options, routes: finalRoutes}
|
||||
router.matcher = new Router({...router.options, routes:[]}).matcher
|
||||
@ -96,16 +97,70 @@ function mergeRoutes(target, source) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化路由的权限配置
|
||||
* @param routes
|
||||
* 深度合并路由
|
||||
* @param target {Route[]}
|
||||
* @param source {Route[]}
|
||||
* @returns {Route[]}
|
||||
*/
|
||||
function formatAuthority(routes) {
|
||||
function deepMergeRoutes(target, source) {
|
||||
// 映射路由数组
|
||||
const mapRoutes = routes => {
|
||||
const routesMap = {}
|
||||
routes.forEach(item => {
|
||||
routesMap[item.path] = {
|
||||
...item,
|
||||
children: item.children ? mapRoutes(item.children) : undefined
|
||||
}
|
||||
})
|
||||
return routesMap
|
||||
}
|
||||
const tarMap = mapRoutes(target)
|
||||
const srcMap = mapRoutes(source)
|
||||
|
||||
// 合并路由
|
||||
const merge = deepMerge(tarMap, srcMap)
|
||||
|
||||
// 转换为 routes 数组
|
||||
const parseRoutesMap = routesMap => {
|
||||
return Object.values(routesMap).map(item => {
|
||||
if (item.children) {
|
||||
item.children = parseRoutesMap(item.children)
|
||||
} else {
|
||||
delete item.children
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
return parseRoutesMap(merge)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化路由
|
||||
* @param routes 路由配置
|
||||
*/
|
||||
function formatRoutes(routes) {
|
||||
routes.forEach(route => {
|
||||
const {path} = route
|
||||
if (!path.startsWith('/') && path !== '*') {
|
||||
route.path = '/' + path
|
||||
}
|
||||
})
|
||||
formatAuthority(routes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化路由的权限配置
|
||||
* @param routes 路由
|
||||
* @param pAuthorities 父级路由权限配置集合
|
||||
*/
|
||||
function formatAuthority(routes, pAuthorities = []) {
|
||||
routes.forEach(route => {
|
||||
const meta = route.meta
|
||||
const defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'}
|
||||
if (meta) {
|
||||
let authority = {}
|
||||
if (!meta.authority) {
|
||||
authority.permission = '*'
|
||||
authority = defaultAuthority
|
||||
}else if (typeof meta.authority === 'string') {
|
||||
authority.permission = meta.authority
|
||||
} else if (typeof meta.authority === 'object') {
|
||||
@ -114,17 +169,18 @@ function formatAuthority(routes) {
|
||||
if (typeof role === 'string') {
|
||||
authority.role = [role]
|
||||
}
|
||||
} else {
|
||||
console.log(typeof meta.authority)
|
||||
if (!authority.permission && !authority.role) {
|
||||
authority = defaultAuthority
|
||||
}
|
||||
}
|
||||
meta.authority = authority
|
||||
} else {
|
||||
route.meta = {
|
||||
authority: {permission: '*'}
|
||||
}
|
||||
const authority = defaultAuthority
|
||||
route.meta = {authority}
|
||||
}
|
||||
route.meta.pAuthorities = pAuthorities
|
||||
if (route.children) {
|
||||
formatAuthority(route.children)
|
||||
formatAuthority(route.children, [...pAuthorities, route.meta.authority])
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -160,4 +216,4 @@ function loadGuards(guards, options) {
|
||||
})
|
||||
}
|
||||
|
||||
export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards}
|
||||
export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes}
|
||||
|
Loading…
x
Reference in New Issue
Block a user