Vue3 页签与路由跳转遮罩的实现

导航

学习内容:

  1. Vue3 页签
  2. Vue3 路由跳转遮罩

Vue3 页签

1.1 基础页签搭建

我们从一个已经渲染好的多标签模块出发,看看具体需要实现什么:

<el-tag
    v-for="(tag, idx) in visitedViews"
    :key="tag.fullPath"
    size="small"
    closable
    :effect="isActive(tag) ? 'dark' : 'plain'"
    @click="toRoute(tag)"
    @close="closeTag(tag, idx)"
    @contextmenu.prevent="openMenu(tag, $event)" 
>
    {{ tag.meta?.title || '未命名' }}
</el-tag>
  • v-for:枚举访问过的标签渲染,使用 Pinia 记录访问过的标签。
  • click:点击标签时跳转到目标路由。
  • close:关闭标签。
  • contextmenu.prevent:重写右键菜单事件。
点击跳转

点击比较简单,触发 router.push 到当前高亮的路由即可:

const isActive = (tag: any) => tag.fullPath === route.fullPath

/* 跳转 */
function toRoute(tag: any) {
  router.push(tag.fullPath)
}
关闭标签

关闭标签需要对 tagsViewStore 的内容进行删除:

function closeTag(tag: any, idx: number) {
  //@ts-expect-error
  tagsViewStore.delView(tag).then(({ visitedViews }) => {
    // 如果关的是当前页,则自动跳到相邻页签
    if (isActive(tag)) {
      const next = visitedViews[idx] || visitedViews[idx - 1]
      if (next) router.push(next.fullPath)
      else router.push('/')
    }
  })
}
delView(view: RouteLocationNormalized) {
    return new Promise(resolve => {
        this.visitedViews = this.visitedViews.filter(v => v.fullPath !== view.fullPath)
        if (view.name) { //同时销毁缓存组件
            const idx = this.cachedViews.indexOf(view.name as string)
            idx > -1 && this.cachedViews.splice(idx, 1)
        }
        resolve({ visitedViews: [...this.visitedViews] })
    })
},
添加标签

有删除自然有增加,使用 watch 监听 route 的跳转:

function addTags() {
  tagsViewStore.addView(route)
}
watch(
    () => route.fullPath,
    () => addTags(),
    { immediate: true } // 初始化时执行一次
)
addView(view: RouteLocationNormalized) {
    if (this.visitedViews.some(v => v.fullPath === view.fullPath)) return
    this.visitedViews.push(Object.assign({}, view)) //深拷贝一份路由对象再塞进数组,从响应式中隔离
    if (view.name && !this.cachedViews.includes(view.name as string)) {
        this.cachedViews.push(view.name as string)
    }
},

1.2 路由缓存机制

缓存是为了保证路由切换后,原界面保持原来的状态。要实现缓存机制,就需要借助于 Vue 的 <keep-alive>

<router-view v-slot="{ Component, route }">
    <keep-alive :include="cachedViews">
        <component :is="Component" :key="route.fullPath" />
    </keep-alive>
</router-view>

<keep-alive> 将组件的 name 作为 key,而我们不好获取到组件的 name,于是将 routename 作为桥梁,来锚定缓存的组件。例如:

addView(view: RouteLocationNormalized) {
    if (this.visitedViews.some(v => v.fullPath === view.fullPath)) return
    this.visitedViews.push(Object.assign({}, view)) //深拷贝一份路由对象再塞进数组
    if (view.name && !this.cachedViews.includes(view.name as string)) {
        this.cachedViews.push(view.name as string)
    }
},

即,需要保证 route.name == component.name

Vue 会把文件名的最后一段(不含扩展名)作为组件的默认 name。例如:

  • 文件路径:src/views/system/SysMenu/index.vue,对应组件 nameSysMenu
  • 文件路径:src/views/manage/SysMenu/index.vue,对应的组件 name 也是:SysMenu

这就会造成缓存被污染,所以显式地指定一个组件名比较好,比如:

defineOptions({ name: 'system_SysMenu_index' })

别忘了,我们现在只生成了组件 name,对应的路由 name 还没有生成呢。我们将 system/SysMenu/index -> system_SysMenu_index 作为路由名:

// 先拿到组件函数
const componentLoader = findMatchingComponent(menu)
// 用路径拼一个唯一且可读的路由 name
const routeName = menu.component
    ? menu.component.replace(/\//g, '_').replace('.vue', '')
    : `menu_${menu.menuId}`

Vue3 路由跳转遮罩

添加 loading 遮罩是为了提升路由跳转时卡顿的观感。

设置全局状态

设置一个 loadingStore 来保存全局状态:

export const useLoadingStore = defineStore('loading', () => {
    const visible = ref(false)
    const show = () => (visible.value = true)
    const hide = () => (visible.value = false)
    return { visible, show, hide }
})

控制遮罩显示

router 被创建完毕后添加两个守卫来控制 loading 的可见性:

/** 加载动画 */
router.beforeEach(() => useLoadingStore().show())
router.afterEach(() => useLoadingStore().hide())

自定义遮罩样式

layout 中设置一个全屏遮罩,或者其他自定义的内容:

<div class="router-view-container"
        v-loading="useLoadingStore().visible"
        element-loading-text="加载中..."
        :element-loading-spinner="svg"
        element-loading-background="rgba(255, 255, 255, 0.8)"
>
    <router-view/>
</div>
const svg = `
        <path class="path" d="
          M 30 15
          L 28 17
          M 25.61 25.61
          A 15 15, 0, 0, 1, 15 30
          A 15 15, 0, 1, 1, 27.99 7.5
          L 15 15
        " style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
      `

更多推荐