同步修改template

master
toesbieya 6 years ago
parent 82ad7d684c
commit 6f61b12934
  1. 74
      vue/template/element-ui-personal/component/Menu/MenuItem.vue
  2. 250
      vue/template/element-ui-personal/component/Menu/Submenu.vue
  3. 53
      vue/template/element-ui-personal/component/Menu/mixin.js
  4. 7
      vue/template/element-ui-personal/index.js
  5. 10
      vue/template/package-lock.json
  6. 2
      vue/template/package.json
  7. 48
      vue/template/src/asset/style/cover/element-ui.scss
  8. 3
      vue/template/src/asset/style/cover/var.scss
  9. 5
      vue/template/src/asset/style/display.scss
  10. 19
      vue/template/src/asset/style/index.scss
  11. 31
      vue/template/src/asset/style/transition.scss
  12. 75
      vue/template/src/asset/style/var.scss
  13. 30
      vue/template/src/component/checkbox/ColorCheckbox.vue
  14. 21
      vue/template/src/component/checkbox/Group.vue
  15. 43
      vue/template/src/component/checkbox/ImgCheckbox.vue
  16. 114
      vue/template/src/component/menu/ContextMenu/index.vue
  17. 175
      vue/template/src/component/menu/NavMenu/index.vue
  18. 154
      vue/template/src/component/menu/NavMenu/item.vue
  19. 109
      vue/template/src/component/menu/NavMenu/style.scss
  20. 60
      vue/template/src/component/menu/NavMenu/theme-dark.scss
  21. 39
      vue/template/src/component/menu/NavMenu/theme-light.scss
  22. 3
      vue/template/src/config/index.js
  23. 68
      vue/template/src/filter/index.js
  24. 56
      vue/template/src/layout/component/Aside/component/MenuSearch.vue
  25. 251
      vue/template/src/layout/component/Aside/component/OnePartRoot.vue
  26. 134
      vue/template/src/layout/component/Aside/component/TwoPartRoot.vue
  27. 110
      vue/template/src/layout/component/Aside/component/TwoPartSub.vue
  28. 34
      vue/template/src/layout/component/Aside/index.vue
  29. 101
      vue/template/src/layout/component/Aside/style.scss
  30. 27
      vue/template/src/layout/component/Aside/theme-dark.scss
  31. 14
      vue/template/src/layout/component/Aside/theme-light.scss
  32. 11
      vue/template/src/layout/component/Footer/index.vue
  33. 9
      vue/template/src/layout/component/Footer/style.scss
  34. 30
      vue/template/src/layout/component/Hamburger.vue
  35. 72
      vue/template/src/layout/component/Logo.vue
  36. 41
      vue/template/src/layout/component/Navbar/component/Bell.vue
  37. 210
      vue/template/src/layout/component/Navbar/component/HeadMenu.vue
  38. 111
      vue/template/src/layout/component/Navbar/index.vue
  39. 50
      vue/template/src/layout/component/Navbar/style.scss
  40. 27
      vue/template/src/layout/component/Navbar/theme-dark.scss
  41. 11
      vue/template/src/layout/component/Navbar/theme-light.scss
  42. 13
      vue/template/src/layout/component/Page/component/BackToTop.vue
  43. 92
      vue/template/src/layout/component/Page/component/KeepRouterViewAlive.vue
  44. 28
      vue/template/src/layout/component/Page/component/PageFooter.vue
  45. 65
      vue/template/src/layout/component/Page/component/PageHeader.vue
  46. 27
      vue/template/src/layout/component/Page/component/PageView.vue
  47. 82
      vue/template/src/layout/component/Page/component/TagsView/ScrollPanel.vue
  48. 207
      vue/template/src/layout/component/Page/component/TagsView/index.vue
  49. 68
      vue/template/src/layout/component/Page/component/TagsView/style.scss
  50. 67
      vue/template/src/layout/component/Page/index.vue
  51. 23
      vue/template/src/layout/component/Page/style.scss
  52. 15
      vue/template/src/layout/component/SettingDrawer/component/checkbox/ColorCheckbox.vue
  53. 15
      vue/template/src/layout/component/SettingDrawer/component/checkbox/Group.vue
  54. 21
      vue/template/src/layout/component/SettingDrawer/component/checkbox/ImgCheckbox.vue
  55. 0
      vue/template/src/layout/component/SettingDrawer/component/checkbox/mixin.js
  56. 34
      vue/template/src/layout/component/SettingDrawer/component/checkbox/style.scss
  57. 52
      vue/template/src/layout/component/SettingDrawer/index.vue
  58. 17
      vue/template/src/layout/component/SettingDrawer/style.scss
  59. 141
      vue/template/src/layout/index.vue
  60. 29
      vue/template/src/layout/mixin/decideRouterTransition.js
  61. 33
      vue/template/src/layout/mixin/hamburger.js
  62. 56
      vue/template/src/layout/mixin/menu.js
  63. 24
      vue/template/src/layout/mixin/menuSearch.js
  64. 4
      vue/template/src/layout/mixin/tagsViewPersistent.js
  65. 34
      vue/template/src/layout/mixin/tagsViewShortcut.js
  66. 83
      vue/template/src/layout/store/app.js
  67. 71
      vue/template/src/layout/store/aside.js
  68. 13
      vue/template/src/layout/store/navbar.js
  69. 65
      vue/template/src/layout/store/page.js
  70. 141
      vue/template/src/layout/store/tagsView.js
  71. 6
      vue/template/src/layout/style.scss
  72. 39
      vue/template/src/layout/util.js
  73. 2
      vue/template/src/main.js
  74. 2
      vue/template/src/router/guardian/avoidReuse.js
  75. 2
      vue/template/src/router/guardian/iframe.js
  76. 11
      vue/template/src/router/util.js
  77. 2
      vue/template/src/store/module/resource.js
  78. 3
      vue/template/src/store/module/user.js
  79. 19
      vue/template/src/util/auth.js
  80. 223
      vue/template/src/util/browser.js
  81. 0
      vue/template/src/util/element-ui/elForm.js
  82. 35
      vue/template/src/util/element-ui/elMenu.js
  83. 22
      vue/template/src/util/element-ui/elTree.js
  84. 211
      vue/template/src/util/index.js
  85. 30
      vue/template/src/util/message.js
  86. 36
      vue/template/src/util/observable.js
  87. 33
      vue/template/src/util/route.js
  88. 10
      vue/template/src/util/storage.js
  89. 205
      vue/template/src/util/tree.js
  90. 43
      vue/template/src/util/validate.js
  91. 23
      vue/template/src/util/vue.js
  92. 2
      vue/template/src/view/_common/SkeletonPage/style.scss
  93. 2
      vue/template/vue.config.js

@ -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"
const components = [MenuItem, Submenu]
export default function (Vue) {
components.forEach(component => {
Vue.component(component.name, component)
})
Vue.prototype.$message = Message
}

@ -4464,6 +4464,11 @@
"integrity": "sha1-SGYSh1c9zFPjZsehrlLDoSDuybo=",
"dev": true
},
"el-admin-layout": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/el-admin-layout/-/el-admin-layout-0.0.2.tgz",
"integrity": "sha512-F+5n9HzlbGyD3EbQi4Uk7RdMlZbSLmPAsw5aoK5pYmEtLJktZCOC70bGMNRESc38bGpADa7Cv1+8bwCruG1/uw=="
},
"electron-to-chromium": {
"version": "1.3.613",
"resolved": "https://registry.npm.taobao.org/electron-to-chromium/download/electron-to-chromium-1.3.613.tgz?cache=0&sync_timestamp=1606876399469&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Felectron-to-chromium%2Fdownload%2Felectron-to-chromium-1.3.613.tgz",
@ -7105,11 +7110,6 @@
"resolved": "https://registry.npm.taobao.org/normalize-wheel/download/normalize-wheel-1.0.1.tgz",
"integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU="
},
"normalize.css": {
"version": "8.0.1",
"resolved": "https://registry.npm.taobao.org/normalize.css/download/normalize.css-8.0.1.tgz",
"integrity": "sha1-m5iiCHOLnMJjTKrLxC0THJdIe/M="
},
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz?cache=0&sync_timestamp=1577053500910&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnpm-run-path%2Fdownload%2Fnpm-run-path-2.0.2.tgz",

@ -9,8 +9,8 @@
},
"dependencies": {
"core-js": "^3.6.5",
"el-admin-layout": "0.0.2",
"element-ui": "^2.13.2",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pako": "^1.0.11",
"vue": "^2.6.11",

@ -9,54 +9,6 @@
}
}
.el-menu {
width: 100%;
border: none !important;
overflow: hidden;
&::-webkit-scrollbar {
display: none;
}
//子级菜单激活时父级同样高亮
.el-menu-item.is-active,
.is-active > .el-submenu__title {
color: $--color-primary !important;
}
//箭头图标与字体同色
.el-submenu__title i {
color: inherit;
}
//原先垂直时右侧展开箭头偏上
.el-submenu__icon-arrow {
margin-top: -4px;
}
//弹出菜单
&.el-menu--popup {
max-height: 88vh;
min-width: 160px;
padding: 0;
margin-bottom: 0.5vh;
overflow-y: auto;
.el-menu-item,
.el-submenu__title {
padding: 0 $menu-padding;
}
}
&--horizontal > .el-menu-item:not(.is-disabled):hover,
&--horizontal > .el-menu-item:not(.is-disabled):focus,
&--horizontal > .el-submenu .el-submenu__title:hover,
&--horizontal .el-menu .el-menu-item,
&--horizontal .el-menu .el-submenu__title {
background-color: inherit;
}
}
.el-message {
@include deep-shadow;
@include max-width-on-mobile;

@ -1,7 +1,4 @@
/*仅覆盖element-ui中的预设变量,不会被使用*/
//el-menu的hover背景颜色
$--menu-item-hover-fill: none;
//引入element的scss时需要覆盖字体路径
$--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,27 +1,27 @@
@import "~normalize.css/normalize.css";
@import "./var.scss";
@import "./cover/var.scss";
@import "./mixin.scss";
@import "~element-ui/packages/theme-chalk/src/index.scss";
@import "~@ele/style/index.scss";
//layout中的样式需要覆盖element-ui原定义所以放element-ui的后面
@import "~@/layout/style.scss";
@import "./cover/element-ui.scss";
@import './display.scss';
@import "./transition.scss";
@import "./nprogress.scss";
html {
height: 100%;
box-sizing: border-box;
}
body {
height: 100%;
margin: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
-webkit-tap-highlight-color: transparent;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
@ -41,7 +41,6 @@ div:focus {
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
@ -57,7 +56,7 @@ a:hover {
&-thumb {
width: 6px;
//border-radius: 5px;
border-radius: 5px;
background-color: rgba($--color-primary, 0.8);
&:hover {

@ -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,7 +1,7 @@
@import "~element-ui/packages/theme-chalk/src/common/var";
//设定移动端的最大宽度只能是number类型
$max-mobile-width: 500px;
$max-mobile-width: $--sm;
//覆盖element颜色
$--color-primary: #1890ff;
@ -9,76 +9,3 @@ $--color-success: #21BA45;
$--color-warning: #FF5722;
$--color-danger: #F44336;
$--color-info: #009688;
//菜单中icon的大小
$menu-icon-size: 14px;
//菜单中icon和文字的距离
$menu-icon-text-gap: 10px;
//菜单的padding
$menu-padding: $menu-icon-size + $menu-icon-text-gap;
//暗色菜单的背景色
$menu-background-dark: #304156;
//暗色子级菜单的背景色
$sub-menu-background-dark: darken($menu-background-dark, 5%);
//暗色主菜单的背景色用于双层侧边栏的主菜单和顶部导航栏
$root-menu-background-dark: #1F293D;
//亮色菜单的文字颜色
$menu-text-color-light: $--color-text-primary;
//暗色菜单的文字颜色
$menu-text-color-dark: rgba(255, 255, 255, .65);
//亮色菜单的文字hover颜色
$menu-text-hover-color-light: $--color-primary;
//暗色菜单的文字hover颜色
$menu-text-hover-color-dark: rgba(255, 255, 255, 1);
//侧边栏宽度
$aside-width: 208px;
//侧边栏折叠时的宽度
$aside-collapse-width: $menu-icon-size + $menu-padding * 2;
//导航栏高度
$nav-height: 48px;
//导航栏的padding-left
$navbar-padding-left: 12px;
//亮色导航栏的功能项颜色
$navbar-item-color-light: $menu-text-color-light;
//暗色导航栏的功能项颜色
$navbar-item-color-dark: rgba(255, 255, 255, .85);
//亮色导航栏的功能项hover背景颜色
$navbar-item-hover-color-light: rgba(255, 255, 255, .85);
//暗色导航栏的功能项hover背景颜色
$navbar-item-hover-color-dark: lighten($root-menu-background-dark, 5%);
//多页签栏高度
$tags-view-height: 32px;
//页头高度用于max-view-height
$page-header-height: 44px;
//页头的上下padding
$page-header-padding: 12px;
//路由页面的margin
$page-view-margin: 24px;
//页脚高度用于max-view-height
$page-footer-height: 64px;
//暗色边框的颜色用于nav-menu和侧边栏
$--border-color-dark: darken($root-menu-background-dark, 5%);
//dialog距离顶部的距离用于<el-dialog>的top属性<el-dialog>中滚动区域的最大高度
$dialog-top: 100px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
maxMobileWidth: $max-mobile-width;
primary: $--color-primary;
info: $--color-info;
success: $--color-success;
danger: $--color-danger;
warning: $--color-warning;
pageViewMargin: $page-view-margin;
dialogTop: $dialog-top;
menuPadding: $menu-padding;
}

@ -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-menuopenuniqueOpened
for (const {fullPath} of expandMenus) {
const sub = submenus[fullPath]
if (!sub || openedMenus.includes(sub.index)) continue
sub.indexPath.forEach(i => {
!openedMenus.includes(i) && openedMenus.push(i)
})
}
},
//
getHighlightMenuParent(children, parent) {
const result = []
children.forEach(child => {
if (child.meta.title.includes(this.realSearchWord)) {
parent && result.push(parent)
}
if (child.children) {
result.push(...this.getHighlightMenuParent(child.children, child))
}
})
return result
}
},
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)
]
}
//showIconMaxDepthdepth
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>mouseleavemouseleave
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;
}
}
}
}
}

@ -9,6 +9,9 @@ module.exports = {
//页面标题、登录注册页的标题
title: 'BiuBiuBiu~',
//logo地址
logo: `/static/img/logo.svg`,
//路由配置
route: {
//路由模式,['hash','history']

@ -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'
},
//truefalse
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
},
//truefalsepc
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 = []
//elMenuinitOpenedMenu()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(),
//truefalsepc
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 = []
//elMenuinitOpenedMenu()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-submenukey
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-menudom
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;
}
// margin0
& + .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.titletab
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;
}
}

@ -1,10 +1,10 @@
<template>
<el-drawer
:visible="value"
:visible="appGetters.showSettingDrawer"
:with-header="false"
append-to-body
size="288px"
@close="$emit('input', false)"
@close="close"
>
<div class="drawer-container">
<el-divider>导航模式</el-divider>
@ -124,14 +124,21 @@
</template>
<script>
import CheckboxGroup from "@/component/checkbox/Group"
import ColorCheckbox from "@/component/checkbox/ColorCheckbox"
import ImgCheckbox from "@/component/checkbox/ImgCheckbox"
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app"
import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside"
import {getters as navbarGetters, mutations as navbarMutations} from "@/layout/store/navbar"
import {getters as pageGetters, mutations as pageMutations} from "@/layout/store/page"
import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView"
import CheckboxGroup from "./component/checkbox/Group"
import ColorCheckbox from "./component/checkbox/ColorCheckbox"
import ImgCheckbox from "./component/checkbox/ImgCheckbox"
import {
appGetters,
appMutations,
asideGetters,
asideMutations,
navbarGetters,
navbarMutations,
pageGetters,
pageMutations,
tagsViewGetters,
tagsViewMutations
} from "el-admin-layout"
import {mergeObj} from "@/util"
import {getLocalPersonalSettings, setLocalPersonalSettings} from "@/util/storage"
@ -140,8 +147,6 @@ export default {
components: {CheckboxGroup, ColorCheckbox, ImgCheckbox},
props: {value: Boolean},
data() {
return {
appGetters, asideGetters, navbarGetters, pageGetters, tagsViewGetters,
@ -201,6 +206,10 @@ export default {
},
methods: {
close() {
appMutations.showSettingDrawer(false)
},
//layoutstore
syncLayoutStore() {
const {app, page, aside, navbar, tagsView} = this.setting
@ -303,22 +312,3 @@ export default {
}
}
</script>
<style lang="scss" scoped>
@import "~@/asset/style/var";
.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;
}
}
</style>

@ -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">
import VAside from './component/Aside'
import VNavbar from './component/Navbar'
import VPage from './component/Page'
import Vue from 'vue'
import config from "@/config"
import ElAdminLayout, {setIconRenderer, appMutations} from 'el-admin-layout'
import Footer from './component/Footer'
import SettingDrawer from './component/SettingDrawer'
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app"
import {getters as pageGetters} from "@/layout/store/page"
import {getters as tagsViewGetters} from "@/layout/store/tagsView"
import tagsViewPersistent from './mixin/tagsViewPersistent'
import tagsViewShortcut from './mixin/tagsViewShortcut'
import {elConfirm} from "@/util/message"
//
appMutations.title(config.title)
appMutations.logo(config.logo)
//
setIconRenderer((h, data) => <v-icon {...data}/>)
export default {
name: 'Layout',
name: "Layout",
components: {VAside, VNavbar, VPage, SettingDrawer},
mixins: [tagsViewPersistent, tagsViewShortcut],
components: {ElAdminLayout},
computed: {
isLeftRight() {
return pageGetters.position === 'left-right'
navbarProps() {
const userInfo = this.$store.state.user
return {
user: {
avatar: userInfo.avatar,
name: userInfo.name
},
renderAside() {
return appGetters.isMobile || ['aside', 'aside-two-part', 'mix'].includes(appGetters.navMode)
userDropdownItems: [
{
icon: 'el-icon-switch-button',
command: 'logout',
content: '退出登录',
handler: this.logout
}
],
renderCustomActions: this.renderNavbarActions
}
},
wrapperClass() {
pageProps() {
return {
'app-wrapper': true,
'flex-column': !this.isLeftRight
renderFooter: () => <Footer/>
}
},
containerClass() {
return [
'main-container',
'has-nav',
`nav-mode-${appGetters.navMode}`,
this.isLeftRight && 'flex-column',
tagsViewGetters.enabled && 'has-tags-view'
]
}
},
methods: {
closeSettingDrawer() {
appMutations.showSettingDrawer(false)
logout() {
if (this.$store.state.user.prepareLogout) {
return
}
},
render() {
const aside = this.renderAside && <v-aside/>
return (
<section class={this.wrapperClass}>
{this.isLeftRight ? aside : <v-navbar/>}
elConfirm('确认退出?').then(() => this.$store.dispatch('user/logout'))
},
<section class={this.containerClass}>
{this.isLeftRight ? <v-navbar/> : aside}
<v-page/>
</section>
openSettingDrawer() {
appMutations.showSettingDrawer(true)
},
<setting-drawer
value={appGetters.showSettingDrawer}
on-input={this.closeSettingDrawer}
/>
</section>
)
}
}
</script>
renderNavbarActions(defaultActions) {
const customActions = [
<div
class="setting-btn navbar-item"
title="个性设置"
on-click={this.openSettingDrawer}
>
<i class="el-icon-s-operation navbar-icon"/>
</div>
]
<style lang="scss">
.app-wrapper.flex-column,
.main-container.flex-column {
flex-direction: column;
return customActions.concat(defaultActions)
}
},
.app-wrapper {
display: flex;
height: 100%;
width: 100%;
> .main-container {
display: flex;
flex: 1;
overflow: hidden;
}
//layout
beforeCreate() {
const Drawer = Vue.extend(SettingDrawer)
const instance = new Drawer().$mount()
document.body.appendChild(instance.$el)
this.$_settingDrawerInstance = instance
},
//z-index
//z-index
&.flex-column .aside {
z-index: 9;
//
beforeDestroy() {
if (this.$_settingDrawerInstance) {
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)
}
}
}

@ -4,7 +4,7 @@
* 页签变化时写入本地存储
*/
import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView"
import {tagsViewGetters, tagsViewMutations} from "el-admin-layout"
import {debounce} from "@/util"
import {getTagsView, setTagsView} from "@/util/storage"
@ -27,7 +27,7 @@ export default {
if (!v) return setTagsView()
//启用时先存储一次(仅在mounted后,否则此时页签数据不完整)
this._isMounted && this.persistentTagsView(this.visitedViews)
this._isMounted && this.persistentTagsView(tagsViewGetters.visitedViews)
this.watchVisitedViewsCallback = this.$watch('visitedViews', this.persistentTagsView)
}

@ -4,7 +4,7 @@
* ctrl + 上一个页签
*/
import {getters as tagsViewGetters} from "@/layout/store/tagsView"
import {tagsViewGetters} from "el-admin-layout"
export default {
computed: {
@ -28,20 +28,36 @@ export default {
methods: {
//上一个页签
gotoViewFront() {
if (this.visitedViews.length <= 1) return
let index = this.visitedViews.findIndex(view => view.path === this.$route.path)
const views = tagsViewGetters.visitedViews
if (views.length <= 1) return
let index = views.findIndex(view => view.path === this.$route.path)
if (index < 0) return
if (index === 0) index = this.visitedViews.length
return this.$router.push({path: this.visitedViews[index - 1].path})
if (index === 0) {
index = views.length
}
return this.$router.push({path: views[index - 1].path})
},
//下一个页签
gotoViewBehind() {
if (this.visitedViews.length <= 1) return
let index = this.visitedViews.findIndex(view => view.path === this.$route.path)
const views = tagsViewGetters.visitedViews
if (views.length <= 1) return
let index = views.findIndex(view => view.path === this.$route.path)
if (index < 0) return
if (index + 1 > this.visitedViews.length - 1) index = -1
return this.$router.push({path: this.visitedViews[index + 1].path})
if (index + 1 > views.length - 1) {
index = -1
}
return this.$router.push({path: views[index + 1].path})
},
//快捷键监听

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

@ -6,11 +6,9 @@ import App from '@/App'
import store from '@/store'
import router from '@/router'
import '@/asset/icon'
import filters from './filter'
Vue.use(Element)
Vue.use(ElementPersonal)
Vue.use(filters)
Vue.config.productionTip = false

@ -16,7 +16,7 @@ export default function (router) {
}
window.addEventListener('unhandledrejection', event => {
if (event.reason.stack.startsWith('Error: Redirected when going from')) {
if (event.reason.stack && event.reason.stack.startsWith('Error: Redirected when going from')) {
event.preventDefault()
}
})

@ -1,4 +1,4 @@
import {mutations as pageMutations} from "@/layout/store/page"
import {pageMutations} from "el-admin-layout"
const beforeEach = (to, from, next) => {
//从iframe页面离开时,判断是否需要删除iframe

@ -4,17 +4,6 @@ import Page500 from '@/view/_app/500'
import {isEmpty} from "@/util"
import {isExternal, isString} from "@/util/validate"
//json字符串转路由配置
export function str2routeConfig(str) {
const parseOpt = (key, value) => {
if (key === 'dynamicTitle' && isString(value)) {
return isEmpty(value) ? undefined : eval(`(${value})`)
}
return value
}
return JSON.parse(str, parseOpt)
}
//根据json数组生成路由
export function generateRoutes(jsonTree) {
//将深度为2的节点合并为由其最末子节点组成的数组

@ -1,5 +1,5 @@
import path from 'path'
import {mutations as appMutations} from "@/layout/store/app"
import {appMutations} from "el-admin-layout"
import {addDynamicRoutes} from '@/router'
import {getDynamicRoutes} from '@/router/define'
import {metaExtend} from "@/router/util"

@ -1,8 +1,7 @@
import {createMutations} from "@/store/util"
import {emptyOrDefault} from "@/util"
import {getUser, setUser} from "@/util/storage"
import {mutations as tagsViewMutations} from "@/layout/store/tagsView"
import {elError} from "@/util/message"
import {tagsViewMutations} from "el-admin-layout"
//刷新时从本地存储中获取用户信息
const user = getUser()

@ -1,5 +1,4 @@
import store from '@/store'
import {uppercaseFirst} from "@/filter"
import {isEmpty} from "@/util"
/**
@ -30,24 +29,6 @@ export function auth(path) {
return true
}
/**
* what i can
* 批量生成canXxx的计算属性
*
* @param apiMap {object} <key:接口名称value:接口对象>
* @return {object}
*/
export function wic(apiMap) {
const attrs = {}
Object.entries(apiMap).forEach(([key, value]) => {
const attrKey = `can${uppercaseFirst(key)}`
attrs[attrKey] = () => auth(value.url)
})
return attrs
}
/**
* 判断是否已登录已登录则返回true
*

@ -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,12 +1,6 @@
/**
* 判断是否为空值undefinednull'' 都视为空值
*
* @param str 不定参数
* @return {boolean} 若为空值返回true否则返回false
*/
export function isEmpty(...str) {
return str.some(i => i === undefined || i === null || i === '')
}
import {isEmpty, getInitialValue, debounce, deepClone} from "el-admin-layout/src/util"
export {isEmpty, getInitialValue, debounce, deepClone}
/**
* 当传入空值时返回默认值
@ -33,37 +27,6 @@ export function replaceAll(str, substr, replacement) {
return str.replace(new RegExp(substr, 'gm'), replacement)
}
/**
* 根据传入值的类型返回基础起始值
*
* @param v
* @return {boolean|{}|string|*[]|number|null}
*/
export function getInitialValue(v) {
if (v === undefined || v === null) return null
if (typeof v === 'string') return ''
if (typeof v === 'boolean') return false
if (typeof v === 'number') return 0
if (typeof v === 'object') return {}
if (Array.isArray(v)) return []
}
/**
* 简单重置对象属性遇到对象时会递归重置
* 重置方法使用{@link #getInitialValue}
*
* @param obj 需要重置的对象
*/
export function resetObj(obj) {
if (isEmpty(obj)) return
Object.keys(obj).forEach(key => {
if (obj[key] !== null && typeof obj[key] === 'object') {
resetObj(obj[key])
}
else obj[key] = getInitialValue(obj[key])
})
}
/**
* 将source合并到target中
* 仅对target中存在的键进行合并
@ -99,171 +62,3 @@ export function mergeObj(target, source) {
target[key] = source[key]
}
}
/**
* 日期格式化
*
* @param fmt {*|string} 格式y(+..y)-M(+M)-d(+d)-H(+H)-m(+m)-s(+s)-S-毫秒
* @param date {Date} 可选被格式化的日期
* @return {string} 格式化后的日期字符串
*/
export function timeFormat(fmt, date = new Date()) {
if (isEmpty(fmt)) fmt = 'yyyy-MM-dd HH:mm:ss'
const o = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"H+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"S": date.getMilliseconds() //毫秒
}
if (/(y+)/.test(fmt)) {
const replace = (date.getFullYear() + "").substring(4 - RegExp.$1.length)
fmt = fmt.replace(RegExp.$1, [...replace].join(''))
}
for (const k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
const firstMatch = RegExp.$1
const replace = firstMatch.length === 1 ? o[k] + "" : ("00" + o[k]).substring(("" + o[k]).length)
fmt = fmt.replace(firstMatch, [...replace].join(''))
}
}
return fmt
}
/**
* 防抖
*
* @param func {function} 原函数
* @param wait {number} 防抖间隔单位毫秒
* @param immediate {boolean} 是否立即执行一次
* @return {function} 经过防抖包装后的函数
*/
export function debounce(func, wait = 100, immediate = false) {
let timeout, args, context, timestamp, result
const later = function () {
// 据上一次触发时间间隔
const last = new Date().getTime() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = window.setTimeout(later, wait - last)
}
else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function () {
context = this
args = arguments
timestamp = new Date().getTime()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = window.setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
export function throttle(func, delay = 100) {
let timeoutID
let lastExec = 0
function wrapper() {
const self = this
const elapsed = Date.now() - lastExec
const args = arguments
function exec() {
lastExec = Date.now()
func.apply(self, args)
}
window.clearTimeout(timeoutID)
if (elapsed > delay) exec()
else timeoutID = window.setTimeout(exec, delay - elapsed)
}
return wrapper
}
/**
* 循环等待成功事件
*
* @param success {function} 返回true时说明成功
* @param callback {function} 成功后的回调
* @param interval {number} 循环间隔毫秒
* @param maxTryTime {number} 最大循环次数超出reject小于1视为Infinity
* @return {Promise}
*/
export function waitUntilSuccess(success, callback, interval = 1000, maxTryTime = 0) {
return new Promise((resolve, reject) => {
let fun, count = 0
const check = () => {
if (success()) {
window.clearInterval(fun)
typeof callback === 'function' && callback()
return resolve()
}
if (maxTryTime >= 1 && ++count >= maxTryTime) {
return reject()
}
}
if (success()) return check()
fun = window.setInterval(check, interval)
})
}
//将传入对象的所有函数的this绑定为其自身
export function bindThis(obj, root = obj) {
if (!obj || typeof obj !== 'object') return
Object.entries(obj).forEach(([k, v]) => {
if (typeof v === 'function') {
obj[k] = v.bind(root)
}
bindThis(v, root)
})
return obj
}
export function deepClone(source) {
if (source === null || typeof source !== 'object' || source instanceof Promise) {
return source
}
if (Array.isArray(source)) {
return source.map(i => deepClone(i))
}
else {
return Object.keys(source).reduce((obj, key) => {
obj[key] = deepClone(source[key])
return obj
}, {})
}
}
//有空值判断的trim
export function trim(str) {
return isEmpty(str) ? str : str.trim()
}

@ -1,10 +1,5 @@
import Message from '@ele/component/Message'
import {MessageBox} from 'element-ui'
import {isEmpty} from "@/util"
export function elError(msg = '操作失败') {
Message.error(msg)
}
export function elSuccess(message = '操作成功') {
Message.success({
@ -13,14 +8,6 @@ export function elSuccess(message = '操作成功') {
})
}
export function elAlert(msg, callback = () => ({})) {
return MessageBox.alert(msg, {
type: 'warning',
cancelButtonClass: 'is-plain',
callback
})
}
export function elConfirm(msg = '确认进行该操作?', ignoreReject = true) {
const promise = MessageBox.confirm(msg, {
type: 'warning',
@ -33,20 +20,3 @@ export function elConfirm(msg = '确认进行该操作?', ignoreReject = true)
return promise
}
export function elPrompt(msg) {
return new Promise(resolve => {
MessageBox.prompt(msg, {
type: 'warning',
cancelButtonClass: 'is-plain',
inputType: 'textarea',
inputValidator: str => {
if (isEmpty(str)) return '请输入内容'
if (str.length > 200) return '长度最多200'
return true
}
})
.then(({value}) => resolve(value))
.catch(() => ({}))
})
}

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

@ -39,16 +39,6 @@ export function set(key, obj, storage = window.sessionStorage, useZip = defaultU
storage.setItem(`${keyPrefix}${key}`, item)
}
/**
* 判断本地存储中是否存在指定键名
* @param key {string} 键名自动加上统一前缀
* @param storage {Storage} window.sessionStorage或window.localStorage
* @return {boolean} 存在则返回true
*/
export function exist(key, storage = window.sessionStorage) {
return !isEmpty(storage.getItem(`${keyPrefix}${key}`))
}
/**
* 根据指定键名移除本地存储对应项
* @param key {string} 键名自动加上统一前缀

@ -1,127 +1,3 @@
import {deepClone} from "@/util"
const DEFAULT_PROPS = {
id: 'id',
pid: 'pid',
children: 'children',
rootPredicate: 0,
leafHasChildren: false
}
/**
* 列表转树形结构
*
* @param list {array}
* @param props 树的配置项
* @param props.id {string} 节点的id属性名称
* @param props.pid {string} 节点的父级id属性名称
* @param props.children {string} 节点的子级节点属性名称
* @param props.rootPredicate {*|function} 根节点的判定方法传入函数时参数为节点对象
* @param props.leafHasChildren {boolean} 叶子节点是否能有children属性
* @return {array}
*/
export function createTree(list, props = {}) {
if (!list || list.length <= 0) return []
const {
id = DEFAULT_PROPS.id,
pid = DEFAULT_PROPS.pid,
children = DEFAULT_PROPS.children,
rootPredicate = DEFAULT_PROPS.rootPredicate,
leafHasChildren = DEFAULT_PROPS.leafHasChildren
} = props
const info = {}
list.forEach(i => {
info[i[id]] = i
if (leafHasChildren) i[children] = []
})
const predicate = (() => {
return typeof rootPredicate === 'function'
? rootPredicate
: key => key === rootPredicate
})()
return list.filter(node => {
const key = node[pid]
const parent = info[key]
if (parent) {
if (!parent[children]) parent[children] = []
parent[children].push(node)
}
return predicate(key, node)
})
}
/**
* 完全树full拿到某些带value的节点数组limit获得裁剪后的树
* 只用于裁剪客户和供应商的行政区域树
*
* @param full {array}
* @param limit {array}
*/
export function createLimitTree(full, limit) {
const map = limit.reduce((m, n) => {
m[n.id] = n.value
return m
}, {})
full = deepClone(full)
const result = shakeTree(full, node => {
const value = map[node.id]
if (value !== undefined) {
node.value = value
return true
}
return false
})
result.forEach(i => calc(i))
return result
}
/**
* createLimitTree的另一版本
*
* @param fullMap 完全树的节点map
* @param limit {array}
*/
export function createLimitTreeByMap(fullMap, limit) {
const resultNodes = {}
//从该节点往上查找父节点
function findParent(node) {
//如果该节点已存在于结果集中,说明包含该节点的上级分支也全部存在,所以跳过
if (resultNodes[node.id]) return
//使用full中的数据,仅保留node的value
const fullNode = fullMap[node.id]
if (!fullNode) return
resultNodes[fullNode.id] = {...node, ...fullNode}
//查找父节点
const parent = fullMap[fullNode.pid]
if (!parent) return
return findParent(parent)
}
for (const node of limit) {
if (!fullMap[node.id]) continue
findParent(node)
}
const result = createTree(Object.values(resultNodes), {rootPredicate: '0'})
result.forEach(i => calc(i))
return result
}
/**
* 根据判断函数裁剪树当节点不满足predicate且无下级节点时将被裁剪
* 此方法会改变原数组的children属性
@ -144,84 +20,3 @@ export function shakeTree(tree, predicate = () => true, childrenKey = 'children'
return predicate(data) || children && children.length > 0
})
}
/**
* 自底向上计算每个节点的value
*
* @param node
* @param valueKey {string}
* @param childrenKey {string}
* @return {number}
*/
function calc(node, valueKey = 'value', childrenKey = 'children') {
if (!node) return 0
const children = node[childrenKey]
const childValue = node[valueKey] || 0
if (!children) return childValue
const value = childValue + children.reduce((v, child) => v + calc(child), 0)
node[valueKey] = value
return value
}
//树转一维id数组
export function getNodeId(arr) {
if (!arr) return []
const res = []
arr.forEach(i => {
res.push(i.id)
if (i.children && i.children.length > 0) {
res.push(...getNodeId(i.children))
}
})
return res
}
//树转一维数组
export function flatTree(tree, childrenKey = 'children') {
const result = []
tree.forEach(node => {
if (node[childrenKey]) {
result.push(node)
result.push.apply(result, flatTree(node[childrenKey], childrenKey))
}
else result.push(node)
})
return result
}
export function getNodesByDfs(node) {
const nodes = []
const stack = [node]
while (stack.length > 0) {
const item = stack.pop()
nodes.push(item)
const children = item.children
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i])
}
}
return nodes
}
export function getNodesByBfs(node) {
const nodes = []
const queue = [node]
while (queue.length > 0) {
const item = queue.shift()
nodes.push(item)
queue.push(...item.children)
}
return nodes
}

@ -2,49 +2,6 @@ export function isString(str) {
return typeof str === 'string' || str instanceof String
}
export function isInteger(v) {
const t = parseFloat(v)
return t.toString() !== 'NaN' && t < 2147483647 && t > -2147483648
}
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
export function isEmail(email) {
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return reg.test(email)
}
export function isImage(suffix) {
const reg = /\.(png|jpg|gif|jpeg|webp|bmp)$/
return reg.test(suffix.toLowerCase())
}
export function isDoc(suffix) {
return /\.(doc|docx)$/.test(suffix.toLowerCase())
}
export function isPdf(suffix) {
return /\.pdf$/.test(suffix.toLowerCase())
}
export function isPpt(suffix) {
return /\.(ppt|pptx)$/.test(suffix.toLowerCase())
}
export function isRar(suffix) {
return /\.rar$/.test(suffix.toLowerCase())
}
export function isXls(suffix) {
return /\.(xls|xlsx)$/.test(suffix.toLowerCase())
}
export function isTxt(suffix) {
return /\.txt$/.test(suffix.toLowerCase())
}
export function isZip(suffix) {
return /\.zip$/.test(suffix.toLowerCase())
}

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

@ -1,4 +1,4 @@
@import "~@/asset/style/var";
@import "~el-admin-layout/src/style/var";
.skeleton-page-wrapper {
margin: $page-view-margin;

@ -14,7 +14,7 @@ module.exports = {
runtimeCompiler: true,
lintOnSave: false,
productionSourceMap: settings.isDev,
parallel: true,
transpileDependencies: ['el-admin-layout'],
devServer: {
port: process.env.port || 8079,
contentBasePublicPath: settings.contextPath,

Loading…
Cancel
Save