前端(Vue)tagsView(子标签页视图切换) 原理及通用解决方案

溜_x_i_a_o_迪 2024-09-30 10:03:05 阅读 96

文章目录

tagsView 方案总结tagsView 原理分析创建 tags 数据源生成 tagsViewtagsView 国际化处理contextMenu 展示处理contextMenu 事件处理处理 contextMenu 的关闭行为处理基于路由的动态过渡

tagsView 方案总结

整个 <code>tagsView 整体来看就是三块大的内容:

tagstagsView 组件contextMenucontextMenu 组件viewappmain 组件

再加上一部分的数据处理(Vuex)即可。

tagsView 原理分析

tagsView 可以分成两部分来去看:

tagsview

image.png

image.png

可以把这两者分开。tags 仅仅就是很简单的 tag 组件。

脱离了 <code>tags 只看 views 就更简单了,所谓 views指的就是一个用来渲染组件的位置容器。

动画(数据)缓存

加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个问题

image.png

再把<code>tags 和 view 合并起来思考。

实现方案:

创建 tagsView 组件:用来处理 tags 的展示处理基于路由的动态过渡,在 tags 区域中进行:用于处理 view 的部分

整个的方案就是这么两大部,但是其中还需要处理一些细节相关的。

完整的方案为

监听路由变化,组成用于渲染 tags 的数据源创建 tags 组件,根据数据源渲染 tag,渲染出来的 tags 需要同时具备

国际化 title路由跳转 处理鼠标右键效果,根据右键处理对应数据源

image.png

处理基于路由的动态过渡

创建 tags 数据源

<code>tags 的数据源分为两部分:

保存数据:视图层父级 组件中进行展示数据:tags 组件中进行

所以 tags 的数据我们最好把它保存到 vuex 中(及localStorage)

创建 tags 数据源:监听路由的变化,监听到的路由保存到 Tags 数据中。

创建 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

})

// 处理重复【添加 tags,不要重复添加,因为用户可能会切换已经存在的 tag】

if (!isFind) {

state.tagsViewList.push(tag)

setItem(TAGS_VIEW, state.tagsViewList)

}

}

},

actions: { }

}

视图层父级组件中监听路由的变化 (动态添加tag)

注意:并不是所有的路由都需要保存的,比如登录页面、404等

判断是否需要,创建工具函数 =>

const whiteList = ['/login', '/import', '/404', '/401']

/**

* path 是否需要被缓存

* @param {*} path

* @returns

*/

export function isTags(path) {

return !whiteList.includes(path)

}

image.png

<code><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 的路由,路径中最后一个元素作为title

const pathArr = route.path.split('/')

title = pathArr[pathArr.length - 1]

} else {

// 包含meta的,直接国际化处理即可

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>

生成 tagsView

创建 storetagsViewList 的快捷访问 (getters)

const getters = {

token: state => state.user.token,

//...

tagsViewList: state => state.app.tagsViewList

}

export default getters

image.png

<code><template>

<div class="tags-view-container">code>

<!-- 每个tag页面就对应一个router-link -->

<!-- router-link 有两种状态,一种是被选中的,另一种是不被选中的。绑定一个动态class => isActive(tag) -->

<!-- 如果是当前被选中的这一项,它的颜色应该是当前的主题色。添加样式即可。 -->

<!-- to表示link跳转的地址 -->

<router-link

class="tags-view-item"code>

:class="isActive(tag) ? 'active' : ''" code>

:style="{

backgroundColor: isActive(tag) ? $store.getters.cssVar.menuBg : '',

borderColor: isActive(tag) ? $store.getters.cssVar.menuBg : ''

}"code>

v-for="(tag, index) in $store.getters.tagsViewList"code>

:key="tag.fullPath"code>

:to="{ path: tag.fullPath }"code>

>

{ -- -->{ tag.title }}

<!-- 未被选中的tag上出现一个X号 -->

<i

v-show="!isActive(tag)"code>

class="el-icon-close"code>

@click.prevent.stop="onCloseClick(index)"code>

/>

</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>code>

.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>

tagsView 国际化处理

tagsView 的国际化处理可以理解为修改现有 tagstitle

tags的数据都保存在了tagsViewList,它里的tile是啥类型语言,tag这里的名字就应该显示啥语言。

=>

监听到语言变化国际化对应的 title 即可

store 中,创建修改 ttilemutations

给某个tag修改title,只需要触发该mutation即可。

/**

* 为指定的 tag 修改 title

*/

changeTagsView(state, { index, tag }) {

state.tagsViewList[index] = tag // 更新最新的tag

setItem(TAGS_VIEW, state.tagsViewList)

}

在 路由视图的父组件 中监听语言变化

import { generateTitle, watchSwitchLang } from '@/utils/i18n'

/**

* 国际化 tags

*/

watchSwitchLang(() => {

store.getters.tagsViewList.forEach((route, index) => {

store.commit('app/changeTagsView', {

index,

tag: {

...route, // 解构route,覆盖掉title即可,其他不变

title: getTitle(route)

}

})

})

})

contextMenu 展示处理

image.png

contextMenu 为 鼠标右键事件

contextMenu 事件的处理分为两部分:

<code>contextMenu 的展示

image.png

右键项对应逻辑处理

image.png

先实现<code>contextMenu 的展示

创建 ContextMenu 组件,作为右键展示部分

先简单实现测试下:

image.png

<code>const visible = ref(false)

/**

* 展示 menu

*/

const openMenu = (e, index) => { -- -->

visible.value = true

}

在router-link下进行基本的展示:

image.png

image.png

接下来实现:

1、绘制视图先不管位置,先处理视图部分

2、视图展示的位置 => 右键点击哪里就在哪里展示,而不是固定展示在一个位置上

1、<code>contextMenu 的展示:

<template>

<ul class="context-menu-container">code>

<!-- 创建三个li,以及国际化 -->

<li @click="onRefreshClick">code>

{ -- -->{ $t('msg.tagsView.refresh') }}

</li>

<li @click="onCloseRightClick">code>

{ -- -->{ $t('msg.tagsView.closeRight') }}

</li>

<li @click="onCloseOtherClick">code>

{ -- -->{ $t('msg.tagsView.closeOther') }}

</li>

</ul>

</template>

<script setup>

import { defineProps } from 'vue'

// 操作具体哪个tag,做标记,创建props

defineProps({

index: {

type: Number,

required: true

}

})

const onRefreshClick = () => { }

const onCloseRightClick = () => { }

const onCloseOtherClick = () => { }

</script>

<style lang="scss" scoped>code>

.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>

image.png

2、 在 <code>tagsview 中控制 contextMenu 的展示

希望context的位置根据鼠标点击的位置移动。

鼠标右键的时候传递了event对象

<template>

<div class="tags-view-container">code>

<el-scrollbar class="tags-view-wrapper">code>

<!-- contextmenu.prevent右击事件 -->

<router-link

class="tags-view-item"code>

:class="isActive(tag) ? 'active' : ''"code>

:style="{

backgroundColor: isActive(tag) ? $store.getters.cssVar.menuBg : '',

borderColor: isActive(tag) ? $store.getters.cssVar.menuBg : ''

}"code>

v-for="(tag, index) in $store.getters.tagsViewList"code>

:key="tag.fullPath"code>

:to="{ path: tag.fullPath }"code>

@contextmenu.prevent="openMenu($event, index)"code>

>

{ -- -->{ tag.title }}

<svg-icon

v-show="!isActive(tag)"code>

icon="close"code>

@click.prevent.stop="onCloseClick(index)"code>

></svg-icon>

</router-link>

</el-scrollbar>

<context-menu

v-show="visible"code>

:style="menuStyle"code>

:index="selectIndex"code>

></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)

const visible = ref(false)

const menuStyle = reactive({

left: 0,

top: 0

})

/**

* 展示 menu

*/

const openMenu = (e, index) => {

const { x, y } = e // 事件对象中,得到鼠标点击的位置

// 作为行内样式绑定

menuStyle.left = x + 'px'

menuStyle.top = y + 'px'

// 点击项

selectIndex.value = index

visible.value = true

}

</script>

contextMenu 事件处理

对于 contextMenu 的事件一共分为三个:

刷新关闭右侧关闭所有

刷新 =>

router.go(n) 是 Vue Router 提供的一个方法,它可以在浏览器的历史记录中前进或后退 n 步。 当 n 为正数时,router.go(n) 会前进 n 步;当 n 为负数时,会后退 n 步;当 n0 时,它会重新加载当前的页面。在 如下 中,router.go(0) 相当于刷新当前页面。

const router = useRouter()

const onRefreshClick = () => {

router.go(0)

}

store 中,创建删除 tagsmutations,该 mutations 需要同时具备以下三个能力:

1. 删除 “右侧”

2. 删除 “其他”

3. 删除 “当前”

/**

* 删除 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) // 删除它之前的

} else if (payload.type === 'right') {

state.tagsViewList.splice(

payload.index + 1,

state.tagsViewList.length - payload.index + 1

) // 删除它之后的

}

setItem(TAGS_VIEW, state.tagsViewList) // 同步本地缓存(localStorage)

},

关闭右侧事件

const store = useStore()

const onCloseRightClick = () => {

store.commit('app/removeTagsView', {

type: 'right',

index: props.index

})

}

关闭其他

const onCloseOtherClick = () => {

store.commit('app/removeTagsView', {

type: 'other',

index: props.index

})

}

关闭当前(tagsview

/**

* 关闭 tag 的点击事件

*/

const store = useStore()

const onCloseClick = index => {

store.commit('app/removeTagsView', {

type: 'index',

index: index

})

}

处理 contextMenu 的关闭行为

其实就改变它的visible,visible为true就为bdoy添加关闭菜单的事件。

/**

* 关闭 menu

*/

const closeMenu = () => {

visible.value = false

}

/**

* 监听变化

*/

watch(visible, val => {

if (val) {

document.body.addEventListener('click', closeMenu)

} else {

document.body.removeEventListener('click', closeMenu)

}

})

处理基于路由的动态过渡

处理基于路由的动态过渡  官方已经给出了示例代码,结合 router-viewtransition 我们可以非常方便的实现这个功能,除此之外再此基础上添加keep-alive。

image.png

<code><template>

<div class="app-main">code>

<!-- 利用v-slot 解构一些值,作用域插槽语法,它允许子组件将数据传递给父组件,父组件通过这个作用域插槽能够接收子组件传递的数据,并可以根据这些数据动态地渲染内容或进行其他逻辑处理 -->

<!-- Component 是当前路由匹配的组件,route 是当前的路由对象,包含路径、参数、查询等信息。 -->

<router-view v-slot="{ Component, route }">code>

<!-- 利用transition 指定动画效果 -->

<transition name="fade-transform" mode="out-in">code>

<keep-alive>

<!-- 动态组件,动态渲染Component -->

<!-- :key="route.path" 用于强制 Vue 在路由变化时重新渲染组件。因为每个路径都是唯一的,所以 key 的变化会触发 Vue 重新创建组件实例,从而确保每个路由组件的独立性 -->code>

<component :is="Component" :key="route.path" />code>

</keep-alive>

</transition>

</router-view>

</div>

</template>

动画

/* fade-transform */

/* 元素进入和离开视图时都会应用 */

.fade-transform-leave-active,

.fade-transform-enter-active { -- -->

/* 表示元素的所有可动画属性在 0.5 秒内从初始状态过渡到最终状态。即:所有参与动画的属性(如 opacity 和 transform)都会在 0.5 秒内完成变化。 */

transition: all 0.5s;

}

/* 进入过渡的初始状态 */

.fade-transform-enter-from {

/* 一开始是完全透明 */

opacity: 0;

/* 一开始是从它本应的位置向左偏移了 30 像素 */

transform: translateX(-30px);

}

/* 离开过渡的结束状态 */

.fade-transform-leave-to {

/*元素在离开时会变得完全透明 */

opacity: 0;

/* 元素在离开时会向右移动 30 像素 */

transform: translateX(30px);

}

进入视图时:

元素从 fade-transform-enter-from 状态开始,透明度为 0,向左偏移 30 像素。然后,在 0.5 秒内,元素的透明度逐渐增加到 1(完全可见),同时它从左边的位置平滑地移动到其正常位置。

离开视图时:

元素开始时是正常位置和完全可见的状态。在 fade-transform-leave-active 触发后,它在 0.5 秒内逐渐变得透明,同时向右移动 30 像素,直到完全消失。

应用场景

这个动画效果通常用于在切换路由或显示/隐藏某个元素时,使得用户界面看起来更加流畅和动态。比如,当用户点击一个按钮切换页面内容时,当前页面内容会向右淡出,而新页面内容会从左边淡入,从而创建一种连贯的过渡效果。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。