我是靠谱客的博主 超级花瓣,这篇文章主要介绍实用的 vue tags 创建缓存导航的过程实现,现在分享给大家,希望可以做个参考。

需求

是要做一个tag,当切换页面的时候保留状态。

效果图:

思路

既然涉及了router跳转,那我们就去查api 发现keep-alive,巧了就用它吧。这里我们用到了include属性,该属性接受一个数组,当组件的name名称包含在inclue里的时候就会触发keep-alive。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Vue, Component, Watch, Mixins } from 'vue-property-decorator'; // 此处省略n行代码 // 这是个计算属性。(至于为什么这么写 这里就不介绍了。) get cachedViews():string[] { return this.$store.state.tagsView.cachedViews; } // 此处省略n行代码 <keep-alive :include="cachedViews"> <router-view :key="key"></router-view> </keep-alive>

那我们接下来就处理cachedViews变量就好了。

vuex实现

复制代码
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
import { Route } from 'vue-router'; // 检测规则 interface TagsState{ visitedViews: Route[]; cachedViews: string[]; } const state = (): TagsState => ({ visitedViews: [], // 展示的菜单 cachedViews: [], // 缓存菜单 用来activeed }); const mutations = { ADD_VISITED_VIEW: (state: TagsState, view: Route) => { if (state.visitedViews.some((v: any) => v.path === view.path)) { return; } state.visitedViews.push( Object.assign({}, view, { title: view.meta.title || 'no-name', }), ); }, ADD_CACHED_VIEW: (state: TagsState, view: Route) => { if (state.cachedViews.includes(view.meta.name)) { return; } if (!view.meta.noCache) { state.cachedViews.push(view.meta.name); } }, DEL_VISITED_VIEW: (state: TagsState, view: Route) => { for (const [i, v] of state.visitedViews.entries()) { if (v.path === view.path) { state.visitedViews.splice(i, 1); break; } } }, DEL_CACHED_VIEW: (state: TagsState, view: Route) => { const index = state.cachedViews.indexOf(view.meta.name); index > -1 && state.cachedViews.splice(index, 1); }, DEL_OTHERS_VISITED_VIEWS: (state: TagsState, view: Route) => { state.visitedViews = state.visitedViews.filter((v: any) => { return v.meta.affix || v.path === view.path; }); }, DEL_OTHERS_CACHED_VIEWS: (state: TagsState, view: Route) => { const index = state.cachedViews.indexOf(view.meta.name); if (index > -1) { state.cachedViews = state.cachedViews.slice(index, index + 1); } else { // if index = -1, there is no cached tags state.cachedViews = []; } }, DEL_ALL_VISITED_VIEWS: (state: TagsState) => { // keep affix tags const affixTags = state.visitedViews.filter((tag: any) => tag.meta.affix); state.visitedViews = affixTags; }, DEL_ALL_CACHED_VIEWS: (state: TagsState) => { state.cachedViews = []; }, UPDATE_VISITED_VIEW: (state: TagsState, view: Route) => { for (let v of state.visitedViews) { if (v.path === view.path) { v = Object.assign(v, view); break; } } }, }; const actions = { addView({ dispatch }: any, view: Route) { dispatch('addVisitedView', view); dispatch('addCachedView', view); }, addVisitedView({ commit }: any, view: Route) { commit('ADD_VISITED_VIEW', view); }, addCachedView({ commit }: any, view: Route) { commit('ADD_CACHED_VIEW', view); }, delView({ dispatch, state }: any, view: Route) { return new Promise((resolve) => { dispatch('delVisitedView', view); dispatch('delCachedView', view); resolve({ visitedViews: [...state.visitedViews], cachedViews: [...state.cachedViews], }); }); }, delVisitedView({ commit, state }: any, view: Route) { return new Promise((resolve) => { commit('DEL_VISITED_VIEW', view); resolve([...state.visitedViews]); }); }, delCachedView({ commit, state }: any, view: Route) { return new Promise((resolve) => { commit('DEL_CACHED_VIEW', view); resolve([...state.cachedViews]); }); }, delOthersViews({ dispatch, state }: any, view: Route) { return new Promise((resolve) => { dispatch('delOthersVisitedViews', view); dispatch('delOthersCachedViews', view); resolve({ visitedViews: [...state.visitedViews], cachedViews: [...state.cachedViews], }); }); }, delOthersVisitedViews({ commit, state }: any, view: Route) { return new Promise((resolve) => { commit('DEL_OTHERS_VISITED_VIEWS', view); resolve([...state.visitedViews]); }); }, delOthersCachedViews({ commit, state }: any, view: Route) { return new Promise((resolve) => { commit('DEL_OTHERS_CACHED_VIEWS', view); resolve([...state.cachedViews]); }); }, delAllViews({ dispatch, state }: any, view: Route) { return new Promise((resolve) => { dispatch('delAllVisitedViews', view); dispatch('delAllCachedViews', view); resolve({ visitedViews: [...state.visitedViews], cachedViews: [...state.cachedViews], }); }); }, delAllVisitedViews({ commit, state }: any) { return new Promise((resolve) => { commit('DEL_ALL_VISITED_VIEWS'); resolve([...state.visitedViews]); }); }, delAllCachedViews({ commit, state }: any) { return new Promise((resolve) => { commit('DEL_ALL_CACHED_VIEWS'); resolve([...state.cachedViews]); }); }, updateVisitedView({ commit }: any, view: Route) { commit('UPDATE_VISITED_VIEW', view); }, }; export default { namespaced: true, state, mutations, actions, };

上面代码,我们定义了一系列的对标签的操作。

组件实现

组件解构如图

TheTagsView.vue

复制代码
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
<script lang="ts"> /** * @author leo * @description #15638 【test_tabs 组件】tab组件 */ import { Component, Vue, Prop, Watch, Mixins } from 'vue-property-decorator'; import ScrollPane from './ScrollPane.vue'; import path from 'path'; @Component({ components: { ScrollPane, }, }) export default class TheTagsView extends Vue { get visitedViews() { return this.$store.state.tagsView.visitedViews; // 点开过的视图 } get routes() { return this.$store.state.permission.routes; } public visible: boolean = false; // 标签右键列表显示隐藏 public top: number = 0; // transform定位 public left: number = 0; // transform定位 public selectedTag: any = {}; // 当前活跃的标签 public affixTags: any[] = []; // 所有标签 @Watch('$route') public watchRoute() { this.addTags(); // 新增当前标签 this.moveToCurrentTag(); // 删除原活动标签 } @Watch('visible') public watchVisible(value: any) { if (value) { document.body.addEventListener('click', this.closeMenu); } else { document.body.removeEventListener('click', this.closeMenu); } } public isActive(route: any) { // 是否当前活动 return route.path === this.$route.path; } public isAffix(tag: any) { // 是否固定 return tag.meta && tag.meta.affix; } // 过滤当前标签于路由 public filterAffixTags(routes: any, basePath = '/') { let tags: any = []; routes.forEach((route: any) => { if (route.meta && route.meta.affix) { const tagPath = path.resolve(basePath, route.path); tags.push({ fullPath: tagPath, path: tagPath, name: route.name, meta: { ...route.meta }, }); } if (route.children) { const tempTags = this.filterAffixTags(route.children, route.path); if (tempTags.length >= 1) { tags = [...tags, ...tempTags]; } } }); return tags; } public addTags() { const { name } = this.$route; if (name) { this.$store.dispatch('tagsView/addView', this.$route); } return false; } public moveToCurrentTag() { const tags: any = this.$refs.tag; this.$nextTick(() => { if (tags) { for (const tag of tags) { if (tag.to.path === this.$route.path) { (this.$refs.scrollPane as any).moveToTarget(tag); // when query is different then update if (tag.to.fullPath !== this.$route.fullPath) { this.$store.dispatch('tagsView/updateVisitedView', this.$route); } break; } } } }); } public refreshSelectedTag(view: any) { this.$store.dispatch('tagsView/delCachedView', view).then(() => { const { fullPath } = view; this.$nextTick(() => { this.$router.replace({ path: '/redirect' + fullPath, }); }); }); } public closeSelectedTag(view: any) { this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => { if (this.isActive(view)) { this.toLastView(visitedViews, view); } }); } public closeOthersTags() { this.$router.push(this.selectedTag); this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => { this.moveToCurrentTag(); }); } public closeAllTags(view: any) { this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => { if (this.affixTags.some((tag) => tag.path === view.path)) { return; } this.toLastView(visitedViews, view); }); } public toLastView(visitedViews: any , view: any) { const latestView = visitedViews.slice(-1)[0]; if (latestView) { this.$router.push(latestView.fullPath); } else { // now the default is to redirect to the home page if there is no tags-view, // you can adjust it according to your needs. if (view.name === 'Dashboard') { // to reload home page this.$router.replace({ path: '/redirect' + view.fullPath }); } else { this.$router.push('/'); } } } public openMenu(tag: any , e: any) { const menuMinWidth = 105; const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left const offsetWidth = this.$el.offsetWidth; // container width const maxLeft = offsetWidth - menuMinWidth; // left boundary const left = e.clientX - offsetLeft + 15 + 160; // 15: margin right if (left > maxLeft) { this.left = maxLeft; } else { this.left = left; } this.top = e.clientY; this.visible = true; this.selectedTag = tag; } public closeMenu() { this.visible = false; } public mounted() { this.initTags(); this.addTags(); // 添加当前页面tag } } </script> <template> <div id="tags-view-container" class="tags-view-container"> <scroll-pane ref="scrollPane" class="tags-view-wrapper"> <router-link v-for="(tag, index) in visitedViews" ref="tag" :key="tag.path" :class="isActive(tag)?'active':''" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" tag="span" class="tags-view-item" @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" @contextmenu.prevent.native="openMenu(tag,$event)" > {{ tag.title }} <!-- <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> --> <span class="tab-border" v-if="index!==visitedViews.length-1"></span> </router-link> </scroll-pane> <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> <li @click="refreshSelectedTag(selectedTag)">刷新</li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭当前标签</li> <li @click="closeOthersTags">关闭其他标签</li> <li @click="closeAllTags(selectedTag)">关闭所有</li> </ul> </div> </template> <style lang="less" scoped> .tags-view-container { height: 46px; width: 100%; background: #fff; .tags-view-wrapper { position: relative; .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 46px; line-height: 46px; // border: 1px solid #d8dce5; color: #495060; background: #fff; padding: 0 4px; font-size: 14px; .tab-border { display: inline-block; height: 10px; width: 1px; background: #f1f1f1; margin-left: 4px; } &:hover { border-bottom: 2px solid #666; } &.active { // background-color: #1F1A16; border-bottom: 2px solid #1F1A16; color: #333; // border-color: #1F1A16; // &::before { // content: ''; // background: #fff; // display: inline-block; // width: 8px; // height: 8px; // border-radius: 50%; // position: relative; // margin-right: 2px; // } } } } .contextmenu { margin: 0; background: #fff; z-index: 3000; position: absolute; list-style-type: none; padding: 5px 0; border-radius: 4px; font-size: 12px; font-weight: 400; color: #333; box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); li { margin: 0; padding: 7px 16px; cursor: pointer; &:hover { background: #eee; } } } } </style> <style lang="less"> //reset element css of el-icon-close .tags-view-wrapper { .tags-view-item { .el-icon-close { width: 16px; height: 16px; vertical-align: 3px; 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: -3px; } &:hover { background-color: #b4bccc; color: #fff; } } } .el-scrollbar__bar{ pointer-events: none; opacity: 0; } } </style>

ScrollPane.vue

复制代码
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
<script lang="ts"> /** * @author leo * @description #15638 【test_tabs 组件】tab组件 */ import { Component, Vue, Prop, Watch, Mixins } from 'vue-property-decorator'; const tagAndTagSpacing = 4; // tagAndTagSpacing @Component({ components: { ScrollPane, }, }) export default class ScrollPane extends Vue { get scrollWrapper() { return (this.$refs.scrollContainer as any).$refs.wrap; } public left: number = 0; public handleScroll(e: any) { const eventDelta = e.wheelDelta || -e.deltaY * 40; const $scrollWrapper = this.scrollWrapper; $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4; } public moveToTarget(currentTag: any) { const $container = (this.$refs.scrollContainer as any).$el; const $containerWidth = $container.offsetWidth; const $scrollWrapper = this.scrollWrapper; const tagList: any = this.$parent.$refs.tag; let firstTag = null; let lastTag = null; // find first tag and last tag if (tagList.length > 0) { firstTag = tagList[0]; lastTag = tagList[tagList.length - 1]; } if (firstTag === currentTag) { $scrollWrapper.scrollLeft = 0; } else if (lastTag === currentTag) { $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth; } else { // find preTag and nextTag const currentIndex = tagList.findIndex((item: any) => item === currentTag); const prevTag = tagList[currentIndex - 1]; const nextTag = tagList[currentIndex + 1]; // the tag's offsetLeft after of nextTag const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing; // the tag's offsetLeft before of prevTag const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing; if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth; } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft; } } } } </script> <template> <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> <slot /> </el-scrollbar> </template> <style lang="less" scoped> .scroll-container { white-space: nowrap; position: relative; overflow: hidden; width: 100%; .el-scrollbar__bar { bottom: 0px; } .el-scrollbar__wrap { height: 49px; } } </style>

index.ts

复制代码
1
2
import TheTagsView from './TheTagsView.vue'; export default TheTagsView;

这样我们的组件就写完啦,有哪里有问题的小伙伴可以留言哦。

组件调用

因为是全局的,所以放在全局下直接调用就好了

总结

这样我们一个简单的能实现alive 页面的tag功能就实现了。大家赶紧尝试一下吧~

兄台,请留步。这里有几点要注意一下哦~

问题1: 开发环境缓存住了,线上环境不好用了

我们是根据组件name值是否是include里包含的来判断的。但是你会发现生产的的时候 class后面的名在线上被打包后变了。 什么?!这岂不是缓存不住了???是的。 所以解决办法如图。一般人我不告诉他0.o

问题2: tags的显示名字我在哪定义呢

tags显示的名字我怎么定义呢,好问题。小兄弟肯定没有仔细读代码

复制代码
1
2
3
4
5
6
7
8
ADD_VISITED_VIEW: (state: TagsState, view: Route) => { if (state.visitedViews.some((v: any) => v.path === view.path)) { return; } state.visitedViews.push( Object.assign({}, view, { title: view.meta.title || 'no-name', /// 我在这里!!!!! }), ); },

由上图我们可知,我是在路由的配置里mate标签里的tile里配置的。至于你,随你哦~

复制代码
1
2
3
4
5
6
7
8
9
{ path: 'index', // 入口 name: 'common-home-index-index', component: () => import(/* webpackChunkName: "auth" */ '@/views/home/index.vue'), meta: { title: '首页', // 看见了么,我就是你要显示的名字 name: 'CommonHome', // 记住,我要跟你的上面name页面组件名字一样 }, }

问题3:我有的页面,跳路由后想刷新了怎么办

那我们页面缓存住了,我怎么让页面刷新呢,比如我新增页面,新增完了需要关闭当前页面跳回列表页面的,我们的思路就是,关闭标签,url参数添加refresh参数

复制代码
1
2
3
4
5
6
7
this.$store .dispatch('tagsView/delView', this.$route) .then(({ visitedViews }) => { EventBus.$emit('gotoOwnerDeliveryOrderIndex', { refresh: true, }); });

然后在activated钩子里判断下是否有这个参数,

复制代码
1
this.$route.query.refresh && this.fetchData();

记得处理完结果后吧refresh删了,不然每次进来都刷新了,我们是在拉去数据的混合里删的

复制代码
1
2
3
if ( this.$route.query.refresh ) { this.$route.query.refresh = ''; }

问题4:有没有彩蛋啊

有的,请看图。 我的哥乖乖,怎么实现的呢。这个留给你们研究吧。上面代码已经实现了。只不过你需要在加一个页面,跟路由。其实就是跳转到一个新空页面路由。重新跳回来一下~

redirect/index.vue

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script lang="ts"> import { Component, Vue, Prop, Watch, Mixins } from 'vue-property-decorator'; @Component export default class Redirect extends Vue { public created() { const { params, query } = this.$route; const { path } = params; // debugger; this.$router.replace({ path: '/' + path, query }); } } </script> <template> </template>
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** * 刷新跳转路由 */ export const redirectRouter: any = { path: '/redirect', name: 'redirect', component: RouterView, children: [ { path: '/redirect/:path*', component: () => import(/* webpackChunkName: "redirect" */ '@/views/redirect/index.vue'), meta: { title: 'title', }, }, ], };

参考

https://github.com/PanJiaChen/vue-element-admin

到此这篇关于实用的 vue tags 创建缓存导航的过程的文章就介绍到这了,更多相关实用的 vue tags 创建缓存导航的过程内容请搜索靠谱客以前的文章或继续浏览下面的相关文章希望大家以后多多支持靠谱客!

最后

以上就是超级花瓣最近收集整理的关于实用的 vue tags 创建缓存导航的过程实现的全部内容,更多相关实用的内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(133)

评论列表共有 0 条评论

立即
投稿
返回
顶部