From 002cf50440c8e3e5d87f934c6edfb09c52043746 Mon Sep 17 00:00:00 2001 From: iczer <1126263215@qq.com> Date: Fri, 28 Aug 2020 14:25:54 +0800 Subject: [PATCH 01/12] update docs; --- docs/advance/async.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/advance/async.md b/docs/advance/async.md index 0e2ff8a..750f6d7 100644 --- a/docs/advance/async.md +++ b/docs/advance/async.md @@ -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 From 094935b758c4de7a15de17d7f8925f6aa1d4b051 Mon Sep 17 00:00:00 2001 From: iczer <1126263215@qq.com> Date: Mon, 31 Aug 2020 12:23:56 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20add=20mixed=20navigation=20mode;?= =?UTF-8?q?=20:star:=20#102=20=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=B7=B7?= =?UTF-8?q?=E5=90=88=E5=AF=BC=E8=88=AA=E8=8F=9C=E5=8D=95=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/menu/menu.js | 12 +++------ src/components/setting/Setting.vue | 1 + src/components/setting/i18n.js | 2 ++ src/layouts/AdminLayout.vue | 41 +++++++++++++++++++++++++---- src/layouts/header/AdminHeader.vue | 17 ++++++++---- src/layouts/header/HeaderSearch.vue | 2 ++ src/layouts/header/index.less | 6 +++++ src/router/guards.js | 25 +++++++++++++++++- src/store/modules/setting.js | 26 ++++++++++++++++++ src/utils/i18n.js | 3 ++- 10 files changed, 115 insertions(+), 20 deletions(-) diff --git a/src/components/menu/menu.js b/src/components/menu/menu.js index 15d18eb..49ef98f 100644 --- a/src/components/menu/menu.js +++ b/src/components/menu/menu.js @@ -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) { diff --git a/src/components/setting/Setting.vue b/src/components/setting/Setting.vue index 93ec7f1..d4bdd4d 100644 --- a/src/components/setting/Setting.vue +++ b/src/components/setting/Setting.vue @@ -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> diff --git a/src/components/setting/i18n.js b/src/components/setting/i18n.js index 03873a7..85fb98d 100644 --- a/src/components/setting/i18n.js +++ b/src/components/setting/i18n.js @@ -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', diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index 92a9075..dd197fe 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -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']), + ...mapGetters('setting', ['firstMenu', 'subMenu']), 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) diff --git a/src/layouts/header/AdminHeader.vue b/src/layouts/header/AdminHeader.vue index b320432..a65b0ad 100644 --- a/src/layouts/header/AdminHeader.vue +++ b/src/layouts/header/AdminHeader.vue @@ -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: { diff --git a/src/layouts/header/HeaderSearch.vue b/src/layouts/header/HeaderSearch.vue index bd408cc..ec4f116 100644 --- a/src/layouts/header/HeaderSearch.vue +++ b/src/layouts/header/HeaderSearch.vue @@ -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) } } } diff --git a/src/layouts/header/index.less b/src/layouts/header/index.less index 443f030..ba90761 100644 --- a/src/layouts/header/index.less +++ b/src/layouts/header/index.less @@ -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; diff --git a/src/router/guards.js b/src/router/guards.js index b8719f9..67de64c 100644 --- a/src/router/guards.js +++ b/src/router/guards.js @@ -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: [] } diff --git a/src/store/modules/setting.js b/src/store/modules/setting.js index 7f0c22b..904c173 100644 --- a/src/store/modules/setting.js +++ b/src/store/modules/setting.js @@ -1,5 +1,6 @@ import config from '@/config' import {ADMIN} from '@/config/default' +import {formatFullPath} from '@/utils/i18n' export default { namespaced: true, state: { @@ -8,8 +9,30 @@ export default { palettes: ADMIN.palettes, pageMinHeight: 0, menuData: [], + activatedFirst: undefined, ...config, }, + getters: { + 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 : [] + } + }, mutations: { setDevice (state, isMobile) { state.isMobile = isMobile @@ -49,6 +72,9 @@ export default { }, setAsyncRoutes(state, asyncRoutes) { state.asyncRoutes = asyncRoutes + }, + setActivatedFirst(state, activatedFirst) { + state.activatedFirst = activatedFirst } } } diff --git a/src/utils/i18n.js b/src/utils/i18n.js index 50e3c36..ec64b9f 100644 --- a/src/utils/i18n.js +++ b/src/utils/i18n.js @@ -73,5 +73,6 @@ function mergeI18nFromRoutes(i18n, routes) { export { initI18n, - mergeI18nFromRoutes + mergeI18nFromRoutes, + formatFullPath } From cbda23e3dbd567165c619e2df677c7ad327a6e32 Mon Sep 17 00:00:00 2001 From: iczer <1126263215@qq.com> Date: Mon, 31 Aug 2020 12:38:49 +0800 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20style=20problem=20of=20tooltip=20i?= =?UTF-8?q?n=20setting=20pane;=20:bug:=20=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=9D=A2=E6=9D=BF=E4=B8=AD=20tooltip=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=9A=84=E6=A0=B7=E5=BC=8F=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/checkbox/ImgCheckbox.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/checkbox/ImgCheckbox.vue b/src/components/checkbox/ImgCheckbox.vue index b59dd22..ca06bdd 100644 --- a/src/components/checkbox/ImgCheckbox.vue +++ b/src/components/checkbox/ImgCheckbox.vue @@ -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"> From 1fb75f491d37d37da775a5f6de5966ecef5ed3fb Mon Sep 17 00:00:00 2001 From: iczer <1126263215@qq.com> Date: Mon, 31 Aug 2020 20:51:58 +0800 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20add=20function=20of=20filtering?= =?UTF-8?q?=20menu=20data=20through=20authority;=20:star:=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=EF=BC=9A=E5=A2=9E=E5=8A=A0=E9=80=9A=E8=BF=87=E6=9D=83?= =?UTF-8?q?=E9=99=90=E8=BF=87=E6=BB=A4=E8=8F=9C=E5=8D=95=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/default/setting.config.js | 1 + src/layouts/AdminLayout.vue | 4 ++-- src/store/modules/setting.js | 11 ++++++++++- src/utils/authority-utils.js | 17 ++++++++++++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/config/default/setting.config.js b/src/config/default/setting.config.js index 6f2c5e8..bcbf470 100644 --- a/src/config/default/setting.config.js +++ b/src/config/default/setting.config.js @@ -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 diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue index dd197fe..ad0dda8 100644 --- a/src/layouts/AdminLayout.vue +++ b/src/layouts/AdminLayout.vue @@ -56,8 +56,8 @@ export default { }, computed: { ...mapState('setting', ['isMobile', 'theme', 'layout', 'footerLinks', 'copyright', 'fixedHeader', 'fixedSideBar', - 'hideSetting', 'menuData']), - ...mapGetters('setting', ['firstMenu', 'subMenu']), + 'hideSetting']), + ...mapGetters('setting', ['firstMenu', 'subMenu', 'menuData']), sideMenuWidth() { return this.collapsed ? '80px' : '256px' }, diff --git a/src/store/modules/setting.js b/src/store/modules/setting.js index 904c173..f6d2aed 100644 --- a/src/store/modules/setting.js +++ b/src/store/modules/setting.js @@ -1,6 +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: { @@ -13,6 +15,13 @@ export default { ...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) { @@ -30,7 +39,7 @@ export default { formatFullPath(menuData) } const current = menuData.find(menu => menu.fullPath === activatedFirst) - return current ? current.children : [] + return current && current.children ? current.children : [] } }, mutations: { diff --git a/src/utils/authority-utils.js b/src/utils/authority-utils.js index 48dd2ea..71ab473 100644 --- a/src/utils/authority-utils.js +++ b/src/utils/authority-utils.js @@ -47,4 +47,19 @@ function hasAnyRole(required, roles) { } } -export {hasPermission, hasRole} +/** + * 根据权限配置过滤菜单数据 + * @param menuData + * @param permissions + * @param roles + */ +function filterMenu(menuData, permissions, roles) { + menuData.forEach(menu => { + menu.meta.invisible = !hasPermission(menu, permissions) && !hasRole(menu, roles) + if (menu.children && menu.children.length > 0) { + filterMenu(menu.children, permissions, roles) + } + }) +} + +export {hasPermission, hasRole, filterMenu} From 5ec6f73d6f8a55ff7e8355548c97253c55e8b7ba Mon Sep 17 00:00:00 2001 From: iczer <1126263215@qq.com> Date: Tue, 1 Sep 2020 19:59:50 +0800 Subject: [PATCH 05/12] =?UTF-8?q?fix:=20problem=20tha=20the=20authority=20?= =?UTF-8?q?config=20of=20route=20be=20overwritten;=20:bug:=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=9A=E8=B7=AF=E7=94=B1=E6=9D=83=E9=99=90=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=A2=AB=E8=A6=86=E7=9B=96=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/authority-utils.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/authority-utils.js b/src/utils/authority-utils.js index 71ab473..e421e12 100644 --- a/src/utils/authority-utils.js +++ b/src/utils/authority-utils.js @@ -55,9 +55,11 @@ function hasAnyRole(required, roles) { */ function filterMenu(menuData, permissions, roles) { menuData.forEach(menu => { - menu.meta.invisible = !hasPermission(menu, permissions) && !hasRole(menu, roles) - if (menu.children && menu.children.length > 0) { - filterMenu(menu.children, permissions, roles) + if (menu.meta && menu.meta.invisible === undefined) { + menu.meta.invisible = !hasPermission(menu, permissions) && !hasRole(menu, roles) + if (menu.children && menu.children.length > 0) { + filterMenu(menu.children, permissions, roles) + } } }) } From 4db7c2deff11864dcc5a4a5e1752d98419b4cdb3 Mon Sep 17 00:00:00 2001 From: iczer <1126263215@qq.com> Date: Wed, 2 Sep 2020 18:55:28 +0800 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20problem=20that=20child=20route=20c?= =?UTF-8?q?an't=20inherit=20authority=20configuration=20of=20it's=20parent?= =?UTF-8?q?;=20:bug:=20=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=AD=90=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E6=97=A0=E6=B3=95=E7=BB=A7=E6=89=BF=E5=85=B6=E7=88=B6?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E7=9A=84=E6=9D=83=E9=99=90=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/guards.js | 4 ++-- src/utils/authority-utils.js | 31 +++++++++++++++++++++++-------- src/utils/routerUtil.js | 18 +++++++++++------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/router/guards.js b/src/router/guards.js index 67de64c..4646765 100644 --- a/src/router/guards.js +++ b/src/router/guards.js @@ -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 { diff --git a/src/utils/authority-utils.js b/src/utils/authority-utils.js index e421e12..a08c474 100644 --- a/src/utils/authority-utils.js +++ b/src/utils/authority-utils.js @@ -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,6 +45,23 @@ function hasAnyRole(required, roles) { } } +/** + * 路由权限校验 + * @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 @@ -56,7 +71,7 @@ function hasAnyRole(required, roles) { function filterMenu(menuData, permissions, roles) { menuData.forEach(menu => { if (menu.meta && menu.meta.invisible === undefined) { - menu.meta.invisible = !hasPermission(menu, permissions) && !hasRole(menu, roles) + menu.meta.invisible = !hasAuthority(menu, permissions, roles) if (menu.children && menu.children.length > 0) { filterMenu(menu.children, permissions, roles) } @@ -64,4 +79,4 @@ function filterMenu(menuData, permissions, roles) { }) } -export {hasPermission, hasRole, filterMenu} +export {filterMenu, hasAuthority} diff --git a/src/utils/routerUtil.js b/src/utils/routerUtil.js index 7fc9160..68416ea 100644 --- a/src/utils/routerUtil.js +++ b/src/utils/routerUtil.js @@ -97,15 +97,16 @@ function mergeRoutes(target, source) { /** * 格式化路由的权限配置 - * @param routes + * @param routes 路由 + * @param pAuthorities 父级路由权限配置集合 */ -function formatAuthority(routes) { +function formatAuthority(routes, pAuthorities = []) { routes.forEach(route => { const meta = route.meta if (meta) { let authority = {} if (!meta.authority) { - authority.permission = '*' + authority = pAuthorities.length > 0 ? pAuthorities[pAuthorities.length - 1] : {permission: '*'} }else if (typeof meta.authority === 'string') { authority.permission = meta.authority } else if (typeof meta.authority === 'object') { @@ -114,17 +115,20 @@ function formatAuthority(routes) { if (typeof role === 'string') { authority.role = [role] } + if (!authority.permission && !authority.role) { + authority = pAuthorities.length > 0 ? pAuthorities[pAuthorities.length - 1] : {permission: '*'} + } } else { console.log(typeof meta.authority) } meta.authority = authority } else { - route.meta = { - authority: {permission: '*'} - } + const authority = pAuthorities.length > 0 ? pAuthorities[pAuthorities.length - 1] : {permission: '*'} + route.meta = {authority} } + route.meta.pAuthorities = pAuthorities if (route.children) { - formatAuthority(route.children) + formatAuthority(route.children, [...pAuthorities, route.meta.authority]) } }) } From 8102c8a9242ee176571a2c551ff8d1d5dd100781 Mon Sep 17 00:00:00 2001 From: chenghongxing <1126263215@qq.com> Date: Thu, 3 Sep 2020 18:24:28 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20add=20deep=20merge=20routes=20fun?= =?UTF-8?q?ction=20for=20routerUtil.js;=20:star:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=EF=BC=9ArouterUtil.js=20=E5=B7=A5=E5=85=B7=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E5=90=88=E5=B9=B6=E8=B7=AF=E7=94=B1=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/routerUtil.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/utils/routerUtil.js b/src/utils/routerUtil.js index 68416ea..dfab740 100644 --- a/src/utils/routerUtil.js +++ b/src/utils/routerUtil.js @@ -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' /** * 根据 路由配置 和 路由组件注册 解析路由 @@ -95,6 +96,44 @@ function mergeRoutes(target, source) { return Object.values(routesMap) } +/** + * 深度合并路由 + * @param target {Route[]} + * @param source {Route[]} + * @returns {Route[]} + */ +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 路由 @@ -164,4 +203,4 @@ function loadGuards(guards, options) { }) } -export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards} +export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes} From 4856f06f41563b8fa2d12aaa1247d8925eb43aa3 Mon Sep 17 00:00:00 2001 From: chenghongxing <1126263215@qq.com> Date: Thu, 3 Sep 2020 18:47:03 +0800 Subject: [PATCH 08/12] =?UTF-8?q?fix:=20configuration=20problem=20of=20fir?= =?UTF-8?q?st=20route's=20path;=20:bug:=20=E4=BF=AE=E5=A4=8D=EF=BC=9A?= =?UTF-8?q?=E4=B8=80=E7=BA=A7=E8=B7=AF=E7=94=B1=20path=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=97=AE=E9=A2=98=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/index.js | 4 ++-- src/utils/routerUtil.js | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/router/index.js b/src/router/index.js index 3bdcc3c..c5aa144 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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} diff --git a/src/utils/routerUtil.js b/src/utils/routerUtil.js index dfab740..34a8941 100644 --- a/src/utils/routerUtil.js +++ b/src/utils/routerUtil.js @@ -66,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 @@ -134,6 +134,20 @@ function deepMergeRoutes(target, source) { 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 路由 @@ -203,4 +217,4 @@ function loadGuards(guards, options) { }) } -export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes} +export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes} From 69b514ee10ff2118dc0089bad560db1910fcd960 Mon Sep 17 00:00:00 2001 From: chenghongxing <1126263215@qq.com> Date: Thu, 3 Sep 2020 19:01:16 +0800 Subject: [PATCH 09/12] chore: optimize the code of routerUtil.js --- src/utils/routerUtil.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/utils/routerUtil.js b/src/utils/routerUtil.js index 34a8941..ce573c7 100644 --- a/src/utils/routerUtil.js +++ b/src/utils/routerUtil.js @@ -156,10 +156,11 @@ function formatRoutes(routes) { 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 = pAuthorities.length > 0 ? pAuthorities[pAuthorities.length - 1] : {permission: '*'} + authority = defaultAuthority }else if (typeof meta.authority === 'string') { authority.permission = meta.authority } else if (typeof meta.authority === 'object') { @@ -169,14 +170,12 @@ function formatAuthority(routes, pAuthorities = []) { authority.role = [role] } if (!authority.permission && !authority.role) { - authority = pAuthorities.length > 0 ? pAuthorities[pAuthorities.length - 1] : {permission: '*'} + authority = defaultAuthority } - } else { - console.log(typeof meta.authority) } meta.authority = authority } else { - const authority = pAuthorities.length > 0 ? pAuthorities[pAuthorities.length - 1] : {permission: '*'} + const authority = defaultAuthority route.meta = {authority} } route.meta.pAuthorities = pAuthorities From 8a7b82ac500502b01a40b5997c02282c1151bc60 Mon Sep 17 00:00:00 2001 From: chenghongxing <1126263215@qq.com> Date: Sat, 5 Sep 2020 12:36:37 +0800 Subject: [PATCH 10/12] update docs --- docs/advance/login.md | 71 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/advance/login.md b/docs/advance/login.md index e0ae3e9..104caa1 100644 --- a/docs/advance/login.md +++ b/docs/advance/login.md @@ -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 文件中找到。 +::: \ No newline at end of file From 8b38e77922027e9e873c727c4b2b970066bc3fe3 Mon Sep 17 00:00:00 2001 From: chenghongxing <1126263215@qq.com> Date: Sat, 5 Sep 2020 21:07:00 +0800 Subject: [PATCH 11/12] update docs --- docs/.vuepress/config.js | 2 +- docs/advance/guard.md | 108 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 7a9d3f1..156ab7d 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -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' ] }, { diff --git a/docs/advance/guard.md b/docs/advance/guard.md index a43508c..274abfe 100644 --- a/docs/advance/guard.md +++ b/docs/advance/guard.md @@ -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] +} +``` +::: \ No newline at end of file From fd13c7d0427c76bfbbdde27fba38daf89bb31b6a Mon Sep 17 00:00:00 2001 From: chenghongxing <1126263215@qq.com> Date: Sat, 5 Sep 2020 21:56:17 +0800 Subject: [PATCH 12/12] update docs --- docs/advance/interceptors.md | 126 ++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/docs/advance/interceptors.md b/docs/advance/interceptors.md index 843bc6a..f34fc94 100644 --- a/docs/advance/interceptors.md +++ b/docs/advance/interceptors.md @@ -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] // 响应拦截 +} +``` +::: \ No newline at end of file