增加了侧边栏搜索功能

NavMenu使用有状态组件写法

修复了一些样式问题

代码优化
master
toesbieya 6 years ago
parent 67b5a534fc
commit ee86939942
  1. 16
      vue/src/asset/style/variables.scss
  2. 142
      vue/src/component/menu/NavMenu/index.vue
  3. 47
      vue/src/component/menu/NavMenu/item.vue
  4. 9
      vue/src/component/menu/NavMenu/style.scss
  5. 56
      vue/src/layout/component/Aside/component/MenuSearch.vue
  6. 42
      vue/src/layout/component/Aside/component/OnePartRoot.vue
  7. 15
      vue/src/layout/component/Aside/component/TwoPartSub.vue
  8. 4
      vue/src/layout/component/Aside/style.scss
  9. 3
      vue/src/layout/component/Aside/theme-dark.scss
  10. 2
      vue/src/layout/component/Logo.vue
  11. 3
      vue/src/layout/component/Navbar/index.vue
  12. 6
      vue/src/layout/component/Navbar/style.scss
  13. 4
      vue/src/layout/component/Navbar/theme-dark.scss
  14. 4
      vue/src/layout/component/Navbar/theme-light.scss
  15. 50
      vue/src/layout/component/SettingDrawer.vue
  16. 3
      vue/src/layout/mixin/hamburger.js
  17. 22
      vue/src/layout/mixin/menu.js
  18. 24
      vue/src/layout/mixin/menuSearch.js
  19. 30
      vue/src/layout/store/app.js
  20. 5
      vue/src/layout/store/aside.js
  21. 39
      vue/src/store/module/resource.js
  22. 12
      vue/src/util/auth.js
  23. 5
      vue/src/util/index.js
  24. 29
      vue/src/util/tree.js

@ -78,10 +78,20 @@ $aside-width: 208px;
//侧边栏折叠时的宽度 //侧边栏折叠时的宽度
$aside-collapse-width: $menu-icon-size + $menu-padding * 2; $aside-collapse-width: $menu-icon-size + $menu-padding * 2;
//头部导航栏高度 //导航栏高度
$nav-height: 48px; $nav-height: 48px;
//导航栏的padding-left
//头部多页签栏高度 $navbar-padding-left: 12px;
//亮色导航栏的功能项颜色
$navbar-item-light-color: $menu-text-light-color;
//暗色导航栏的功能项颜色
$navbar-item-dark-color: rgba(255, 255, 255, .85);
//亮色导航栏的功能项hover颜色
$navbar-item-hover-light-color: rgba(255, 255, 255, .85);
//暗色导航栏的功能项hover颜色
$navbar-item-hover-dark-color: #0f2438;
//多页签栏高度
$tags-view-height: 32px; $tags-view-height: 32px;
//页头行高 //页头行高

@ -1,9 +1,12 @@
<script type="text/jsx"> <script type="text/jsx">
import cssVariables from '@/asset/style/variables.scss' import cssVariables from '@/asset/style/variables.scss'
import Item from './item' import Item from './item'
import {isEmpty, deepClone, trim} from "@/util"
const inlineIndent = parseFloat(cssVariables.menuPadding)
export default { export default {
functional: true, name: 'NavMenu',
props: { props: {
// //
@ -21,6 +24,9 @@ export default {
// //
showParentOnCollapse: Boolean, showParentOnCollapse: Boolean,
//
searchWord: String,
// <0 // <0
showIconMaxDepth: {type: Number, default: 1}, showIconMaxDepth: {type: Number, default: 1},
@ -31,41 +37,133 @@ export default {
switchTransitionName: String switchTransitionName: String
}, },
render(h, context) { data() {
const { return {
data, children, //使computed
props: { realSearchWord: ''
mode, menus, theme, collapse, showParentOnCollapse, }
showIconMaxDepth, switchTransition, switchTransitionName },
computed: {
//
realMenus() {
return isEmpty(this.realSearchWord)
? this.menus
: this.filterAfterSearch(deepClone(this.menus))
},
//使
useSwitchTransition() {
return this.switchTransition && !isEmpty(this.switchTransitionName)
},
themeClass() {
return `el-menu--${this.theme}`
},
menuClass() {
return `el-menu--${this.mode} ${this.themeClass}`
}
},
watch: {
//
searchWord(v) {
this.realSearchWord = trim(v)
},
//
realSearchWord() {
this.$nextTick(this.expandAfterSearch)
}
},
methods: {
//
filterAfterSearch(menus) {
if (!menus) return undefined
return menus.filter(menu => {
//
if (menu.meta.title.includes(this.realSearchWord)) {
return true
} }
} = context
const themeClassName = `el-menu--${theme}` const children = this.filterAfterSearch(menu.children)
const inlineIndent = parseFloat(cssVariables.menuPadding)
if (children) menu.children = children
return children && children.length > 0
})
},
//
expandAfterSearch() {
const menu = this.$refs['el-menu']
//
if (isEmpty(this.realSearchWord)) {
menu.openedMenus = []
return this.$nextTick(menu.initOpenedMenu)
}
const {openedMenus, submenus} = menu
const expandMenus = [...new Set(this.getHighlightMenuParent(this.realMenus))]
//el-menuopenuniqueOpened
for (const {fullPath} of expandMenus) {
const sub = submenus[fullPath]
if (!sub || openedMenus.includes(sub.index)) continue
sub.indexPath.forEach(i => {
!openedMenus.includes(i) && openedMenus.push(i)
})
}
},
//
getHighlightMenuParent(children, parent) {
const result = []
children.forEach(child => {
if (child.meta.title.includes(this.realSearchWord)) {
parent && result.push(parent)
}
if (child.children) {
result.push(...this.getHighlightMenuParent(child.children, child))
}
})
return result
}
},
let items = children || menus.map(m => ( render() {
let items = this.realMenus.map(m => (
<Item <Item
menu={m} menu={m}
popper-class={themeClassName} popper-class={this.themeClass}
show-parent={showParentOnCollapse} highlight={this.realSearchWord}
collapse={collapse} show-parent={this.showParentOnCollapse}
collapse={this.collapse}
inline-indent={inlineIndent} inline-indent={inlineIndent}
show-icon-max-depth={showIconMaxDepth} show-icon-max-depth={this.showIconMaxDepth}
/> />
)) ))
if (switchTransition && switchTransitionName) { if (this.useSwitchTransition) {
items = <transition-group name={switchTransitionName}>{items}</transition-group> items = <transition-group name={this.switchTransitionName}>{items}</transition-group>
} }
return ( return (
<el-menu <el-menu
ref="menu" ref="el-menu"
class={`el-menu--${mode} ${themeClassName}`} class={this.menuClass}
mode={mode} mode={this.mode}
collapse={collapse} collapse={this.collapse}
collapse-transition={false} collapse-transition={false}
{...data} {...{attrs: this.$attrs, on: this.$listeners}}
> >
{items} {items}
</el-menu> </el-menu>

@ -1,16 +1,40 @@
<script type="text/jsx"> <script type="text/jsx">
import {isEmpty} from "@/util"
const MenuItemContent = { const MenuItemContent = {
functional: true, functional: true,
props: {icon: String, title: String}, props: {
icon: String,
title: String,
highlight: String
},
render(h, context) { render(h, context) {
const {icon, title} = context.props const {icon, title, highlight} = context.props
const content = isEmpty(highlight)
? title
: getHighlightContent(h, title, highlight)
return [<v-icon icon={icon}/>, <span slot="title" class="menu-item-content">{title}</span>] return [<v-icon icon={icon}/>, <span slot="title" class="menu-item-content">{content}</span>]
} }
} }
//vnode
function getHighlightContent(h, title, highlight) {
const start = title.indexOf(highlight)
if (start === -1) return title
const end = start + highlight.length
return [
title.substring(0, start),
<span class="menu-highlight-result">{title.substring(start, end)}</span>,
title.substring(end)
]
}
//showIconMaxDepthdepth //showIconMaxDepthdepth
function getIcon({icon, showIconMaxDepth, depth}) { function getIcon({icon, showIconMaxDepth, depth}) {
if (showIconMaxDepth == null || showIconMaxDepth < 0) { if (showIconMaxDepth == null || showIconMaxDepth < 0) {
@ -30,18 +54,18 @@ function getOnlyChild(menu) {
return null return null
} }
function renderSingleMenu(h, {index, inlineIndent, icon, title}) { function renderSingleMenu(h, {index, inlineIndent, icon, title, highlight}) {
return ( return (
<el-menu-item key={index} index={index} inline-indent={inlineIndent}> <el-menu-item key={index} index={index} inline-indent={inlineIndent}>
<MenuItemContent icon={icon} title={title}/> <MenuItemContent icon={icon} title={title} highlight={highlight}/>
</el-menu-item> </el-menu-item>
) )
} }
function renderSubMenu(h, {index, inlineIndent, icon, title, popperClass, children}) { function renderSubMenu(h, {index, inlineIndent, icon, title, popperClass, highlight, children}) {
return ( return (
<el-submenu key={index} index={index} inline-indent={inlineIndent} popper-class={popperClass}> <el-submenu key={index} index={index} inline-indent={inlineIndent} popper-class={popperClass}>
<MenuItemContent slot="title" icon={icon} title={title}/> <MenuItemContent slot="title" icon={icon} title={title} highlight={highlight}/>
{children} {children}
</el-submenu> </el-submenu>
) )
@ -57,7 +81,7 @@ function renderChildrenWithParentMenu(h, {icon, title, children}) {
} }
function renderMenu(h, props) { function renderMenu(h, props) {
const {menu, inlineIndent, popperClass, showParent, collapse, showIconMaxDepth, depth = 1} = props const {menu, inlineIndent, popperClass, highlight, showParent, collapse, showIconMaxDepth, depth = 1} = props
const onlyOneChild = getOnlyChild(menu) const onlyOneChild = getOnlyChild(menu)
@ -69,7 +93,8 @@ function renderMenu(h, props) {
index: fullPath, index: fullPath,
inlineIndent, inlineIndent,
icon: getIcon({icon, showIconMaxDepth, depth}), icon: getIcon({icon, showIconMaxDepth, depth}),
title title,
highlight
}) })
} }
@ -91,6 +116,7 @@ function renderMenu(h, props) {
icon: getIcon({icon, showIconMaxDepth, depth}), icon: getIcon({icon, showIconMaxDepth, depth}),
title, title,
popperClass, popperClass,
highlight,
children children
}) })
} }
@ -105,6 +131,9 @@ export default {
// //
popperClass: String, popperClass: String,
//
highlight: String,
// //
showParent: Boolean, showParent: Boolean,

@ -1,14 +1,21 @@
@import "~@/asset/style/variables.scss"; @import "~@/asset/style/variables.scss";
//设置菜单图标的大小和与文字的间隔
.el-menu { .el-menu {
//图标的大小
.icon { .icon {
font-size: $menu-icon-size; font-size: $menu-icon-size;
} }
//图标与文字的间隔
.icon + .menu-item-content { .icon + .menu-item-content {
margin-left: $menu-icon-text-gap; margin-left: $menu-icon-text-gap;
} }
//菜单的高亮关键字
.menu-highlight-result {
vertical-align: unset;
color: $--color-success;
}
} }
//原始样式使用 '>' 可能导致侧边栏折叠时还会显示右侧箭头图标 //原始样式使用 '>' 可能导致侧边栏折叠时还会显示右侧箭头图标

@ -0,0 +1,56 @@
<template>
<div class="aside-search">
<el-input
v-model="value"
size="mini"
clearable
placeholder="搜索菜单"
prefix-icon="el-icon-search"
@input="search"
/>
</div>
</template>
<script>
import {debounce} from "@/util"
export default {
name: "MenuSearch",
data: () => ({value: ''}),
methods: {
search(v) {
this.$emit('search', v)
}
},
created() {
this.search = debounce(this.search)
},
beforeDestroy() {
this.$emit('search', '')
}
}
</script>
<style lang="scss">
@import "~@/asset/style/variables.scss";
//
.collapse .aside-search {
display: none;
}
.aside-search {
margin: 12px 8px;
}
//
.dark .aside-search .el-input__inner {
border: none;
color: #ffffff;
background-color: lighten($menu-dark-background, 5%);
}
</style>

@ -1,10 +1,10 @@
<script type="text/jsx"> <script type="text/jsx">
import hamburgerMixin from '@/layout/mixin/hamburger' import hamburgerMixin from '@/layout/mixin/hamburger'
import menuMixin from "@/layout/mixin/menu" import menuMixin from "@/layout/mixin/menu"
import menuSearchMixin from '@/layout/mixin/menuSearch'
import {getters as appGetters} from "@/layout/store/app" import {getters as appGetters} from "@/layout/store/app"
import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside" import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside"
import {getters as pageGetters} from "@/layout/store/page" import {getters as pageGetters} from "@/layout/store/page"
import Hamburger from '@/layout/component/Hamburger'
import Logo from '@/layout/component/Logo' import Logo from '@/layout/component/Logo'
import NavMenu from '@/component/menu/NavMenu' import NavMenu from '@/component/menu/NavMenu'
import {getSidebarMenus, getActiveMenuByRoute} from "@/layout/util" import {getSidebarMenus, getActiveMenuByRoute} from "@/layout/util"
@ -13,9 +13,9 @@ import {moveToActiveMenuVertically} from "@/util/element-ui/elMenu"
export default { export default {
name: "Sidebar", name: "Sidebar",
mixins: [hamburgerMixin, menuMixin], mixins: [hamburgerMixin, menuMixin, menuSearchMixin],
components: {Hamburger, Logo, NavMenu}, components: {Logo, NavMenu},
data() { data() {
return { return {
@ -30,7 +30,7 @@ export default {
// //
menus: () => getSidebarMenus(), menus: () => getSidebarMenus(),
// //
renderInDrawer() { renderInDrawer() {
return this.isMobile || asideGetters.autoHide return this.isMobile || asideGetters.autoHide
}, },
@ -89,6 +89,11 @@ export default {
return !this.isMobile && appGetters.navMode !== 'aside' return !this.isMobile && appGetters.navMode !== 'aside'
}, },
//
renderMenuSearch() {
return !this.isMobile && asideGetters.search
},
className() { className() {
return {'sidebar': true, 'collapse': this.collapse} return {'sidebar': true, 'collapse': this.collapse}
} }
@ -103,7 +108,7 @@ export default {
this.activeMenu = getActiveMenuByRoute(this.$route) this.activeMenu = getActiveMenuByRoute(this.$route)
const menu = this.$refs.menu const menu = this.$_getElMenuInstance()
if (!menu) return if (!menu) return
const item = menu.items[this.activeMenu] const item = menu.items[this.activeMenu]
@ -154,7 +159,7 @@ export default {
onSelect(index, indexPath, item, jump = true) { onSelect(index, indexPath, item, jump = true) {
// //
if (asideGetters.uniqueOpen && indexPath.length === 1) { if (asideGetters.uniqueOpen && indexPath.length === 1) {
const menu = this.$refs.menu const menu = this.$_getElMenuInstance()
const opened = menu.openedMenus const opened = menu.openedMenus
opened.forEach(i => i !== index && menu.closeMenu(i)) opened.forEach(i => i !== index && menu.closeMenu(i))
} }
@ -168,9 +173,25 @@ export default {
} }
}, },
//el-menu
watchOpenedMenus() {
//
if (this.watchOpenedMenusCallback) {
this.watchOpenedMenusCallback()
this.watchOpenedMenusCallback = null
}
const menu = this.$_getElMenuInstance()
if (!menu) return
this.watchOpenedMenusCallback = menu.$watch('openedMenus', v => {
this.openedMenuNum = v.length
})
},
// //
moveToCurrentMenu() { moveToCurrentMenu() {
moveToActiveMenuVertically(this.$refs.menu) moveToActiveMenuVertically(this.$_getElMenuInstance())
} }
}, },
@ -188,7 +209,11 @@ export default {
on-mouseenter={() => this.mouseOutside = false} on-mouseenter={() => this.mouseOutside = false}
> >
{this.showLogo && <logo show-title={!this.collapse}/>} {this.showLogo && <logo show-title={!this.collapse}/>}
{this.renderMenuSearch && <menu-search on-search={this.handlerSearch}/>}
<nav-menu <nav-menu
ref="nav-menu"
menus={this.menus} menus={this.menus}
theme={asideGetters.theme} theme={asideGetters.theme}
collapse={this.collapse} collapse={this.collapse}
@ -199,7 +224,8 @@ export default {
switch-transition-name="sidebar" switch-transition-name="sidebar"
on={{'select': this.onSelect, 'hook:mounted': this.watchOpenedMenus}} on={{'select': this.onSelect, 'hook:mounted': this.watchOpenedMenus}}
/> />
{this.renderHamburger && <hamburger class="el-menu-item"/>}
{!this.renderInDrawer && this.renderHamburger && <hamburger class="el-menu-item"/>}
</div> </div>
) )

@ -1,5 +1,7 @@
<script type="text/jsx"> <script type="text/jsx">
import hamburgerMixin from '@/layout/mixin/hamburger'
import menuMixin from "@/layout/mixin/menu" import menuMixin from "@/layout/mixin/menu"
import menuSearchMixin from '@/layout/mixin/menuSearch'
import {getters as appGetters} from "@/layout/store/app" import {getters as appGetters} from "@/layout/store/app"
import {getters as asideGetters} from "@/layout/store/aside" import {getters as asideGetters} from "@/layout/store/aside"
import NavMenu from "@/component/menu/NavMenu" import NavMenu from "@/component/menu/NavMenu"
@ -9,7 +11,7 @@ import {moveToActiveMenuVertically} from "@/util/element-ui/elMenu"
export default { export default {
name: "SubSidebar", name: "SubSidebar",
mixins: [menuMixin], mixins: [hamburgerMixin, menuMixin, menuSearchMixin],
components: {NavMenu}, components: {NavMenu},
@ -42,7 +44,7 @@ export default {
this.activeMenu = getActiveMenuByRoute(this.$route) this.activeMenu = getActiveMenuByRoute(this.$route)
const menu = this.$refs.menu const menu = this.$_getElMenuInstance()
if (!menu) return if (!menu) return
const item = menu.items[this.activeMenu] const item = menu.items[this.activeMenu]
@ -63,7 +65,7 @@ export default {
onSelect(index, indexPath, item, jump = true) { onSelect(index, indexPath, item, jump = true) {
// //
if (asideGetters.uniqueOpen && indexPath.length === 1) { if (asideGetters.uniqueOpen && indexPath.length === 1) {
const menu = this.$refs.menu const menu = this.$_getElMenuInstance()
const opened = menu.openedMenus const opened = menu.openedMenus
opened.forEach(i => i !== index && menu.closeMenu(i)) opened.forEach(i => i !== index && menu.closeMenu(i))
} }
@ -73,7 +75,7 @@ export default {
// //
moveToCurrentMenu() { moveToCurrentMenu() {
moveToActiveMenuVertically(this.$refs.menu) moveToActiveMenuVertically(this.$_getElMenuInstance())
} }
}, },
@ -85,6 +87,9 @@ export default {
<div class="sub-sidebar-title"> <div class="sub-sidebar-title">
{this.rootMenu && this.rootMenu.meta.title} {this.rootMenu && this.rootMenu.meta.title}
</div> </div>
{asideGetters.search && <menu-search on-search={this.handlerSearch}/>}
<nav-menu <nav-menu
menus={this.menus} menus={this.menus}
theme={asideGetters.theme} theme={asideGetters.theme}
@ -96,6 +101,8 @@ export default {
switch-transition-name="sidebar" switch-transition-name="sidebar"
on-select={this.onSelect} on-select={this.onSelect}
/> />
{this.renderHamburger && <hamburger class="el-menu-item"/>}
</div> </div>
) )
} }

@ -4,9 +4,9 @@
display: flex; display: flex;
z-index: 11; z-index: 11;
//移动端下侧边栏抽屉不能设置z-index否则会被modal遮住 //移动端下侧边栏抽屉不能设置z-index否则会被el-drawer的遮罩遮住
@include mobile { @include mobile {
z-index: auto; z-index: auto !important;
} }
//单层和双层侧边栏 //单层和双层侧边栏

@ -1,5 +1,8 @@
.aside.dark { .aside.dark {
&,
> .el-drawer__wrapper .el-drawer__body {
background-color: $menu-dark-background; background-color: $menu-dark-background;
}
.root-sidebar, .root-sidebar,
.root-sidebar > .el-menu--vertical.el-menu { .root-sidebar > .el-menu--vertical.el-menu {

@ -34,7 +34,7 @@ $logoSize: 30px;
width: 100%; width: 100%;
height: $nav-height; height: $nav-height;
line-height: $nav-height; line-height: $nav-height;
padding-left: $menu-padding; padding: 0 $menu-padding;
.logo-link { .logo-link {
display: block; display: block;

@ -3,7 +3,6 @@ import {mapState} from 'vuex'
import guideMixin from '@/mixin/guide' import guideMixin from '@/mixin/guide'
import hamburgerMixin from '@/layout/mixin/hamburger' import hamburgerMixin from '@/layout/mixin/hamburger'
import Bell from "./component/Bell" import Bell from "./component/Bell"
import Hamburger from '@/layout/component/Hamburger'
import HeadMenu from "./component/HeadMenu" import HeadMenu from "./component/HeadMenu"
import Logo from "@/layout/component/Logo" import Logo from "@/layout/component/Logo"
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" import {getters as appGetters, mutations as appMutations} from "@/layout/store/app"
@ -17,7 +16,7 @@ export default {
mixins: [guideMixin.navbar, hamburgerMixin], mixins: [guideMixin.navbar, hamburgerMixin],
components: {Bell, Hamburger, HeadMenu, Logo}, components: {Bell, HeadMenu, Logo},
computed: { computed: {
...mapState('user', ['avatar', 'name', 'prepareLogout']), ...mapState('user', ['avatar', 'name', 'prepareLogout']),

@ -6,7 +6,7 @@
align-items: center; align-items: center;
flex-wrap: nowrap; flex-wrap: nowrap;
height: $nav-height; height: $nav-height;
padding-left: 12px; padding-left: $navbar-padding-left;
z-index: 10; z-index: 10;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08); box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
transition: all .2s; transition: all .2s;
@ -30,8 +30,8 @@
.logo-container { .logo-container {
width: auto; width: auto;
min-width: 192px; min-width: $aside-width - $navbar-padding-left;
padding-left: 8px; padding-left: $menu-padding - $navbar-padding-left;
} }
.avatar-wrapper { .avatar-wrapper {

@ -2,10 +2,10 @@
background-color: $root-menu-dark-background; background-color: $root-menu-dark-background;
.navbar-item { .navbar-item {
color: rgba(255, 255, 255, .85); color: $navbar-item-dark-color;
&:hover { &:hover {
background: $--color-primary; background: $navbar-item-hover-dark-color;
} }
} }
} }

@ -2,10 +2,10 @@
background-color: #ffffff; background-color: #ffffff;
.navbar-item { .navbar-item {
color: $menu-text-light-color; color: $navbar-item-light-color;
&:hover { &:hover {
background: rgba(0, 0, 0, .025); background: $navbar-item-hover-light-color;
} }
} }
} }

@ -28,10 +28,6 @@
</div> </div>
<el-divider>页面设置</el-divider> <el-divider>页面设置</el-divider>
<div class="drawer-item">
<span>显示logo</span>
<el-switch :value="pageGetters.showLogo" @input="changePageShowLogo"/>
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>分层结构</span> <span>分层结构</span>
<el-select <el-select
@ -44,6 +40,10 @@
<el-option value="left-right" label="左右"/> <el-option value="left-right" label="左右"/>
</el-select> </el-select>
</div> </div>
<div class="drawer-item">
<span>显示logo</span>
<el-switch :value="pageGetters.showLogo" @input="changePageShowLogo"/>
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>显示页头</span> <span>显示页头</span>
<el-switch :value="pageGetters.showPageHeader" @input="changePageShowHeader"/> <el-switch :value="pageGetters.showPageHeader" @input="changePageShowHeader"/>
@ -66,6 +66,18 @@
<el-option value="dark" label="暗色"/> <el-option value="dark" label="暗色"/>
</el-select> </el-select>
</div> </div>
<div class="drawer-item">
<span>汉堡包位置</span>
<el-select
:value="asideGetters.hamburgerPosition"
size="mini"
style="width: 90px"
@input="changeAsideHamburgerPosition"
>
<el-option value="aside" label="侧边栏"/>
<el-option value="head" label="导航栏"/>
</el-select>
</div>
<div class="drawer-item"> <div class="drawer-item">
<span>手风琴效果</span> <span>手风琴效果</span>
<el-switch :value="asideGetters.uniqueOpen" @input="changeAsideUniqueOpen"/> <el-switch :value="asideGetters.uniqueOpen" @input="changeAsideUniqueOpen"/>
@ -83,16 +95,8 @@
<el-switch :value="asideGetters.autoHide" @input="changeAsideAutoHide"/> <el-switch :value="asideGetters.autoHide" @input="changeAsideAutoHide"/>
</div> </div>
<div class="drawer-item"> <div class="drawer-item">
<span>汉堡包位置</span> <span>显示搜索框</span>
<el-select <el-switch :value="asideGetters.search" @input="changeAsideSearch"/>
:value="asideGetters.hamburgerPosition"
size="mini"
style="width: 90px"
@input="changeAsideHamburgerPosition"
>
<el-option value="aside" label="侧边栏"/>
<el-option value="head" label="导航栏"/>
</el-select>
</div> </div>
<el-divider>导航栏设置</el-divider> <el-divider>导航栏设置</el-divider>
@ -181,18 +185,19 @@ export default {
navMode: 'mix' navMode: 'mix'
}, },
page: { page: {
showLogo: true,
position: 'left-right', position: 'left-right',
showLogo: true,
showPageHeader: true, showPageHeader: true,
showBackToTop: true showBackToTop: true
}, },
aside: { aside: {
theme: 'light', theme: 'light',
hamburgerPosition: 'aside',
uniqueOpen: true, uniqueOpen: true,
collapse: false, collapse: false,
showParentOnCollapse: false, showParentOnCollapse: false,
autoHide: false, autoHide: false,
hamburgerPosition: 'aside' search: true
}, },
navbar: { navbar: {
theme: 'light' theme: 'light'
@ -241,12 +246,12 @@ export default {
appMutations.navMode(v) appMutations.navMode(v)
}, },
changePageShowLogo(v) {
pageMutations.showLogo(v)
},
changePagePosition(v) { changePagePosition(v) {
pageMutations.position(v) pageMutations.position(v)
}, },
changePageShowLogo(v) {
pageMutations.showLogo(v)
},
changePageShowHeader(v) { changePageShowHeader(v) {
pageMutations.showPageHeader(v) pageMutations.showPageHeader(v)
}, },
@ -257,6 +262,9 @@ export default {
changeAsideTheme(v) { changeAsideTheme(v) {
asideMutations.theme(v) asideMutations.theme(v)
}, },
changeAsideHamburgerPosition(v) {
asideMutations.hamburgerPosition(v)
},
changeAsideUniqueOpen(v) { changeAsideUniqueOpen(v) {
asideMutations.uniqueOpen(v) asideMutations.uniqueOpen(v)
}, },
@ -269,8 +277,8 @@ export default {
changeAsideAutoHide(v) { changeAsideAutoHide(v) {
asideMutations.autoHide(v) asideMutations.autoHide(v)
}, },
changeAsideHamburgerPosition(v) { changeAsideSearch(v) {
asideMutations.hamburgerPosition(v) asideMutations.search(v)
}, },
changeNavbarTheme(v) { changeNavbarTheme(v) {

@ -1,3 +1,4 @@
import Hamburger from '@/layout/component/Hamburger'
import {getters as appGetters} from "@/layout/store/app" import {getters as appGetters} from "@/layout/store/app"
import {getters as asideGetters} from "@/layout/store/aside" import {getters as asideGetters} from "@/layout/store/aside"
import {getSidebarMenus} from "@/layout/util" import {getSidebarMenus} from "@/layout/util"
@ -7,6 +8,8 @@ import {getSidebarMenus} from "@/layout/util"
*/ */
export default { export default {
components: {Hamburger},
computed: { computed: {
//渲染汉堡包的条件 //渲染汉堡包的条件
//①侧边栏有菜单 //①侧边栏有菜单

@ -4,6 +4,7 @@
import {mutations as tagsViewMutations} from "@/layout/store/tagsView" import {mutations as tagsViewMutations} from "@/layout/store/tagsView"
import {refreshPage} from "@/util/route" import {refreshPage} from "@/util/route"
import {isExternal} from "@/util/validate" import {isExternal} from "@/util/validate"
import {findComponentByTag} from "@/util/vue"
export default { export default {
data() { data() {
@ -23,7 +24,6 @@ export default {
//外部链接时打开新窗口 //外部链接时打开新窗口
if (isExternal(fullPath)) { if (isExternal(fullPath)) {
window.open(fullPath) window.open(fullPath)
//重置高亮菜单
return this.resetActiveMenu() return this.resetActiveMenu()
} }
@ -39,24 +39,18 @@ export default {
//由于侧边栏菜单数组更新后,el-menu不一定会更新(当数组中不存在单级菜单时) //由于侧边栏菜单数组更新后,el-menu不一定会更新(当数组中不存在单级菜单时)
//所以手动更新el-menu的当前高亮菜单 //所以手动更新el-menu的当前高亮菜单
resetActiveMenu() { resetActiveMenu() {
const menu = this.$refs.menu const menu = this.$_getElMenuInstance()
menu && menu.updateActiveIndex(this.activeMenu) menu && menu.updateActiveIndex(this.activeMenu)
}, },
//渲染el-menu时监听其展开菜单 //获取el-menu实例
watchOpenedMenus() { //目前被侧边栏和双层侧边栏的主菜单使用
//尝试取消之前设置的监听 $_getElMenuInstance() {
if (this.watchOpenedMenusCallback) { if (!this.$_elMenuInstance) {
this.watchOpenedMenusCallback() this.$_elMenuInstance = findComponentByTag(this, 'el-menu')
this.watchOpenedMenusCallback = null
} }
const menu = this.$refs.menu return this.$_elMenuInstance
if (!menu) return
this.watchOpenedMenusCallback = menu.$watch('openedMenus', v => {
this.openedMenuNum = v.length
})
} }
} }
} }

@ -0,0 +1,24 @@
/**
* 侧边栏搜索框混入
*/
import MenuSearch from '@/layout/component/Aside/component/MenuSearch'
import {trim} from "@/util"
import {findComponentByTag} from "@/util/vue"
export default {
components: {MenuSearch},
methods: {
$_getNavMenuInstance() {
if (!this.$_navMenuInstance) {
this.$_navMenuInstance = findComponentByTag(this, 'nav-menu')
}
return this.$_navMenuInstance
},
handlerSearch(v) {
this.$_getNavMenuInstance().realSearchWord = trim(v)
}
}
}

@ -42,33 +42,41 @@ function sort(routes) {
return return
} }
//菜单排序值的空值处理
const getSortValue = item => { const getSortValue = item => {
const sort = deepTap(item) const sort = deepGetSortValue(item)
return isEmpty(sort) ? 10000 : sort return isEmpty(sort) ? 10000 : sort
} }
const deepTap = item => {
const {name, children = [], meta: {title, hidden, sort} = {}} = item //获取菜单的排序值
const deepGetSortValue = item => {
const {children = [], meta: {hidden, sort} = {}} = item
if (hidden) return null if (hidden) return null
if (!isEmpty(sort)) return sort if (!isEmpty(sort)) return sort
//如果是类似首页那样的路由层级
if (isEmpty(name, title) && children.length === 1) { //如果只有一个子节点,那么取子节点的排序值
return deepTap(children[0]) if (children.length === 1) {
return deepGetSortValue(children[0])
} }
return null return null
} }
//对根节点排序
routes.sort((pre, next) => { routes.sort((pre, next) => {
pre = getSortValue(pre) pre = getSortValue(pre)
next = getSortValue(next) next = getSortValue(next)
if (pre < next) return -1 if (pre < next) return -1
else if (pre === next) return 0 if (pre === next) return 0
else return 1 return 1
}) })
//对每一个根节点的子级排序
routes.forEach(route => { routes.forEach(route => {
const {children} = route const {children} = route
if (children && children.length) { children && children.length && sort(children)
sort(children)
}
}) })
} }

@ -26,7 +26,10 @@ const state = {
autoHide: false, autoHide: false,
//汉堡包的位置,aside 或 head //汉堡包的位置,aside 或 head
hamburgerPosition: 'aside' hamburgerPosition: 'aside',
//是否显示搜索框
search: true
} }
const store = Vue.observable(state) const store = Vue.observable(state)

@ -7,7 +7,7 @@ import {str2routeConfig, metaExtend} from "@/router/util"
import {getAll} from "@/api/system/resource" import {getAll} from "@/api/system/resource"
import {isEmpty, deepClone} from "@/util" import {isEmpty, deepClone} from "@/util"
import {needAuth, auth} from "@/util/auth" // 此处存在循环引用? import {needAuth, auth} from "@/util/auth" // 此处存在循环引用?
import {createTree} from "@/util/tree" import {createTree, shakeTree} from "@/util/tree"
import {isExternal} from "@/util/validate" import {isExternal} from "@/util/validate"
const state = { const state = {
@ -42,7 +42,7 @@ const mutations = {
} }
const actions = { const actions = {
init({state, commit}, {resources, admin}) { init({state, commit}, {admin}) {
return getAll return getAll
.request() .request()
.then(({data}) => { .then(({data}) => {
@ -55,7 +55,7 @@ const actions = {
!state.init && addDynamicRoutes(routes) !state.init && addDynamicRoutes(routes)
//生成经过权限过滤后的菜单 //生成经过权限过滤后的菜单
appMutations.menus(getAuthorizedMenus({resources, admin}, routes)) appMutations.menus(getAuthorizedMenus(routes))
//设置初始化完成的标志 //设置初始化完成的标志
commit('init', true) commit('init', true)
@ -70,7 +70,7 @@ function transformOriginRouteData(data) {
//未开启后端动态路由功能时,返回前端预设的静态路由 //未开启后端动态路由功能时,返回前端预设的静态路由
if (!routeConfig.useBackendDataAsRoute) return getDynamicRoutes() if (!routeConfig.useBackendDataAsRoute) return getDynamicRoutes()
data = deepClone(data).filter(i => { const treeArray = deepClone(data).filter(i => {
//过滤掉数据接口或者未启用的项 //过滤掉数据接口或者未启用的项
if (i.type === 3 || !i.enable) { if (i.type === 3 || !i.enable) {
return false return false
@ -84,16 +84,22 @@ function transformOriginRouteData(data) {
return true return true
}) })
return createTree(data) return createTree(treeArray)
} }
//获取经过权限控制后的菜单 //获取经过权限控制后的菜单
function getAuthorizedMenus({resources, admin}, menus) { function getAuthorizedMenus(menus) {
menus = deepClone(menus) menus = deepClone(menus)
clean(menus) clean(menus)
addFullPath(menus) addFullPath(menus)
filter(menus, i => !needAuth(i) || auth(i.fullPath)) return shakeTree(menus, i => {
return menus //非叶子节点时,当定义了children属性却没有子节点时移除
if (i.children && i.children.length === 0) {
return false
}
return !needAuth(i) || auth(i.fullPath)
})
} }
//删除不显示的菜单(没有children且没有meta.title) //删除不显示的菜单(没有children且没有meta.title)
@ -135,23 +141,8 @@ function addFullPath(routes, basePath = '/') {
}) })
} }
//若没有children且传入的validate校验方法未通过,则删除,若有,当children长度为0时删除
function filter(arr, validate) {
for (let i = arr.length - 1; i >= 0; i--) {
const {children} = arr[i]
if (!children || children.length <= 0) {
!validate(arr[i]) && arr.splice(i, 1)
continue
}
filter(children, validate)
children.length <= 0 && arr.splice(i, 1)
}
}
//根据权限列表生成哈希表:<权限路径,权限id> //根据权限列表生成哈希表:<权限路径,权限id>
//该方法只处理叶子节点或者是接口类型的节点
function generateResourceMap(resources) { function generateResourceMap(resources) {
if (!Array.isArray(resources) || resources.length <= 0) { if (!Array.isArray(resources) || resources.length <= 0) {
return {} return {}

@ -4,15 +4,19 @@ import {isEmpty} from "@/util"
/** /**
* 判断是否需要鉴权需要则返回true * 判断是否需要鉴权需要则返回true
* 以下情况不需要鉴权①无meta②设置了noAuth=true③没有名称 * 以下情况不需要鉴权①无meta是redirect③没有名称设置了noAuth=true
* *
* @param route vue-router的routeConfig或项目中说明的路由配置项 * @param route vue-router的routeConfig或项目中说明的路由配置项
* @return {boolean} * @return {boolean}
*/ */
export function needAuth(route) { export function needAuth(route) {
if (route.path.startsWith('/redirect')) return false const {path, meta} = route
const {meta} = route
return !meta || !meta.noAuth || isEmpty(meta.title) && isEmpty(meta.dynamicTitle) if (!meta || path.startsWith('/redirect')) return false
if (isEmpty(meta.title) && isEmpty(meta.dynamicTitle)) return false
return meta.noAuth !== true
} }
/** /**

@ -262,3 +262,8 @@ export function deepClone(source) {
}, {}) }, {})
} }
} }
//有空值判断的trim
export function trim(str) {
return isEmpty(str) ? str : str.trim()
}

@ -1,4 +1,4 @@
import {deepClone} from "@/util/index" import {deepClone} from "@/util"
import {createWorker} from '@/util/worker' import {createWorker} from '@/util/worker'
const DEFAULT_PROPS = {id: 'id', pid: 'pid', children: 'children', leafHasChildren: true} const DEFAULT_PROPS = {id: 'id', pid: 'pid', children: 'children', leafHasChildren: true}
@ -114,26 +114,34 @@ export function createLimitTreeByMap(fullMap, limit) {
/** /**
* 根据判断函数裁剪树当节点不满足predicate且无下级节点时将被裁剪 * 根据判断函数裁剪树当节点不满足predicate且无下级节点时将被裁剪
* 此方法会改变原数组的children属性
* *
* @param tree {array} 原始树 * @param tree {array} 原始树
* @param predicate {function} 判断节点是否满足条件的函数传入一个节点data * @param predicate {function} 返回false时移除该节点传入当前节点
* @param childrenKey {string} 节点中代表子节点数组含义的字段名称比如[{ch:[...]}]就是'ch' * @param childrenKey {string} 节点中代表子节点数组含义的字段名称比如[{ch:[...]}]就是'ch'
* @return {array} 经过裁剪后的树 * @return {array} 经过裁剪后的树
*/ */
function shakeTree(tree = [], predicate = () => true, childrenKey = 'children') { export function shakeTree(tree, predicate = () => true, childrenKey = 'children') {
if (!tree) return undefined
return tree.filter(data => { return tree.filter(data => {
data[childrenKey] = shakeTree(data[childrenKey], predicate) const children = shakeTree(data[childrenKey], predicate, childrenKey)
return predicate(data) || data[childrenKey].length
if (children) {
data[childrenKey] = children
}
return predicate(data) || children && children.length > 0
}) })
} }
/** /**
* 自底向上计算每个节点下有多少子节点 * 自底向上计算每个节点的value
* *
* @param node * @param node
* @param valueKey {string} * @param valueKey {string}
* @param childrenKey {string} * @param childrenKey {string}
* @return {*|number} * @return {number}
*/ */
function calc(node, valueKey = 'value', childrenKey = 'children') { function calc(node, valueKey = 'value', childrenKey = 'children') {
if (!node) return 0 if (!node) return 0
@ -181,9 +189,7 @@ export function flatTree(tree, childrenKey = 'children') {
export function getNodesByDfs(node) { export function getNodesByDfs(node) {
const nodes = [] const nodes = []
const stack = [] const stack = [node]
stack.push(node)
while (stack.length > 0) { while (stack.length > 0) {
const item = stack.pop() const item = stack.pop()
@ -199,8 +205,7 @@ export function getNodesByDfs(node) {
export function getNodesByBfs(node) { export function getNodesByBfs(node) {
const nodes = [] const nodes = []
const queue = [] const queue = [node]
queue.unshift(node)
while (queue.length > 0) { while (queue.length > 0) {
const item = queue.shift() const item = queue.shift()

Loading…
Cancel
Save