qdt-admin-layout 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -0
  3. package/package.json +45 -0
  4. package/src/component/Aside/DefaultSidebar/index.vue +354 -0
  5. package/src/component/Aside/DefaultSidebar/style.scss +32 -0
  6. package/src/component/Aside/index.vue +30 -0
  7. package/src/component/Aside/style.scss +23 -0
  8. package/src/component/Aside/theme-dark.scss +19 -0
  9. package/src/component/Aside/theme-light.scss +18 -0
  10. package/src/component/Breadcrumb/index.vue +111 -0
  11. package/src/component/Breadcrumb/style.scss +27 -0
  12. package/src/component/CachedRouterView/index.vue +162 -0
  13. package/src/component/ContextMenu/functionalUse.js +26 -0
  14. package/src/component/ContextMenu/index.vue +140 -0
  15. package/src/component/ContextMenu/style.scss +30 -0
  16. package/src/component/ElMenu/item.vue +66 -0
  17. package/src/component/ElMenu/mixin.js +36 -0
  18. package/src/component/ElMenu/sub.vue +118 -0
  19. package/src/component/Hamburger/index.vue +27 -0
  20. package/src/component/Hamburger/style.scss +3 -0
  21. package/src/component/Header/index.vue +131 -0
  22. package/src/component/Header/style.scss +74 -0
  23. package/src/component/Header/theme-dark.scss +31 -0
  24. package/src/component/Header/theme-light.scss +15 -0
  25. package/src/component/HorizontalResizableMenu/GhostMenu.vue +101 -0
  26. package/src/component/HorizontalResizableMenu/index.vue +280 -0
  27. package/src/component/HorizontalScroller/index.vue +91 -0
  28. package/src/component/HorizontalScroller/style.scss +12 -0
  29. package/src/component/Layout/index.vue +153 -0
  30. package/src/component/Layout/style.scss +42 -0
  31. package/src/component/LoadingSpinner/index.vue +17 -0
  32. package/src/component/Logo/index.vue +41 -0
  33. package/src/component/Logo/style.scss +26 -0
  34. package/src/component/NavMenu/index.vue +206 -0
  35. package/src/component/NavMenu/style.scss +159 -0
  36. package/src/component/NavMenu/theme-dark.scss +59 -0
  37. package/src/component/NavMenu/theme-light.scss +81 -0
  38. package/src/component/Page/content.vue +58 -0
  39. package/src/component/Page/iframe.vue +63 -0
  40. package/src/component/Page/index.vue +22 -0
  41. package/src/component/Page/style.scss +48 -0
  42. package/src/component/Redirect/index.vue +19 -0
  43. package/src/component/TagsView/index.vue +255 -0
  44. package/src/component/TagsView/style.scss +51 -0
  45. package/src/component/TagsView/util.js +67 -0
  46. package/src/config/const.js +24 -0
  47. package/src/config/defaultRoute.js +23 -0
  48. package/src/config/index.js +4 -0
  49. package/src/config/logic.js +53 -0
  50. package/src/helper.js +43 -0
  51. package/src/index.js +15 -0
  52. package/src/mixin/menu.js +72 -0
  53. package/src/store/app.js +132 -0
  54. package/src/store/aside.js +92 -0
  55. package/src/store/header.js +37 -0
  56. package/src/store/index.js +20 -0
  57. package/src/store/page.js +67 -0
  58. package/src/store/tagsView.js +186 -0
  59. package/src/store/util.js +35 -0
  60. package/src/style/index.scss +23 -0
  61. package/src/style/maxViewHeight.scss +65 -0
  62. package/src/style/transition.scss +71 -0
  63. package/src/style/var.scss +81 -0
  64. package/src/util.js +69 -0
  65. package/types/config.d.ts +12 -0
  66. package/types/helper.d.ts +10 -0
  67. package/types/index.d.ts +5 -0
  68. package/types/menu.d.ts +17 -0
  69. package/types/route.d.ts +15 -0
  70. package/types/store.d.ts +156 -0
  71. package/types/vue-router.d.ts +7 -0
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <div class="horizontal-scroller" @wheel="handleScroll">
3
+ <slot/>
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ /**
9
+ * 水平滚动区域,能够通过鼠标滚轮来进行水平滚动,用于页签栏
10
+ * 借鉴[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
11
+ */
12
+
13
+ export default {
14
+ name: 'HorizontalScroller',
15
+
16
+ props: {
17
+ // 元素之间的距离(px)
18
+ between: { type: Number, default: 4 }
19
+ },
20
+
21
+ methods: {
22
+ /**
23
+ * @param e {WheelEvent}
24
+ */
25
+ handleScroll(e) {
26
+ const eventDelta = e.deltaY
27
+ const { scrollLeft, scrollWidth, clientWidth } = this.$el
28
+
29
+ /**
30
+ * ①滚轮向上滚动并且是最左边
31
+ * ②滚轮向下滚动并且是最右边
32
+ */
33
+ if (eventDelta < 0 && scrollLeft === 0
34
+ || eventDelta > 0 && scrollWidth <= scrollLeft + clientWidth) {
35
+ return
36
+ }
37
+
38
+ this.$el.scrollLeft += eventDelta
39
+ },
40
+
41
+ /**
42
+ * 将指定元素以及其相邻元素移动至视窗内,外部调用
43
+ * @param target {Element}
44
+ */
45
+ moveToTarget(target) {
46
+ const nodes = Array.from(this.$el.children)
47
+ const { offsetWidth: containerWidth, scrollWidth, scrollLeft } = this.$el
48
+
49
+ if (containerWidth >= scrollWidth) return
50
+
51
+ let first = null
52
+ let last = null
53
+
54
+ // find first el and last el
55
+ if (nodes.length > 0) {
56
+ first = nodes[0]
57
+ last = nodes[nodes.length - 1]
58
+ }
59
+
60
+ if (first === target) {
61
+ return this.scrollLeft(0)
62
+ }
63
+ if (last === target) {
64
+ return this.scrollLeft(scrollWidth - containerWidth)
65
+ }
66
+
67
+ // find prev and next
68
+ const currentIndex = nodes.findIndex(item => item === target)
69
+ const prev = nodes[currentIndex - 1]
70
+ const next = nodes[currentIndex + 1]
71
+
72
+ // the el's offsetLeft after of next
73
+ const afterNextOffsetLeft = next.offsetLeft + next.offsetWidth + this.between
74
+
75
+ // the el's offsetLeft before of prev
76
+ const beforePrevOffsetLeft = prev.offsetLeft - this.between
77
+
78
+ if (afterNextOffsetLeft > scrollLeft + containerWidth) {
79
+ this.scrollLeft(afterNextOffsetLeft - containerWidth)
80
+ }
81
+ else if (beforePrevOffsetLeft < scrollLeft) {
82
+ this.scrollLeft(beforePrevOffsetLeft)
83
+ }
84
+ },
85
+
86
+ scrollLeft(val) {
87
+ this.$el.scrollTo({ left: val, behavior: 'smooth' })
88
+ }
89
+ }
90
+ }
91
+ </script>
@@ -0,0 +1,12 @@
1
+ .horizontal-scroller {
2
+ display: flex;
3
+ position: relative;
4
+ height: 100%;
5
+ white-space: nowrap;
6
+ overflow-x: auto;
7
+ overflow-y: hidden;
8
+
9
+ &::-webkit-scrollbar {
10
+ display: none;
11
+ }
12
+ }
@@ -0,0 +1,153 @@
1
+ <script>
2
+ import { Const } from '../../config'
3
+ import Aside from '../Aside'
4
+ import Header from '../Header'
5
+ import TagsView from '../TagsView'
6
+ import Page from '../Page'
7
+ import {
8
+ appGetters,
9
+ appMutations,
10
+ asideMutations,
11
+ headerMutations,
12
+ tagsViewGetters,
13
+ tagsViewMutations,
14
+ pageMutations
15
+ } from '../../store'
16
+ import { isMobile } from '../../helper'
17
+ import { debounce } from '../../util'
18
+
19
+ export default {
20
+ name: 'ElAdminLayout',
21
+
22
+ computed: {
23
+ isLeftRight() {
24
+ return appGetters.struct === 'left-right'
25
+ },
26
+ renderAside() {
27
+ return appGetters.isMobile || ['aside', 'mix'].includes(appGetters.navMode)
28
+ }
29
+ },
30
+
31
+ methods: {
32
+ // render时调用,根据插槽的变化修改store数据
33
+ mutateStoreSlot() {
34
+ const cache = this.$_cachedScopedSlots
35
+ const curr = this.$scopedSlots
36
+
37
+ // 减少的
38
+ Object.keys(cache).forEach(k => {
39
+ // 缓存中有,本次没有
40
+ if (!curr[k]) {
41
+ const f = this.getMutationBySlot(k)
42
+ f(undefined)
43
+ }
44
+ })
45
+
46
+ // 新增的
47
+ Object.keys(curr).forEach(k => {
48
+ // 本次有,缓存中没有
49
+ if (!cache[k]) {
50
+ const f = this.getMutationBySlot(k)
51
+ // scopedSlots: (props) => VNode
52
+ // render: (h, props) => VNode
53
+ f((h, props) => curr[k](props))
54
+ }
55
+ })
56
+
57
+ this.$_cachedScopedSlots = curr
58
+ },
59
+ getMutations(prefix) {
60
+ switch (prefix) {
61
+ case 'aside':
62
+ return asideMutations
63
+ case 'header':
64
+ return headerMutations
65
+ case 'page':
66
+ return pageMutations
67
+ }
68
+ },
69
+ getMutationBySlot(slot) {
70
+ const cache = this.$_slotMutationsMap
71
+
72
+ if (cache[slot]) return cache[slot]
73
+
74
+ // 不规则的
75
+ if (slot === 'logo') {
76
+ cache[slot] = appMutations.logoSlot
77
+ return cache[slot]
78
+ }
79
+ if (slot === 'tags-view-item') {
80
+ cache[slot] = tagsViewMutations.itemSlot
81
+ return cache[slot]
82
+ }
83
+
84
+ // aside -> asideMutations.defaultSlot
85
+ // aside-header -> asideMutations.headerSlot
86
+ // aside-menu-icon -> asideMutations.menuIconSlot
87
+ const arr = slot.split('-')
88
+ const mutations = this.getMutations(arr[0])
89
+
90
+ if (arr.length === 1) {
91
+ cache[slot] = mutations.defaultSlot
92
+ }
93
+ else {
94
+ let method = arr[1]
95
+
96
+ for (let i = 2, len = arr.length; i < len; i++) {
97
+ method += arr[i][0].toUpperCase() + [...arr[i].slice(1)].join('')
98
+ }
99
+
100
+ cache[slot] = mutations[method += 'Slot']
101
+ }
102
+
103
+ return cache[slot]
104
+ }
105
+ },
106
+
107
+ beforeCreate() {
108
+ // 缓存当前的插槽
109
+ this.$_cachedScopedSlots = Object.create(null)
110
+
111
+ // 用于加速根据插槽名查找store mutation的缓存map
112
+ this.$_slotMutationsMap = Object.create(null)
113
+ },
114
+
115
+ mounted() {
116
+ this.$_resize = debounce(() => {
117
+ !document.hidden && appMutations.isMobile(isMobile())
118
+ })
119
+ window.addEventListener('resize', this.$_resize)
120
+ },
121
+
122
+ beforeDestroy() {
123
+ if (this.$_resize) {
124
+ window.removeEventListener('resize', this.$_resize)
125
+ delete this.$_resize
126
+ }
127
+
128
+ delete this.$_cachedScopedSlots
129
+ delete this.$_slotMutationsMap
130
+ },
131
+
132
+ render() {
133
+ Const.enableLayoutSlot && this.mutateStoreSlot()
134
+
135
+ return (
136
+ <div class={{
137
+ 'el-admin-layout': true,
138
+ 'has-header': true,
139
+ 'has-tags-view': tagsViewGetters.enabled,
140
+ 'left-right': this.isLeftRight
141
+ }}>
142
+ <Header ref="header"/>
143
+
144
+ {tagsViewGetters.enabled && <TagsView ref="tags-view"/>}
145
+
146
+ {this.renderAside && <Aside ref="aside"/>}
147
+
148
+ <Page ref="page"/>
149
+ </div>
150
+ )
151
+ }
152
+ }
153
+ </script>
@@ -0,0 +1,42 @@
1
+ .el-admin-layout {
2
+ display: grid;
3
+ grid-template-columns: min-content 1fr;
4
+ grid-template-rows: min-content min-content 1fr;
5
+ height: 100%;
6
+ width: 100%;
7
+ overflow: hidden;
8
+
9
+ > header {
10
+ grid-column-start: 1;
11
+ grid-column-end: 3;
12
+ }
13
+
14
+ > nav {
15
+ grid-column: 2 / 3;
16
+ }
17
+
18
+ > aside {
19
+ display: flex;
20
+ grid-row-start: 2;
21
+ grid-row-end: 4;
22
+ }
23
+
24
+ > main {
25
+ grid-area: 3 / 2 / 4 / 3;
26
+ }
27
+
28
+ // 左右结构
29
+ &.left-right {
30
+ > header {
31
+ grid-column-start: 2;
32
+ }
33
+
34
+ > aside {
35
+ grid-row-start: 1;
36
+
37
+ // 左右结构时,侧边栏z-index需大于顶栏
38
+ // 上下结构时,侧边栏z-index需小于顶栏
39
+ z-index: $header-z-index + 1;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,17 @@
1
+ <template v-once>
2
+ <div class="el-loading-spinner">
3
+ <svg class="circular" viewBox="25 25 50 50">
4
+ <circle class="path" cx="50" cy="50" r="20" fill="none"/>
5
+ </svg>
6
+ </div>
7
+ </template>
8
+
9
+ <script>
10
+ /**
11
+ * 加载圈,用于侧边栏菜单、顶栏菜单的加载状态
12
+ */
13
+
14
+ export default {
15
+ name: 'LoadingSpinner'
16
+ }
17
+ </script>
@@ -0,0 +1,41 @@
1
+ <script>
2
+ import { appGetters } from '../../store'
3
+ import { isEmpty } from '../../util'
4
+
5
+ export default {
6
+ name: 'Logo',
7
+
8
+ props: { showTitle: Boolean },
9
+
10
+ methods: {
11
+ onClick(e) {
12
+ const { onLogoClick } = appGetters
13
+ if (onLogoClick) return onLogoClick(e)
14
+
15
+ const to = appGetters.logoRoute
16
+ const method = typeof to === 'object' && to.replace ? 'replace' : 'push'
17
+
18
+ this.$router[method](to, () => undefined)
19
+ }
20
+ },
21
+
22
+ render(h) {
23
+ const { logo: src, title: txt, logoSlot } = appGetters
24
+
25
+ const img = src && <img src={src}/>
26
+ const title = !isEmpty(txt) && this.showTitle && <h1>{txt}</h1>
27
+
28
+ const children = logoSlot ? logoSlot(h, { img, title, props: this.$props }) : [img, title]
29
+
30
+ if (!children || Array.isArray(children) && children.filter(Boolean).length === 0) {
31
+ return
32
+ }
33
+
34
+ return (
35
+ <div class="logo-container" on-click={this.onClick}>
36
+ {children}
37
+ </div>
38
+ )
39
+ }
40
+ }
41
+ </script>
@@ -0,0 +1,26 @@
1
+ .logo-container {
2
+ display: flex;
3
+ align-items: center;
4
+ width: 100%;
5
+ height: $header-height;
6
+ cursor: pointer;
7
+ padding: 0 $menu-padding;
8
+ box-sizing: border-box;
9
+
10
+ img {
11
+ height: $logo-size;
12
+
13
+ & + h1 {
14
+ margin-left: 12px;
15
+ }
16
+ }
17
+
18
+ h1 {
19
+ margin: 0;
20
+ overflow: hidden;
21
+ text-overflow: ellipsis;
22
+ white-space: nowrap;
23
+ font-size: 18px;
24
+ font-weight: $--font-weight-primary;
25
+ }
26
+ }
@@ -0,0 +1,206 @@
1
+ <script>
2
+ /**
3
+ * 基于el-menu封装的无限级菜单
4
+ * 自带亮色、暗色两种主题
5
+ */
6
+
7
+ import MenuItem from "../../component/ElMenu/item";
8
+ import SubMenu from "../../component/ElMenu/sub";
9
+ import { isEmpty } from "../../util";
10
+
11
+ /**
12
+ * 判断菜单是否没有子级,没有则返回true,否则false
13
+ *
14
+ * @param menu {MenuItem}
15
+ * @returns {boolean}
16
+ */
17
+ function isSingleMenu(menu) {
18
+ return !Array.isArray(menu.children) || menu.children.length === 0;
19
+ }
20
+
21
+ export default {
22
+ name: "NavMenu",
23
+
24
+ props: {
25
+ // 路由配置项组成的树形数组
26
+ menus: { type: Array, default: () => [] },
27
+
28
+ // 主题,light 或 dark
29
+ theme: { type: String, default: "light" },
30
+
31
+ // 垂直模式下子菜单的单位缩进距离
32
+ inlineIndent: Number,
33
+
34
+ // 水平模式下是否显示展开折叠图标
35
+ showCollapseIcon: Boolean,
36
+
37
+ // 折叠时的展开菜单是否显示父级
38
+ showParentOnCollapse: Boolean,
39
+
40
+ // menus变化时的过渡动画名称
41
+ switchTransitionName: String,
42
+
43
+ // 自定义渲染菜单图标,(h, {menu, depth}) => VNode
44
+ menuIconSlot: Function,
45
+
46
+ // 自定义渲染菜单内容,(h, {menu, depth}) => VNode
47
+ menuContentSlot: Function,
48
+
49
+ /*--------------el-menu原有props开始-------------*/
50
+ /*https://element.eleme.cn/#/zh-CN/component/menu*/
51
+
52
+ mode: { type: String, default: "vertical" }, // 在el-menu原效果上加了样式名
53
+ collapse: Boolean,
54
+ defaultActive: String,
55
+ defaultOpeneds: Array,
56
+ uniqueOpened: Boolean,
57
+ },
58
+
59
+ computed: {
60
+ children() {
61
+ const h = this.$createElement;
62
+ let children = this.renderMenus(h, this.menus);
63
+
64
+ const { switchTransitionName: name } = this;
65
+ if (!isEmpty(name)) {
66
+ children = h("transition-group", { props: { name } }, children);
67
+ }
68
+
69
+ return children;
70
+ },
71
+
72
+ themeClass() {
73
+ return `el-menu--${this.theme}`;
74
+ },
75
+ menuClass() {
76
+ return [
77
+ `el-menu--${this.mode}`,
78
+ this.themeClass,
79
+ !this.showCollapseIcon && "hide-collapse-icon",
80
+ ];
81
+ },
82
+ },
83
+
84
+ methods: {
85
+ // 将el-menu的select事件传递给外部
86
+ onSelect(...args) {
87
+ this.$emit("select", ...args);
88
+ },
89
+
90
+ // 渲染菜单图标
91
+ renderMenuIcon(h, menu, depth) {
92
+ const slot = this.menuIconSlot;
93
+
94
+ if (slot) return slot(h, { menu, depth });
95
+
96
+ const icon = menu.meta.icon;
97
+ return icon && <i style="margin-top:-18px" class={`menu-icon ${icon}`} />;
98
+ },
99
+ // 渲染菜单内容
100
+ renderMenuContent(h, menu, depth) {
101
+ const slot = this.menuContentSlot;
102
+ return slot ? slot(h, { menu, depth }) : <span>{menu.meta.title}</span>;
103
+ },
104
+ // 渲染无子级的菜单
105
+ renderSingleMenu(h, menu, depth) {
106
+ const { fullPath } = menu;
107
+ return (
108
+ <MenuItem
109
+ key={fullPath}
110
+ index={fullPath}
111
+ inline-indent={this.inlineIndent}
112
+ >
113
+ <template slot="title">
114
+ {this.renderMenuContent(h, menu, depth)}
115
+ </template>
116
+ </MenuItem>
117
+ );
118
+ },
119
+ // 渲染有子级的菜单
120
+ renderSubMenu(h, menu, children, depth) {
121
+ const { fullPath } = menu;
122
+ const noContent =
123
+ depth === 1 && this.collapse && this.mode === "vertical";
124
+ return (
125
+ <div key="index">
126
+ <SubMenu
127
+ key={fullPath}
128
+ index={fullPath}
129
+ inline-indent={this.inlineIndent}
130
+ popper-class={this.themeClass}
131
+ popper-append-to-body
132
+ >
133
+ <template slot="title">
134
+ {depth == 1 && (
135
+ <div>
136
+ {this.renderMenuIcon(h, menu, depth)}
137
+ <div style="margin-top:-24px;font-size:14px;margin-left:-2px">
138
+ {menu.meta.title}
139
+ </div>
140
+ </div>
141
+ )}
142
+ {!noContent && this.renderMenuContent(h, menu, depth)}
143
+ </template>
144
+ {children}
145
+ </SubMenu>
146
+ </div>
147
+ );
148
+ },
149
+ // 渲染有子级且需要显示父级的菜单
150
+ renderChildrenWithParentMenu(h, menu, children, depth) {
151
+ let data = [];
152
+ menu.children.forEach((x) => {
153
+ let childrens = [];
154
+ x.children.forEach((y) => {
155
+ childrens.push(this.renderSingleMenu(h, y, depth + 1));
156
+ });
157
+ data.push(
158
+ <div>
159
+ <div class="popover-menu__title">
160
+ {this.renderMenuContent(h, x, depth)}
161
+ </div>
162
+ <div class="el-menu el-menu--inline">{childrens}</div>
163
+ </div>
164
+ );
165
+ });
166
+ return data;
167
+ },
168
+ // 渲染菜单项
169
+ renderMenus(h, menus, depth = 1) {
170
+ return menus.map((menu) => {
171
+ let children = [];
172
+ //弹出菜单显示父级信息;
173
+ if (this.collapse && this.showParentOnCollapse) {
174
+ // 这里认为父级的深度应该+1
175
+ children = this.renderChildrenWithParentMenu(
176
+ h,
177
+ menu,
178
+ children,
179
+ depth + 1
180
+ );
181
+ }
182
+ // 一级菜单
183
+ return this.renderSubMenu(h, menu, children, depth);
184
+ });
185
+ },
186
+ },
187
+
188
+ render() {
189
+ return (
190
+ <el-menu
191
+ ref="el-menu"
192
+ class={this.menuClass}
193
+ mode={this.mode}
194
+ collapse={this.collapse}
195
+ collapse-transition={false}
196
+ unique-opened={this.uniqueOpened}
197
+ default-active={this.defaultActive}
198
+ default-openeds={this.defaultOpeneds}
199
+ on-select={this.onSelect}
200
+ >
201
+ {this.children}
202
+ </el-menu>
203
+ );
204
+ },
205
+ };
206
+ </script>