Blog
首页
文档
收藏
关于
  • 在线转换时间戳 (opens new window)
  • 在线压缩图片 (opens new window)
  • Float-Double转二进制 (opens new window)
  • 文件转Hex字符串 (opens new window)

HiuZing

🍑
首页
文档
收藏
关于
  • 在线转换时间戳 (opens new window)
  • 在线压缩图片 (opens new window)
  • Float-Double转二进制 (opens new window)
  • 文件转Hex字符串 (opens new window)
  • 前端面试题

  • JavaScript

  • Vue2

  • port

  • CSS

  • Node.js

  • JavaScript优化

  • uniapp

  • Mini Program

  • TypeScript

  • 面向对象编程

  • UI组件

  • Plugin

  • Vue3

    • 教程

    • Vue Router

    • API

    • Vuex

    • 实例处理方案

      • 图标处理方案
      • 本地缓存处理方案
      • 响应拦截器处理方案
      • 登录鉴权处理方案
      • 退出登录处理方案
      • 国际化处理方案
      • 动态换肤处理方案
      • Screenfull全屏处理方案
      • HeaderSearch 处理方案
      • TagsView处理方案
        • TagsView 原理及方案分析
        • 方案落地:创建 tags 数据源
        • 方案落地:生成 tagsView
        • 方案落地:tagsView 国际化处理
        • 方案落地:contextMenu 展示处理
        • 方案落地:contextMenu 事件处理
        • 方案落地:处理 contextMenu 的关闭行为
        • 方案落地:处理基于路由的动态过渡
        • tagsView 方案总结
      • Guide 处理方案
      • Excel 导入处理方案
      • 打印详情处理方案
      • 权限受控处理方案
      • 动态表格处理方案
      • 富文本和markdown处理方案
      • 项目部署处理方案
      • 可视化处理方案
    • 文档

    • 用法

  • 性能优化

  • Axios

  • 状态管理

  • React

  • Mock

  • Icon

  • Template

  • 构建工具

  • 项目规范配置

  • Taro

  • SVG

  • React Native

  • 前端
  • Vue3
  • 实例处理方案
HiuZing
2023-03-18
目录

TagsView处理方案

# TagsView 原理及方案分析

分成两部分来去看:

  1. tags
  2. view

tags

位于 appmain 之上的标签

那么现在我们忽略掉 view,现在只有一个要求:在 view 之上渲染这个 tag

views:

一个用来渲染组件的位置,就像我们之前的 Appmain 一样,只不过这里的 views 可能稍微复杂一点,因为它需要在渲染的基础上增加:

  1. 动画
  2. 缓存

加上这两个功能之后可能会略显复杂,但是 官网已经帮助我们处理了这个问题 (opens new window) 。所以 单看 views 也是一个很简单的功能。

需要做的就是把 tags 和 view 合并起来.

实现方案:

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

完整的方案:

  1. 监听路由变化,组成用于渲染 tags 的数据源
  2. 创建 tags 组件,根据数据源渲染 tag,渲染出来的 tags 需要同时具备
    1. 国际化 title
    2. 路由跳转
  3. 处理鼠标右键效果,根据右键处理对应数据源
  4. 处理基于路由的动态过渡

# 方案落地:创建 tags 数据源

tags 的数据源分为两部分:

  1. 保存数据:appmain 组件中进行
  2. 展示数据:tags 组件中进行

所以 tags 的数据我们最好把它保存到 vuex 中。

  1. 在 constant 中新建常量

    // tags
    export const TAGS_VIEW = 'tagsView'
    
    1
    2
  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
  3. 在 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
  4. 创建 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

  1. 创建 store/app 中 tagsViewList 的快捷访问

      tagsViewList: state => state.app.tagsViewList
    
    1
  2. 创建 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
  3. 在 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。

  1. 监听到语言变化

  2. 国际化对应的 title 即可

  3. 在 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
  4. 在 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) 事件的处理分为两部分:

  1. contextMenu 的展示
  2. 右键项对应逻辑处理

contextMenu 的展示:

  1. 创建 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
  2. 在 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 的事件一共分为三个:

  1. 刷新
  2. 关闭右侧
  3. 关闭所有

我们之前 关闭单个 tags 的事件还没有进行处理,所以这一小节我们一共需要处理 4 个对应的事件

  1. 刷新事件

    const router = useRouter()
    const onRefreshClick = () => {
      router.go(0)
    }
    
    1
    2
    3
    4
  2. 在 store/app 中,创建删除 tags 的 mutations,该 mutations 需要同时具备以下三个能力:

    1. 删除 “右侧”
    2. 删除 “其他”
    3. 删除 “当前”
  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) // 从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
  4. 关闭右侧事件

    const store = useStore()
    const onCloseRightClick = () => {
      store.commit('app/removeTagsView', {
        type: 'right',
        index: props.index
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
  5. 关闭其他

    const onCloseOtherClick = () => {
      store.commit('app/removeTagsView', {
        type: 'other',
        index: props.index
      })
    }
    
    1
    2
    3
    4
    5
    6
  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)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 方案落地:处理基于路由的动态过渡

处理基于路由的动态过渡 (opens new window) 官方已经给出了示例代码,结合 router-view 和 transition 我们可以非常方便的实现这个功能

  1. 在 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
  2. 增加了 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
  3. 在 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 整体来看就是三块大的内容:

  1. tags:tagsView 组件
  2. contextMenu:contextMenu 组件
  3. view:appmain 组件

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

上次更新: 2025/06/23, 07:26:12
HeaderSearch 处理方案
Guide 处理方案

← HeaderSearch 处理方案 Guide 处理方案→

最近更新
01
CodePush
06-22
02
打包发布
03-09
03
常用命令
03-09
更多文章>
Theme by Vdoing | Copyright © 2021-2025 WeiXiaojing | 友情链接
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式