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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 toesbieya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # el-admin-layout
2
+
3
+ 这是一个基于`element-ui`的后台管理layout组件,需要配合`vue2`、`vue-router3`使用。
4
+
5
+
6
+ ## 功能特性
7
+
8
+ - flex布局、有限的移动端适配
9
+ - 仿`ant-design-pro`的三种导航
10
+ - 多页签
11
+ - 侧边栏和顶栏的深色\亮色主题
12
+ - 侧边栏可搜索
13
+ - 路由和菜单分离
14
+ - 支持各种页面缓存(不支持嵌套路由)
15
+ - 支持iframe、外链
16
+ - 数据外部可控,满足大部分定制化需求
17
+
18
+ ## 安装
19
+
20
+ ```
21
+ npm i el-admin-layout -S
22
+ ```
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "qdt-admin-layout",
3
+ "version": "1.0.0",
4
+ "description": "基于element-ui的后台管理layout",
5
+ "main": "src/index.js",
6
+ "module": "dist/esm/index.js",
7
+ "jsdelivr": "dist/index.umd.min.js",
8
+ "types": "types/index.d.ts",
9
+ "author": "toesbieya",
10
+ "license": "MIT",
11
+ "homepage": "",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": ""
15
+ },
16
+ "bugs": {
17
+ "url": ""
18
+ },
19
+ "scripts": {
20
+ "serve": "vue-cli-service serve",
21
+ "build:example": "vue-cli-service build",
22
+ "build:lib-umd": "vue-cli-service build --target lib --no-clean --formats umd-min --name index build/umd.js",
23
+ "build:lib-esm": "node build/esm.js",
24
+ "build:css": "node build/css.js",
25
+ "dist": "npm run build:lib-umd && npm run build:lib-esm && npm run build:css"
26
+ },
27
+ "peerDependencies": {
28
+ "element-ui": "^2.15.0",
29
+ "vue": "^2.6.12"
30
+ },
31
+ "devDependencies": {
32
+ "@vue/cli-plugin-babel": "^5.0.0-rc.1",
33
+ "@vue/cli-service": "^5.0.0-rc.1",
34
+ "element-ui": "^2.15.7",
35
+ "sass": "^1.43.4",
36
+ "sass-loader": "^12.3.0",
37
+ "vue": "^2.6.14",
38
+ "vue-router": "^3.5.3",
39
+ "vue-template-compiler": "^2.6.14",
40
+ "vuex": "^3.6.2"
41
+ },
42
+ "browserslist": [
43
+ "Chrome > 80"
44
+ ]
45
+ }
@@ -0,0 +1,354 @@
1
+ <script>
2
+ /**
3
+ * 默认的侧边栏实现,支持以抽屉形式渲染
4
+ */
5
+
6
+ import menuMixin from '../../../mixin/menu'
7
+ import { appGetters, asideGetters, asideMutations } from '../../../store'
8
+ import Logo from '../../../component/Logo'
9
+ import NavMenu from '../../../component/NavMenu'
10
+ import Hamburger from '../../../component/Hamburger'
11
+ import LoadingSpinner from '../../../component/LoadingSpinner'
12
+ import { getRouterActiveMenu, isRedirectRouter } from '../../../config/logic'
13
+
14
+ export default {
15
+ name: 'DefaultSidebar',
16
+
17
+ mixins: [menuMixin],
18
+
19
+ data() {
20
+ return {
21
+ // 开启了自动隐藏时,判断鼠标是否在侧边栏外
22
+ mouseOutside: true,
23
+ // 开启了自动隐藏时,用于判断鼠标是否在弹出菜单内
24
+ openedMenuNum: 0
25
+ }
26
+ },
27
+
28
+ computed: {
29
+ // 侧边栏菜单
30
+ sidebarMenus() {
31
+ const menus = appGetters.menus
32
+ let finalData
33
+
34
+ // 移动端时,侧边栏只会按侧边栏导航模式渲染
35
+ if (appGetters.isMobile) finalData = menus
36
+ else {
37
+ // 只有导航模式为aside或mix时才会渲染侧边栏
38
+ switch (appGetters.navMode) {
39
+ case 'aside':
40
+ finalData = menus
41
+ break
42
+ case 'mix':
43
+ const root = menus.find(i => i.fullPath === appGetters.activeRootMenu)
44
+ finalData = root ? root.children || [] : []
45
+ break
46
+ default:
47
+ finalData = []
48
+ }
49
+ }
50
+
51
+ const f = asideGetters.postMenus
52
+ return f ? f(this.copyMenus(finalData)) : finalData
53
+ },
54
+
55
+ // 当是移动端或设置了侧边栏自动隐藏时将侧边栏用抽屉包裹
56
+ renderInDrawer() {
57
+ return appGetters.isMobile || asideGetters.autoHide
58
+ },
59
+ // el-drawer的自定义类名
60
+ drawerClass() {
61
+ const behindHeader = !appGetters.isMobile && appGetters.struct === 'top-bottom'
62
+ return `sidebar-drawer${behindHeader ? ' behind-header' : ''}`
63
+ },
64
+
65
+ // 侧边栏的显隐状态,true显示、false隐藏
66
+ show() {
67
+ // store中要显示那就显示
68
+ if (asideGetters.show) return true
69
+
70
+ // 移动端,false
71
+ if (appGetters.isMobile) return false
72
+
73
+ // 未设置侧边栏自动隐藏,false
74
+ if (!asideGetters.autoHide) return false
75
+
76
+ // 鼠标在侧边栏内,true
77
+ if (!this.mouseOutside) return true
78
+
79
+ // 侧边栏处于折叠状态 且 存在弹出的子菜单,true
80
+ return this.collapse && this.openedMenuNum > 0
81
+ },
82
+
83
+ // 侧边栏的折叠状态,true折叠、false展开,仅在pc端可折叠
84
+ collapse() {
85
+ return asideGetters.collapse && !appGetters.isMobile
86
+ },
87
+
88
+ // 是否需要显示logo
89
+ showLogo() {
90
+ return appGetters.showLogo && (appGetters.isMobile || appGetters.struct === 'left-right')
91
+ },
92
+
93
+ sidebarClass() {
94
+ return { 'sidebar': true, 'collapse': this.collapse }
95
+ },
96
+
97
+ // 只有设置了自动隐藏时才需要绑定鼠标的移入移出事件
98
+ sidebarEvent() {
99
+ return !appGetters.isMobile && asideGetters.autoHide
100
+ ? { mouseleave: this.onMouseLeave, mouseenter: this.onMouseEnter }
101
+ : undefined
102
+ }
103
+ },
104
+
105
+ watch: {
106
+ // 记录高亮菜单以及一些其他操作
107
+ $route: {
108
+ immediate: true,
109
+ handler(to) {
110
+ if (isRedirectRouter(to)) return
111
+
112
+ this.activeMenu = getRouterActiveMenu(this.$route)
113
+
114
+ const elMenu = this.$_getElMenuInstance()
115
+ if (!elMenu) return
116
+
117
+ this.resetActiveMenu()
118
+
119
+ const item = elMenu.items[this.activeMenu]
120
+ if (!item) return
121
+
122
+ // 由于elMenu的initOpenedMenu()不会触发select事件,所以选择手动触发
123
+ this.onSelect(item.index, item.indexPath, item, false)
124
+
125
+ // 滚动至激活的菜单
126
+ this.$nextTick(this.moveToActiveMenuVertically)
127
+ }
128
+ }
129
+ },
130
+
131
+ methods: {
132
+ // 返回传入的菜单数据的拷贝副本
133
+ copyMenus(menus) {
134
+ return menus.map(menu => {
135
+ const result = { ...menu }
136
+ if (result.children) {
137
+ result.children = this.copyMenus(result.children)
138
+ }
139
+ return result
140
+ })
141
+ },
142
+
143
+ // 模拟选中菜单
144
+ onSelect(index, indexPath, item, jump = true) {
145
+ // 非折叠且开启手风琴模式时,收起其它展开项
146
+ if (!asideGetters.collapse && asideGetters.uniqueOpen) {
147
+ const elMenu = this.$_getElMenuInstance()
148
+ elMenu.openedMenus = indexPath.slice(0, -1)
149
+ }
150
+
151
+ jump && this.actionOnSelectMenu(index)
152
+
153
+ // 抽屉模式下需要关闭抽屉
154
+ if (this.renderInDrawer && this.show) {
155
+ asideMutations.close()
156
+ this.mouseOutside = true
157
+ }
158
+ },
159
+
160
+ // 将当前激活的菜单移动到视窗中
161
+ moveToActiveMenuVertically() {
162
+ const elMenu = this.$_getElMenuInstance()
163
+ if (!elMenu) return
164
+
165
+ const cur = elMenu.activeIndex
166
+ if (!cur) return
167
+
168
+ const curInstance = elMenu.items[cur]
169
+ if (!curInstance) return
170
+
171
+ let el = curInstance.$el
172
+
173
+ // 当侧边栏折叠时,需要滚动至可视区域的元素是激活菜单的最顶层父节点
174
+ if (elMenu.collapse) {
175
+ let rootParent = curInstance
176
+ while (rootParent.$parent.$options.componentName !== 'ElMenu') {
177
+ rootParent = rootParent.$parent
178
+ }
179
+ el = rootParent.$el
180
+ }
181
+
182
+ /*
183
+ * 这里考虑了菜单展开时的200ms动画时间
184
+ * 为什么不分情况讨论?比如当subMenu已经是展开状态时,无需延时滚动
185
+ * 但这种情况无法判断,因为这时menu.openedMenus已经包含了subMenu,无论subMenu之前是否展开
186
+ * 所以统一延时300ms
187
+ * */
188
+ window.setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 300)
189
+ },
190
+
191
+ // 开启侧边栏自动隐藏后的鼠标移动事件
192
+ onMouseMove(e) {
193
+ // 鼠标移动至屏幕左侧边缘时,标识鼠标在侧边栏内部
194
+ if (e.clientX <= 1) this.mouseOutside = false
195
+ },
196
+ onMouseLeave() {
197
+ this.mouseOutside = true
198
+ },
199
+ onMouseEnter() {
200
+ this.mouseOutside = false
201
+ },
202
+
203
+ // 渲染el-menu时监听其展开菜单
204
+ watchOpenedMenus() {
205
+ // 尝试取消之前设置的监听
206
+ if (this.watchOpenedMenusCallback) {
207
+ this.watchOpenedMenusCallback()
208
+ this.watchOpenedMenusCallback = null
209
+ }
210
+
211
+ const elMenu = this.$_getElMenuInstance()
212
+ if (!elMenu) return
213
+
214
+ this.watchOpenedMenusCallback = elMenu.$watch('openedMenus', v => {
215
+ this.openedMenuNum = v.length
216
+ })
217
+ },
218
+
219
+ // 抽屉打开的过渡动画结束时,滚动至高亮菜单
220
+ onDrawerOpened() {
221
+ this.$nextTick(this.moveToActiveMenuVertically)
222
+ },
223
+
224
+ renderHeader(h) {
225
+ const defaultContent = this.showLogo && <Logo show-title={!this.collapse}/>
226
+ const { headerSlot } = asideGetters
227
+
228
+ return headerSlot ? headerSlot(h, defaultContent) : defaultContent
229
+ },
230
+ renderFooter(h) {
231
+ const defaultContent = asideGetters.showHamburger && !this.renderInDrawer && <Hamburger/>
232
+ const { footerSlot } = asideGetters
233
+
234
+ let children
235
+
236
+ if (footerSlot) {
237
+ let renderResult = footerSlot(h, defaultContent)
238
+ if (Array.isArray(renderResult)) {
239
+ renderResult = renderResult.filter(Boolean)
240
+ if (renderResult.length > 0) {
241
+ children = renderResult
242
+ }
243
+ }
244
+ else children = renderResult
245
+ }
246
+ else children = defaultContent
247
+
248
+ return children && (
249
+ <div class="sidebar-footer">
250
+ {children}
251
+ </div>
252
+ )
253
+ }
254
+ },
255
+
256
+ created() {
257
+ // 侧边栏菜单变化时设置当前的高亮菜单
258
+ // 在此前,activeMenu会在watch:$route中发生第一次变化(不会真有人把menu.meta.activeMenu设成''吧?)
259
+ this.$watch('sidebarMenus', this.setDefaultActiveMenu, { immediate: true })
260
+
261
+ // 添加或移除鼠标移动事件
262
+ this.$watch(
263
+ () => {
264
+ // 如果是移动端,false
265
+ if (appGetters.isMobile) return false
266
+
267
+ // 如果未启用侧边栏自动隐藏,false
268
+ if (!asideGetters.autoHide) return false
269
+
270
+ // 侧边栏为打开状态,false
271
+ if (this.show) return false
272
+
273
+ // 鼠标在侧边栏外部,true
274
+ return this.mouseOutside
275
+ },
276
+ v => {
277
+ // 尝试移除之前可能添加的事件
278
+ window.removeEventListener('mousemove', this.onMouseMove)
279
+
280
+ v && window.addEventListener('mousemove', this.onMouseMove)
281
+ },
282
+ { immediate: true }
283
+ )
284
+
285
+ // 切换至移动端 或 切换至桌面端且设置了自动隐藏时,收起侧边栏
286
+ this.$watch(
287
+ () => appGetters.isMobile,
288
+ v => (v || asideGetters.autoHide) && asideMutations.close()
289
+ )
290
+ },
291
+
292
+ beforeDestroy() {
293
+ window.removeEventListener('mousemove', this.onMouseMove)
294
+ },
295
+
296
+ render(h) {
297
+ // 没有菜单时,仅当设置了alwaysRender才退出后续渲染
298
+ if (this.sidebarMenus.length === 0 && !asideGetters.alwaysRender) {
299
+ return
300
+ }
301
+
302
+ const sidebar = (
303
+ <div {...{ class: this.sidebarClass, on: this.sidebarEvent }}>
304
+ {this.renderHeader(h)}
305
+
306
+ {appGetters.loadingMenu
307
+ ? (
308
+ <div style="position: relative;flex: 1">
309
+ <LoadingSpinner/>
310
+ </div>
311
+ )
312
+ : <NavMenu
313
+ ref="nav-menu"
314
+ menus={this.sidebarMenus}
315
+ collapse={this.collapse}
316
+ default-active={this.defaultActive}
317
+ default-openeds={asideGetters.defaultOpeneds}
318
+ theme={asideGetters.theme}
319
+ unique-opened={asideGetters.uniqueOpen}
320
+ show-parent-on-collapse={asideGetters.showParentOnCollapse}
321
+ inline-indent={asideGetters.inlineIndent}
322
+ switch-transition-name={asideGetters.switchTransitionName}
323
+ menu-icon-slot={asideGetters.menuIconSlot}
324
+ menu-content-slot={asideGetters.menuContentSlot}
325
+ {...{
326
+ // 只能在nav-menu的mounted里,自身mounted时nav-menu可能还未渲染
327
+ on: { select: this.onSelect, 'hook:mounted': this.watchOpenedMenus }
328
+ }}
329
+ />
330
+ }
331
+
332
+ {this.renderFooter(h)}
333
+ </div>
334
+ )
335
+
336
+ if (!this.renderInDrawer) return sidebar
337
+
338
+ return (
339
+ <el-drawer
340
+ visible={this.show}
341
+ with-header={false}
342
+ custom-class={this.drawerClass}
343
+ modal={appGetters.isMobile} // 设置自动隐藏时不使用遮罩
344
+ direction="ltr"
345
+ size="auto"
346
+ on-close={asideMutations.close}
347
+ on-opened={this.onDrawerOpened}
348
+ >
349
+ {sidebar}
350
+ </el-drawer>
351
+ )
352
+ }
353
+ }
354
+ </script>
@@ -0,0 +1,32 @@
1
+ .sidebar {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ width: $aside-width;
6
+ transition: width 0.2s;
7
+
8
+ > .el-menu {
9
+ flex: 1;
10
+ }
11
+
12
+ &.collapse {
13
+ width: 90px;
14
+
15
+ .logo-container {
16
+ padding-left: ($aside-collapse-width - $logo-size) / 2;
17
+ }
18
+ }
19
+ }
20
+
21
+ //侧边栏抽屉模式
22
+ .sidebar-drawer {
23
+ // 页面为上下结构时,侧边栏的抽屉模式需要扣除顶栏的高度
24
+ &.behind-header {
25
+ top: $header-height;
26
+ }
27
+
28
+ // 为了el-menu能够滚动
29
+ > .el-drawer__body {
30
+ height: 100%;
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ <script>
2
+ /**
3
+ * 侧边栏
4
+ * 拆成Aside和DefaultSidebar是为了能让用户自定义侧边栏内容
5
+ */
6
+
7
+ import { appGetters, asideGetters } from '../../store'
8
+ import DefaultSidebar from './DefaultSidebar'
9
+
10
+ export default {
11
+ name: 'Aside',
12
+
13
+ computed: {
14
+ // 移动端下不能设置z-index,否则会被el-drawer的遮罩遮住
15
+ style() {
16
+ return appGetters.isMobile ? 'z-index: auto !important' : undefined
17
+ }
18
+ },
19
+
20
+ render(h) {
21
+ const { defaultSlot } = asideGetters
22
+
23
+ return (
24
+ <aside class={`aside ${asideGetters.theme}`} style={this.style}>
25
+ {defaultSlot ? defaultSlot(h) : <DefaultSidebar ref="default-sidebar"/>}
26
+ </aside>
27
+ )
28
+ }
29
+ }
30
+ </script>
@@ -0,0 +1,23 @@
1
+ @import './DefaultSidebar/style';
2
+
3
+ .aside {
4
+ z-index: $header-z-index - 1;
5
+
6
+ // 侧边栏底部区域
7
+ .sidebar-footer {
8
+ border-top-width: 1px;
9
+ border-top-style: solid;
10
+ }
11
+
12
+ // 底部汉堡包
13
+ .hamburger {
14
+ padding-left: $menu-padding;
15
+ height: 48px;
16
+ line-height: 48px;
17
+ font-size: $menu-icon-size;
18
+ cursor: pointer;
19
+ }
20
+ }
21
+
22
+ @import './theme-light';
23
+ @import './theme-dark';
@@ -0,0 +1,19 @@
1
+ .aside.dark {
2
+ &,
3
+ // 侧边栏可能为抽屉
4
+ .el-drawer {
5
+ background-color: $menu-background-dark;
6
+ }
7
+
8
+ .logo-container {
9
+ color: $--color-white;
10
+ }
11
+
12
+ .sidebar-footer {
13
+ border-top-color: $--border-color-dark;
14
+ }
15
+
16
+ .hamburger {
17
+ color: $menu-text-color-dark;
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ .aside.light {
2
+ .logo-container {
3
+ color: $--color-primary;
4
+ }
5
+
6
+ .root-sidebar {
7
+ background-color: $--color-white;
8
+ border-right-color: $--border-color-light;
9
+ }
10
+
11
+ .sidebar-footer {
12
+ border-top-color: $--border-color-light;
13
+ }
14
+
15
+ .hamburger {
16
+ color: $menu-text-color-light;
17
+ }
18
+ }
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div class="breadcrumb">
3
+ <transition-group name="breadcrumb">
4
+ <div
5
+ v-for="({fullPath, meta: {title}}, index) in items"
6
+ v-if="index !== items.length - 1"
7
+ :key="fullPath"
8
+ class="breadcrumb-item"
9
+ >
10
+ <span class="breadcrumb-inner is-link" @click="() => onClick(fullPath)">
11
+ {{ title }}
12
+ </span>
13
+ <span class="breadcrumb-separator">/</span>
14
+ </div>
15
+
16
+ <div v-if="lastItem" :key="lastItem.fullPath" class="breadcrumb-item">
17
+ <span class="breadcrumb-inner">{{ lastItem.meta.title }}</span>
18
+ </div>
19
+ </transition-group>
20
+ </div>
21
+ </template>
22
+
23
+ <script>
24
+ /**
25
+ * 面包屑,用于页面(Page)的页头
26
+ */
27
+
28
+ import { getMenuByFullPath } from '../../store'
29
+ import { getRouterKey, getRouterTitle, isRedirectRouter } from '../../config/logic'
30
+
31
+ export default {
32
+ name: 'Breadcrumb',
33
+
34
+ data() {
35
+ return {
36
+ items: []
37
+ }
38
+ },
39
+
40
+ computed: {
41
+ lastItem() {
42
+ return this.items.length <= 1
43
+ ? undefined
44
+ : this.items[this.items.length - 1]
45
+ }
46
+ },
47
+
48
+ watch: {
49
+ $route: {
50
+ handler(to) {
51
+ const result = this.generateBreadcrumb(to)
52
+ if (Array.isArray(result) && result.length !== 0) {
53
+ this.items = result
54
+ }
55
+ },
56
+ immediate: true
57
+ }
58
+ },
59
+
60
+ methods: {
61
+ onClick(fullPath) {
62
+ let menu = getMenuByFullPath(fullPath)
63
+ if (!menu) return
64
+
65
+ // 找到其叶子菜单
66
+ while (menu.children && menu.children.length >= 1) {
67
+ menu = menu.children[0]
68
+ }
69
+
70
+ const { route } = this.$router.resolve(menu.fullPath)
71
+
72
+ // 不刷新
73
+ if (getRouterKey(route) !== getRouterKey(this.$route)) {
74
+ this.$router.push(route)
75
+ }
76
+ },
77
+
78
+ generateBreadcrumb(route) {
79
+ const { path, fullPath, meta: { activeMenu } } = route
80
+
81
+ // 刷新时返回undefined
82
+ if (isRedirectRouter(route)) return
83
+
84
+ // 使用route.path而非fullPath进行匹配
85
+ const menuFullPath = activeMenu || path
86
+ const menu = getMenuByFullPath(menuFullPath)
87
+
88
+ // 没有匹配的菜单
89
+ if (!menu) return []
90
+
91
+ // 将菜单的所有父级放入数组
92
+ const items = [menu]
93
+ let parent = menu.parent
94
+ while (parent) {
95
+ items.unshift(parent)
96
+ parent = parent.parent
97
+ }
98
+
99
+ // 使用了activeMenu的还需要拼接上自己的标题
100
+ if (activeMenu) {
101
+ items.push({
102
+ fullPath,
103
+ meta: { title: getRouterTitle(route) }
104
+ })
105
+ }
106
+
107
+ return items
108
+ }
109
+ }
110
+ }
111
+ </script>