Merge branch 'master' into master

This commit is contained in:
iczer
2020-09-06 11:06:58 +08:00
committed by GitHub
20 changed files with 549 additions and 61 deletions

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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',

View File

@@ -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

View File

@@ -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)

View File

@@ -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: {

View File

@@ -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)
}
}
}

View File

@@ -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;

View File

@@ -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: []
}

View File

@@ -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}

View File

@@ -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
}
}
}

View File

@@ -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}

View File

@@ -73,5 +73,6 @@ function mergeI18nFromRoutes(i18n, routes) {
export {
initI18n,
mergeI18nFromRoutes
mergeI18nFromRoutes,
formatFullPath
}

View File

@@ -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}