TagsView处理方案
# TagsView 原理及方案分析
分成两部分来去看:
- tags
- view
tags
位于 appmain
之上的标签
那么现在我们忽略掉
view
,现在只有一个要求:在view
之上渲染这个tag
views:
一个用来渲染组件的位置,就像我们之前的 Appmain
一样,只不过这里的 views
可能稍微复杂一点,因为它需要在渲染的基础上增加:
- 动画
- 缓存
加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个问题 (opens new window) 。所以 单看
views
也是一个很简单的功能。
需要做的就是把 tags
和 view
合并起来.
实现方案:
- 创建
tagsView
组件:用来处理tags
的展示 - 处理基于路由的动态过渡,在
AppMain
中进行:用于处理view
的部分
完整的方案:
- 监听路由变化,组成用于渲染
tags
的数据源 - 创建
tags
组件,根据数据源渲染tag
,渲染出来的tags
需要同时具备- 国际化
title
- 路由跳转
- 国际化
- 处理鼠标右键效果,根据右键处理对应数据源
- 处理基于路由的动态过渡
# 方案落地:创建 tags 数据源
tags
的数据源分为两部分:
- 保存数据:
appmain
组件中进行- 展示数据:
tags
组件中进行
所以 tags
的数据我们最好把它保存到 vuex
中。
在
constant
中新建常量// tags export const TAGS_VIEW = 'tagsView'
1
2在
store/app
中创建tagsViewList
import { LANG, TAGS_VIEW } from '@/constant' import { getItem, setItem } from '@/utils/storage' export default { namespaced: true, state: () => ({ ... tagsViewList: getItem(TAGS_VIEW) || [] }), mutations: { ... /** * 添加 tags */ addTagsViewList(state, tag) { const isFind = state.tagsViewList.find(item => { return item.path === tag.path }) // 处理重复 if (!isFind) { state.tagsViewList.push(tag) setItem(TAGS_VIEW, state.tagsViewList) } } }, actions: {} }
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在
appmain
中监听路由的变化<script setup> import { watch } from 'vue' import { isTags } from '@/utils/tags' import { generateTitle } from '@/utils/i18n' import { useRoute } from 'vue-router' import { useStore } from 'vuex' const route = useRoute() /** * 生成 title */ const getTitle = route => { let title = '' if (!route.meta) { // 处理无 meta 的路由 const pathArr = route.path.split('/') title = pathArr[pathArr.length - 1] } else { title = generateTitle(route.meta.title) } return title } /** * 监听路由变化 */ const store = useStore() watch( route, (to, from) => { if (!isTags(to.path)) return const { fullPath, meta, name, params, path, query } = to store.commit('app/addTagsViewList', { fullPath, meta, name, params, path, query, title: getTitle(to) }) }, { immediate: true } ) </script>
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创建
utils/tags
const whiteList = ['/login', '/import', '/404', '/401'] /** * path 是否需要被缓存 * @param {*} path * @returns */ export function isTags(path) { return !whiteList.includes(path) }
1
2
3
4
5
6
7
8
9
10
# 方案落地:生成 tagsView
目前数据已经被保存到
store
中,那么接下来我们就依赖数据渲染tags
创建
store/app
中tagsViewList
的快捷访问tagsViewList: state => state.app.tagsViewList
1创建
components/tagsview
<template> <div class="tags-view-container"> <router-link class="tags-view-item" :class="isActive(tag) ? 'active' : ''" :style="{ backgroundColor: isActive(tag) ? $store.getters.cssVar.menuBg : '', borderColor: isActive(tag) ? $store.getters.cssVar.menuBg : '' }" v-for="(tag, index) in $store.getters.tagsViewList" :key="tag.fullPath" :to="{ path: tag.fullPath }" > {{ tag.title }} <i v-show="!isActive(tag)" class="el-icon-close" @click.prevent.stop="onCloseClick(index)" /> </router-link> </div> </template> <script setup> import { useRoute } from 'vue-router' const route = useRoute() /** * 是否被选中 */ const isActive = tag => { return tag.path === route.path } /** * 关闭 tag 的点击事件 */ const onCloseClick = index => {} </script> <style lang="scss" scoped> .tags-view-container { height: 34px; width: 100%; background: #fff; border-bottom: 1px solid #d8dce5; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 26px; line-height: 26px; border: 1px solid #d8dce5; color: #495060; background: #fff; padding: 0 8px; font-size: 12px; margin-left: 5px; margin-top: 4px; &:first-of-type { margin-left: 15px; } &:last-of-type { margin-right: 15px; } &.active { color: #fff; &::before { content: ''; background: #fff; display: inline-block; width: 8px; height: 8px; border-radius: 50%; position: relative; margin-right: 4px; } } // close 按钮 .el-icon-close { width: 16px; height: 16px; line-height: 10px; vertical-align: 2px; border-radius: 50%; text-align: center; transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); transform-origin: 100% 50%; &:before { transform: scale(0.6); display: inline-block; vertical-align: -3px; } &: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在
layout/index
中导入<div class="fixed-header"> <!-- 顶部的 navbar --> <navbar /> <!-- tags --> <tags-view></tags-view> </div> import TagsView from '@/components/TagsView'
1
2
3
4
5
6
7
8
# 方案落地:tagsView 国际化处理
tagsView
的国际化处理可以理解为修改现有 tags
的 title
。
监听到语言变化
国际化对应的
title
即可在
store/app
中,创建修改ttile
的mutations
/** * 为指定的 tag 修改 title */ changeTagsView(state, { index, tag }) { state.tagsViewList[index] = tag setItem(TAGS_VIEW, state.tagsViewList) }
1
2
3
4
5
6
7在
appmain
中监听语言变化import { generateTitle, watchSwitchLang } from '@/utils/i18n' /** * 国际化 tags */ watchSwitchLang(() => { store.getters.tagsViewList.forEach((route, index) => { store.commit('app/changeTagsView', { index, tag: { ...route,// route解构过来 title: getTitle(route) // 将title覆盖 } }) }) })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 方案落地:contextMenu 展示处理
contextMenu (opens new window) 为 鼠标右键事件
contextMenu (opens new window) 事件的处理分为两部分:
contextMenu
的展示- 右键项对应逻辑处理
contextMenu
的展示:
创建
components/TagsView/ContextMenu
组件,作为右键展示部分<template> <ul class="context-menu-container"> <li @click="onRefreshClick"> {{ $t('msg.tagsView.refresh') }} </li> <li @click="onCloseRightClick"> {{ $t('msg.tagsView.closeRight') }} </li> <li @click="onCloseOtherClick"> {{ $t('msg.tagsView.closeOther') }} </li> </ul> </template> <script setup> import { defineProps } from 'vue' defineProps({ index: { type: Number, required: true } }) const onRefreshClick = () => {} const onCloseRightClick = () => {} const onCloseOtherClick = () => {} </script> <style lang="scss" scoped> .context-menu-container { position: fixed; background: #fff; z-index: 3000; 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, 0.3); li { margin: 0; padding: 7px 16px; cursor: pointer; &:hover { background: #eee; } } } </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在
tagsview
中控制contextMenu
的展示<template> <div class="tags-view-container"> <el-scrollbar class="tags-view-wrapper"> <router-link ... @contextmenu.prevent="openMenu($event, index)" > ... </el-scrollbar> <context-menu v-show="visible" :style="menuStyle" :index="selectIndex" ></context-menu> </div> </template> <script setup> import ContextMenu from './ContextMenu.vue' import { ref, reactive, watch } from 'vue' import { useRoute } from 'vue-router' ... // contextMenu 相关 const selectIndex = ref(0) // 为true时 显示contextMenu const visible = ref(false) const menuStyle = reactive({ left: 0, top: 0 }) /** * 展示 menu */ const openMenu = (e, index) => { const { x, y } = e // 通过x,y left和top来修改值改变ContextMenu的位置 menuStyle.left = x + 'px' menuStyle.top = y + 'px' selectIndex.value = index visible.value = true } </script>
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
# 方案落地:contextMenu 事件处理
对于 contextMenu
的事件一共分为三个:
- 刷新
- 关闭右侧
- 关闭所有
我们之前 关闭单个 tags
的事件还没有进行处理,所以这一小节我们一共需要处理 4 个对应的事件
刷新事件
const router = useRouter() const onRefreshClick = () => { router.go(0) }
1
2
3
4在
store/app
中,创建删除tags
的mutations
,该mutations
需要同时具备以下三个能力:- 删除 “右侧”
- 删除 “其他”
- 删除 “当前”
根据以上理论得出以下代码:
/** * 删除 tag * @param {type: 'other'||'right'||'index', index: index} payload */ removeTagsView(state, payload) { if (payload.type === 'index') { state.tagsViewList.splice(payload.index, 1) return } else if (payload.type === 'other') { state.tagsViewList.splice( payload.index + 1, state.tagsViewList.length - payload.index + 1 // 删除当前的位置往后到所有的 ) state.tagsViewList.splice(0, payload.index) // 从0开始到当前位置 } else if (payload.type === 'right') { state.tagsViewList.splice( payload.index + 1, state.tagsViewList.length - payload.index + 1 ) } setItem(TAGS_VIEW, state.tagsViewList) },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22关闭右侧事件
const store = useStore() const onCloseRightClick = () => { store.commit('app/removeTagsView', { type: 'right', index: props.index }) }
1
2
3
4
5
6
7关闭其他
const onCloseOtherClick = () => { store.commit('app/removeTagsView', { type: 'other', index: props.index }) }
1
2
3
4
5
6关闭当前(
tagsview
)/** * 关闭 tag 的点击事件 */ const store = useStore() const onCloseClick = index => { store.commit('app/removeTagsView', { type: 'index', index: index }) }
1
2
3
4
5
6
7
8
9
10
# 方案落地:处理 contextMenu 的关闭行为
/**
* 关闭 menu
*/
const closeMenu = () => {
visible.value = false
}
/**
* 监听变化
*/
watch(visible, val => {
if (val) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方案落地:处理基于路由的动态过渡
处理基于路由的动态过渡 (opens new window) 官方已经给出了示例代码,结合 router-view
和 transition
我们可以非常方便的实现这个功能
在
appmain
中处理对应代码逻辑<template> <div class="app-main"> <router-view v-slot="{ Component, route }"> <transition name="fade-transform" mode="out-in"> <keep-alive> <component :is="Component" :key="route.path" /> </keep-alive> </transition> </router-view> </div> </template>
1
2
3
4
5
6
7
8
9
10
11增加了
tags
之后,app-main
的位置需要进行以下处理<style lang="scss" scoped> .app-main { min-height: calc(100vh - 50px - 43px); ... padding: 104px 20px 20px 20px; ... } </style>
1
2
3
4
5
6
7
8在
styles/transition
中增加动画渲染/* fade-transform */ .fade-transform-leave-active, .fade-transform-enter-active { transition: all 0.5s; } .fade-transform-enter-from { opacity: 0; transform: translateX(-30px); } .fade-transform-leave-to { opacity: 0; transform: translateX(30px); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# tagsView 方案总结
那么到这里关于 tagsView
的内容我们就已经处理完成了。
整个 tagsView
就像我们之前说的,拆开来看之后,会显得明确很多。
整个 tagsView
整体来看就是三块大的内容:
tags
:tagsView
组件contextMenu
:contextMenu
组件view
:appmain
组件
再加上一部分的数据处理即可。