tagsView 方案总结
整个 tagsView 整体来看就是三块大的内容:
- tags:tagsView 组件
- contextMenu:contextMenu 组件
- view:appmain 组件
再加上一部门的数据处理(Vuex)即可。
tagsView 原理分析
tagsView 可以分成两部门来去看:
可以把这两者分开。tags 仅仅就是很简单的 tag 组件。
离开了 tags 只看 views 就更简单了,所谓 views :指的就是一个用来渲染组件的位置容器。
加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个标题
再把tags 和 view 合并起来思考。
实现方案:
- 创建 tagsView 组件:用来处理 tags 的展示
- 处理基于路由的动态过渡,在 tags 区域中进行:用于处理 view 的部门
整个的方案就是这么两大部,但是此中还需要处理一些细节相关的。
完整的方案为:
- 监听路由变革,构成用于渲染 tags 的数据源
- 创建 tags 组件,根据数据源渲染 tag,渲染出来的 tags 需要同时具备
- 处理鼠标右键效果,根据右键处理对应数据源
创建 tags 数据源
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)
- }
复制代码
- <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
创建 store 中 tagsViewList 的快捷访问 (getters)
- const getters = {
- token: state => state.user.token,
- //...
- tagsViewList: state => state.app.tagsViewList
- }
- export default getters
复制代码
- <template>
- <div class="tags-view-container">
- <!-- 每个tag页面就对应一个router-link -->
- <!-- router-link 有两种状态,一种是被选中的,另一种是不被选中的。绑定一个动态class => isActive(tag) -->
- <!-- 如果是当前被选中的这一项,它的颜色应该是当前的主题色。添加样式即可。 -->
- <!-- to表示link跳转的地址 -->
- <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 }}
- <!-- 未被选中的tag上出现一个X号 -->
- <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>
复制代码 tagsView 国际化处理
tagsView 的国际化处理可以理解为修改现有 tags 的 title。
tags的数据都保存在了tagsViewList,它里的tile是啥范例语言,tag这里的名字就应该显示啥语言。
=>
在 store 中,创建修改 ttile 的 mutations
给某个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 展示处理
contextMenu 为 鼠标右键变乱
contextMenu 变乱的处理分为两部门:
- contextMenu 的展示
- 右键项对应逻辑处理
先实现contextMenu 的展示
- 创建 ContextMenu 组件,作为右键展示部门
先简单实现测试下:
- const visible = ref(false)
- /**
- * 展示 menu
- */
- const openMenu = (e, index) => {
- visible.value = true
- }
复制代码 在router-link下进行基本的展示:
接下来实现:
1、绘制视图先不管位置,先处理视图部门
2、视图展示的位置 => 右键点击那里就在那里展示,而不是固定展示在一个位置上
1、contextMenu 的展示:
- <template>
- <ul class="context-menu-container">
- <!-- 创建三个li,以及国际化 -->
- <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'
- // 操作具体哪个tag,做标记,创建props
- 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>
复制代码
2、 在 tagsview 中控制 contextMenu 的展示
希望context的位置根据鼠标点击的位置移动。
鼠标右键的时候传递了event对象
- <template>
- <div class="tags-view-container">
- <el-scrollbar class="tags-view-wrapper">
- <!-- contextmenu.prevent右击事件 -->
- <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 }"
- @contextmenu.prevent="openMenu($event, index)"
- >
- {{ tag.title }}
- <svg-icon
- v-show="!isActive(tag)"
- icon="close"
- @click.prevent.stop="onCloseClick(index)"
- ></svg-icon>
- </router-link>
- </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)
- 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 步;当 n 为 0 时,它会重新加载当前的页面。在 如下 中,router.go(0) 相当于革新当前页面。
- const router = useRouter()
- const onRefreshClick = () => {
- router.go(0)
- }
复制代码 在 store 中,创建删除 tags 的 mutations,该 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-view 和 transition 我们可以非常方便的实现这个功能,除此之外再此底子上添加keep-alive。
- <template>
- <div class="app-main">
- <!-- 利用v-slot 解构一些值,作用域插槽语法,它允许子组件将数据传递给父组件,父组件通过这个作用域插槽能够接收子组件传递的数据,并可以根据这些数据动态地渲染内容或进行其他逻辑处理 -->
- <!-- Component 是当前路由匹配的组件,route 是当前的路由对象,包含路径、参数、查询等信息。 -->
- <router-view v-slot="{ Component, route }">
- <!-- 利用transition 指定动画效果 -->
- <transition name="fade-transform" mode="out-in">
- <keep-alive>
- <!-- 动态组件,动态渲染Component -->
- <!-- :key="route.path" 用于强制 Vue 在路由变化时重新渲染组件。因为每个路径都是唯一的,所以 key 的变化会触发 Vue 重新创建组件实例,从而确保每个路由组件的独立性 -->
- <component :is="Component" :key="route.path" />
- </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 像素,直到完全消失。
应用场景
- 这个动画效果通常用于在切换路由或显示/隐蔽某个元素时,使得用户界面看起来更加流畅和动态。比如,当用户点击一个按钮切换页面内容时,当前页面内容会向右淡出,而新页面内容会从左边淡入,从而创建一种连贯的过渡效果。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |