parent
82ad7d684c
commit
6f61b12934
@ -1,74 +0,0 @@ |
|||||||
<template> |
|
||||||
<li |
|
||||||
class="el-menu-item" |
|
||||||
:style="paddingStyle" |
|
||||||
:class="{'is-active': active,'is-disabled': disabled}" |
|
||||||
@click="handleClick" |
|
||||||
> |
|
||||||
<el-tooltip |
|
||||||
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title" |
|
||||||
effect="dark" |
|
||||||
placement="right" |
|
||||||
> |
|
||||||
<template v-slot:content> |
|
||||||
<slot name="title"/> |
|
||||||
</template> |
|
||||||
<div :style="iconContainerStyle"> |
|
||||||
<slot/> |
|
||||||
</div> |
|
||||||
</el-tooltip> |
|
||||||
|
|
||||||
<template v-else> |
|
||||||
<slot/> |
|
||||||
<slot name="title"/> |
|
||||||
</template> |
|
||||||
</li> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script> |
|
||||||
import MenuMixin from './mixin' |
|
||||||
import Emitter from 'element-ui/lib/mixins/emitter' |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'ElMenuItem', |
|
||||||
|
|
||||||
componentName: 'ElMenuItem', |
|
||||||
|
|
||||||
mixins: [MenuMixin, Emitter], |
|
||||||
|
|
||||||
props: { |
|
||||||
index: String, |
|
||||||
route: [String, Object], |
|
||||||
disabled: Boolean |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
active() { |
|
||||||
return this.index === this.rootMenu.activeIndex |
|
||||||
}, |
|
||||||
|
|
||||||
iconContainerStyle() { |
|
||||||
return `position: absolute;left: 0;top: 0;height: 100%;width: 100%;display: inline-block;box-sizing: border-box;padding: 0 ${this.inlineIndent}px;` |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
handleClick() { |
|
||||||
if (!this.disabled) { |
|
||||||
this.dispatch('ElMenu', 'item-click', this) |
|
||||||
this.$emit('click', this) |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
mounted() { |
|
||||||
this.parentMenu.addItem(this) |
|
||||||
this.rootMenu.addItem(this) |
|
||||||
}, |
|
||||||
|
|
||||||
beforeDestroy() { |
|
||||||
this.parentMenu.removeItem(this) |
|
||||||
this.rootMenu.removeItem(this) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,250 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import MenuMixin from './mixin' |
|
||||||
import Emitter from 'element-ui/lib/mixins/emitter' |
|
||||||
import Popper from 'element-ui/lib/utils/vue-popper' |
|
||||||
|
|
||||||
const PopperMixin = { |
|
||||||
props: { |
|
||||||
transformOrigin: {type: [Boolean, String], default: false}, |
|
||||||
offset: Popper.props.offset, |
|
||||||
boundariesPadding: Popper.props.boundariesPadding, |
|
||||||
popperOptions: Popper.props.popperOptions |
|
||||||
}, |
|
||||||
data: Popper.data, |
|
||||||
methods: Popper.methods, |
|
||||||
beforeDestroy: Popper.beforeDestroy, |
|
||||||
deactivated: Popper.deactivated |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'ElSubmenu', |
|
||||||
|
|
||||||
componentName: 'ElSubmenu', |
|
||||||
|
|
||||||
mixins: [MenuMixin, Emitter, PopperMixin], |
|
||||||
|
|
||||||
props: { |
|
||||||
index: String, |
|
||||||
showTimeout: {type: Number, default: 300}, |
|
||||||
hideTimeout: {type: Number, default: 300}, |
|
||||||
popperClass: String, |
|
||||||
disabled: Boolean, |
|
||||||
popperAppendToBody: {type: Boolean, default: true} |
|
||||||
}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
popperJS: null, |
|
||||||
timeout: null, |
|
||||||
items: {}, |
|
||||||
submenus: {}, |
|
||||||
mouseInChild: false |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
appendToBody() { |
|
||||||
return this.popperAppendToBody === undefined |
|
||||||
? this.isFirstLevel |
|
||||||
: this.popperAppendToBody |
|
||||||
}, |
|
||||||
menuTransitionName() { |
|
||||||
return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top' |
|
||||||
}, |
|
||||||
opened() { |
|
||||||
return this.rootMenu.openedMenus.includes(this.index) |
|
||||||
}, |
|
||||||
active() { |
|
||||||
const active = Object.values(this.items).some(v => v.active) |
|
||||||
return active || Object.values(this.submenus).some(v => v.active) |
|
||||||
}, |
|
||||||
mode() { |
|
||||||
return this.rootMenu.mode |
|
||||||
}, |
|
||||||
isMenuPopup() { |
|
||||||
return this.rootMenu.isMenuPopup |
|
||||||
}, |
|
||||||
isFirstLevel() { |
|
||||||
const arr = ['ElSubmenu', 'ElMenuItemGroup'] |
|
||||||
let isFirstLevel = true |
|
||||||
let parent = this.$parent |
|
||||||
|
|
||||||
while (parent && parent !== this.rootMenu) { |
|
||||||
if (arr.includes(parent.$options.componentName)) { |
|
||||||
isFirstLevel = false |
|
||||||
break |
|
||||||
} |
|
||||||
else parent = parent.$parent |
|
||||||
} |
|
||||||
return isFirstLevel |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
opened() { |
|
||||||
this.isMenuPopup && this.$nextTick(this.updatePopper) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
handleCollapseToggle(value) { |
|
||||||
value ? this.initPopper() : this.doDestroy() |
|
||||||
}, |
|
||||||
|
|
||||||
addItem(item) { |
|
||||||
this.$set(this.items, item.index, item) |
|
||||||
}, |
|
||||||
removeItem(item) { |
|
||||||
delete this.items[item.index] |
|
||||||
}, |
|
||||||
addSubmenu(item) { |
|
||||||
this.$set(this.submenus, item.index, item) |
|
||||||
}, |
|
||||||
removeSubmenu(item) { |
|
||||||
delete this.submenus[item.index] |
|
||||||
}, |
|
||||||
|
|
||||||
handleClick() { |
|
||||||
const {rootMenu, disabled} = this |
|
||||||
if ( |
|
||||||
(rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') || |
|
||||||
(rootMenu.collapse && rootMenu.mode === 'vertical') || |
|
||||||
disabled |
|
||||||
) { |
|
||||||
return |
|
||||||
} |
|
||||||
this.dispatch('ElMenu', 'submenu-click', this) |
|
||||||
}, |
|
||||||
|
|
||||||
handleMouseenter(event, showTimeout = this.showTimeout) { |
|
||||||
if (!('ActiveXObject' in window) && event.type === 'focus' && !event.relatedTarget) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
const {rootMenu, disabled} = this |
|
||||||
|
|
||||||
if (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal' |
|
||||||
|| !rootMenu.collapse && rootMenu.mode === 'vertical' |
|
||||||
|| disabled) return |
|
||||||
|
|
||||||
this.dispatch('ElSubmenu', 'mouse-enter-child') |
|
||||||
|
|
||||||
window.clearTimeout(this.timeout) |
|
||||||
this.timeout = window.setTimeout(() => this.rootMenu.openMenu(this.index, this.indexPath), showTimeout) |
|
||||||
|
|
||||||
this.appendToBody && this.$parent.$el.dispatchEvent(new MouseEvent('mouseenter')) |
|
||||||
}, |
|
||||||
|
|
||||||
handleMouseleave(deepDispatch = false) { |
|
||||||
const {rootMenu} = this |
|
||||||
|
|
||||||
if (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal' |
|
||||||
|| !rootMenu.collapse && rootMenu.mode === 'vertical') return |
|
||||||
|
|
||||||
this.dispatch('ElSubmenu', 'mouse-leave-child') |
|
||||||
|
|
||||||
window.clearTimeout(this.timeout) |
|
||||||
this.timeout = window.setTimeout(() => !this.mouseInChild && this.rootMenu.closeMenu(this.index), this.hideTimeout) |
|
||||||
|
|
||||||
if (this.appendToBody && deepDispatch) { |
|
||||||
if (this.$parent.$options.name === 'ElSubmenu') { |
|
||||||
this.$parent.handleMouseleave(true) |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
updatePlacement() { |
|
||||||
this.placement = this.mode === 'horizontal' && this.isFirstLevel |
|
||||||
? 'bottom-start' |
|
||||||
: 'right-start' |
|
||||||
}, |
|
||||||
|
|
||||||
initPopper() { |
|
||||||
this.referenceElm = this.$el |
|
||||||
this.popperElm = this.$refs.menu |
|
||||||
this.updatePlacement() |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
created() { |
|
||||||
this.$on('toggle-collapse', this.handleCollapseToggle) |
|
||||||
this.$on('mouse-enter-child', () => { |
|
||||||
this.mouseInChild = true |
|
||||||
window.clearTimeout(this.timeout) |
|
||||||
}) |
|
||||||
this.$on('mouse-leave-child', () => { |
|
||||||
this.mouseInChild = false |
|
||||||
window.clearTimeout(this.timeout) |
|
||||||
}) |
|
||||||
}, |
|
||||||
|
|
||||||
mounted() { |
|
||||||
this.parentMenu.addSubmenu(this) |
|
||||||
this.rootMenu.addSubmenu(this) |
|
||||||
this.initPopper() |
|
||||||
}, |
|
||||||
|
|
||||||
beforeDestroy() { |
|
||||||
this.parentMenu.removeSubmenu(this) |
|
||||||
this.rootMenu.removeSubmenu(this) |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
const {rootMenu, $slots} = this |
|
||||||
|
|
||||||
const popupMenu = ( |
|
||||||
<transition name={this.menuTransitionName}> |
|
||||||
<div |
|
||||||
ref="menu" |
|
||||||
v-show={this.opened} |
|
||||||
class={[`el-menu--${this.mode}`, this.popperClass]} |
|
||||||
on-mouseenter={e => this.handleMouseenter(e, 100)} |
|
||||||
on-mouseleave={() => this.handleMouseleave(true)} |
|
||||||
on-focus={e => this.handleMouseenter(e, 100)}> |
|
||||||
<ul class={['el-menu el-menu--popup', `el-menu--popup-${this.currentPlacement}`]}> |
|
||||||
{$slots.default} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
</transition> |
|
||||||
) |
|
||||||
|
|
||||||
const inlineMenu = ( |
|
||||||
<el-collapse-transition> |
|
||||||
<ul v-show={this.opened} class="el-menu el-menu--inline"> |
|
||||||
{$slots.default} |
|
||||||
</ul> |
|
||||||
</el-collapse-transition> |
|
||||||
) |
|
||||||
|
|
||||||
const submenuTitleIcon = ( |
|
||||||
rootMenu.mode === 'horizontal' && this.isFirstLevel || |
|
||||||
rootMenu.mode === 'vertical' && !rootMenu.collapse |
|
||||||
) ? 'el-icon-arrow-down' : 'el-icon-arrow-right' |
|
||||||
|
|
||||||
return ( |
|
||||||
<li |
|
||||||
class={{ |
|
||||||
'el-submenu': true, |
|
||||||
'is-active': this.active, |
|
||||||
'is-opened': this.opened, |
|
||||||
'is-disabled': this.disabled |
|
||||||
}} |
|
||||||
on-mouseenter={this.handleMouseenter} |
|
||||||
on-mouseleave={() => this.handleMouseleave(false)} |
|
||||||
on-focus={this.handleMouseenter} |
|
||||||
> |
|
||||||
<div |
|
||||||
ref="submenu-title" |
|
||||||
class="el-submenu__title" |
|
||||||
style={this.paddingStyle} |
|
||||||
on-click={this.handleClick} |
|
||||||
> |
|
||||||
{$slots.title} |
|
||||||
<i class={['el-submenu__icon-arrow', submenuTitleIcon]}/> |
|
||||||
</div> |
|
||||||
{this.isMenuPopup ? popupMenu : inlineMenu} |
|
||||||
</li> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,53 +0,0 @@ |
|||||||
/** |
|
||||||
* 在原基础上增加了 inlineIndent 属性,用于确定每级节点的单位缩进距离 |
|
||||||
*/ |
|
||||||
export default { |
|
||||||
inject: ['rootMenu'], |
|
||||||
|
|
||||||
props: { |
|
||||||
inlineIndent: {type: Number, default: 20} |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
indexPath() { |
|
||||||
const path = [this.index] |
|
||||||
let parent = this.$parent |
|
||||||
while (parent.$options.componentName !== 'ElMenu') { |
|
||||||
if (parent.index) { |
|
||||||
path.unshift(parent.index) |
|
||||||
} |
|
||||||
parent = parent.$parent |
|
||||||
} |
|
||||||
return path |
|
||||||
}, |
|
||||||
|
|
||||||
parentMenu() { |
|
||||||
let parent = this.$parent |
|
||||||
while ( |
|
||||||
parent && |
|
||||||
['ElMenu', 'ElSubmenu'].indexOf(parent.$options.componentName) === -1 |
|
||||||
) { |
|
||||||
parent = parent.$parent |
|
||||||
} |
|
||||||
return parent |
|
||||||
}, |
|
||||||
|
|
||||||
paddingStyle() { |
|
||||||
if (this.rootMenu.mode !== 'vertical') return {} |
|
||||||
|
|
||||||
let padding = this.inlineIndent |
|
||||||
|
|
||||||
if (!this.rootMenu.collapse) { |
|
||||||
let parent = this.$parent |
|
||||||
while (parent && parent.$options.componentName !== 'ElMenu') { |
|
||||||
if (parent.$options.componentName === 'ElSubmenu') { |
|
||||||
padding += this.inlineIndent |
|
||||||
} |
|
||||||
parent = parent.$parent |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return {'padding-left': `${padding}px`} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,13 +1,6 @@ |
|||||||
import MenuItem from "./component/Menu/MenuItem" |
|
||||||
import Submenu from "./component/Menu/Submenu" |
|
||||||
import Message from "./component/Message" |
import Message from "./component/Message" |
||||||
|
|
||||||
const components = [MenuItem, Submenu] |
|
||||||
|
|
||||||
export default function (Vue) { |
export default function (Vue) { |
||||||
components.forEach(component => { |
|
||||||
Vue.component(component.name, component) |
|
||||||
}) |
|
||||||
|
|
||||||
Vue.prototype.$message = Message |
Vue.prototype.$message = Message |
||||||
} |
} |
||||||
|
|||||||
@ -1,7 +1,4 @@ |
|||||||
/*仅覆盖element-ui中的预设变量,不会被使用*/ |
/*仅覆盖element-ui中的预设变量,不会被使用*/ |
||||||
|
|
||||||
//el-menu的hover背景颜色 |
|
||||||
$--menu-item-hover-fill: none; |
|
||||||
|
|
||||||
//引入element的scss时需要覆盖字体路径 |
//引入element的scss时需要覆盖字体路径 |
||||||
$--font-path: '~element-ui/lib/theme-chalk/fonts'; |
$--font-path: '~element-ui/lib/theme-chalk/fonts'; |
||||||
|
|||||||
@ -1,5 +0,0 @@ |
|||||||
@media only screen and (max-width: $--sm) { |
|
||||||
.hidden-xs { |
|
||||||
display: none !important |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,31 +0,0 @@ |
|||||||
.right-out, |
|
||||||
.left-out { |
|
||||||
&-leave-active, |
|
||||||
&-enter-active { |
|
||||||
transition: all .2s; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.right-out { |
|
||||||
&-enter { |
|
||||||
opacity: 0; |
|
||||||
transform: translateX(-30px); |
|
||||||
} |
|
||||||
|
|
||||||
&-leave-to { |
|
||||||
opacity: 0; |
|
||||||
transform: translateX(30px); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.left-out { |
|
||||||
&-enter { |
|
||||||
opacity: 0; |
|
||||||
transform: translateX(30px); |
|
||||||
} |
|
||||||
|
|
||||||
&-leave-to { |
|
||||||
opacity: 0; |
|
||||||
transform: translateX(-30px); |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,30 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import common from './common' |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "ColorCheckBox", |
|
||||||
|
|
||||||
mixins: [common], |
|
||||||
|
|
||||||
render() { |
|
||||||
return ( |
|
||||||
<div class="color-checkbox" style={{backgroundColor: this.value}} on-click={this.click}> |
|
||||||
{this.checked && <i class="el-icon-check"/>} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style> |
|
||||||
.color-checkbox { |
|
||||||
width: 20px; |
|
||||||
height: 20px; |
|
||||||
margin: 5px; |
|
||||||
border-radius: 2px; |
|
||||||
cursor: pointer; |
|
||||||
text-align: center; |
|
||||||
font-weight: bold; |
|
||||||
color: #ffffff; |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,21 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
export default { |
|
||||||
name: "CheckboxGroup", |
|
||||||
|
|
||||||
props: {value: String}, |
|
||||||
|
|
||||||
data: () => ({children: []}), |
|
||||||
|
|
||||||
render() { |
|
||||||
return <div class="checkbox-group">{this.$slots.default}</div> |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style> |
|
||||||
.checkbox-group { |
|
||||||
display: flex; |
|
||||||
flex-wrap: wrap; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,43 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import common from './common' |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "ImgCheckBox", |
|
||||||
|
|
||||||
mixins: [common], |
|
||||||
|
|
||||||
props: { |
|
||||||
img: String, |
|
||||||
label: String |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
return ( |
|
||||||
<div class="img-checkbox" title={this.label} on-click={this.click}> |
|
||||||
<img src={this.img}/> |
|
||||||
{this.checked && <i className="el-icon-check"/>} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss"> |
|
||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.img-checkbox { |
|
||||||
width: 55px; |
|
||||||
margin: 10px; |
|
||||||
border-radius: 2px; |
|
||||||
cursor: pointer; |
|
||||||
position: relative; |
|
||||||
|
|
||||||
.el-icon-check { |
|
||||||
position: absolute; |
|
||||||
color: $--color-primary; |
|
||||||
font-weight: bold; |
|
||||||
top: 25px; |
|
||||||
right: 10px; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,114 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
const Item = { |
|
||||||
functional: true, |
|
||||||
|
|
||||||
render(h, context) { |
|
||||||
return <li class="context-menu-item" {...context.data}>{context.children}</li> |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "ContextMenu", |
|
||||||
|
|
||||||
components: {Item}, |
|
||||||
|
|
||||||
props: { |
|
||||||
value: Boolean, |
|
||||||
items: Array, |
|
||||||
left: Number, |
|
||||||
top: Number |
|
||||||
}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
realLeft: '0px', |
|
||||||
realTop: '0px' |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
value(v) { |
|
||||||
document.body[v ? 'addEventListener' : 'removeEventListener']('click', this.close) |
|
||||||
v && this.$nextTick(this.autoAdapt) |
|
||||||
}, |
|
||||||
left(v) { |
|
||||||
v && this.autoAdaptLeft(v) |
|
||||||
}, |
|
||||||
top(v) { |
|
||||||
v && this.autoAdaptTop(v) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
close() { |
|
||||||
this.$emit('input', false) |
|
||||||
}, |
|
||||||
autoAdapt() { |
|
||||||
this.autoAdaptTop(this.top) |
|
||||||
this.autoAdaptLeft(this.left) |
|
||||||
}, |
|
||||||
autoAdaptTop(v) { |
|
||||||
if (!this.value) return |
|
||||||
|
|
||||||
const elOffsetHeight = this.$el.offsetHeight |
|
||||||
|
|
||||||
if (elOffsetHeight > document.body.clientHeight - v && v > elOffsetHeight) { |
|
||||||
this.realTop = v - elOffsetHeight + 'px' |
|
||||||
} |
|
||||||
else this.realTop = v + 'px' |
|
||||||
}, |
|
||||||
autoAdaptLeft(v) { |
|
||||||
if (!this.value) return |
|
||||||
|
|
||||||
const elOffsetWidth = this.$el.offsetWidth |
|
||||||
|
|
||||||
if (elOffsetWidth > document.body.clientWidth - v) { |
|
||||||
this.realLeft = v - elOffsetWidth + 'px' |
|
||||||
} |
|
||||||
else this.realLeft = v + 'px' |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
if (!Array.isArray(this.items)) return |
|
||||||
|
|
||||||
const style = {left: this.realLeft, top: this.realTop} |
|
||||||
|
|
||||||
return ( |
|
||||||
<ul v-show={this.value} class="context-menu" style={style}> |
|
||||||
{this.items.map(i => ( |
|
||||||
i && <item on-click={i.click}>{i.content}</item> |
|
||||||
))} |
|
||||||
</ul> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss"> |
|
||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.context-menu { |
|
||||||
margin: 0; |
|
||||||
background-color: $menu-background-dark; |
|
||||||
z-index: 10; |
|
||||||
position: fixed; |
|
||||||
list-style-type: none; |
|
||||||
padding: 5px 0; |
|
||||||
border-radius: 4px; |
|
||||||
font-size: 12px; |
|
||||||
font-weight: 400; |
|
||||||
color: #ffffff; |
|
||||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); |
|
||||||
|
|
||||||
&-item { |
|
||||||
margin: 0; |
|
||||||
padding: 7px 16px; |
|
||||||
cursor: pointer; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
color: $--color-primary; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,175 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import cssVar from '@/asset/style/var.scss' |
|
||||||
import Item from './item' |
|
||||||
import {isEmpty, deepClone, trim} from "@/util" |
|
||||||
|
|
||||||
const inlineIndent = parseFloat(cssVar.menuPadding) |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'NavMenu', |
|
||||||
|
|
||||||
props: { |
|
||||||
//路由配置项组成的树形数组 |
|
||||||
menus: Array, |
|
||||||
|
|
||||||
//垂直还是水平,在el-menu原效果上加了样式名 |
|
||||||
mode: {type: String, default: 'vertical'}, |
|
||||||
|
|
||||||
//主题,light 或 dark |
|
||||||
theme: {type: String, default: 'light'}, |
|
||||||
|
|
||||||
//是否折叠 |
|
||||||
collapse: Boolean, |
|
||||||
|
|
||||||
//折叠时的展开菜单是否显示父级 |
|
||||||
showParentOnCollapse: Boolean, |
|
||||||
|
|
||||||
//搜索词 |
|
||||||
searchWord: String, |
|
||||||
|
|
||||||
//能够显示图标的最大深度,不传 或 <0 则不作限制 |
|
||||||
showIconMaxDepth: {type: Number, default: 1}, |
|
||||||
|
|
||||||
//menus变化时是否使用过渡动画 |
|
||||||
switchTransition: Boolean, |
|
||||||
|
|
||||||
//menus过渡动画名称 |
|
||||||
switchTransitionName: String |
|
||||||
}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
//去除空格后的搜索词,不使用computed是为了第三方能直接响应式地修改数据 |
|
||||||
realSearchWord: '' |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
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 |
|
||||||
} |
|
||||||
|
|
||||||
const children = this.filterAfterSearch(menu.children) |
|
||||||
|
|
||||||
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-menu的open方法是为了避免uniqueOpened |
|
||||||
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 |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
let items = this.realMenus.map(m => ( |
|
||||||
<Item |
|
||||||
menu={m} |
|
||||||
popper-class={this.themeClass} |
|
||||||
highlight={this.realSearchWord} |
|
||||||
show-parent={this.showParentOnCollapse} |
|
||||||
collapse={this.collapse} |
|
||||||
inline-indent={inlineIndent} |
|
||||||
show-icon-max-depth={this.showIconMaxDepth} |
|
||||||
/> |
|
||||||
)) |
|
||||||
|
|
||||||
if (this.useSwitchTransition) { |
|
||||||
items = <transition-group name={this.switchTransitionName}>{items}</transition-group> |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<el-menu |
|
||||||
ref="el-menu" |
|
||||||
class={this.menuClass} |
|
||||||
mode={this.mode} |
|
||||||
collapse={this.collapse} |
|
||||||
collapse-transition={false} |
|
||||||
{...{attrs: this.$attrs, on: this.$listeners}} |
|
||||||
> |
|
||||||
{items} |
|
||||||
</el-menu> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss"></style> |
|
||||||
@ -1,154 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import {isEmpty} from "@/util" |
|
||||||
|
|
||||||
const MenuItemContent = { |
|
||||||
functional: true, |
|
||||||
|
|
||||||
props: { |
|
||||||
icon: String, |
|
||||||
title: String, |
|
||||||
highlight: String |
|
||||||
}, |
|
||||||
|
|
||||||
render(h, context) { |
|
||||||
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">{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) |
|
||||||
] |
|
||||||
} |
|
||||||
|
|
||||||
//根据showIconMaxDepth、depth判断是否需要限制图标的显示 |
|
||||||
function getIcon({icon, showIconMaxDepth, depth}) { |
|
||||||
if (showIconMaxDepth == null || showIconMaxDepth < 0) { |
|
||||||
return icon |
|
||||||
} |
|
||||||
return showIconMaxDepth < depth ? undefined : icon |
|
||||||
} |
|
||||||
|
|
||||||
//获取不需要嵌套展示的菜单 |
|
||||||
function getOnlyChild(menu) { |
|
||||||
const {children = [], meta: {alwaysShow} = {}} = menu |
|
||||||
|
|
||||||
if (!children.length) return {...menu, children: undefined} |
|
||||||
|
|
||||||
if (children.length === 1) return alwaysShow ? null : getOnlyChild(children[0]) |
|
||||||
|
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
function renderSingleMenu(h, {index, inlineIndent, icon, title, highlight}) { |
|
||||||
return ( |
|
||||||
<el-menu-item key={index} index={index} inline-indent={inlineIndent}> |
|
||||||
<MenuItemContent icon={icon} title={title} highlight={highlight}/> |
|
||||||
</el-menu-item> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
function renderSubMenu(h, {index, inlineIndent, icon, title, popperClass, highlight, children}) { |
|
||||||
return ( |
|
||||||
<el-submenu key={index} index={index} inline-indent={inlineIndent} popper-class={popperClass}> |
|
||||||
<MenuItemContent slot="title" icon={icon} title={title} highlight={highlight}/> |
|
||||||
{children} |
|
||||||
</el-submenu> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
function renderChildrenWithParentMenu(h, {icon, title, children}) { |
|
||||||
return [ |
|
||||||
<div class="popover-menu__title el-menu-item"> |
|
||||||
<MenuItemContent icon={icon} title={title}/> |
|
||||||
</div>, |
|
||||||
<div class="el-menu el-menu--inline">{children}</div> |
|
||||||
] |
|
||||||
} |
|
||||||
|
|
||||||
function renderMenu(h, props) { |
|
||||||
const {menu, inlineIndent, popperClass, highlight, showParent, collapse, showIconMaxDepth, depth = 1} = props |
|
||||||
|
|
||||||
const onlyOneChild = getOnlyChild(menu) |
|
||||||
|
|
||||||
const showSingle = onlyOneChild && !onlyOneChild.children |
|
||||||
|
|
||||||
if (showSingle) { |
|
||||||
const {fullPath, meta: {icon, title}} = onlyOneChild |
|
||||||
return renderSingleMenu(h, { |
|
||||||
index: fullPath, |
|
||||||
inlineIndent, |
|
||||||
icon: getIcon({icon, showIconMaxDepth, depth}), |
|
||||||
title, |
|
||||||
highlight |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const {icon, title} = menu.meta |
|
||||||
|
|
||||||
let children = menu.children.map(child => renderMenu(h, {...props, menu: child, depth: depth + 1})) |
|
||||||
|
|
||||||
//这里弹出菜单如果包裹了<el-scrollbar>,则在触发mouseleave时,不会触发父菜单的mouseleave事件 |
|
||||||
if (collapse) { |
|
||||||
//弹出菜单显示父级信息 |
|
||||||
if (showParent) { |
|
||||||
children = renderChildrenWithParentMenu(h, {icon, title, children}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return renderSubMenu(h, { |
|
||||||
index: menu.fullPath, |
|
||||||
inlineIndent, |
|
||||||
icon: getIcon({icon, showIconMaxDepth, depth}), |
|
||||||
title, |
|
||||||
popperClass, |
|
||||||
highlight, |
|
||||||
children |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
functional: true, |
|
||||||
|
|
||||||
props: { |
|
||||||
//菜单对象,即路由配置项 |
|
||||||
menu: Object, |
|
||||||
|
|
||||||
//弹出菜单的自定义类名 |
|
||||||
popperClass: String, |
|
||||||
|
|
||||||
//需要高亮的字符串 |
|
||||||
highlight: String, |
|
||||||
|
|
||||||
//折叠时的展开菜单是否显示父级 |
|
||||||
showParent: Boolean, |
|
||||||
|
|
||||||
//向左缩进的单位距离 |
|
||||||
inlineIndent: Number, |
|
||||||
|
|
||||||
//是否为折叠状态 |
|
||||||
collapse: Boolean, |
|
||||||
|
|
||||||
//能够显示图标的最大深度,不传 或 <0 则不作限制 |
|
||||||
showIconMaxDepth: Number |
|
||||||
}, |
|
||||||
|
|
||||||
render(h, context) { |
|
||||||
return renderMenu(h, context.props) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,109 +0,0 @@ |
|||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.el-menu { |
|
||||||
//图标的大小 |
|
||||||
.icon { |
|
||||||
font-size: $menu-icon-size; |
|
||||||
} |
|
||||||
|
|
||||||
//图标与文字的间隔 |
|
||||||
.icon + .menu-item-content { |
|
||||||
margin-left: $menu-icon-text-gap; |
|
||||||
} |
|
||||||
|
|
||||||
//菜单的高亮关键字 |
|
||||||
.menu-highlight-result { |
|
||||||
vertical-align: unset; |
|
||||||
color: $--color-success; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//原始样式使用 '>' 可能导致侧边栏折叠时还会显示右侧箭头图标 |
|
||||||
//此样式只对垂直状态生效 |
|
||||||
.el-menu--collapse .el-submenu .el-submenu__title .el-submenu__icon-arrow { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
|
|
||||||
//hover时,菜单左侧图标放大(弹出菜单的父级不放大) |
|
||||||
.el-menu-item:not(.popover-menu__title), |
|
||||||
.el-submenu__title { |
|
||||||
> .icon, |
|
||||||
> .el-tooltip > .icon { |
|
||||||
transition: transform .1s; |
|
||||||
} |
|
||||||
|
|
||||||
&:hover > .icon, |
|
||||||
&:hover > .el-tooltip > .icon { |
|
||||||
transform: scale(1.2); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//侧边栏垂直菜单 |
|
||||||
.el-menu--vertical { |
|
||||||
&.el-menu { |
|
||||||
overflow-y: auto; |
|
||||||
} |
|
||||||
|
|
||||||
&.el-menu, |
|
||||||
.el-menu { |
|
||||||
//菜单文字 |
|
||||||
.el-menu-item, |
|
||||||
.el-submenu__title { |
|
||||||
> .menu-item-content { |
|
||||||
text-overflow: ellipsis; |
|
||||||
overflow: hidden; |
|
||||||
//侧边栏宽度、菜单左右padding、左侧图标宽度、距离左右的距离、右侧图标宽度 |
|
||||||
width: $aside-width - $menu-padding * 2 - $menu-icon-size - $menu-icon-text-gap * 2 - 12px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//弹出菜单的父级 |
|
||||||
&.el-menu--popup .popover-menu__title { |
|
||||||
cursor: auto; |
|
||||||
border-bottom-width: 1px; |
|
||||||
border-bottom-style: solid; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//顶部水平菜单 |
|
||||||
.el-menu--horizontal { |
|
||||||
//顶部菜单的顶级节点 |
|
||||||
&.el-menu { |
|
||||||
//不显示折叠展开按钮 |
|
||||||
> .el-submenu > .el-submenu__title > .el-submenu__icon-arrow { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
|
|
||||||
//顶级节点的高度以及hover颜色 |
|
||||||
> .el-menu-item, |
|
||||||
> .el-submenu > .el-submenu__title { |
|
||||||
height: $nav-height; |
|
||||||
line-height: $nav-height; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
border-bottom: 2px solid $--color-primary; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&.el-menu, |
|
||||||
.el-menu { |
|
||||||
//菜单的文字以及hover颜色 |
|
||||||
.el-menu-item, |
|
||||||
.el-submenu__title { |
|
||||||
> span { |
|
||||||
max-width: 100px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//submenu的右侧箭头会稍微偏上,不懂为啥 |
|
||||||
.el-submenu__icon-arrow { |
|
||||||
margin-top: -4px; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
@import "./theme-light"; |
|
||||||
@import "./theme-dark"; |
|
||||||
@ -1,60 +0,0 @@ |
|||||||
.el-menu--dark { |
|
||||||
//侧边栏垂直菜单 |
|
||||||
&.el-menu--vertical { |
|
||||||
&.el-menu, |
|
||||||
.el-menu { |
|
||||||
background-color: $menu-background-dark; |
|
||||||
|
|
||||||
//菜单的文字以及hover颜色 |
|
||||||
.el-menu-item, |
|
||||||
.el-submenu__title { |
|
||||||
color: $menu-text-color-dark; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
color: $menu-text-hover-color-dark; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//子级菜单的背景颜色 |
|
||||||
&.el-menu--inline { |
|
||||||
background-color: $sub-menu-background-dark; |
|
||||||
} |
|
||||||
|
|
||||||
//子级菜单内阴影 |
|
||||||
&.el-menu--inline { |
|
||||||
box-shadow: inset 0 2px 8px darken($sub-menu-background-dark, 10%) |
|
||||||
} |
|
||||||
|
|
||||||
//设置了显示弹出菜单的父级 |
|
||||||
&.el-menu--popup .popover-menu__title { |
|
||||||
color: $menu-text-hover-color-dark; |
|
||||||
border-bottom-color: $--border-color-dark; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//顶部水平菜单 |
|
||||||
&.el-menu--horizontal { |
|
||||||
&.el-menu, |
|
||||||
.el-menu { |
|
||||||
background-color: $root-menu-background-dark; |
|
||||||
|
|
||||||
//菜单的文字以及hover颜色 |
|
||||||
.el-menu-item, |
|
||||||
.el-submenu__title { |
|
||||||
color: $menu-text-color-dark; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
color: $menu-text-hover-color-dark; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&.el-menu > .el-menu-item, |
|
||||||
&.el-menu > .el-submenu > .el-submenu__title, |
|
||||||
&.el-menu > .el-menu-item:hover, |
|
||||||
&.el-menu > .el-submenu > .el-submenu__title:hover { |
|
||||||
border-bottom: none; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,39 +0,0 @@ |
|||||||
.el-menu--light { |
|
||||||
//侧边栏垂直菜单 |
|
||||||
&.el-menu--vertical { |
|
||||||
&.el-menu, |
|
||||||
.el-menu { |
|
||||||
//菜单的文字以及hover颜色 |
|
||||||
.el-menu-item, |
|
||||||
.el-submenu__title { |
|
||||||
color: $menu-text-color-light; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
color: $menu-text-hover-color-light; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//弹出菜单的父级 |
|
||||||
&.el-menu--popup .popover-menu__title { |
|
||||||
color: $menu-text-hover-color-light; |
|
||||||
border-bottom-color: $--border-color-light; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//顶部水平菜单 |
|
||||||
&.el-menu--horizontal { |
|
||||||
&.el-menu, |
|
||||||
.el-menu { |
|
||||||
//菜单的文字以及hover颜色 |
|
||||||
.el-menu-item, |
|
||||||
.el-submenu__title { |
|
||||||
color: $menu-text-color-light; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
color: $menu-text-hover-color-light; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,68 +0,0 @@ |
|||||||
import {isEmpty, timeFormat} from '@/util' |
|
||||||
|
|
||||||
//时间戳格式化
|
|
||||||
export function timestamp2Date(timestamp) { |
|
||||||
if (isEmpty(timestamp)) return '' |
|
||||||
return timeFormat(null, new Date(timestamp)) |
|
||||||
} |
|
||||||
|
|
||||||
//秒数格式化,最低时间为分钟
|
|
||||||
export function secondFormat(time) { |
|
||||||
if (!time || isNaN(time)) return ' NaN' |
|
||||||
if (time < 3600) return ~~(time / 60) + ' 分' |
|
||||||
if (time < 86400) return ~~(time / 3600) + ' 小时' |
|
||||||
return ~~(time / 86400) + ' 天' |
|
||||||
} |
|
||||||
|
|
||||||
//格式化当前和给定的时间戳之间的秒数差
|
|
||||||
export function secondBetweenFormat(time) { |
|
||||||
const between = Date.now() / 1000 - Number(time) |
|
||||||
return secondFormat(between) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 数字转存储单位,1024 => KB |
|
||||||
* |
|
||||||
* @param num {number} |
|
||||||
* @param digits {number} 保留几位小数 |
|
||||||
* @return {string} |
|
||||||
*/ |
|
||||||
export function number2StorageUnit(num, digits = 2) { |
|
||||||
if (isEmpty(num)) return '' |
|
||||||
const si = [ |
|
||||||
{value: 1024 * 1024 * 1024 * 1024, symbol: 'TB'}, |
|
||||||
{value: 1024 * 1024 * 1024, symbol: 'GB'}, |
|
||||||
{value: 1024 * 1024, symbol: 'MB'}, |
|
||||||
{value: 1024, symbol: 'KB'} |
|
||||||
] |
|
||||||
for (let i = 0; i < si.length; i++) { |
|
||||||
if (num >= si[i].value) { |
|
||||||
return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol |
|
||||||
} |
|
||||||
} |
|
||||||
return num.toString() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 10000 => "10,000" |
|
||||||
* @param num {number} |
|
||||||
*/ |
|
||||||
export function toThousandFilter(num) { |
|
||||||
if (isEmpty(num)) return '' |
|
||||||
return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Upper case first char |
|
||||||
* @param string {string} |
|
||||||
*/ |
|
||||||
export function uppercaseFirst(string) { |
|
||||||
if (isEmpty(string)) return '' |
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1) |
|
||||||
} |
|
||||||
|
|
||||||
export default function (Vue) { |
|
||||||
Object |
|
||||||
.entries({timestamp2Date, secondFormat, secondBetweenFormat, number2StorageUnit, toThousandFilter, uppercaseFirst}) |
|
||||||
.forEach(([key, value]) => Vue.filter(key, value)) |
|
||||||
} |
|
||||||
@ -1,56 +0,0 @@ |
|||||||
<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/var"; |
|
||||||
|
|
||||||
//侧边栏折叠时不显示 |
|
||||||
.collapse .aside-search { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
|
|
||||||
.aside-search { |
|
||||||
margin: 12px 8px; |
|
||||||
} |
|
||||||
|
|
||||||
//暗色主题 |
|
||||||
.dark .aside-search .el-input__inner { |
|
||||||
border: none; |
|
||||||
color: $--color-white; |
|
||||||
background-color: lighten($menu-background-dark, 5%); |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,251 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import hamburgerMixin from '@/layout/mixin/hamburger' |
|
||||||
import menuMixin from "@/layout/mixin/menu" |
|
||||||
import menuSearchMixin from '@/layout/mixin/menuSearch' |
|
||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside" |
|
||||||
import {getters as pageGetters} from "@/layout/store/page" |
|
||||||
import Logo from '@/layout/component/Logo' |
|
||||||
import NavMenu from '@/component/menu/NavMenu' |
|
||||||
import {getSidebarMenus, getActiveMenuByRoute} from "@/layout/util" |
|
||||||
import {moveToActiveMenuVertically} from "@/util/element-ui/elMenu" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "Sidebar", |
|
||||||
|
|
||||||
mixins: [hamburgerMixin, menuMixin, menuSearchMixin], |
|
||||||
|
|
||||||
components: {Logo, NavMenu}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
//鼠标是否在侧边栏外 |
|
||||||
mouseOutside: true |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
isMobile: () => appGetters.isMobile, |
|
||||||
|
|
||||||
//侧边栏菜单 |
|
||||||
menus: () => getSidebarMenus(), |
|
||||||
|
|
||||||
//当是移动端或设置了侧边栏自动隐藏时将侧边栏用抽屉包裹 |
|
||||||
renderInDrawer() { |
|
||||||
return this.isMobile || asideGetters.autoHide |
|
||||||
}, |
|
||||||
//el-drawer的自定义类名 |
|
||||||
drawerClass() { |
|
||||||
return this.isMobile || pageGetters.position !== 'top-bottom' |
|
||||||
? undefined |
|
||||||
: 'drawer-behind-header' |
|
||||||
}, |
|
||||||
|
|
||||||
//侧边栏的显隐状态,true显示、false隐藏 |
|
||||||
show() { |
|
||||||
//store中要显示那就显示 |
|
||||||
if (asideGetters.show) return true |
|
||||||
|
|
||||||
//移动端,false |
|
||||||
if (this.isMobile) return false |
|
||||||
|
|
||||||
//未设置侧边栏自动隐藏,false |
|
||||||
if (!asideGetters.autoHide) return false |
|
||||||
|
|
||||||
//鼠标在侧边栏内,true |
|
||||||
if (!this.mouseOutside) return true |
|
||||||
|
|
||||||
//侧边栏处于折叠状态 且 存在弹出的子菜单,true |
|
||||||
return this.collapse && this.openedMenuNum > 0 |
|
||||||
}, |
|
||||||
|
|
||||||
//侧边栏的折叠状态,true折叠、false展开,仅在pc端可折叠 |
|
||||||
collapse() { |
|
||||||
return asideGetters.collapse && !this.isMobile |
|
||||||
}, |
|
||||||
|
|
||||||
//判断是否需要绑定鼠标移动的事件 |
|
||||||
shouldBindMouseMoveEvent() { |
|
||||||
//如果是移动端,false |
|
||||||
if (this.isMobile) return false |
|
||||||
|
|
||||||
//如果未启用侧边栏自动隐藏,false |
|
||||||
if (!asideGetters.autoHide) return false |
|
||||||
|
|
||||||
//侧边栏为打开状态,false |
|
||||||
if (this.show) return false |
|
||||||
|
|
||||||
//鼠标在侧边栏外部,true |
|
||||||
return this.mouseOutside |
|
||||||
}, |
|
||||||
|
|
||||||
//是否需要显示logo |
|
||||||
showLogo() { |
|
||||||
return pageGetters.showLogo && (this.isMobile || pageGetters.position === 'left-right') |
|
||||||
}, |
|
||||||
|
|
||||||
//是否需要使用侧边栏的切换动画 |
|
||||||
switchTransition() { |
|
||||||
return !this.isMobile && appGetters.navMode !== 'aside' |
|
||||||
}, |
|
||||||
|
|
||||||
//是否需要显示搜索框 |
|
||||||
renderMenuSearch() { |
|
||||||
return !this.isMobile && asideGetters.search |
|
||||||
}, |
|
||||||
|
|
||||||
className() { |
|
||||||
return {'sidebar': true, 'collapse': this.collapse} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
'$route.path': { |
|
||||||
immediate: true, |
|
||||||
handler(v) { |
|
||||||
//如果是redirect跳转,则跳过 |
|
||||||
if (v.startsWith('/redirect')) return |
|
||||||
|
|
||||||
this.activeMenu = getActiveMenuByRoute(this.$route) |
|
||||||
|
|
||||||
const menu = this.$_getElMenuInstance() |
|
||||||
if (!menu) return |
|
||||||
const item = menu.items[this.activeMenu] |
|
||||||
|
|
||||||
//如果侧边栏中没有对应的激活菜单,则收起全部 |
|
||||||
if (!item) return menu.openedMenus = [] |
|
||||||
|
|
||||||
//由于elMenu的initOpenedMenu()不会触发select事件,所以选择手动触发 |
|
||||||
this.onSelect(item.index, item.indexPath, item, false) |
|
||||||
|
|
||||||
//仅当非抽屉模式下滚动至激活的菜单 |
|
||||||
!this.renderInDrawer && this.$nextTick(this.moveToCurrentMenu) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
//切换至移动端时收起侧边栏 |
|
||||||
isMobile: { |
|
||||||
immediate: true, |
|
||||||
handler(v) { |
|
||||||
v && asideMutations.close() |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
//抽屉模式下侧边栏显示时滚动至激活菜单 |
|
||||||
show(v) { |
|
||||||
v && this.$nextTick(this.moveToCurrentMenu) |
|
||||||
}, |
|
||||||
|
|
||||||
//添加或移除鼠标移动事件 |
|
||||||
shouldBindMouseMoveEvent: { |
|
||||||
immediate: true, |
|
||||||
handler(v) { |
|
||||||
//尝试移除之前可能添加的事件 |
|
||||||
window.removeEventListener('mousemove', this.moveEvent) |
|
||||||
|
|
||||||
v && window.addEventListener('mousemove', this.moveEvent) |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
//开启侧边栏自动隐藏后的鼠标移动事件 |
|
||||||
moveEvent(e) { |
|
||||||
//鼠标移动至屏幕左侧边缘时,标识鼠标在侧边栏内部 |
|
||||||
if (e.clientX <= 1) this.mouseOutside = false |
|
||||||
}, |
|
||||||
|
|
||||||
//模拟选中菜单 |
|
||||||
onSelect(index, indexPath, item, jump = true) { |
|
||||||
//开启手风琴模式时,激活没有子级的菜单时收起其它展开项 |
|
||||||
if (asideGetters.uniqueOpen && indexPath.length === 1) { |
|
||||||
const menu = this.$_getElMenuInstance() |
|
||||||
const opened = menu.openedMenus |
|
||||||
opened.forEach(i => i !== index && menu.closeMenu(i)) |
|
||||||
} |
|
||||||
|
|
||||||
jump && this.actionOnSelectMenu(index) |
|
||||||
|
|
||||||
//抽屉模式下需要关闭抽屉 |
|
||||||
if (this.renderInDrawer && this.show) { |
|
||||||
asideMutations.close() |
|
||||||
this.mouseOutside = true |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
//渲染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() { |
|
||||||
moveToActiveMenuVertically(this.$_getElMenuInstance()) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
beforeDestroy() { |
|
||||||
window.removeEventListener('mousemove', this.moveEvent) |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
if (this.menus.length <= 0) return |
|
||||||
|
|
||||||
const aside = ( |
|
||||||
<div |
|
||||||
class={this.className} |
|
||||||
on-mouseleave={() => this.mouseOutside = true} |
|
||||||
on-mouseenter={() => this.mouseOutside = false} |
|
||||||
> |
|
||||||
{this.showLogo && <logo show-title={!this.collapse}/>} |
|
||||||
|
|
||||||
{this.renderMenuSearch && <menu-search on-search={this.handlerSearch}/>} |
|
||||||
|
|
||||||
<nav-menu |
|
||||||
ref="nav-menu" |
|
||||||
menus={this.menus} |
|
||||||
theme={asideGetters.theme} |
|
||||||
collapse={this.collapse} |
|
||||||
default-active={this.activeMenu} |
|
||||||
unique-opened={asideGetters.uniqueOpen} |
|
||||||
show-parent-on-collapse={asideGetters.showParentOnCollapse} |
|
||||||
switch-transition={this.switchTransition} |
|
||||||
switch-transition-name="sidebar" |
|
||||||
on={{'select': this.onSelect, 'hook:mounted': this.watchOpenedMenus}} |
|
||||||
/> |
|
||||||
|
|
||||||
{!this.renderInDrawer && this.renderHamburger && <hamburger class="el-menu-item"/>} |
|
||||||
</div> |
|
||||||
) |
|
||||||
|
|
||||||
if (this.renderInDrawer) { |
|
||||||
return ( |
|
||||||
<el-drawer |
|
||||||
visible={this.show} |
|
||||||
with-header={false} |
|
||||||
custom-class={this.drawerClass} |
|
||||||
modal={this.isMobile} //设置自动隐藏时不使用遮罩 |
|
||||||
direction="ltr" |
|
||||||
size="auto" |
|
||||||
on-close={asideMutations.close} |
|
||||||
> |
|
||||||
{aside} |
|
||||||
</el-drawer> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
return aside |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,134 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import menuMixin from "@/layout/mixin/menu" |
|
||||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
|
||||||
import {getters as asideGetters} from "@/layout/store/aside" |
|
||||||
import {getters as pageGetters} from "@/layout/store/page" |
|
||||||
import Logo from '@/layout/component/Logo' |
|
||||||
import {scrollIntoViewVertically} from "@/util/browser" |
|
||||||
|
|
||||||
const Item = { |
|
||||||
functional: true, |
|
||||||
|
|
||||||
props: { |
|
||||||
title: String, |
|
||||||
icon: String, |
|
||||||
active: Boolean |
|
||||||
}, |
|
||||||
|
|
||||||
render(h, context) { |
|
||||||
const {title, icon, active} = context.props |
|
||||||
const {click} = context.listeners |
|
||||||
|
|
||||||
return ( |
|
||||||
<li |
|
||||||
class={{'el-menu-item': true, 'is-active': active}} |
|
||||||
on-click={click} |
|
||||||
> |
|
||||||
<v-icon icon={icon}/> |
|
||||||
<span class="menu-item-content">{title}</span> |
|
||||||
</li> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "RootSidebar", |
|
||||||
|
|
||||||
mixins: [menuMixin], |
|
||||||
|
|
||||||
components: {Logo, Item}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
//鼠标是否在外部,用于判断是否需要展开折叠 |
|
||||||
mouseOutside: true |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
menus() { |
|
||||||
return appGetters.menus.map(root => ({...root, children: undefined})) |
|
||||||
}, |
|
||||||
|
|
||||||
//是否需要显示logo |
|
||||||
showLogo() { |
|
||||||
return pageGetters.showLogo && pageGetters.position === 'left-right' |
|
||||||
}, |
|
||||||
|
|
||||||
containerClass() { |
|
||||||
return {'root-sidebar': true, 'collapse': this.mouseOutside} |
|
||||||
}, |
|
||||||
|
|
||||||
menuClass() { |
|
||||||
return `el-menu el-menu--vertical el-menu--${asideGetters.theme}` |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
//路由变化时设置高亮菜单 |
|
||||||
$route: { |
|
||||||
immediate: true, |
|
||||||
handler(to) { |
|
||||||
const {path, matched} = to |
|
||||||
|
|
||||||
//使用/redirect跳转 或 无匹配路由 时跳过 |
|
||||||
if (path.startsWith('/redirect') || matched.length === 0) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
this.setActiveRootMenu(this.navMode, to) |
|
||||||
|
|
||||||
//滚动至激活菜单 |
|
||||||
if (!this._isMounted) return |
|
||||||
this.$nextTick(() => { |
|
||||||
scrollIntoViewVertically( |
|
||||||
this.$el.querySelector('.el-menu-item.is-active'), |
|
||||||
this.$el.querySelector('.el-menu.el-menu--vertical') |
|
||||||
) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
//设置当前激活的顶部菜单 |
|
||||||
setActiveRootMenu(navMode = this.navMode, {matched} = this.$route) { |
|
||||||
const rootPath = matched[0].path || '/' |
|
||||||
appMutations.activeRootMenu(rootPath) |
|
||||||
}, |
|
||||||
onSelect(index) { |
|
||||||
//点击相同菜单不刷新页面 |
|
||||||
if (index !== appGetters.activeRootMenu) { |
|
||||||
this.actionOnSelectMenu(index) |
|
||||||
} |
|
||||||
|
|
||||||
//收起 |
|
||||||
this.mouseOutside = true |
|
||||||
}, |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
return ( |
|
||||||
<div class="root-sidebar-container"> |
|
||||||
<div |
|
||||||
class={this.containerClass} |
|
||||||
on-mouseleave={() => this.mouseOutside = true} |
|
||||||
on-mouseenter={() => this.mouseOutside = false} |
|
||||||
> |
|
||||||
{this.showLogo && <logo show-title={!this.mouseOutside}/>} |
|
||||||
<ul class={this.menuClass}> |
|
||||||
{this.menus.map(({fullPath, meta: {title, icon}}) => ( |
|
||||||
<item |
|
||||||
title={title} |
|
||||||
icon={icon} |
|
||||||
active={fullPath === appGetters.activeRootMenu} |
|
||||||
on-click={() => this.onSelect(fullPath)} |
|
||||||
/> |
|
||||||
))} |
|
||||||
</ul> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,110 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import hamburgerMixin from '@/layout/mixin/hamburger' |
|
||||||
import menuMixin from "@/layout/mixin/menu" |
|
||||||
import menuSearchMixin from '@/layout/mixin/menuSearch' |
|
||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
import {getters as asideGetters} from "@/layout/store/aside" |
|
||||||
import NavMenu from "@/component/menu/NavMenu" |
|
||||||
import {getActiveMenuByRoute, getSidebarMenus} from "@/layout/util" |
|
||||||
import {moveToActiveMenuVertically} from "@/util/element-ui/elMenu" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "SubSidebar", |
|
||||||
|
|
||||||
mixins: [hamburgerMixin, menuMixin, menuSearchMixin], |
|
||||||
|
|
||||||
components: {NavMenu}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
//父级菜单 |
|
||||||
rootMenu() { |
|
||||||
const active = appGetters.activeRootMenu |
|
||||||
return appGetters.menus.find(i => i.fullPath === active) |
|
||||||
}, |
|
||||||
|
|
||||||
//侧边栏菜单 |
|
||||||
menus: () => getSidebarMenus(), |
|
||||||
|
|
||||||
//侧边栏的折叠状态,true折叠、false展开,仅在pc端可折叠 |
|
||||||
collapse() { |
|
||||||
return asideGetters.collapse && !asideGetters.isMobile |
|
||||||
}, |
|
||||||
|
|
||||||
className() { |
|
||||||
return {'sub-sidebar': true, 'collapse': this.collapse} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
'$route.path': { |
|
||||||
immediate: true, |
|
||||||
handler(v) { |
|
||||||
//如果是redirect跳转,则跳过 |
|
||||||
if (v.startsWith('/redirect')) return |
|
||||||
|
|
||||||
this.activeMenu = getActiveMenuByRoute(this.$route) |
|
||||||
|
|
||||||
const menu = this.$_getElMenuInstance() |
|
||||||
if (!menu) return |
|
||||||
const item = menu.items[this.activeMenu] |
|
||||||
|
|
||||||
//如果侧边栏中没有对应的激活菜单,则收起全部 |
|
||||||
if (!item) return menu.openedMenus = [] |
|
||||||
|
|
||||||
//由于elMenu的initOpenedMenu()不会触发select事件,所以选择手动触发 |
|
||||||
this.onSelect(item.index, item.indexPath, item, false) |
|
||||||
|
|
||||||
//滚动至激活的菜单 |
|
||||||
this.$nextTick(this.moveToCurrentMenu) |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
//模拟选中菜单 |
|
||||||
onSelect(index, indexPath, item, jump = true) { |
|
||||||
//开启手风琴模式时,激活没有子级的菜单时收起其它展开项 |
|
||||||
if (asideGetters.uniqueOpen && indexPath.length === 1) { |
|
||||||
const menu = this.$_getElMenuInstance() |
|
||||||
const opened = menu.openedMenus |
|
||||||
opened.forEach(i => i !== index && menu.closeMenu(i)) |
|
||||||
} |
|
||||||
|
|
||||||
jump && this.actionOnSelectMenu(index) |
|
||||||
}, |
|
||||||
|
|
||||||
//滚动至当前激活的菜单 |
|
||||||
moveToCurrentMenu() { |
|
||||||
moveToActiveMenuVertically(this.$_getElMenuInstance()) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
if (this.menus.length <= 0) return |
|
||||||
|
|
||||||
return ( |
|
||||||
<div class={this.className}> |
|
||||||
<div class="sub-sidebar-title"> |
|
||||||
{this.rootMenu && this.rootMenu.meta.title} |
|
||||||
</div> |
|
||||||
|
|
||||||
{asideGetters.search && <menu-search on-search={this.handlerSearch}/>} |
|
||||||
|
|
||||||
<nav-menu |
|
||||||
menus={this.menus} |
|
||||||
theme={asideGetters.theme} |
|
||||||
collapse={this.collapse} |
|
||||||
default-active={this.activeMenu} |
|
||||||
unique-opened={asideGetters.uniqueOpen} |
|
||||||
show-parent-on-collapse={asideGetters.showParentOnCollapse} |
|
||||||
switch-transition |
|
||||||
switch-transition-name="sidebar" |
|
||||||
on-select={this.onSelect} |
|
||||||
/> |
|
||||||
|
|
||||||
{this.renderHamburger && <hamburger class="el-menu-item"/>} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,34 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import OnePart from './component/OnePartRoot' |
|
||||||
import TwoPartRoot from './component/TwoPartRoot' |
|
||||||
import TwoPartSub from './component/TwoPartSub' |
|
||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
import {getters as asideGetters} from "@/layout/store/aside" |
|
||||||
|
|
||||||
export default { |
|
||||||
functional: true, |
|
||||||
|
|
||||||
render() { |
|
||||||
let children |
|
||||||
|
|
||||||
//移动端只能使用抽屉模式的单层侧边栏 |
|
||||||
if (appGetters.isMobile) { |
|
||||||
children = <OnePart/> |
|
||||||
} |
|
||||||
else { |
|
||||||
switch (appGetters.navMode) { |
|
||||||
case 'mix': |
|
||||||
case 'aside': |
|
||||||
children = <OnePart/> |
|
||||||
break |
|
||||||
case 'aside-two-part': |
|
||||||
children = [<TwoPartRoot/>, <TwoPartSub/>] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return <aside class={`aside ${asideGetters.theme}`}>{children}</aside> |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss"></style> |
|
||||||
@ -1,101 +0,0 @@ |
|||||||
@import "~@/asset/style/var"; |
|
||||||
@import "~@/asset/style/mixin.scss"; |
|
||||||
|
|
||||||
.aside { |
|
||||||
display: flex; |
|
||||||
z-index: 11; |
|
||||||
|
|
||||||
//移动端下侧边栏抽屉不能设置z-index,否则会被el-drawer的遮罩遮住 |
|
||||||
@include mobile { |
|
||||||
z-index: auto !important; |
|
||||||
} |
|
||||||
|
|
||||||
//单层和双层侧边栏 |
|
||||||
.sidebar, |
|
||||||
.root-sidebar, |
|
||||||
.sub-sidebar { |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
height: 100%; |
|
||||||
width: $aside-width; |
|
||||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); |
|
||||||
transition: all 0.2s; |
|
||||||
|
|
||||||
//折叠状态 |
|
||||||
&.collapse { |
|
||||||
width: $aside-collapse-width; |
|
||||||
|
|
||||||
//隐藏菜单文字 |
|
||||||
.menu-item-content, |
|
||||||
.sub-sidebar-title { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//垂直菜单占满剩余空间 |
|
||||||
.el-menu--vertical { |
|
||||||
flex: 1; |
|
||||||
} |
|
||||||
|
|
||||||
//底部汉堡包 |
|
||||||
.hamburger { |
|
||||||
border-top-width: 1px; |
|
||||||
border-top-style: solid; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//页面为上下结构时,侧边栏的抽屉模式需要扣除导航栏的高度 |
|
||||||
.el-drawer.drawer-behind-header { |
|
||||||
top: $nav-height; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
/* 双侧侧边栏开始 */ |
|
||||||
|
|
||||||
.root-sidebar-container { |
|
||||||
position: relative; |
|
||||||
width: $aside-collapse-width; |
|
||||||
height: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
.root-sidebar { |
|
||||||
position: absolute; |
|
||||||
overflow: hidden; |
|
||||||
z-index: 1; |
|
||||||
box-shadow: none !important; |
|
||||||
border-right-width: 1px; |
|
||||||
border-right-style: solid; |
|
||||||
} |
|
||||||
|
|
||||||
.sub-sidebar-title { |
|
||||||
height: $nav-height; |
|
||||||
line-height: $nav-height; |
|
||||||
padding-left: $menu-padding; |
|
||||||
font-size: 16px; |
|
||||||
} |
|
||||||
|
|
||||||
/* 双侧侧边栏结束 */ |
|
||||||
|
|
||||||
|
|
||||||
/* 侧边栏切换动画开始 */ |
|
||||||
|
|
||||||
.sidebar-enter-active { |
|
||||||
transition: all 0.2s; |
|
||||||
} |
|
||||||
|
|
||||||
.sidebar-enter, |
|
||||||
.sidebar-leave-active { |
|
||||||
opacity: 0; |
|
||||||
transform: translateY(30px) skewY(10deg) |
|
||||||
} |
|
||||||
|
|
||||||
.sidebar-leave-active { |
|
||||||
position: absolute; |
|
||||||
} |
|
||||||
|
|
||||||
/* 侧边栏切换动画结束 */ |
|
||||||
|
|
||||||
|
|
||||||
@import "theme-light"; |
|
||||||
@import "theme-dark"; |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
.aside.dark { |
|
||||||
&, |
|
||||||
> .el-drawer__wrapper .el-drawer__body { |
|
||||||
background-color: $menu-background-dark; |
|
||||||
} |
|
||||||
|
|
||||||
.root-sidebar, |
|
||||||
.root-sidebar > .el-menu--vertical.el-menu { |
|
||||||
background-color: $root-menu-background-dark; |
|
||||||
} |
|
||||||
|
|
||||||
.root-sidebar { |
|
||||||
border-right-color: $--border-color-dark; |
|
||||||
} |
|
||||||
|
|
||||||
.sub-sidebar-title { |
|
||||||
color: $--color-white; |
|
||||||
} |
|
||||||
|
|
||||||
.hamburger { |
|
||||||
border-top-color: $--border-color-dark; |
|
||||||
|
|
||||||
i { |
|
||||||
color: $menu-text-color-dark; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,14 +0,0 @@ |
|||||||
.aside.light { |
|
||||||
.root-sidebar { |
|
||||||
background-color: $--color-white; |
|
||||||
border-right-color: $--border-color-light; |
|
||||||
} |
|
||||||
|
|
||||||
.hamburger { |
|
||||||
border-top-color: $--border-color-light; |
|
||||||
|
|
||||||
i { |
|
||||||
color: $menu-text-color-light; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<template functional> |
||||||
|
<div class="copyright"> |
||||||
|
Copyright © 2020 - <a href="https://github.com/toesbieya" target="_blank">toesbieya</a> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "Footer" |
||||||
|
} |
||||||
|
</script> |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
.page-footer > .copyright { |
||||||
|
color: $--color-text-secondary; |
||||||
|
font-size: 14px; |
||||||
|
|
||||||
|
> a:hover { |
||||||
|
color: $--color-primary; |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
} |
||||||
@ -1,30 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Hamburger', |
|
||||||
|
|
||||||
render() { |
|
||||||
//考虑侧边栏可能是抽屉的情况 |
|
||||||
const collapsed = appGetters.isMobile ? asideGetters.show : !asideGetters.collapse |
|
||||||
const className = `hamburger ${collapsed ? 'collapsed' : ''}` |
|
||||||
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
class={className} |
|
||||||
title={`${collapsed ? '折叠' : '展开'}侧边栏`} |
|
||||||
on-click={asideMutations.switch} |
|
||||||
> |
|
||||||
<i class="el-icon-s-unfold"/> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style> |
|
||||||
.hamburger.collapsed > i { |
|
||||||
transform: rotate(180deg); |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import {title} from '@/config' |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Logo', |
|
||||||
|
|
||||||
functional: true, |
|
||||||
|
|
||||||
props: {showTitle: Boolean}, |
|
||||||
|
|
||||||
render(h, context) { |
|
||||||
const {showTitle} = context.props |
|
||||||
|
|
||||||
const logoClass = {'logo-container': true, 'no-title': !showTitle} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div class={logoClass}> |
|
||||||
<router-link class="logo-link" to="/"> |
|
||||||
<img src={`${process.env.BASE_URL}static/img/logo.svg`}/> |
|
||||||
{showTitle && <h1>{title}</h1>} |
|
||||||
</router-link> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss"> |
|
||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
$logoSize: 30px; |
|
||||||
|
|
||||||
.logo-container { |
|
||||||
width: 100%; |
|
||||||
height: $nav-height; |
|
||||||
line-height: $nav-height; |
|
||||||
padding: 0 $menu-padding; |
|
||||||
|
|
||||||
.logo-link { |
|
||||||
display: block; |
|
||||||
height: 100%; |
|
||||||
|
|
||||||
> img { |
|
||||||
display: inline-block; |
|
||||||
height: $logoSize; |
|
||||||
vertical-align: middle; |
|
||||||
|
|
||||||
& + h1 { |
|
||||||
margin-left: 12px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
> h1 { |
|
||||||
display: inline-block; |
|
||||||
margin: 0; |
|
||||||
color: $--color-primary; |
|
||||||
font-weight: 600; |
|
||||||
line-height: $nav-height; |
|
||||||
font-size: 18px; |
|
||||||
vertical-align: middle; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&.no-title { |
|
||||||
padding-left: ($aside-collapse-width - $logoSize) / 2; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.dark .logo-container .logo-link > h1 { |
|
||||||
color: #ffffff; |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,41 +0,0 @@ |
|||||||
<template> |
|
||||||
<el-badge :hidden="hidden" class="bell-badge" is-dot title="消息提醒" @click.native="jump"> |
|
||||||
<i class="el-icon-bell navbar-icon"/> |
|
||||||
</el-badge> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script> |
|
||||||
import {refreshPage} from "@/util/route" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "Bell", |
|
||||||
|
|
||||||
computed: { |
|
||||||
hidden() { |
|
||||||
return this.$store.state.message.unreadCount < 1 |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
jump() { |
|
||||||
const target = '/message/user' |
|
||||||
|
|
||||||
this.$route.path === target |
|
||||||
? refreshPage() |
|
||||||
: this.$router.push(target) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
mounted() { |
|
||||||
this.$store.dispatch('message/refresh') |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style> |
|
||||||
.bell-badge .el-badge__content { |
|
||||||
top: 15px !important; |
|
||||||
right: 10px !important; |
|
||||||
border: none; |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,210 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
/** |
|
||||||
* 顶部菜单,参考了ant design的响应式设计 |
|
||||||
*/ |
|
||||||
import menuMixin from "@/layout/mixin/menu" |
|
||||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
|
||||||
import NavMenu from "@/component/menu/NavMenu" |
|
||||||
import NavMenuItem from "@/component/menu/NavMenu/item" |
|
||||||
import {getActiveMenuByRoute} from "@/layout/util" |
|
||||||
import {isDom} from "@/util/browser" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "HeadMenu", |
|
||||||
|
|
||||||
mixins: [menuMixin], |
|
||||||
|
|
||||||
components: {NavMenu, NavMenuItem}, |
|
||||||
|
|
||||||
props: { |
|
||||||
//是否在只有一个顶部菜单时仍然渲染 |
|
||||||
alwaysShow: Boolean, |
|
||||||
|
|
||||||
//主题由父组件控制 |
|
||||||
theme: String |
|
||||||
}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
//最后一个不被隐藏的顶部菜单的数组下标 |
|
||||||
//为undefined时,说明不需要隐藏菜单,为-1时,说明需要隐藏全部菜单 |
|
||||||
lastVisibleIndex: undefined, |
|
||||||
|
|
||||||
//用于生成el-submenu的key |
|
||||||
seed: 0 |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
navMode: () => appGetters.navMode, |
|
||||||
|
|
||||||
menus() { |
|
||||||
if (appGetters.isMobile) return [] |
|
||||||
switch (this.navMode) { |
|
||||||
case 'aside': |
|
||||||
return [] |
|
||||||
case 'head' : |
|
||||||
return appGetters.menus |
|
||||||
case 'mix': |
|
||||||
return appGetters.menus.map(root => ({...root, children: undefined})) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
//实际用于渲染的菜单数组 |
|
||||||
realMenus() { |
|
||||||
const {lastVisibleIndex, menus} = this |
|
||||||
|
|
||||||
//不需要隐藏菜单 |
|
||||||
if (lastVisibleIndex === undefined) { |
|
||||||
return menus |
|
||||||
} |
|
||||||
|
|
||||||
const fullPath = `_${this.seed}` |
|
||||||
|
|
||||||
//隐藏全部菜单 |
|
||||||
if (lastVisibleIndex === -1) { |
|
||||||
return [{fullPath, meta: {icon: 'el-icon-menu'}, children: menus}] |
|
||||||
} |
|
||||||
|
|
||||||
const visible = menus.slice(0, lastVisibleIndex + 1) |
|
||||||
const hidden = menus.slice(lastVisibleIndex + 1) |
|
||||||
|
|
||||||
visible.push({fullPath, meta: {title: '...'}, children: hidden}) |
|
||||||
|
|
||||||
return visible |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
//路由变化时设置高亮菜单 |
|
||||||
$route: { |
|
||||||
immediate: true, |
|
||||||
handler(to) { |
|
||||||
const {path, matched} = to |
|
||||||
//使用/redirect跳转 或 无匹配路由 时跳过 |
|
||||||
if (path.startsWith('/redirect') || matched.length === 0) { |
|
||||||
return |
|
||||||
} |
|
||||||
this.setActiveMenu(this.navMode, to) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
//切换导航模式时重新设置高亮菜单 |
|
||||||
navMode: { |
|
||||||
immediate: true, |
|
||||||
handler(mode) { |
|
||||||
this.setActiveMenu(mode) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
//变动时修改种子,避免组件不更新 |
|
||||||
lastVisibleIndex() { |
|
||||||
this.seed++ |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
setActiveMenu(navMode = this.navMode, {path, meta, matched} = this.$route) { |
|
||||||
//设置当前激活的顶部菜单 |
|
||||||
const rootPath = matched[0].path || '/' |
|
||||||
appMutations.activeRootMenu(rootPath) |
|
||||||
|
|
||||||
//只有在混合导航模式下才将当前激活的顶部菜单认为是当前菜单 |
|
||||||
if (navMode === 'mix') { |
|
||||||
this.activeMenu = rootPath |
|
||||||
} |
|
||||||
//否则按照路由配置项设置 |
|
||||||
else this.activeMenu = getActiveMenuByRoute({path, meta}) |
|
||||||
}, |
|
||||||
onSelect(index) { |
|
||||||
//混合导航模式下,点击相同菜单不刷新页面 |
|
||||||
if (this.navMode === 'mix' && index === appGetters.activeRootMenu) { |
|
||||||
return |
|
||||||
} |
|
||||||
this.actionOnSelectMenu(index) |
|
||||||
}, |
|
||||||
|
|
||||||
//获取el-menu的dom |
|
||||||
getMenuEl() { |
|
||||||
return this.$el |
|
||||||
}, |
|
||||||
//获取初始菜单的总宽度,只在mounted时调用一次 |
|
||||||
setChildrenWidth() { |
|
||||||
const ul = this.getMenuEl() |
|
||||||
if (!isDom(ul)) return |
|
||||||
|
|
||||||
const menuItemNodes = ul.children |
|
||||||
if (!menuItemNodes || menuItemNodes.length === 0) return |
|
||||||
|
|
||||||
//'更多'菜单的宽度,由于不考虑自定义,所以直接写死 |
|
||||||
this.overflowedIndicatorWidth = 50 |
|
||||||
this.menuItemSizes = Array.from(menuItemNodes).map(i => i.getBoundingClientRect().width) |
|
||||||
this.originalTotalWidth = this.menuItemSizes.reduce((acc, cur) => acc + cur, 0) |
|
||||||
}, |
|
||||||
|
|
||||||
resize() { |
|
||||||
const width = this.getMenuEl().getBoundingClientRect().width |
|
||||||
|
|
||||||
let lastVisibleIndex = undefined |
|
||||||
|
|
||||||
//如果初始菜单的总宽度超出容器宽度 |
|
||||||
if (this.originalTotalWidth > width) { |
|
||||||
lastVisibleIndex = -1 |
|
||||||
|
|
||||||
//得到满足总宽度不超出容器宽度的最大菜单下标 |
|
||||||
let currentSumWidth = 0 |
|
||||||
for (const liWidth of this.menuItemSizes) { |
|
||||||
currentSumWidth += liWidth |
|
||||||
if (currentSumWidth + this.overflowedIndicatorWidth > width) { |
|
||||||
break |
|
||||||
} |
|
||||||
lastVisibleIndex += 1 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
this.lastVisibleIndex = lastVisibleIndex |
|
||||||
}, |
|
||||||
createResizeObserver() { |
|
||||||
//菜单未渲染时,移除之前的observer |
|
||||||
if (!isDom(this.getMenuEl())) { |
|
||||||
if (this.resizeObserver) { |
|
||||||
this.resizeObserver.disconnect() |
|
||||||
this.resizeObserver = null |
|
||||||
} |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
//如果已创建observer,则返回 |
|
||||||
if (this.resizeObserver) return |
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(this.resize) |
|
||||||
this.resizeObserver.observe(this.getMenuEl()) |
|
||||||
|
|
||||||
this.$once('hook:beforeDestroy', () => { |
|
||||||
this.resizeObserver && this.resizeObserver.disconnect() |
|
||||||
}) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
mounted() { |
|
||||||
this.setChildrenWidth() |
|
||||||
this.createResizeObserver() |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
if (this.menus.length <= 0 || this.menus.length === 1 && !this.alwaysShow) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<nav-menu |
|
||||||
menus={this.realMenus} |
|
||||||
mode="horizontal" |
|
||||||
theme={this.theme} |
|
||||||
default-active={this.activeMenu} |
|
||||||
on-select={this.onSelect} |
|
||||||
/> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,111 +0,0 @@ |
|||||||
<script type="jsx"> |
|
||||||
import {mapState} from 'vuex' |
|
||||||
import hamburgerMixin from '@/layout/mixin/hamburger' |
|
||||||
import HeadMenu from "./component/HeadMenu" |
|
||||||
import Logo from "@/layout/component/Logo" |
|
||||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
|
||||||
import {getters as navbarGetters} from "@/layout/store/navbar" |
|
||||||
import {getters as pageGetters} from "@/layout/store/page" |
|
||||||
import {elConfirm} from "@/util/message" |
|
||||||
import {refreshPage} from "@/util/route" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'navbar', |
|
||||||
|
|
||||||
mixins: [hamburgerMixin], |
|
||||||
|
|
||||||
components: {HeadMenu, Logo}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
...mapState('user', ['avatar', 'name', 'prepareLogout']), |
|
||||||
|
|
||||||
//渲染导航栏logo的条件 |
|
||||||
//①桌面端 |
|
||||||
//②设置了显示logo |
|
||||||
//③导航模式为顶部导航或页面为上下结构 |
|
||||||
renderLogo() { |
|
||||||
return !appGetters.isMobile |
|
||||||
&& pageGetters.showLogo |
|
||||||
&& (appGetters.navMode === 'head' || pageGetters.position === 'top-bottom') |
|
||||||
}, |
|
||||||
|
|
||||||
//渲染顶部导航菜单的条件 |
|
||||||
//①桌面端 |
|
||||||
//②导航模式为顶部导航或混合导航 |
|
||||||
renderHeadMenu() { |
|
||||||
return !appGetters.isMobile && ['head', 'mix'].includes(appGetters.navMode) |
|
||||||
}, |
|
||||||
|
|
||||||
className() { |
|
||||||
return `navbar ${navbarGetters.theme}` |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
command(command) { |
|
||||||
if (!this.prepareLogout) { |
|
||||||
elConfirm('确认退出?') |
|
||||||
.then(() => this.$store.dispatch('user/logout')) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
clickRefreshBtn() { |
|
||||||
refreshPage() |
|
||||||
}, |
|
||||||
|
|
||||||
openSettingDrawer() { |
|
||||||
appMutations.showSettingDrawer(true) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
return ( |
|
||||||
<nav class={this.className}> |
|
||||||
{this.renderLogo && <logo show-title/>} |
|
||||||
|
|
||||||
{this.renderHamburger && <hamburger class="navbar-item navbar-icon"/>} |
|
||||||
|
|
||||||
<div style="flex: 1"> |
|
||||||
{this.renderHeadMenu && <head-menu always-show theme={navbarGetters.theme}/>} |
|
||||||
</div> |
|
||||||
|
|
||||||
<div> |
|
||||||
<div |
|
||||||
class="navbar-item" |
|
||||||
title="刷新" |
|
||||||
on-click={this.clickRefreshBtn} |
|
||||||
> |
|
||||||
<i class="el-icon-refresh-right navbar-icon"/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div |
|
||||||
class="setting-btn navbar-item" |
|
||||||
title="个性设置" |
|
||||||
on-click={this.openSettingDrawer} |
|
||||||
> |
|
||||||
<i class="el-icon-s-operation navbar-icon"/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<el-dropdown class="navbar-item" on-command={this.command}> |
|
||||||
<div class="avatar-wrapper"> |
|
||||||
<el-avatar size={30} src={this.avatar} icon="el-icon-user-solid"/> |
|
||||||
<span class="hidden-xs">{this.name}</span> |
|
||||||
</div> |
|
||||||
<el-dropdown-menu |
|
||||||
slot="dropdown" |
|
||||||
class={navbarGetters.theme} |
|
||||||
visible-arrow={false} |
|
||||||
> |
|
||||||
<el-dropdown-item icon="el-icon-switch-button" command="logout"> |
|
||||||
退出登录 |
|
||||||
</el-dropdown-item> |
|
||||||
</el-dropdown-menu> |
|
||||||
</el-dropdown> |
|
||||||
</div> |
|
||||||
</nav> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss"></style> |
|
||||||
@ -1,50 +0,0 @@ |
|||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.navbar { |
|
||||||
display: flex; |
|
||||||
justify-content: space-between; |
|
||||||
align-items: center; |
|
||||||
flex-wrap: nowrap; |
|
||||||
height: $nav-height; |
|
||||||
padding-left: $navbar-padding-left; |
|
||||||
z-index: 10; |
|
||||||
box-shadow: 0 1px 4px rgba(0, 21, 41, .08); |
|
||||||
transition: all .2s; |
|
||||||
|
|
||||||
> div { |
|
||||||
display: flex; |
|
||||||
flex-wrap: nowrap; |
|
||||||
height: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
.navbar-icon { |
|
||||||
font-size: 18px; |
|
||||||
} |
|
||||||
|
|
||||||
.navbar-item { |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
padding: 0 8px; |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
.logo-container { |
|
||||||
width: auto; |
|
||||||
min-width: $aside-width - $navbar-padding-left; |
|
||||||
padding-left: $menu-padding - $navbar-padding-left; |
|
||||||
} |
|
||||||
|
|
||||||
.avatar-wrapper { |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
height: 100%; |
|
||||||
|
|
||||||
span { |
|
||||||
margin-right: 5px; |
|
||||||
font-size: 18px; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@import "theme-light"; |
|
||||||
@import "theme-dark"; |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
.navbar.dark { |
|
||||||
background-color: $root-menu-background-dark; |
|
||||||
|
|
||||||
.navbar-item { |
|
||||||
color: $navbar-item-color-dark; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
background-color: $navbar-item-hover-color-dark; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//暗色主题的下拉菜单 |
|
||||||
.dark.el-dropdown-menu { |
|
||||||
background-color: $root-menu-background-dark; |
|
||||||
border: none; |
|
||||||
|
|
||||||
.el-dropdown-menu__item { |
|
||||||
color: $menu-text-color-dark; |
|
||||||
|
|
||||||
&:hover, |
|
||||||
&:focus { |
|
||||||
background-color: unset; |
|
||||||
color: $menu-text-hover-color-dark; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,11 +0,0 @@ |
|||||||
.navbar.light { |
|
||||||
background-color: $--color-white; |
|
||||||
|
|
||||||
.navbar-item { |
|
||||||
color: $navbar-item-color-light; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
background-color: $navbar-item-hover-color-light; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,13 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
export default { |
|
||||||
functional: true, |
|
||||||
|
|
||||||
render() { |
|
||||||
return ( |
|
||||||
<el-backtop target=".page-main .scroll-container" visibility-height={400} bottom={66}> |
|
||||||
<v-icon icon="el-icon-top"/> |
|
||||||
</el-backtop> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,92 +0,0 @@ |
|||||||
<script> |
|
||||||
import {isEmpty} from "@/util" |
|
||||||
import {getRouterViewCacheKey} from "@/layout/util" |
|
||||||
|
|
||||||
const KEY = '_routerViewKey' |
|
||||||
|
|
||||||
function getFirstComponentChild(children) { |
|
||||||
if (Array.isArray(children)) { |
|
||||||
return children.find(c => !isEmpty(c) && (!isEmpty(c.componentOptions) || c.isComment && c.asyncFactory)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function removeCache(cache, key) { |
|
||||||
const cached = cache[key] |
|
||||||
cached && cached.componentInstance && cached.componentInstance.$destroy() |
|
||||||
delete cache[key] |
|
||||||
} |
|
||||||
|
|
||||||
function getCacheKey(route, componentOptions) { |
|
||||||
if (KEY in componentOptions) return componentOptions[KEY] |
|
||||||
|
|
||||||
const key = getRouterViewCacheKey(route) |
|
||||||
|
|
||||||
if (key) componentOptions[KEY] = key |
|
||||||
|
|
||||||
return key |
|
||||||
} |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'keep-router-view-alive', |
|
||||||
|
|
||||||
abstract: true, |
|
||||||
|
|
||||||
props: {include: Array}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
include: { |
|
||||||
deep: true, |
|
||||||
handler(val) { |
|
||||||
const {cache, $route} = this |
|
||||||
for (const [k, v] of Object.entries(cache)) { |
|
||||||
const cacheKey = getCacheKey($route, v.componentOptions) |
|
||||||
if (!val || !val.includes(cacheKey)) { |
|
||||||
removeCache(cache, k) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
created() { |
|
||||||
this.cache = Object.create(null) |
|
||||||
}, |
|
||||||
|
|
||||||
beforeDestroy() { |
|
||||||
for (const key of Object.keys(this.cache)) { |
|
||||||
removeCache(this.cache, key) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
const slot = this.$slots.default |
|
||||||
const vnode = getFirstComponentChild(slot) |
|
||||||
|
|
||||||
let componentOptions = vnode && vnode.componentOptions |
|
||||||
|
|
||||||
if (componentOptions) { |
|
||||||
//忽略<transition/>组件 |
|
||||||
const child = componentOptions.tag === 'transition' ? componentOptions.children[0] : vnode |
|
||||||
componentOptions = child && child.componentOptions |
|
||||||
|
|
||||||
if (componentOptions) { |
|
||||||
const key = getCacheKey(this.$route, componentOptions) |
|
||||||
const {include, cache} = this |
|
||||||
|
|
||||||
if (include && !include.includes(key)) { |
|
||||||
return vnode |
|
||||||
} |
|
||||||
|
|
||||||
if (cache[key]) { |
|
||||||
child.componentInstance = cache[key].componentInstance |
|
||||||
} |
|
||||||
else cache[key] = child |
|
||||||
|
|
||||||
child.data.keepAlive = true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return vnode || (slot && slot[0]) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,28 +0,0 @@ |
|||||||
<template functional> |
|
||||||
<footer class="page-footer"> |
|
||||||
<div class="copyright"> |
|
||||||
Copyright © 2020 - <a href="https://github.com/toesbieya" target="_blank">toesbieya</a> |
|
||||||
</div> |
|
||||||
</footer> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script> |
|
||||||
export default { |
|
||||||
name: "PageFooter" |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss"> |
|
||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.page-footer { |
|
||||||
height: $page-footer-height; |
|
||||||
padding: $page-view-margin; |
|
||||||
text-align: center; |
|
||||||
|
|
||||||
.copyright { |
|
||||||
color: #808695; |
|
||||||
font-size: 14px; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
<template> |
|
||||||
<div class="page-header"> |
|
||||||
<span class="page-header-title">{{ title }}</span> |
|
||||||
|
|
||||||
<el-breadcrumb> |
|
||||||
<el-breadcrumb-item v-for="(item,index) in data" :key="item.path"> |
|
||||||
<span :class="{'no-redirect': index !== data.length - 1}">{{ item.meta.title }}</span> |
|
||||||
</el-breadcrumb-item> |
|
||||||
</el-breadcrumb> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script> |
|
||||||
export default { |
|
||||||
name: "PageHeader", |
|
||||||
|
|
||||||
data: () => ({data: [], title: ''}), |
|
||||||
|
|
||||||
watch: { |
|
||||||
$route: { |
|
||||||
immediate: true, |
|
||||||
handler(to) { |
|
||||||
const {path, meta: {title}, matched} = to |
|
||||||
if (!path.startsWith('/redirect')) { |
|
||||||
this.title = title |
|
||||||
this.data = matched.filter(item => item.meta.title) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss"> |
|
||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.page-header { |
|
||||||
display: flex; |
|
||||||
justify-content: space-between; |
|
||||||
align-items: center; |
|
||||||
flex-wrap: nowrap; |
|
||||||
font-size: 14px; |
|
||||||
height: $page-header-height; |
|
||||||
padding: $page-header-padding $page-view-margin $page-header-padding $page-view-margin; |
|
||||||
|
|
||||||
> .el-breadcrumb .no-redirect { |
|
||||||
color: $--color-text-secondary; |
|
||||||
cursor: text; |
|
||||||
} |
|
||||||
|
|
||||||
&-title { |
|
||||||
color: $--color-text-primary; |
|
||||||
font-weight: 600; |
|
||||||
font-size: 18px; |
|
||||||
overflow: hidden; |
|
||||||
white-space: nowrap; |
|
||||||
text-overflow: ellipsis; |
|
||||||
} |
|
||||||
|
|
||||||
// 页头渲染时页面顶部margin为0 |
|
||||||
& + .page-view { |
|
||||||
margin-top: 0 !important; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -1,27 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import KeepRouterViewAlive from "./KeepRouterViewAlive" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: "PageView", |
|
||||||
|
|
||||||
functional: true, |
|
||||||
|
|
||||||
props: {include: Array, transitionName: String, cacheable: Boolean}, |
|
||||||
|
|
||||||
render(h, context) { |
|
||||||
const {include, transitionName, cacheable} = context.props |
|
||||||
|
|
||||||
let view = ( |
|
||||||
<transition name={transitionName} mode="out-in"> |
|
||||||
<router-view/> |
|
||||||
</transition> |
|
||||||
) |
|
||||||
|
|
||||||
if (cacheable) { |
|
||||||
view = <KeepRouterViewAlive include={include}>{view}</KeepRouterViewAlive> |
|
||||||
} |
|
||||||
|
|
||||||
return <div class="page-view">{view}</div> |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,82 +0,0 @@ |
|||||||
<template> |
|
||||||
<div class="tags-view-wrapper" @wheel="handleScroll"> |
|
||||||
<slot/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<script> |
|
||||||
import {scrollTo} from "@/util/browser" |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'ScrollPanel', |
|
||||||
|
|
||||||
props: { |
|
||||||
between: {type: Number, default: 4} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
handleScroll(e) { |
|
||||||
const eventDelta = e.wheelDelta || -(e.detail || 0) * 40 |
|
||||||
const {scrollLeft, scrollWidth, clientWidth} = this.$el |
|
||||||
if (eventDelta > 0 && scrollLeft === 0 |
|
||||||
|| eventDelta < 0 && scrollWidth <= scrollLeft + clientWidth) { |
|
||||||
return |
|
||||||
} |
|
||||||
this.smoothScroll(-eventDelta) |
|
||||||
}, |
|
||||||
|
|
||||||
smoothScroll(val) { |
|
||||||
if (this.rAF) { |
|
||||||
window.cancelAnimationFrame(this.rAF) |
|
||||||
this.rAf = null |
|
||||||
} |
|
||||||
|
|
||||||
this.rAf = scrollTo(this.$el, this.$el.scrollLeft + val, {direction: 'left', duration: 100}) |
|
||||||
}, |
|
||||||
|
|
||||||
moveToTarget(currentTag, tagInstances) { |
|
||||||
const {offsetWidth: containerWidth, scrollWidth, scrollLeft} = this.$el |
|
||||||
|
|
||||||
if (containerWidth >= scrollWidth) return |
|
||||||
|
|
||||||
let firstTag = null |
|
||||||
let lastTag = null |
|
||||||
|
|
||||||
// find first tag and last tag |
|
||||||
if (tagInstances.length > 0) { |
|
||||||
firstTag = tagInstances[0] |
|
||||||
lastTag = tagInstances[tagInstances.length - 1] |
|
||||||
} |
|
||||||
|
|
||||||
if (firstTag === currentTag) { |
|
||||||
return this.scrollLeft(0) |
|
||||||
} |
|
||||||
if (lastTag === currentTag) { |
|
||||||
return this.scrollLeft(scrollWidth - containerWidth) |
|
||||||
} |
|
||||||
|
|
||||||
// find preTag and nextTag |
|
||||||
const currentIndex = tagInstances.findIndex(item => item === currentTag) |
|
||||||
const prevTag = tagInstances[currentIndex - 1] |
|
||||||
const nextTag = tagInstances[currentIndex + 1] |
|
||||||
|
|
||||||
// the tag's offsetLeft after of nextTag |
|
||||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + this.between |
|
||||||
|
|
||||||
// the tag's offsetLeft before of prevTag |
|
||||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - this.between |
|
||||||
|
|
||||||
if (afterNextTagOffsetLeft > scrollLeft + containerWidth) { |
|
||||||
this.scrollLeft(afterNextTagOffsetLeft - containerWidth) |
|
||||||
} |
|
||||||
else if (beforePrevTagOffsetLeft < scrollLeft) { |
|
||||||
this.scrollLeft(beforePrevTagOffsetLeft) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
scrollLeft(val) { |
|
||||||
this.$el.scrollTo({left: val, behavior: 'smooth'}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
@ -1,207 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import shortcutMixin from '@/layout/mixin/tagsViewShortcut' |
|
||||||
import decideRouterTransitionMixin from '@/layout/mixin/decideRouterTransition' |
|
||||||
import persistenceMixin from '@/layout/mixin/tagsViewPersistent' |
|
||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView" |
|
||||||
import ContextMenu from "@/component/menu/ContextMenu" |
|
||||||
import ScrollPanel from './ScrollPanel' |
|
||||||
import {refreshPage} from "@/util/route" |
|
||||||
|
|
||||||
export default { |
|
||||||
mixins: [shortcutMixin, decideRouterTransitionMixin, persistenceMixin], |
|
||||||
|
|
||||||
components: {ContextMenu, ScrollPanel}, |
|
||||||
|
|
||||||
data() { |
|
||||||
return { |
|
||||||
contextmenu: { |
|
||||||
show: false, |
|
||||||
top: 0, |
|
||||||
left: 0, |
|
||||||
}, |
|
||||||
selectedTag: {} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
visitedViews: () => tagsViewGetters.visitedViews, |
|
||||||
|
|
||||||
menus: () => appGetters.menus, |
|
||||||
|
|
||||||
contextMenuItems() { |
|
||||||
return [ |
|
||||||
{content: '刷新', click: this.refreshSelectedTag}, |
|
||||||
this.isAffix(this.selectedTag) |
|
||||||
? undefined |
|
||||||
: { |
|
||||||
content: '关闭', |
|
||||||
click: () => this.closeSelectedTag(this.selectedTag) |
|
||||||
}, |
|
||||||
{content: '关闭其他', click: this.closeOthersTags}, |
|
||||||
{content: '关闭全部', click: this.closeAllTags} |
|
||||||
] |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
watch: { |
|
||||||
$route(to) { |
|
||||||
this.addTag(to) |
|
||||||
this.$nextTick(this.moveToCurrentTag) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
//判断页签是否激活,考虑redirect刷新的情况 |
|
||||||
isActive({path}) { |
|
||||||
const {path: routePath} = this.$route |
|
||||||
return routePath === path || routePath === `/redirect${path}` |
|
||||||
}, |
|
||||||
isAffix(tag) { |
|
||||||
return tag.meta && tag.meta.affix |
|
||||||
}, |
|
||||||
|
|
||||||
initTags() { |
|
||||||
//获取所有需要固定显示的页签 |
|
||||||
function getAffixTags(menus) { |
|
||||||
const tags = [] |
|
||||||
menus.forEach(({name, fullPath, children, meta}) => { |
|
||||||
if (meta && meta.title && meta.affix) { |
|
||||||
tags.push({ |
|
||||||
//注意,此处的fullPath并不是$route.fullPath,而是路由树拼接后的全路径 |
|
||||||
fullPath, |
|
||||||
path: fullPath, |
|
||||||
name, |
|
||||||
meta: {...meta} |
|
||||||
}) |
|
||||||
} |
|
||||||
if (children) { |
|
||||||
const tempTags = getAffixTags(children) |
|
||||||
tempTags.length && tags.push(...tempTags) |
|
||||||
} |
|
||||||
}) |
|
||||||
return tags |
|
||||||
} |
|
||||||
|
|
||||||
//添加所有固定显示的页签 |
|
||||||
for (const tag of getAffixTags(this.menus)) { |
|
||||||
tagsViewMutations.addTagOnly(tag) |
|
||||||
} |
|
||||||
|
|
||||||
//将当前路由对象添加为页签 |
|
||||||
this.addTag(this.$route) |
|
||||||
}, |
|
||||||
//将具有meta.title的路由对象添加为tab页 |
|
||||||
addTag(to) { |
|
||||||
to.meta.title && tagsViewMutations.addTagAndCache(to) |
|
||||||
}, |
|
||||||
|
|
||||||
//横向滚动条移动至当前tab |
|
||||||
moveToCurrentTag() { |
|
||||||
//获取所有页签的componentInstance |
|
||||||
const tagInstances = this.$refs.scrollPanel.$children |
|
||||||
const tag = tagInstances.find(i => this.isActive(i.to)) |
|
||||||
tag && this.$refs.scrollPanel.moveToTarget(tag, tagInstances) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 右键菜单选项 |
|
||||||
* 刷新所选、关闭所选、关闭其他、关闭所有 |
|
||||||
*/ |
|
||||||
refreshSelectedTag() { |
|
||||||
if (!this.selectedTag) return |
|
||||||
tagsViewMutations.delCacheOnly(this.selectedTag) |
|
||||||
this.$nextTick(() => refreshPage(this.selectedTag)) |
|
||||||
}, |
|
||||||
closeSelectedTag(view) { |
|
||||||
if (this.isAffix(view)) return |
|
||||||
tagsViewMutations.delTagAndCache(view) |
|
||||||
this.isActive(view) && this.gotoLastView() |
|
||||||
}, |
|
||||||
closeOthersTags() { |
|
||||||
tagsViewMutations.delOtherTagAndCache(this.selectedTag) |
|
||||||
//当前选中的页签不是当前路由时,跳转到选中页签的地址 |
|
||||||
if (this.selectedTag.path !== this.$route.path) { |
|
||||||
return this.$router.push(this.selectedTag) |
|
||||||
} |
|
||||||
}, |
|
||||||
closeAllTags() { |
|
||||||
tagsViewMutations.delAllTagAndCache() |
|
||||||
this.gotoLastView() |
|
||||||
}, |
|
||||||
|
|
||||||
gotoLastView() { |
|
||||||
if (this.visitedViews.length === 0) { |
|
||||||
return this.$router.push('/') |
|
||||||
} |
|
||||||
const latest = this.visitedViews[this.visitedViews.length - 1] |
|
||||||
//只有当页签路径与当前路由路径不同时才跳转,否则刷新 |
|
||||||
if (this.$route.path !== latest.path) { |
|
||||||
this.$router.push(latest.path) |
|
||||||
} |
|
||||||
else refreshPage() |
|
||||||
}, |
|
||||||
|
|
||||||
openContextMenu(e, tag) { |
|
||||||
e.preventDefault() |
|
||||||
|
|
||||||
const contextMenuWidth = 105 //右键菜单的假定宽度 |
|
||||||
const {left: elLeft, width: elWidth} = this.$el.getBoundingClientRect() |
|
||||||
const maxLeft = elWidth + elLeft - contextMenuWidth |
|
||||||
const left = e.clientX |
|
||||||
|
|
||||||
this.contextmenu.left = (left > maxLeft ? maxLeft : left) + 15 |
|
||||||
this.contextmenu.top = e.clientY |
|
||||||
this.contextmenu.show = true |
|
||||||
|
|
||||||
this.selectedTag = tag |
|
||||||
}, |
|
||||||
|
|
||||||
renderTags() { |
|
||||||
return this.visitedViews.map(tag => { |
|
||||||
const {path, query, fullPath, meta: {title}} = tag |
|
||||||
const active = this.isActive(tag), affix = this.isAffix(tag) |
|
||||||
const closeSelectedTag = (e, tag) => { |
|
||||||
e.preventDefault() |
|
||||||
this.closeSelectedTag(tag) |
|
||||||
} |
|
||||||
return ( |
|
||||||
<router-link |
|
||||||
key={path} |
|
||||||
class={{'tags-view-item': true, active}} |
|
||||||
to={{path, query, fullPath}} |
|
||||||
v-on:contextmenu_native={e => this.openContextMenu(e, tag)} |
|
||||||
v-on:dblclick_native={e => closeSelectedTag(e, tag)} |
|
||||||
> |
|
||||||
<span>{title}</span> |
|
||||||
{!affix && <i class="el-icon-close" on-click={e => closeSelectedTag(e, tag)}/>} |
|
||||||
</router-link> |
|
||||||
) |
|
||||||
}) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
mounted() { |
|
||||||
this.initTags() |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
return ( |
|
||||||
<div class="tags-view-container"> |
|
||||||
<scroll-panel ref="scrollPanel" class="tags-view-wrapper"> |
|
||||||
{this.renderTags()} |
|
||||||
</scroll-panel> |
|
||||||
|
|
||||||
<context-menu |
|
||||||
v-model={this.contextmenu.show} |
|
||||||
items={this.contextMenuItems} |
|
||||||
left={this.contextmenu.left} |
|
||||||
top={this.contextmenu.top} |
|
||||||
/> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss"></style> |
|
||||||
@ -1,68 +0,0 @@ |
|||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.tags-view-container { |
|
||||||
height: $tags-view-height; |
|
||||||
box-shadow: inset 0 0 3px 2px hsla(0, 0%, 39.2%, .1); |
|
||||||
|
|
||||||
.tags-view-wrapper { |
|
||||||
height: 100%; |
|
||||||
white-space: nowrap; |
|
||||||
overflow-x: auto; |
|
||||||
|
|
||||||
&::-webkit-scrollbar { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
|
|
||||||
.tags-view-item { |
|
||||||
$margin: 4px; |
|
||||||
$border-width: 1px; |
|
||||||
|
|
||||||
display: inline-block; |
|
||||||
cursor: pointer; |
|
||||||
height: #{$tags-view-height - $margin * 2}; |
|
||||||
line-height: #{$tags-view-height - $margin * 2 - $border-width * 2}; |
|
||||||
color: $--color-text-secondary; |
|
||||||
background-color: #ffffff; |
|
||||||
border-radius: 3px; |
|
||||||
border: $border-width solid $--border-color-base; |
|
||||||
padding: 0 12px; |
|
||||||
font-size: 12px; |
|
||||||
margin: $margin; |
|
||||||
user-select: none; |
|
||||||
|
|
||||||
&:first-of-type { |
|
||||||
margin-left: 15px; |
|
||||||
} |
|
||||||
|
|
||||||
&:last-of-type { |
|
||||||
margin-right: 15px; |
|
||||||
} |
|
||||||
|
|
||||||
&::before { |
|
||||||
content: ''; |
|
||||||
position: relative; |
|
||||||
background-color: #e8eaec; |
|
||||||
display: inline-block; |
|
||||||
width: 12px; |
|
||||||
height: 12px; |
|
||||||
border-radius: 50%; |
|
||||||
top: 1px; |
|
||||||
margin-right: 5px; |
|
||||||
} |
|
||||||
|
|
||||||
&.active { |
|
||||||
color: $--color-primary; |
|
||||||
|
|
||||||
&::before { |
|
||||||
background-color: $--color-primary; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.el-icon-close { |
|
||||||
border-radius: 50%; |
|
||||||
font-size: 14px; |
|
||||||
margin-left: 10px; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,67 +0,0 @@ |
|||||||
<script type="text/jsx"> |
|
||||||
import {getters as pageGetters} from "@/layout/store/page" |
|
||||||
import {getters as tagsViewGetters} from "@/layout/store/tagsView" |
|
||||||
import BackToTop from "./component/BackToTop" |
|
||||||
import PageHeader from "./component/PageHeader" |
|
||||||
import PageView from "./component/PageView" |
|
||||||
import PageFooter from "./component/PageFooter" |
|
||||||
import TagsView from './component/TagsView' |
|
||||||
|
|
||||||
export default { |
|
||||||
name: 'Page', |
|
||||||
|
|
||||||
components: {BackToTop, PageHeader, PageView, PageFooter, TagsView}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
renderPageHeader() { |
|
||||||
return pageGetters.showPageHeader && this.$route.meta.pageHeader !== false |
|
||||||
}, |
|
||||||
pageClass() { |
|
||||||
return { |
|
||||||
'scroll-container': true, |
|
||||||
'has-page-header': this.renderPageHeader, |
|
||||||
'has-page-footer': true |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
const {transition, showIframe, iframeList, currentIframe} = pageGetters |
|
||||||
const {cachedViews, enabled} = tagsViewGetters |
|
||||||
|
|
||||||
return ( |
|
||||||
<main class="page-main"> |
|
||||||
{enabled && <tags-view/>} |
|
||||||
|
|
||||||
<div v-show={!showIframe} class={this.pageClass}> |
|
||||||
{this.renderPageHeader && <page-header/>} |
|
||||||
|
|
||||||
<page-view |
|
||||||
include={cachedViews} |
|
||||||
transition-name={transition.curr} |
|
||||||
cacheable={enabled} |
|
||||||
/> |
|
||||||
|
|
||||||
<page-footer/> |
|
||||||
</div> |
|
||||||
|
|
||||||
{iframeList.map(src => ( |
|
||||||
<iframe |
|
||||||
v-show={showIframe && src === currentIframe} |
|
||||||
id={src} |
|
||||||
key={src} |
|
||||||
src={src} |
|
||||||
frameBorder="0" |
|
||||||
height="100%" |
|
||||||
width="100%" |
|
||||||
/> |
|
||||||
))} |
|
||||||
|
|
||||||
{pageGetters.showBackToTop && <back-to-top/>} |
|
||||||
</main> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss"></style> |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
@import "~@/asset/style/var"; |
|
||||||
|
|
||||||
.page-main { |
|
||||||
display: flex; |
|
||||||
flex-direction: column; |
|
||||||
flex: 1; |
|
||||||
position: relative; |
|
||||||
overflow: hidden; |
|
||||||
background-color: #f0f2f5; |
|
||||||
|
|
||||||
> .scroll-container { |
|
||||||
display: flex; |
|
||||||
flex: 1; |
|
||||||
flex-direction: column; |
|
||||||
overflow-y: overlay; |
|
||||||
overflow-x: inherit; |
|
||||||
|
|
||||||
> .page-view { |
|
||||||
margin: $page-view-margin; |
|
||||||
flex: 1; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,15 @@ |
|||||||
|
<template> |
||||||
|
<div class="color-checkbox" :style="{'background-color': value}" @click="click"> |
||||||
|
<i v-if="checked" class="el-icon-check"/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import mixin from './mixin' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "ColorCheckBox", |
||||||
|
|
||||||
|
mixins: [mixin] |
||||||
|
} |
||||||
|
</script> |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
<template> |
||||||
|
<div class="checkbox-group"> |
||||||
|
<slot/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "CheckboxGroup", |
||||||
|
|
||||||
|
props: {value: String}, |
||||||
|
|
||||||
|
data: () => ({children: []}) |
||||||
|
} |
||||||
|
</script> |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
<template> |
||||||
|
<div class="img-checkbox" :title="label" @click="click"> |
||||||
|
<img :src="img"/> |
||||||
|
<i v-if="checked" class="el-icon-check"/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import mixin from './mixin' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "ImgCheckBox", |
||||||
|
|
||||||
|
mixins: [mixin], |
||||||
|
|
||||||
|
props: { |
||||||
|
img: String, |
||||||
|
label: String |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
@import "~@/asset/style/var"; |
||||||
|
|
||||||
|
.checkbox-group { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.color-checkbox { |
||||||
|
width: 20px; |
||||||
|
height: 20px; |
||||||
|
margin: 5px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
text-align: center; |
||||||
|
font-weight: bold; |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
.img-checkbox { |
||||||
|
width: 55px; |
||||||
|
margin: 10px; |
||||||
|
border-radius: 2px; |
||||||
|
cursor: pointer; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
.el-icon-check { |
||||||
|
position: absolute; |
||||||
|
color: $--color-primary; |
||||||
|
font-weight: bold; |
||||||
|
top: 25px; |
||||||
|
right: 10px; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
@import "~@/asset/style/var"; |
||||||
|
@import "./component/checkbox/style"; |
||||||
|
|
||||||
|
.drawer-container { |
||||||
|
padding: 24px; |
||||||
|
font-size: 14px; |
||||||
|
line-height: 1.5; |
||||||
|
word-wrap: break-word; |
||||||
|
|
||||||
|
.drawer-item { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
color: $--color-text-regular; |
||||||
|
font-size: 14px; |
||||||
|
padding: 12px 0; |
||||||
|
} |
||||||
|
} |
||||||
@ -1,90 +1,101 @@ |
|||||||
|
<template> |
||||||
|
<el-admin-layout :navbar-props="navbarProps" :page-props="pageProps"/> |
||||||
|
</template> |
||||||
|
|
||||||
<script type="text/jsx"> |
<script type="text/jsx"> |
||||||
import VAside from './component/Aside' |
import Vue from 'vue' |
||||||
import VNavbar from './component/Navbar' |
import config from "@/config" |
||||||
import VPage from './component/Page' |
import ElAdminLayout, {setIconRenderer, appMutations} from 'el-admin-layout' |
||||||
|
import Footer from './component/Footer' |
||||||
import SettingDrawer from './component/SettingDrawer' |
import SettingDrawer from './component/SettingDrawer' |
||||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
import tagsViewPersistent from './mixin/tagsViewPersistent' |
||||||
import {getters as pageGetters} from "@/layout/store/page" |
import tagsViewShortcut from './mixin/tagsViewShortcut' |
||||||
import {getters as tagsViewGetters} from "@/layout/store/tagsView" |
import {elConfirm} from "@/util/message" |
||||||
|
|
||||||
|
//设置一些基础信息 |
||||||
|
appMutations.title(config.title) |
||||||
|
appMutations.logo(config.logo) |
||||||
|
|
||||||
|
//设置图标渲染方式 |
||||||
|
setIconRenderer((h, data) => <v-icon {...data}/>) |
||||||
|
|
||||||
export default { |
export default { |
||||||
name: 'Layout', |
name: "Layout", |
||||||
|
|
||||||
components: {VAside, VNavbar, VPage, SettingDrawer}, |
mixins: [tagsViewPersistent, tagsViewShortcut], |
||||||
|
|
||||||
|
components: {ElAdminLayout}, |
||||||
|
|
||||||
computed: { |
computed: { |
||||||
isLeftRight() { |
navbarProps() { |
||||||
return pageGetters.position === 'left-right' |
const userInfo = this.$store.state.user |
||||||
|
return { |
||||||
|
user: { |
||||||
|
avatar: userInfo.avatar, |
||||||
|
name: userInfo.name |
||||||
}, |
}, |
||||||
renderAside() { |
userDropdownItems: [ |
||||||
return appGetters.isMobile || ['aside', 'aside-two-part', 'mix'].includes(appGetters.navMode) |
{ |
||||||
|
icon: 'el-icon-switch-button', |
||||||
|
command: 'logout', |
||||||
|
content: '退出登录', |
||||||
|
handler: this.logout |
||||||
|
} |
||||||
|
], |
||||||
|
renderCustomActions: this.renderNavbarActions |
||||||
|
} |
||||||
}, |
}, |
||||||
wrapperClass() { |
|
||||||
|
pageProps() { |
||||||
return { |
return { |
||||||
'app-wrapper': true, |
renderFooter: () => <Footer/> |
||||||
'flex-column': !this.isLeftRight |
|
||||||
} |
} |
||||||
}, |
|
||||||
containerClass() { |
|
||||||
return [ |
|
||||||
'main-container', |
|
||||||
'has-nav', |
|
||||||
`nav-mode-${appGetters.navMode}`, |
|
||||||
this.isLeftRight && 'flex-column', |
|
||||||
tagsViewGetters.enabled && 'has-tags-view' |
|
||||||
] |
|
||||||
} |
} |
||||||
}, |
}, |
||||||
|
|
||||||
methods: { |
methods: { |
||||||
closeSettingDrawer() { |
logout() { |
||||||
appMutations.showSettingDrawer(false) |
if (this.$store.state.user.prepareLogout) { |
||||||
|
return |
||||||
} |
} |
||||||
}, |
|
||||||
|
|
||||||
render() { |
|
||||||
const aside = this.renderAside && <v-aside/> |
|
||||||
|
|
||||||
return ( |
elConfirm('确认退出?').then(() => this.$store.dispatch('user/logout')) |
||||||
<section class={this.wrapperClass}> |
}, |
||||||
{this.isLeftRight ? aside : <v-navbar/>} |
|
||||||
|
|
||||||
<section class={this.containerClass}> |
openSettingDrawer() { |
||||||
{this.isLeftRight ? <v-navbar/> : aside} |
appMutations.showSettingDrawer(true) |
||||||
<v-page/> |
}, |
||||||
</section> |
|
||||||
|
|
||||||
<setting-drawer |
renderNavbarActions(defaultActions) { |
||||||
value={appGetters.showSettingDrawer} |
const customActions = [ |
||||||
on-input={this.closeSettingDrawer} |
<div |
||||||
/> |
class="setting-btn navbar-item" |
||||||
</section> |
title="个性设置" |
||||||
) |
on-click={this.openSettingDrawer} |
||||||
} |
> |
||||||
} |
<i class="el-icon-s-operation navbar-icon"/> |
||||||
</script> |
</div> |
||||||
|
] |
||||||
|
|
||||||
<style lang="scss"> |
return customActions.concat(defaultActions) |
||||||
.app-wrapper.flex-column, |
|
||||||
.main-container.flex-column { |
|
||||||
flex-direction: column; |
|
||||||
} |
} |
||||||
|
}, |
||||||
|
|
||||||
.app-wrapper { |
//提前创建设置抽屉,避免初始化同步设置数据时导致layout重新渲染 |
||||||
display: flex; |
beforeCreate() { |
||||||
height: 100%; |
const Drawer = Vue.extend(SettingDrawer) |
||||||
width: 100%; |
const instance = new Drawer().$mount() |
||||||
|
document.body.appendChild(instance.$el) |
||||||
> .main-container { |
this.$_settingDrawerInstance = instance |
||||||
display: flex; |
}, |
||||||
flex: 1; |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
//左右结构时,侧边栏z-index需大于导航栏 |
//销毁时清除设置抽屉 |
||||||
//上下结构时,侧边栏z-index需小于导航栏 |
beforeDestroy() { |
||||||
&.flex-column .aside { |
if (this.$_settingDrawerInstance) { |
||||||
z-index: 9; |
this.$_settingDrawerInstance.$destroy() |
||||||
|
this.$_settingDrawerInstance.$el.remove() |
||||||
|
delete this.$_settingDrawerInstance |
||||||
} |
} |
||||||
|
}, |
||||||
} |
} |
||||||
</style> |
</script> |
||||||
|
|||||||
@ -1,29 +0,0 @@ |
|||||||
import {getters as pageGetters, mutations as pageViewMutations} from "@/layout/store/page" |
|
||||||
|
|
||||||
export default { |
|
||||||
watch: { |
|
||||||
$route(to, from) { |
|
||||||
this.decideRouteTransition(to, from) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
//根据访问的tab页的左右顺序来确定路由动画
|
|
||||||
decideRouteTransition(to, from) { |
|
||||||
const {next, prev} = pageGetters.transition |
|
||||||
|
|
||||||
let transitionName = prev |
|
||||||
|
|
||||||
//这里认为页签数量不会太多,所以为了可读性使用两次循环查找
|
|
||||||
const fromIndex = this.visitedViews.findIndex(i => i.path === from.path) |
|
||||||
const toIndex = this.visitedViews.findIndex(i => i.path === to.path) |
|
||||||
|
|
||||||
//新开tab也认为顺序高于上一个tab
|
|
||||||
if (toIndex === -1 || fromIndex < toIndex) { |
|
||||||
transitionName = next |
|
||||||
} |
|
||||||
|
|
||||||
pageViewMutations.transition({curr: transitionName}) |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,33 +0,0 @@ |
|||||||
import Hamburger from '@/layout/component/Hamburger' |
|
||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
import {getters as asideGetters} from "@/layout/store/aside" |
|
||||||
import {getSidebarMenus} from "@/layout/util" |
|
||||||
|
|
||||||
/** |
|
||||||
* 汉堡包的渲染条件混入 |
|
||||||
*/ |
|
||||||
|
|
||||||
export default { |
|
||||||
components: {Hamburger}, |
|
||||||
|
|
||||||
computed: { |
|
||||||
//渲染汉堡包的条件
|
|
||||||
//①侧边栏有菜单
|
|
||||||
//②移动端 或
|
|
||||||
//③桌面端且汉堡包位置正确(侧边栏引入则需要是aside,反之需要是head)
|
|
||||||
//④桌面端且是双层侧边栏导航
|
|
||||||
//⑤桌面端且是侧边栏导航或混合导航时,未设置侧边栏自动隐藏
|
|
||||||
renderHamburger() { |
|
||||||
if (getSidebarMenus().length <= 0) return false |
|
||||||
|
|
||||||
const isMobile = appGetters.isMobile, |
|
||||||
correctPosition = |
|
||||||
asideGetters.hamburgerPosition === (this.$options.name === 'navbar' ? 'head' : 'aside'), |
|
||||||
correctMode = |
|
||||||
['aside', 'mix'].includes(appGetters.navMode) && !asideGetters.autoHide |
|
||||||
|| appGetters.navMode === 'aside-two-part' |
|
||||||
|
|
||||||
return isMobile || correctPosition && correctMode |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,56 +0,0 @@ |
|||||||
/** |
|
||||||
* 顶部菜单和侧边栏菜单的公共混入 |
|
||||||
*/ |
|
||||||
import {mutations as tagsViewMutations} from "@/layout/store/tagsView" |
|
||||||
import {refreshPage} from "@/util/route" |
|
||||||
import {isExternal} from "@/util/validate" |
|
||||||
import {findComponentByTag} from "@/util/vue" |
|
||||||
|
|
||||||
export default { |
|
||||||
data() { |
|
||||||
return { |
|
||||||
//当前激活的菜单的fullPath
|
|
||||||
//之所以手动维护是因为el-menu在点击后就会设置activeIndex
|
|
||||||
activeMenu: '', |
|
||||||
|
|
||||||
//用于判断鼠标是否在弹出菜单内
|
|
||||||
openedMenuNum: 0 |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
//点击菜单后的动作
|
|
||||||
actionOnSelectMenu(fullPath, refreshWhenSame = true) { |
|
||||||
//外部链接时打开新窗口
|
|
||||||
if (isExternal(fullPath)) { |
|
||||||
window.open(fullPath) |
|
||||||
return this.resetActiveMenu() |
|
||||||
} |
|
||||||
|
|
||||||
//触发的菜单路径是当前路由时,根据参数判断是否进行刷新
|
|
||||||
if (this.$route.path === fullPath) { |
|
||||||
if (!refreshWhenSame) return |
|
||||||
tagsViewMutations.delCacheOnly(this.$route) |
|
||||||
this.$nextTick(() => refreshPage()) |
|
||||||
} |
|
||||||
else this.$router.push(fullPath) |
|
||||||
}, |
|
||||||
|
|
||||||
//由于侧边栏菜单数组更新后,el-menu不一定会更新(当数组中不存在单级菜单时)
|
|
||||||
//所以手动更新el-menu的当前高亮菜单
|
|
||||||
resetActiveMenu() { |
|
||||||
const menu = this.$_getElMenuInstance() |
|
||||||
menu && menu.updateActiveIndex(this.activeMenu) |
|
||||||
}, |
|
||||||
|
|
||||||
//获取el-menu实例
|
|
||||||
//目前被侧边栏和双层侧边栏的主菜单使用
|
|
||||||
$_getElMenuInstance() { |
|
||||||
if (!this.$_elMenuInstance) { |
|
||||||
this.$_elMenuInstance = findComponentByTag(this, 'el-menu') |
|
||||||
} |
|
||||||
|
|
||||||
return this.$_elMenuInstance |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,24 +0,0 @@ |
|||||||
/** |
|
||||||
* 侧边栏搜索框混入 |
|
||||||
*/ |
|
||||||
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) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,83 +0,0 @@ |
|||||||
import Vue from 'vue' |
|
||||||
import {debounce, isEmpty} from "@/util" |
|
||||||
import {isMobile} from "@/util/browser" |
|
||||||
import {createGetters, createMutations} from "@/util/observable" |
|
||||||
|
|
||||||
const state = { |
|
||||||
//区分pc和移动端
|
|
||||||
isMobile: isMobile(), |
|
||||||
|
|
||||||
//设置抽屉的显隐
|
|
||||||
//放这里的原因是navbar可能会重新渲染,从而导致抽屉的重新渲染
|
|
||||||
showSettingDrawer: false, |
|
||||||
|
|
||||||
//当前激活的顶部菜单的fullPath
|
|
||||||
activeRootMenu: '', |
|
||||||
|
|
||||||
//所有的树形菜单,每个元素为顶部菜单,顶部菜单的子级(如果有)为侧边栏菜单
|
|
||||||
menus: [], |
|
||||||
|
|
||||||
//导航模式,'aside'、'aside-two-part'、'head'、'mix'
|
|
||||||
navMode: 'mix' |
|
||||||
} |
|
||||||
|
|
||||||
const store = Vue.observable(state) |
|
||||||
|
|
||||||
export const getters = createGetters(store) |
|
||||||
|
|
||||||
export const mutations = { |
|
||||||
...createMutations(store), |
|
||||||
|
|
||||||
menus(v) { |
|
||||||
sort(v) |
|
||||||
store.menus = v |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//菜单排序
|
|
||||||
function sort(routes) { |
|
||||||
if (!Array.isArray(routes) || routes.length === 0) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
//菜单排序值的空值处理
|
|
||||||
const getSortValue = item => { |
|
||||||
const sort = deepGetSortValue(item) |
|
||||||
return isEmpty(sort) ? 10000 : sort |
|
||||||
} |
|
||||||
|
|
||||||
//获取菜单的排序值
|
|
||||||
const deepGetSortValue = item => { |
|
||||||
const {children = [], meta: {hidden, sort} = {}} = item |
|
||||||
|
|
||||||
if (hidden) return null |
|
||||||
|
|
||||||
if (!isEmpty(sort)) return sort |
|
||||||
|
|
||||||
//如果只有一个子节点,那么取子节点的排序值
|
|
||||||
if (children.length === 1) { |
|
||||||
return deepGetSortValue(children[0]) |
|
||||||
} |
|
||||||
|
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
//对根节点排序
|
|
||||||
routes.sort((pre, next) => { |
|
||||||
pre = getSortValue(pre) |
|
||||||
next = getSortValue(next) |
|
||||||
if (pre < next) return -1 |
|
||||||
if (pre === next) return 0 |
|
||||||
return 1 |
|
||||||
}) |
|
||||||
|
|
||||||
//对每一个根节点的子级排序
|
|
||||||
routes.forEach(route => { |
|
||||||
const {children} = route |
|
||||||
children && children.length && sort(children) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
window.addEventListener('resize', debounce(() => { |
|
||||||
!document.hidden && mutations.isMobile(isMobile()) |
|
||||||
})) |
|
||||||
@ -1,71 +0,0 @@ |
|||||||
/** |
|
||||||
* 侧边栏的响应式数据 |
|
||||||
*/ |
|
||||||
import Vue from 'vue' |
|
||||||
import {bindThis} from "@/util" |
|
||||||
import {createGetters, createMutations} from "@/util/observable" |
|
||||||
import {getters as appGetters} from "./app" |
|
||||||
|
|
||||||
const state = { |
|
||||||
//抽屉模式时的显隐
|
|
||||||
show: false, |
|
||||||
|
|
||||||
//主题,light 或 dark
|
|
||||||
theme: 'light', |
|
||||||
|
|
||||||
//手风琴效果
|
|
||||||
uniqueOpen: true, |
|
||||||
|
|
||||||
//是否折叠
|
|
||||||
collapse: false, |
|
||||||
|
|
||||||
//折叠时显示上级
|
|
||||||
showParentOnCollapse: false, |
|
||||||
|
|
||||||
//自动隐藏
|
|
||||||
autoHide: false, |
|
||||||
|
|
||||||
//汉堡包的位置,aside 或 head
|
|
||||||
hamburgerPosition: 'aside', |
|
||||||
|
|
||||||
//是否显示搜索框
|
|
||||||
search: true |
|
||||||
} |
|
||||||
|
|
||||||
const store = Vue.observable(state) |
|
||||||
|
|
||||||
export const getters = createGetters(store) |
|
||||||
|
|
||||||
export const mutations = bindThis({ |
|
||||||
...createMutations(store), |
|
||||||
|
|
||||||
/*移动端或设置了侧边栏自动隐藏时打开关闭抽屉,否则展开折叠*/ |
|
||||||
open() { |
|
||||||
if (appGetters.isMobile || store.autoHide) { |
|
||||||
store.show = true |
|
||||||
} |
|
||||||
else store.collapse = false |
|
||||||
}, |
|
||||||
close() { |
|
||||||
if (appGetters.isMobile || store.autoHide) { |
|
||||||
store.show = false |
|
||||||
} |
|
||||||
else store.collapse = true |
|
||||||
}, |
|
||||||
//切换侧边栏的状态
|
|
||||||
switch(action) { |
|
||||||
switch (action) { |
|
||||||
case 'open': |
|
||||||
return this.open() |
|
||||||
case 'close': |
|
||||||
return this.close() |
|
||||||
default : |
|
||||||
let open = true |
|
||||||
if (appGetters.isMobile) { |
|
||||||
open = !store.show |
|
||||||
} |
|
||||||
else open = store.collapse |
|
||||||
return open ? this.open() : this.close() |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
@ -1,13 +0,0 @@ |
|||||||
import Vue from 'vue' |
|
||||||
import {createGetters, createMutations} from "@/util/observable" |
|
||||||
|
|
||||||
const state = { |
|
||||||
//主题,light 或 dark
|
|
||||||
theme: 'light', |
|
||||||
} |
|
||||||
|
|
||||||
const store = Vue.observable(state) |
|
||||||
|
|
||||||
export const getters = createGetters(store) |
|
||||||
|
|
||||||
export const mutations = createMutations(store) |
|
||||||
@ -1,65 +0,0 @@ |
|||||||
/** |
|
||||||
* 路由页面的响应式数据 |
|
||||||
*/ |
|
||||||
import Vue from 'vue' |
|
||||||
import {bindThis} from "@/util" |
|
||||||
import {createGetters, createMutations} from "@/util/observable" |
|
||||||
|
|
||||||
const state = { |
|
||||||
//路由过渡动画
|
|
||||||
transition: { |
|
||||||
//当未启用多页签时的路由动画
|
|
||||||
default: 'el-fade-in-linear', |
|
||||||
//要访问的tab顺序高于上一个访问的tab时的路由动画
|
|
||||||
next: 'left-out', |
|
||||||
//要访问的tab顺序不高于上一个访问的tab时的路由动画
|
|
||||||
prev: 'right-out', |
|
||||||
//当前使用的路由动画
|
|
||||||
curr: 'el-fade-in-linear' |
|
||||||
}, |
|
||||||
|
|
||||||
/*iframe的控制*/ |
|
||||||
showIframe: false, |
|
||||||
currentIframe: '', |
|
||||||
iframeList: [], |
|
||||||
|
|
||||||
//是否显示侧边栏或顶部导航栏的logo
|
|
||||||
showLogo: true, |
|
||||||
//分层结构,上下('top-bottom')、左右('left-right')
|
|
||||||
position: 'left-right', |
|
||||||
//是否显示页头
|
|
||||||
showPageHeader: true, |
|
||||||
//是否显示返回顶部按钮
|
|
||||||
showBackToTop: true |
|
||||||
} |
|
||||||
|
|
||||||
const store = Vue.observable(state) |
|
||||||
|
|
||||||
export const getters = createGetters(store) |
|
||||||
|
|
||||||
export const mutations = bindThis({ |
|
||||||
...createMutations(store), |
|
||||||
|
|
||||||
//修改transition时使用Object.assign
|
|
||||||
transition(obj) { |
|
||||||
Object.assign(store.transition, obj) |
|
||||||
}, |
|
||||||
|
|
||||||
addIframe(src) { |
|
||||||
!store.iframeList.includes(src) && store.iframeList.push(src) |
|
||||||
}, |
|
||||||
delIframe(src) { |
|
||||||
const index = store.iframeList.findIndex(i => i === src) |
|
||||||
index > -1 && store.iframeList.splice(index, 1) |
|
||||||
}, |
|
||||||
openIframe({src}) { |
|
||||||
store.showIframe = true |
|
||||||
store.currentIframe = src |
|
||||||
this.addIframe(src) |
|
||||||
}, |
|
||||||
closeIframe({src, del}) { |
|
||||||
store.showIframe = false |
|
||||||
store.currentIframe = '' |
|
||||||
del && this.delIframe(src) |
|
||||||
} |
|
||||||
}) |
|
||||||
@ -1,141 +0,0 @@ |
|||||||
/** |
|
||||||
* 多页签的响应式数据 |
|
||||||
*/ |
|
||||||
import Vue from 'vue' |
|
||||||
import {getters as pageGetters, mutations as pageMutations} from "@/layout/store/page" |
|
||||||
import {getRouterViewCacheKey} from "@/layout/util" |
|
||||||
import {bindThis} from "@/util" |
|
||||||
import {createGetters, createMutations} from "@/util/observable" |
|
||||||
|
|
||||||
const state = { |
|
||||||
//是否启用
|
|
||||||
enabled: true, |
|
||||||
//是否启用快捷键切换功能
|
|
||||||
shortcut: true, |
|
||||||
//是否将页签持久化到sessionStorage
|
|
||||||
persistent: true, |
|
||||||
|
|
||||||
//显示的页签,vue-router的routeConfig对象数组
|
|
||||||
visitedViews: [], |
|
||||||
|
|
||||||
//缓存的页签,用于<keep-router-view-alive/>:include
|
|
||||||
cachedViews: [] |
|
||||||
} |
|
||||||
|
|
||||||
const store = Vue.observable(state) |
|
||||||
|
|
||||||
export const getters = createGetters(store) |
|
||||||
|
|
||||||
export const mutations = bindThis({ |
|
||||||
...createMutations(store), |
|
||||||
|
|
||||||
/** |
|
||||||
* 多页签的启用/停用 |
|
||||||
* 停用时会移除所有缓存,并且重置路由过渡动画 |
|
||||||
* @param v 启用为true,停用为false |
|
||||||
*/ |
|
||||||
enabled(v) { |
|
||||||
store.enabled = v |
|
||||||
|
|
||||||
if (!v) { |
|
||||||
pageMutations.transition({curr: pageGetters.transition.default}) |
|
||||||
this.delAllTagAndCache() |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 在页签栏上添加一个页签,path已存在的不会重复添加,调用时需要保证meta.title有值 |
|
||||||
* @param view {routeConfig} |
|
||||||
*/ |
|
||||||
addTagOnly(view) { |
|
||||||
const {name, path, fullPath, meta} = view |
|
||||||
|
|
||||||
if (store.visitedViews.some(v => v.path === path)) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
store.visitedViews.push({name, path, fullPath, meta: {...meta}}) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 将传入的routeConfig加入<keep-router-view-alive>的缓存中 |
|
||||||
* 以下调用无效:设置了不缓存、是iframe页、未设置唯一标识、已缓存 |
|
||||||
* @param view {routeConfig} |
|
||||||
*/ |
|
||||||
addCacheOnly(view) { |
|
||||||
const {noCache, iframe, usePathKey, useFullPathKey} = view.meta || {} |
|
||||||
|
|
||||||
if (noCache || iframe || !view.name && !usePathKey && !useFullPathKey) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
const key = getRouterViewCacheKey(view) |
|
||||||
|
|
||||||
if (store.cachedViews.includes(key)) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
store.cachedViews.push(key) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 同时调用{@link #addTagOnly}、{@link #addCacheOnly} |
|
||||||
* @param view {routeConfig} |
|
||||||
*/ |
|
||||||
addTagAndCache(view) { |
|
||||||
this.addTagOnly(view) |
|
||||||
this.addCacheOnly(view) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 根据path从页签栏中移除一个页签 |
|
||||||
* @param view {path},routeConfig |
|
||||||
*/ |
|
||||||
delTagOnly(view) { |
|
||||||
const index = store.visitedViews.findIndex(i => i.path === view.path) |
|
||||||
index > -1 && store.visitedViews.splice(index, 1) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 删除<keep-router-view-alive>对应的缓存 |
|
||||||
* @param view {routeConfig} |
|
||||||
*/ |
|
||||||
delCacheOnly(view) { |
|
||||||
const key = getRouterViewCacheKey(view) |
|
||||||
const index = store.cachedViews.indexOf(key) |
|
||||||
index > -1 && store.cachedViews.splice(index, 1) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 同时调用{@link #delTagOnly}、{@link #delCacheOnly},移除iframe页 |
|
||||||
* @param view {routeConfig} |
|
||||||
*/ |
|
||||||
delTagAndCache(view) { |
|
||||||
this.delTagOnly(view) |
|
||||||
this.delCacheOnly(view) |
|
||||||
|
|
||||||
const iframe = view.meta && view.meta.iframe |
|
||||||
iframe && pageMutations.delIframe(iframe) |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 从页签栏上移除除了routeConfig以外的所有非固定页签 |
|
||||||
* 并且从<keep-router-view-alive>中移除除了routeConfig以外的所有缓存 |
|
||||||
* @param view {routeConfig} |
|
||||||
*/ |
|
||||||
delOtherTagAndCache(view) { |
|
||||||
const visitedViews = store.visitedViews.filter(v => v.meta.affix || v.path === view.path) |
|
||||||
const key = store.cachedViews.find(key => key === getRouterViewCacheKey(view)) |
|
||||||
|
|
||||||
store.visitedViews = visitedViews |
|
||||||
store.cachedViews = key ? [key] : [] |
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* 从页签栏上移除所有非固定页签,并且移除<keep-router-view-alive>的所有缓存 |
|
||||||
*/ |
|
||||||
delAllTagAndCache() { |
|
||||||
store.visitedViews = store.visitedViews.filter(tag => tag.meta && tag.meta.affix) |
|
||||||
store.cachedViews = [] |
|
||||||
} |
|
||||||
}) |
|
||||||
@ -0,0 +1,6 @@ |
|||||||
|
@import "~el-admin-layout/src/style"; |
||||||
|
|
||||||
|
@import "./component/Footer/style"; |
||||||
|
@import "./component/SettingDrawer/style"; |
||||||
|
|
||||||
|
|
||||||
@ -1,39 +0,0 @@ |
|||||||
import {getters as appGetters} from "@/layout/store/app" |
|
||||||
|
|
||||||
//获取路由页面缓存所需的key
|
|
||||||
export function getRouterViewCacheKey({name, path, fullPath, meta = {}}) { |
|
||||||
const {usePathKey, useFullPathKey} = meta |
|
||||||
return usePathKey ? path : useFullPathKey ? fullPath : name |
|
||||||
} |
|
||||||
|
|
||||||
//获取侧边栏的菜单,如果是双层侧边栏导航时,获取的是子菜单
|
|
||||||
export function getSidebarMenus() { |
|
||||||
const menus = appGetters.menus |
|
||||||
|
|
||||||
if (!Array.isArray(menus)) { |
|
||||||
return [] |
|
||||||
} |
|
||||||
|
|
||||||
//移动端时,侧边栏只会按侧边栏导航模式渲染
|
|
||||||
if (appGetters.isMobile) { |
|
||||||
return menus |
|
||||||
} |
|
||||||
|
|
||||||
switch (appGetters.navMode) { |
|
||||||
case 'aside': |
|
||||||
return menus |
|
||||||
case 'head': |
|
||||||
return [] |
|
||||||
case 'aside-two-part': |
|
||||||
case 'mix': |
|
||||||
const root = menus.find(i => i.path === appGetters.activeRootMenu) |
|
||||||
return root ? root.children || [] : [] |
|
||||||
default: |
|
||||||
return [] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//根据路由获取当前激活的菜单
|
|
||||||
export function getActiveMenuByRoute({path, meta}) { |
|
||||||
return meta.activeMenu || path |
|
||||||
} |
|
||||||
@ -1,223 +0,0 @@ |
|||||||
import cssVar from '@/asset/style/var.scss' |
|
||||||
|
|
||||||
const maxMobileWidth = parseFloat(cssVar.maxMobileWidth) |
|
||||||
|
|
||||||
/** |
|
||||||
* 根据body宽度判断是否为移动端,是则返回true |
|
||||||
* |
|
||||||
* @return {boolean} |
|
||||||
*/ |
|
||||||
export function isMobile() { |
|
||||||
const rect = document.body.getBoundingClientRect() |
|
||||||
return rect.width <= maxMobileWidth |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 判断是否为dom元素,是则返回true |
|
||||||
* |
|
||||||
* @param obj |
|
||||||
* @return {boolean} |
|
||||||
*/ |
|
||||||
export function isDom(obj) { |
|
||||||
return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string' |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取元素的内宽(扣除左右padding后) |
|
||||||
* |
|
||||||
* @param el {HTMLElement} |
|
||||||
* @return {number} |
|
||||||
*/ |
|
||||||
export function getElementInnerWidth(el) { |
|
||||||
if (!el) return 0 |
|
||||||
|
|
||||||
const style = window.getComputedStyle(el) |
|
||||||
|
|
||||||
return parseFloat(style.width) - (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取元素的真实高度 |
|
||||||
* |
|
||||||
* @param el {HTMLElement} |
|
||||||
* @param style 元素的style键值对 |
|
||||||
* @return {number} |
|
||||||
*/ |
|
||||||
export function getElementHeight(el, style) { |
|
||||||
if (!el) return 0 |
|
||||||
|
|
||||||
const node = el.cloneNode(true) |
|
||||||
node.style.opacity = '0' |
|
||||||
|
|
||||||
if (style) { |
|
||||||
Object.keys(style).forEach(item => { |
|
||||||
node.style[item] = style[item] |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const id = 'temp-id-' + Date.now() |
|
||||||
node.setAttribute('id', id) |
|
||||||
document.body.append(node) |
|
||||||
|
|
||||||
const height = node.offsetHeight |
|
||||||
document.body.removeChild(node) |
|
||||||
|
|
||||||
return height |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取元素距离其容器的顶部距离 |
|
||||||
* |
|
||||||
* @param el {HTMLElement} 目标元素 |
|
||||||
* @param container {HTMLElement|Window} 容器元素 |
|
||||||
* @return {number} |
|
||||||
*/ |
|
||||||
export function getTopDistance(el, container) { |
|
||||||
if (!el) return 0 |
|
||||||
|
|
||||||
if (!el.getClientRects().length) { |
|
||||||
return 0 |
|
||||||
} |
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect() |
|
||||||
|
|
||||||
if (rect.width || rect.height) { |
|
||||||
if (container === window) { |
|
||||||
container = el.ownerDocument.documentElement |
|
||||||
return rect.top - container.clientTop |
|
||||||
} |
|
||||||
return rect.top - container.getBoundingClientRect().top |
|
||||||
} |
|
||||||
|
|
||||||
return rect.top |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 加载js或css |
|
||||||
* |
|
||||||
* @param url {string} 资源地址 |
|
||||||
* @param type {string} 'js'或'css' |
|
||||||
* @return {Promise<String>} 加载成功返回url(若资源此前已加载,返回undefined) |
|
||||||
*/ |
|
||||||
export function loadExternalResource(url, type = 'js') { |
|
||||||
return new Promise((resolve, reject) => { |
|
||||||
let tag |
|
||||||
|
|
||||||
if (type === "css") { |
|
||||||
const links = Array.from(document.getElementsByTagName('link')) |
|
||||||
if (links.some(link => link.getAttribute('href') === url)) { |
|
||||||
return resolve() |
|
||||||
} |
|
||||||
|
|
||||||
tag = document.createElement("link") |
|
||||||
tag.rel = "stylesheet" |
|
||||||
tag.href = url |
|
||||||
} |
|
||||||
else if (type === "js") { |
|
||||||
const scripts = Array.from(document.getElementsByTagName('script')) |
|
||||||
if (scripts.some(script => script.getAttribute('src') === url)) { |
|
||||||
return resolve() |
|
||||||
} |
|
||||||
|
|
||||||
tag = document.createElement("script") |
|
||||||
tag.src = url |
|
||||||
} |
|
||||||
|
|
||||||
if (tag) { |
|
||||||
tag.onload = () => resolve(url) |
|
||||||
tag.onerror = () => reject(url) |
|
||||||
document.head.appendChild(tag) |
|
||||||
} |
|
||||||
else reject(`没有这个东东,url:${url},type:${type}`) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 将dom按最小距离垂直地滚动至视窗内 |
|
||||||
* 比如dom在视窗下,那么会滚动到视窗底部 |
|
||||||
* |
|
||||||
* @param child {HTMLElement} 需要滚动的dom |
|
||||||
* @param parent {HTMLElement|Window} 包含child的容器 |
|
||||||
*/ |
|
||||||
export function scrollIntoViewVertically(child, parent = window) { |
|
||||||
const {scrollTop, scrollHeight, offsetHeight: containerHeight} = parent |
|
||||||
|
|
||||||
//当菜单高度不足以滚动时跳过
|
|
||||||
if (scrollHeight <= containerHeight) return |
|
||||||
|
|
||||||
const elHeight = child.offsetHeight, between = getTopDistance(child, parent) |
|
||||||
|
|
||||||
//计算需要滚动的距离,undefined说明不需要滚动
|
|
||||||
let distance |
|
||||||
|
|
||||||
if (between < 0) distance = between |
|
||||||
else if (between + elHeight > containerHeight) { |
|
||||||
distance = between + elHeight - containerHeight |
|
||||||
} |
|
||||||
|
|
||||||
if (distance !== undefined) { |
|
||||||
parent.scrollTo({top: scrollTop + distance, behavior: 'smooth'}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 平滑滚动至指定的位置 |
|
||||||
* |
|
||||||
* @param el {Window|HTMLElement|string|function} 滚动容器,或可用于querySelector的字符串,或一个返回DOM的函数 |
|
||||||
* @param position {number} 滚动的目的地 |
|
||||||
* @param options 配置项 |
|
||||||
* @param options.callback {function} 滚动完成的回调 |
|
||||||
* @param options.duration {number} 滚动耗时 |
|
||||||
* @param options.direction {string} 滚动方向,top滚动至距元素顶部distance的位置,left滚动至距元素左边distance的位置 |
|
||||||
*/ |
|
||||||
export function scrollTo(el, position, options) { |
|
||||||
const {callback, duration = 300, direction = 'top'} = options || {} |
|
||||||
|
|
||||||
if (typeof el === 'string') { |
|
||||||
el = document.querySelector(el) |
|
||||||
} |
|
||||||
else if (typeof el === 'function') { |
|
||||||
el = el() |
|
||||||
} |
|
||||||
|
|
||||||
if (!isDom(el)) return |
|
||||||
|
|
||||||
const toTop = direction === 'top' |
|
||||||
const elPosition = getScroll(el, toTop) |
|
||||||
const scrollFunc = (el => { |
|
||||||
if (el === window) { |
|
||||||
return toTop |
|
||||||
? y => el.scrollTo(window.pageXOffset, window.pageYOffset + y) |
|
||||||
: x => el.scrollTo(window.pageXOffset + x, window.pageYOffset) |
|
||||||
} |
|
||||||
return toTop |
|
||||||
? y => el.scrollTop += y |
|
||||||
: x => el.scrollLeft += x |
|
||||||
})(el) |
|
||||||
|
|
||||||
let times = duration / 16, distance = (position - elPosition) / (times + 1) |
|
||||||
|
|
||||||
const frameFunc = () => { |
|
||||||
if (times > 0) { |
|
||||||
scrollFunc(distance) |
|
||||||
times-- |
|
||||||
return window.requestAnimationFrame(frameFunc) |
|
||||||
} |
|
||||||
typeof callback === 'function' && callback() |
|
||||||
} |
|
||||||
|
|
||||||
return window.requestAnimationFrame(frameFunc) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取元素的滚动距离 |
|
||||||
* |
|
||||||
* @param el {Window|HTMLElement} |
|
||||||
* @param top {boolean} 是否获取垂直的滚动距离 |
|
||||||
* @return {number} |
|
||||||
*/ |
|
||||||
export function getScroll(el, top) { |
|
||||||
return el === window |
|
||||||
? el[top ? 'pageYOffset' : 'pageXOffset'] |
|
||||||
: el[top ? 'scrollTop' : 'scrollLeft'] |
|
||||||
} |
|
||||||
@ -1,35 +0,0 @@ |
|||||||
import {scrollIntoViewVertically} from "@/util/browser" |
|
||||||
|
|
||||||
/** |
|
||||||
* 将当前激活的菜单移动到视窗中(仅限垂直菜单) |
|
||||||
* |
|
||||||
* @param menu el-menu实例 |
|
||||||
*/ |
|
||||||
export function moveToActiveMenuVertically(menu) { |
|
||||||
if (!menu) return |
|
||||||
|
|
||||||
const cur = menu.activeIndex |
|
||||||
if (!cur) return |
|
||||||
|
|
||||||
const curInstance = menu.items[cur] |
|
||||||
if (!curInstance) return |
|
||||||
|
|
||||||
let el = curInstance.$el |
|
||||||
|
|
||||||
//当侧边栏折叠时,需要滚动至可视区域的元素是激活菜单的最顶层父节点
|
|
||||||
if (menu.collapse) { |
|
||||||
let rootParent = curInstance |
|
||||||
while (rootParent.$parent.$options.componentName !== 'ElMenu') { |
|
||||||
rootParent = rootParent.$parent |
|
||||||
} |
|
||||||
el = rootParent.$el |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* 这里考虑了菜单展开时的200ms动画时间 |
|
||||||
* 为什么不分情况讨论?比如当subMenu已经是展开状态时,无需延时滚动 |
|
||||||
* 但这种情况无法判断,因为这时menu.openedMenus已经包含了subMenu,无论subMenu之前是否展开 |
|
||||||
* 所以统一延时300ms |
|
||||||
* */ |
|
||||||
window.setTimeout(() => scrollIntoViewVertically(el, menu.$el), 300) |
|
||||||
} |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
/** |
|
||||||
* el-tree展开折叠控制 |
|
||||||
* |
|
||||||
* @param ref el-tree实例 |
|
||||||
* @param action 'expand' | 'collapse' |
|
||||||
* @param level 展开的节点最大深度,为0时匹配全部节点 |
|
||||||
* @param func 自定义展开的函数,传入一个node,返回boolean,优先使用 |
|
||||||
*/ |
|
||||||
export function expandControl(ref, action = 'expand', level = 1, func) { |
|
||||||
const handler = function () { |
|
||||||
if (typeof func === 'function') { |
|
||||||
return node => func(node) |
|
||||||
} |
|
||||||
|
|
||||||
const expand = action === 'expand' |
|
||||||
const forAll = level === 0 |
|
||||||
|
|
||||||
return node => node.expanded = forAll || node.level <= level ? expand : !expand |
|
||||||
}() |
|
||||||
|
|
||||||
ref.store._getAllNodes().forEach(handler) |
|
||||||
} |
|
||||||
@ -1,36 +0,0 @@ |
|||||||
import {getInitialValue} from "@/util" |
|
||||||
|
|
||||||
//为Vue.observer返回的对象设置getter
|
|
||||||
export function createGetters(store) { |
|
||||||
const getters = Object.create({}) |
|
||||||
Object.defineProperties( |
|
||||||
getters, |
|
||||||
Object.keys(store).reduce((obj, key) => { |
|
||||||
obj[key] = { |
|
||||||
enumerable: true, |
|
||||||
get() { |
|
||||||
return store[key] |
|
||||||
} |
|
||||||
} |
|
||||||
return obj |
|
||||||
}, {}) |
|
||||||
) |
|
||||||
return getters |
|
||||||
} |
|
||||||
|
|
||||||
//设置mutation
|
|
||||||
export function createMutations(store, all = false) { |
|
||||||
const keys = Object.keys(store) |
|
||||||
const obj = {} |
|
||||||
keys.forEach(key => { |
|
||||||
obj[key] = v => store[key] = v |
|
||||||
}) |
|
||||||
if (all) { |
|
||||||
obj['$all'] = v => { |
|
||||||
keys.forEach(key => { |
|
||||||
store[key] = v && v[key] || getInitialValue(store[key]) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
return obj |
|
||||||
} |
|
||||||
@ -1,33 +0,0 @@ |
|||||||
/** |
|
||||||
* 路由控制工具类 |
|
||||||
*/ |
|
||||||
|
|
||||||
import {isEmpty} from "@/util" |
|
||||||
import {isString} from "@/util/validate" |
|
||||||
import router from '@/router' |
|
||||||
import {mutations as tagsViewMutations} from "@/layout/store/tagsView" |
|
||||||
|
|
||||||
/** |
|
||||||
* 路由刷新 |
|
||||||
* |
|
||||||
* @param route {string|route} 需要刷新的路由,不传时为当前路由,如果是字符串时请确保以'/'开头 |
|
||||||
* @param replace {boolean} 是否使用replace进行跳转 |
|
||||||
* @return {Promise} 返回vue-router跳转的结果 |
|
||||||
*/ |
|
||||||
export function refreshPage(route = router.currentRoute, replace = true) { |
|
||||||
const target = `/redirect${isString(route) ? route : route.fullPath}` |
|
||||||
return router[replace ? 'replace' : 'push'](target) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 关闭当前页,如果传入next则跳转到next页面 |
|
||||||
* |
|
||||||
* @param next {string|route} 跳转的目标页面,作为第一个参数传入vue-router.replace |
|
||||||
* @return {undefined|Promise} 仅在next有值时,返回vue-router.replace的结果 |
|
||||||
*/ |
|
||||||
export function closeCurrentPage(next) { |
|
||||||
tagsViewMutations.delTagAndCache(router.currentRoute) |
|
||||||
if (!isEmpty(next)) { |
|
||||||
return router.replace(next) |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,23 +0,0 @@ |
|||||||
/** |
|
||||||
* 根据组件标签名称获取组件实例 |
|
||||||
* 使用广度优先搜索 |
|
||||||
* |
|
||||||
* @param instance 从哪个组件实例开始查找,一般是this |
|
||||||
* @param tag 要查找的组件的标签名称 |
|
||||||
*/ |
|
||||||
export function findComponentByTag(instance, tag) { |
|
||||||
if (!instance || !tag) return |
|
||||||
|
|
||||||
const queue = [instance] |
|
||||||
|
|
||||||
while (queue.length > 0) { |
|
||||||
const item = queue.shift() |
|
||||||
const {$options, $children} = item |
|
||||||
|
|
||||||
if ($options._componentTag === tag) return item |
|
||||||
|
|
||||||
if (Array.isArray($children)) { |
|
||||||
queue.push(...$children) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
Loading…
Reference in new issue