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

打印 上一主题 下一主题

主题 833|帖子 833|积分 2499

tagsView 方案总结

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

  • tags:tagsView 组件
  • contextMenu:contextMenu 组件
  • view:appmain 组件
再加上一部门的数据处理(Vuex)即可。
tagsView 原理分析

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

  • tags
  • view


可以把这两者分开。tags 仅仅就是很简单的 tag 组件。
离开了 tags 只看 views 就更简单了,所谓 views :指的就是一个用来渲染组件的位置容器。

  • 动画
  • (数据)缓存
加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个标题

再把tags 和 view 合并起来思考。
实现方案:

  • 创建 tagsView 组件:用来处理 tags 的展示
  • 处理基于路由的动态过渡,在 tags 区域中进行:用于处理 view 的部门
整个的方案就是这么两大部,但是此中还需要处理一些细节相关的。
完整的方案为

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

    • 国际化 title
    • 路由跳转

  • 处理鼠标右键效果,根据右键处理对应数据源


  • 处理基于路由的动态过渡
创建 tags 数据源

tags 的数据源分为两部门:

  • 保存数据:视图层父级 组件中进行
  • 展示数据:tags 组件中进行
以是 tags 的数据我们最好把它保存到 vuex 中(及localStorage)
创建 tags 数据源:监听路由的变革,监听到的路由保存到 Tags 数据中。
创建 tagsViewList
  1. import { LANG, TAGS_VIEW } from '@/constant'
  2. import { getItem, setItem } from '@/utils/storage'
  3. export default {
  4.   namespaced: true,
  5.   state: () => ({
  6.     ...
  7.       tagsViewList: getItem(TAGS_VIEW) || []
  8.   }),
  9.   mutations: {
  10.     ...
  11.       /**
  12.      * 添加 tags
  13.      */
  14.       addTagsViewList(state, tag) {
  15.       const isFind = state.tagsViewList.find(item => {
  16.       return item.path === tag.path
  17.     })
  18.   // 处理重复【添加 tags,不要重复添加,因为用户可能会切换已经存在的 tag】
  19.     if (!isFind) {
  20.       state.tagsViewList.push(tag)
  21.       setItem(TAGS_VIEW, state.tagsViewList)
  22.     }
  23.   }
  24. },
  25. actions: {}
  26. }
复制代码
视图层父级组件中监听路由的变革 (动态添加tag)
留意:并不是所有的路由都需要保存的,比如登录页面、404等
判定是否需要,创建工具函数 =>
  1. const whiteList = ['/login', '/import', '/404', '/401']
  2. /**
  3. * path 是否需要被缓存
  4. * @param {*} path
  5. * @returns
  6. */
  7. export function isTags(path) {
  8.   return !whiteList.includes(path)
  9. }
复制代码

  1. <script setup>
  2. import { watch } from 'vue'
  3. import { isTags } from '@/utils/tags'
  4. import { generateTitle } from '@/utils/i18n'
  5. import { useRoute } from 'vue-router'
  6. import { useStore } from 'vuex'
  7. const route = useRoute()
  8. /**
  9. * 生成 title
  10. */
  11. const getTitle = route => {
  12.   let title = ''
  13.   if (!route.meta) {
  14.     // 处理无 meta 的路由,路径中最后一个元素作为title
  15.     const pathArr = route.path.split('/')
  16.     title = pathArr[pathArr.length - 1]
  17.   } else {
  18.     // 包含meta的,直接国际化处理即可
  19.     title = generateTitle(route.meta.title)
  20.   }
  21.   return title
  22. }
  23. /**
  24. * 监听路由变化
  25. */
  26. const store = useStore()
  27. watch(
  28.   route,
  29.   (to, from) => {
  30.     if (!isTags(to.path)) return
  31.     // 保存需要保存的路由属性
  32.     const { fullPath, meta, name, params, path, query } = to
  33.     store.commit('app/addTagsViewList', {
  34.       fullPath,
  35.       meta,
  36.       name,
  37.       params,
  38.       path,
  39.       query,
  40.       title: getTitle(to)
  41.     })
  42.   },
  43.   {
  44.     // 组件初始化的时候也需被执行一次
  45.     immediate: true
  46.   }
  47. )
  48. </script>
复制代码
生成 tagsView

创建 store 中 tagsViewList 的快捷访问 (getters)
  1. const getters = {
  2.   token: state => state.user.token,
  3.   //...
  4.   tagsViewList: state => state.app.tagsViewList
  5. }
  6. export default getters
复制代码

  1. <template>
  2.   <div class="tags-view-container">
  3.     <!-- 每个tag页面就对应一个router-link -->
  4.     <!-- router-link 有两种状态,一种是被选中的,另一种是不被选中的。绑定一个动态class =>  isActive(tag)  -->
  5.     <!-- 如果是当前被选中的这一项,它的颜色应该是当前的主题色。添加样式即可。 -->
  6.     <!-- to表示link跳转的地址 -->
  7.       <router-link
  8.         class="tags-view-item"
  9.         :class="isActive(tag) ? 'active' : ''"  
  10.         :style="{
  11.           backgroundColor: isActive(tag) ? $store.getters.cssVar.menuBg : '',
  12.           borderColor: isActive(tag) ? $store.getters.cssVar.menuBg : ''
  13.         }"
  14.         v-for="(tag, index) in $store.getters.tagsViewList"
  15.         :key="tag.fullPath"
  16.         :to="{ path: tag.fullPath }"
  17.       >
  18.         {{ tag.title }}
  19.         <!-- 未被选中的tag上出现一个X号 -->
  20.         <i
  21.           v-show="!isActive(tag)"
  22.           class="el-icon-close"
  23.           @click.prevent.stop="onCloseClick(index)"
  24.         />
  25.       </router-link>
  26.   </div>
  27. </template>
  28. <script setup>
  29. import { useRoute } from 'vue-router'
  30. const route = useRoute()
  31. /**
  32. * 是否被选中
  33. */
  34. const isActive = tag => {
  35.   return tag.path === route.path
  36. }
  37. /**
  38. * 关闭 tag 的点击事件
  39. */
  40. const onCloseClick = index => {}
  41. </script>
  42. <style lang="scss" scoped>
  43. .tags-view-container {
  44.   height: 34px;
  45.   width: 100%;
  46.   background: #fff;
  47.   border-bottom: 1px solid #d8dce5;
  48.   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  49.     .tags-view-item {
  50.       display: inline-block;
  51.       position: relative;
  52.       cursor: pointer;
  53.       height: 26px;
  54.       line-height: 26px;
  55.       border: 1px solid #d8dce5;
  56.       color: #495060;
  57.       background: #fff;
  58.       padding: 0 8px;
  59.       font-size: 12px;
  60.       margin-left: 5px;
  61.       margin-top: 4px;
  62.       &:first-of-type {
  63.         margin-left: 15px;
  64.       }
  65.       &:last-of-type {
  66.         margin-right: 15px;
  67.       }
  68.       &.active {
  69.         color: #fff;
  70.         &::before {
  71.           content: '';
  72.           background: #fff;
  73.           display: inline-block;
  74.           width: 8px;
  75.           height: 8px;
  76.           border-radius: 50%;
  77.           position: relative;
  78.           margin-right: 4px;
  79.         }
  80.       }
  81.       // close 按钮
  82.       .el-icon-close {
  83.         width: 16px;
  84.         height: 16px;
  85.         line-height: 10px;
  86.         vertical-align: 2px;
  87.         border-radius: 50%;
  88.         text-align: center;
  89.         transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  90.         transform-origin: 100% 50%;
  91.         &:before {
  92.           transform: scale(0.6);
  93.           display: inline-block;
  94.           vertical-align: -3px;
  95.         }
  96.         &:hover {
  97.           background-color: #b4bccc;
  98.           color: #fff;
  99.         }
  100.       }
  101.    
  102.   }
  103. }
  104. </style>
复制代码
tagsView 国际化处理

tagsView 的国际化处理可以理解为修改现有 tags 的 title。
tags的数据都保存在了tagsViewList,它里的tile是啥范例语言,tag这里的名字就应该显示啥语言。
=>

  • 监听到语言变革
  • 国际化对应的 title 即可
在 store 中,创建修改 ttile 的 mutations
给某个tag修改title,只需要触发该mutation即可。
  1. /**
  2. * 为指定的 tag 修改 title
  3. */
  4. changeTagsView(state, { index, tag }) {
  5.   state.tagsViewList[index] = tag // 更新最新的tag
  6.   setItem(TAGS_VIEW, state.tagsViewList)
  7. }
复制代码
在 路由视图的父组件 中监听语言变革
  1. import { generateTitle, watchSwitchLang } from '@/utils/i18n'
  2. /**
  3. * 国际化 tags
  4. */
  5. watchSwitchLang(() => {
  6.   store.getters.tagsViewList.forEach((route, index) => {
  7.     store.commit('app/changeTagsView', {
  8.       index,
  9.       tag: {
  10.         ...route,  // 解构route,覆盖掉title即可,其他不变
  11.         title: getTitle(route)
  12.       }
  13.     })
  14.   })
  15. })
复制代码
contextMenu 展示处理


contextMenu 为 鼠标右键变乱

contextMenu 变乱的处理分为两部门:

  • contextMenu 的展示



  • 右键项对应逻辑处理



先实现contextMenu 的展示

  • 创建 ContextMenu 组件,作为右键展示部门
先简单实现测试下:

  1. const visible = ref(false)
  2. /**
  3. * 展示 menu
  4. */
  5. const openMenu = (e, index) => {
  6.   visible.value = true
  7. }
复制代码
在router-link下进行基本的展示:


接下来实现:
1、绘制视图先不管位置,先处理视图部门
2、视图展示的位置 => 右键点击那里就在那里展示,而不是固定展示在一个位置上
1、contextMenu 的展示:
  1. <template>
  2.   <ul class="context-menu-container">
  3.     <!-- 创建三个li,以及国际化 -->
  4.     <li @click="onRefreshClick">
  5.       {{ $t('msg.tagsView.refresh') }}
  6.     </li>
  7.     <li @click="onCloseRightClick">
  8.       {{ $t('msg.tagsView.closeRight') }}
  9.     </li>
  10.     <li @click="onCloseOtherClick">
  11.       {{ $t('msg.tagsView.closeOther') }}
  12.     </li>
  13.   </ul>
  14. </template>
  15. <script setup>
  16. import { defineProps } from 'vue'
  17. // 操作具体哪个tag,做标记,创建props
  18. defineProps({
  19.   index: {
  20.     type: Number,
  21.     required: true
  22.   }
  23. })
  24. const onRefreshClick = () => {}
  25. const onCloseRightClick = () => {}
  26. const onCloseOtherClick = () => {}
  27. </script>
  28. <style lang="scss" scoped>
  29. .context-menu-container {
  30.   position: fixed;
  31.   background: #fff;
  32.   z-index: 3000;
  33.   list-style-type: none;
  34.   padding: 5px 0;
  35.   border-radius: 4px;
  36.   font-size: 12px;
  37.   font-weight: 400;
  38.   color: #333;
  39.   box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
  40.   li {
  41.     margin: 0;
  42.     padding: 7px 16px;
  43.     cursor: pointer;
  44.     &:hover {
  45.       background: #eee;
  46.     }
  47.   }
  48. }
  49. </style>
复制代码

2、 在 tagsview 中控制 contextMenu 的展示
希望context的位置根据鼠标点击的位置移动。
鼠标右键的时候传递了event对象
  1. <template>
  2.   <div class="tags-view-container">
  3.     <el-scrollbar class="tags-view-wrapper">
  4.       <!-- contextmenu.prevent右击事件 -->
  5.       <router-link
  6.         class="tags-view-item"
  7.         :class="isActive(tag) ? 'active' : ''"
  8.         :style="{
  9.           backgroundColor: isActive(tag) ? $store.getters.cssVar.menuBg : '',
  10.           borderColor: isActive(tag) ? $store.getters.cssVar.menuBg : ''
  11.         }"
  12.         v-for="(tag, index) in $store.getters.tagsViewList"
  13.         :key="tag.fullPath"
  14.         :to="{ path: tag.fullPath }"
  15.         @contextmenu.prevent="openMenu($event, index)"
  16.       >
  17.         {{ tag.title }}
  18.         <svg-icon
  19.           v-show="!isActive(tag)"
  20.           icon="close"
  21.           @click.prevent.stop="onCloseClick(index)"
  22.         ></svg-icon>
  23.       </router-link>
  24.       </el-scrollbar>
  25.     <context-menu
  26.       v-show="visible"
  27.       :style="menuStyle"
  28.       :index="selectIndex"
  29.       ></context-menu>
  30.   </div>
  31. </template>
  32. <script setup>
  33.   import ContextMenu from './ContextMenu.vue'
  34.   import { ref, reactive, watch } from 'vue'
  35.   import { useRoute } from 'vue-router'
  36.   ...
  37.   // contextMenu 相关
  38.   const selectIndex = ref(0)
  39.   const visible = ref(false)
  40.   const menuStyle = reactive({
  41.     left: 0,
  42.     top: 0
  43.   })
  44.   /**
  45. * 展示 menu
  46. */
  47.   const openMenu = (e, index) => {
  48.     const { x, y } = e // 事件对象中,得到鼠标点击的位置
  49.     // 作为行内样式绑定
  50.     menuStyle.left = x + 'px'
  51.     menuStyle.top = y + 'px'
  52.     // 点击项
  53.     selectIndex.value = index
  54.     visible.value = true
  55.   }
  56. </script>
复制代码
contextMenu 变乱处理

对于 contextMenu 的变乱一共分为三个:

  • 革新
  • 关闭右侧
  • 关闭所有
革新 =>
router.go(n) 是 Vue Router 提供的一个方法,它可以在欣赏器的历史纪录中进步或后退 n 步。 当 n 为正数时,router.go(n) 会进步 n 步;当 n 为负数时,会后退 n 步;当 n 为 0 时,它会重新加载当前的页面。在 如下 中,router.go(0) 相当于革新当前页面。
  1. const router = useRouter()
  2. const onRefreshClick = () => {
  3.   router.go(0)
  4. }
复制代码
在 store 中,创建删除 tags 的 mutations,该 mutations 需要同时具备以下三个能力:
  1.   1. 删除 “右侧”
  2.   2. 删除 “其他”
  3.   3. 删除 “当前”
复制代码
  1. /**
  2.    * 删除 tag
  3.    * @param {type: 'other'||'right'||'index', index: index} payload
  4. */
  5. removeTagsView(state, payload) {
  6.     if (payload.type === 'index') { // 删除当前项
  7.       state.tagsViewList.splice(payload.index, 1)
  8.       return
  9.     } else if (payload.type === 'other') { // 保留自己,删掉它之前和之后
  10.       state.tagsViewList.splice(
  11.         payload.index + 1,
  12.         state.tagsViewList.length - payload.index + 1
  13.       )  // 删除它之后的所有的
  14.       state.tagsViewList.splice(0, payload.index) // 删除它之前的
  15.     } else if (payload.type === 'right') {
  16.       state.tagsViewList.splice(
  17.         payload.index + 1,
  18.         state.tagsViewList.length - payload.index + 1
  19.       ) // 删除它之后的
  20.     }
  21.     setItem(TAGS_VIEW, state.tagsViewList) // 同步本地缓存(localStorage)
  22. },
复制代码
关闭右侧变乱
  1. const store = useStore()
  2. const onCloseRightClick = () => {
  3.   store.commit('app/removeTagsView', {
  4.     type: 'right',
  5.     index: props.index
  6.   })
  7. }
复制代码
关闭其他
  1. const onCloseOtherClick = () => {
  2.   store.commit('app/removeTagsView', {
  3.     type: 'other',
  4.     index: props.index
  5.   })
  6. }
复制代码
关闭当前(tagsview)
  1. /**
  2. * 关闭 tag 的点击事件
  3. */
  4. const store = useStore()
  5. const onCloseClick = index => {
  6.   store.commit('app/removeTagsView', {
  7.     type: 'index',
  8.     index: index
  9.   })
  10. }
复制代码
处理 contextMenu 的关闭行为

其实就改变它的visible,visible为true就为bdoy添加关闭菜单的变乱。
  1. /**
  2. * 关闭 menu
  3. */
  4. const closeMenu = () => {
  5.   visible.value = false
  6. }
  7. /**
  8. * 监听变化
  9. */
  10. watch(visible, val => {
  11.   if (val) {
  12.     document.body.addEventListener('click', closeMenu)
  13.   } else {
  14.     document.body.removeEventListener('click', closeMenu)
  15.   }
  16. })
复制代码
处理基于路由的动态过渡

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

  1. <template>
  2.   <div class="app-main">
  3.     <!-- 利用v-slot 解构一些值,作用域插槽语法,它允许子组件将数据传递给父组件,父组件通过这个作用域插槽能够接收子组件传递的数据,并可以根据这些数据动态地渲染内容或进行其他逻辑处理 -->
  4.     <!-- Component 是当前路由匹配的组件,route 是当前的路由对象,包含路径、参数、查询等信息。 -->
  5.     <router-view v-slot="{ Component, route }">
  6.       <!-- 利用transition 指定动画效果 -->
  7.       <transition name="fade-transform" mode="out-in">
  8.         <keep-alive>
  9.           <!-- 动态组件,动态渲染Component -->
  10.           <!-- :key="route.path" 用于强制 Vue 在路由变化时重新渲染组件。因为每个路径都是唯一的,所以 key 的变化会触发 Vue 重新创建组件实例,从而确保每个路由组件的独立性 -->
  11.           <component :is="Component" :key="route.path" />
  12.         </keep-alive>
  13.       </transition>
  14.     </router-view>
  15.   </div>
  16. </template>
复制代码
动画
  1. /* fade-transform */
  2. /* 元素进入和离开视图时都会应用 */
  3. .fade-transform-leave-active,
  4. .fade-transform-enter-active {
  5.   /* 表示元素的所有可动画属性在 0.5 秒内从初始状态过渡到最终状态。即:所有参与动画的属性(如 opacity 和 transform)都会在 0.5 秒内完成变化。 */
  6.   transition: all 0.5s;
  7. }
  8. /* 进入过渡的初始状态 */
  9. .fade-transform-enter-from {
  10.   /* 一开始是完全透明 */
  11.   opacity: 0;
  12.   /* 一开始是从它本应的位置向左偏移了 30 像素 */
  13.   transform: translateX(-30px);
  14. }
  15. /* 离开过渡的结束状态 */
  16. .fade-transform-leave-to {
  17.   /*元素在离开时会变得完全透明 */
  18.   opacity: 0;
  19.   /*  元素在离开时会向右移动 30 像素 */
  20.   transform: translateX(30px);
  21. }
复制代码
进入视图时:


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


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


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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曹旭辉

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表