Appearance
标签导航支持横向滑动 #
当标签导航太多时,超出页面宽度时,可以左右横向滑动
滑动后
4-1 添加 scrollbar 组件 #
element.ts 中导入 el-scrollbar #
创建 ScrollPanel 组件 #
src/layout/components/TagsView/ScrollPanel.vue
vue
<template>
<el-scrollbar
wrap-class="scroll-wrapper"
>
<slot />
</el-scrollbar>
</template>
<script>
export default {
name: 'ScrollPanel'
}
</script>
<style lang="scss">
.scroll-wrapper {
position: relative;
width: 100%;
white-space: nowrap;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
以插槽形式包裹 tagsview 里面内容
4-2 修改 tagsview #
用 scrollpanel 组件包裹 tagsview 组件
这里样式简单做了个修改
src/layout/components/TagsView/index.vue
vue
<template>
<div class="tags-view-container">
<scroll-panel>
<div class="tags-view-wrapper">
<router-link
class="tags-view-item"
:class="{
active: isActive(tag)
}"
v-for="(tag, index) in visitedTags"
:key="index"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
>
{{ tag.title }}
<!-- affix固定的路由tag是无法删除 -->
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
></span>
</router-link>
</div>
</scroll-panel>
</div>
</template>
<script lang="ts">
import path from 'path'
import { defineComponent, computed, watch, onMounted } from 'vue'
import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'
import { useStore } from '@/store'
import { RouteLocationWithFullPath } from '@/store/modules/tagsView'
import { routes } from '@/router'
import ScrollPanel from './ScrollPanel.vue'
export default defineComponent({
name: 'TagsView',
components: {
ScrollPanel
},
setup() {
const store = useStore()
const router = useRouter()
const route = useRoute()
// 可显示的tags view
const visitedTags = computed(() => store.state.tagsView.visitedViews)
// 从路由表中过滤出要affixed tagviews
const fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {
let tags: RouteLocationWithFullPath[] = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
// 把路由路径解析成完整路径,路由可能是相对路径
const tagPath = path.resolve(basePath, route.path)
tags.push({
name: route.name,
path: tagPath,
fullPath: tagPath,
meta: { ...route.meta }
} as RouteLocationWithFullPath)
}
// 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)
if (route.children) {
const childTags = fillterAffixTags(route.children, route.path)
if (childTags.length) {
tags = [...tags, ...childTags]
}
}
})
return tags
}
// 初始添加affix的tag
const initTags = () => {
const affixTags = fillterAffixTags(routes)
for (const tag of affixTags) {
if (tag.name) {
store.dispatch('tagsView/addVisitedView', tag)
}
}
}
// 添加tag
const addTags = () => {
const { name } = route
if (name) {
store.dispatch('tagsView/addView', route)
}
}
// 路径发生变化追加tags view
watch(() => route.path, () => {
addTags()
})
// 最近当前router到tags view
onMounted(() => {
initTags()
addTags()
})
// 当前是否是激活的tag
const isActive = (tag: RouteRecordRaw) => {
return tag.path === route.path
}
// 让删除后tags view集合中最后一个为选中状态
const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {
// 得到集合中最后一个项tag view 可能没有
const lastView = visitedViews[visitedViews.length - 1]
if (lastView) {
router.push(lastView.fullPath as string)
} else { // 集合中都没有tag view时
// 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)
if (view.name === 'Dashboard') {
router.replace({ path: '/redirect' + view.fullPath as string })
} else {
// tag都没有了 删除的也不是Dashboard 只能跳转首页
router.push('/')
}
}
}
// 关闭当前右键的tag路由
const closeSelectedTag = (view: RouteLocationWithFullPath) => {
// 关掉并移除view
store.dispatch('tagsView/delView', view).then(() => {
// 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
if (isActive(view)) {
toLastView(visitedTags.value, view)
}
})
}
// 是否是始终固定在tagsview上的tag
const isAffix = (tag: RouteLocationWithFullPath) => {
return tag.meta && tag.meta.affix
}
return {
visitedTags,
isActive,
closeSelectedTag,
isAffix
}
}
})
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
overflow: hidden;
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
background: #fff;
color: #495060;
padding: 0 8px;
box-sizing: border-box;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
position: relative;
display: inline-block;
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
background: #fff;
}
}
}
}
}
</style>
<style lang="scss">
.tags-view-container {
.el-icon-close {
width: 16px;
height: 16px;
position: relative;
left: 2px;
border-radius: 50%;
text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(.6);
display: inline-block;
vertical-align: -1px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
4-3 样式调整 #
src/layout/index.vue
vue
<template>
<div class="app-wrapper">
<div class="sidebar-container">
<Sidebar />
</div>
<div class="main-container">
<div class="header">
<navbar />
<tags-view />
</div>
<!-- AppMain router-view -->
<app-main />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Sidebar from './components/Sidebar/index.vue'
import AppMain from './components/AppMain.vue'
import Navbar from './components/Navbar.vue'
import TagsView from './components/TagsView/index.vue'
export default defineComponent({
components: {
Sidebar,
AppMain,
Navbar,
TagsView
}
})
</script>
<style lang="scss" scoped>
.app-wrapper {
display: flex;
width: 100%;
height: 100%;
.main-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.app-main {
/* 50= navbar 50 如果有tagsview + 34 */
min-height: calc(100vh - 84px);
}
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
4-4 注意使用标签导航 #
使用标签导航的路由 必须要 name 属性 因为方便我们根据 name 进行路由筛选和缓存 keep-alive
本节参考源码 #
https://gitee.com/brolly/vue3-element-admin/commit/59741362e40d74bd4834f752ee61752d165b2e54