vue3.0+ts+element-plus多页签应用模板:头部工具栏(上)

系列文章

一、组件分析

还是老套路,在进行实际开发之前,我们需要先进行组件的分析。这次我们要编写的,是位于页面顶部的头部工具条。我们先来看一下这个头部工具条的作用是什么吧。

1. 这玩意能干啥

  • 提供一些快捷操作:对侧边栏的展开和折叠,页面的刷新,全屏,系统设置等等
  • 显示当前路由位置的面包屑
  • 显示用户的基本信息:avatar和username (nickname)
  • 提供一个下拉快捷菜单,承载一些有关用户的操作
  • 提供一个标签栏,承载所有已经打开的标签页

2. 我要讲的内容

上面已经列出了我们的头部工具条所具有的功能,但是我不会全都讲述一遍,因为有些内容也不需要多费口舌,很简单就能实现。我只讲有一些难度的内容:

  1. 面包屑的实现
  2. 自定义全屏指令
  3. 标签栏的实现
  4. 页面的刷新

二、路由定位——面包屑导航

当用户在一个多级路由的系统中频繁地切换路由时,我们需要一种东西,能够很清晰的显示当前所访问的路由,像这样:
在这里插入图片描述
这就是面包屑导航,简单的说一下他的作用:

  • 让用户了解当前所处位置,以及当前页面在整个网站中的位置
  • 体现了网站的架构层级,能够帮助用户快速学习和了解网站内容和组织方式,从而形成很好的位置感
  • 提供返回各个层级的快速入口,方便用户操作

是不是很符合当前我们所需要的东西呢?那么我们就开始实现吧。

1. 获取路由位置

因为面包屑导航最本质的作用就是让用户了解当前所处位置,所以每次切换路由的时候,我们必须要获取切换到当前路由会经过的所有路由,并生成一个路由数组,作为面包屑导航的基础。其实这步操作我们已经在前面的章节实现过了,大家可以回去看一下同系列文章:多级路由缓存的路由扁平化部分,或者接着往下看:

由于我的所有子级菜单项,都依赖于一个名叫AppMain的组件(不明白我在说什么的可以参照项目源码wyb199877/multi-tabs),也就是说,所有路由模块都没有直接引用Layout组件(你的主布局组件),而是引用了一个只有router-view的AppMain组件。所以在路由扁平化的时候,会删掉所有引用AppMain组件的中间路由。这就导致了一个问题,我们的route.mathed在经过处理之后,只会剩下两级路由记录,无法明确的表示当前路由的所在位置及其层级关系。所以,我们需要在删除路由之前,将路由记录保存下来,作为面包屑导航的基础。

// 路由扁平化,将二级以上的路由转换成二级路由
const handleKeepAlive = (to: RouteLocationNormalized) => {
  // 判断目标路由记录是否大于2
  if (to.matched && to.matched.length > 2) {
    // 遍历路由记录,制作面包屑导航列表,并删除依赖于AppMain组件的中间路由,实现路由扁平化
    for (let i = 0; i < to.matched.length; i++) {
      const element = to.matched[i]
      /* 从这里开始处理面包屑 */
      if (to.meta.breadcrumb && element.name !== 'index') {
        ;(to.meta.breadcrumb as BreadcrumbItemProps[]).push({
          name: element.name!,
          label: element.meta.label as string,
          type: element.meta.type as MenuItemType
        })
      }
      if (!to.meta.breadcrumb) {
        to.meta.breadcrumb = []
      }
      /* 从这里结束处理面包屑 */
      if (element.components.default.name === 'AppMain') {
        to.matched.splice(i, 1)
        handleKeepAlive(to)
      }
    }
  } else {
    // 当路由已经扁平化完毕的时候,将目标路由自身加入面包屑中
    /* 从这里开始处理面包屑 */
    const toBreadcrumb = to.meta.breadcrumb as BreadcrumbItemProps[]
    if (!toBreadcrumb) return
    const isToInToBreadcrumb = toBreadcrumb.some(
      (item: BreadcrumbItemProps) => item.name === to.name
    )
    if (!isToInToBreadcrumb) {
      ;(to.meta.breadcrumb as BreadcrumbItemProps[]).push({
        name: to.name!,
        label: to.meta.label as string,
        type: to.meta.type as MenuItemType
      })
    }
    /* 从这里结束处理面包屑 */
  }
}

router.beforeEach((to, from, next) => {
  handleKeepAlive(to)
  next()
})

2. 创建面包屑导航PathBreadbrcumb组件

贴代码,样式部分就省略了,然后这部分比较简单,没啥需要细讲的:

<template>
  <el-breadcrumb separator="/" class="pathBreadcrumb">
    <el-breadcrumb-item v-for="item in breadcrumb" :key="item.name">
      <!-- 如果item.type = 'item',说明是可点击跳转的路由,即AsideBar菜单中的可点击跳转的菜单项 -->
      <router-link v-if="item.type === 'item'" :to="{ name: item.name }">
        {{ item.label }}
      </router-link>
      <span v-else class="no-redirect">{{ item.label }}</span>
    </el-breadcrumb-item>
  </el-breadcrumb>
</template>

<script lang="ts">
import { MenuItemType } from '@/layout/components/AsideBar/typings'
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
import { BreadcrumbItemProps } from '../../typings'

export default defineComponent({
  name: 'PathBreadcrumb',
  setup() {
    const route = useRoute()
    const breadcrumb = computed<BreadcrumbItemProps[]>(() => {
      // 取出我们之前处理好的路由记录数组
      let result = route.meta.breadcrumb as BreadcrumbItemProps[]
      // 如果result中是undefined,说明当前路由是工作台workbench
      if (!result) {
        result = [{ name: 'workbench', label: '工作台', type: 'item' as MenuItemType }]
      }
      // 如果路由记录数组中的第一个路由不是工作台,那就把工作台放在第一位
      if (result[0].name !== 'workbench') {
        result.unshift({ name: 'workbench', label: '工作台', type: 'item' as MenuItemType })
      }
      return result
    })

    return {
      breadcrumb
    }
  }
})
</script>

三、自定义指令——全屏

其实这个全屏功能用自定义指令去实现是有点多余的,因为我们这里是整个document都直接全屏,如果是仅需要某个元素全屏显示的话,倒是很有必要使用自定义指令。所以这里我还是通过自定义指令去实现这个功能。

1. 什么是自定义指令?

详细了解可移步官方文档-教程-自定义指令

虽然在vue中,并不提倡开发者进行实际DOM的操作,而是通过组件的形式去进行代码复用和抽象。然而,在很多情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令,因为在自定义指令中是可以很方便的操作DOM元素的。

2. 开始行动吧

在定义指令之前,我们需要先定义一个控制全屏和退出全屏的方法:

public static fullscreen(element: HTMLElement | Element, action: 'enable' | 'cancel'): void {
  // 检测当前浏览器是否支持直接全屏操作,若不支持则调用F11键的全屏功能
  if (typeof window.ActiveXObject !== 'undefined') {
    const wscript = new ActiveXObject('WScript.Shell')
    wscript !== null && wscript.SendKeys('{F11}')
    return
  }
  // 分别对传进来的DOM进行全屏和退出全屏操作
  let requestMethod
  switch (action) {
    case 'enable':
      requestMethod = element.requestFullscreen
      if (requestMethod) {
        requestMethod.call(element)
        return
      }
      break
    case 'cancel':
      document.fullscreenElement && document.exitFullscreen()
      return
  }
  // 如果能走到这里说明这个浏览器太low了,直接弹消息吧
  window.$toast('warning', '您当前使用的浏览器不支持全屏功能,推荐使用谷歌浏览器访问')
  store.commit(Consts.MutationKey.SET_FULLSCREEN, false)
}

然后我们就可以去定义全屏指令了,我在src/directive/fullscreen目录下新建文件index.ts

import { App } from 'vue'
import Utils from '@/utils'
import store from '@/store'

const vFullscreen = (app: App<Element>) => {
  app.directive('fullscreen', (el: HTMLElement | Element, binding) => {
    // 将下面的document.documentElement改成el,即可实现仅对指令所在DOM元素进行全屏
    Utils.fullscreen(document.documentElement, binding.value ? 'enable' : 'cancel')
  })
  document.onfullscreenchange = () => {
    store.commit('appModule/SET_FULLSCREEN', Boolean(document.fullscreenElement))
  }
}

export default vFullscreen

src/directive目录下新建文件index.ts

import { App } from 'vue'

// 自动引入directive目录下的所有自定义指令
const directiveFiles = require.context('.', true, /\.ts$/)
const directives = directiveFiles
  .keys()
  .filter((key) => key !== './index.ts')
  .map((key) => directiveFiles(key).default)

const directive = {
  install: (app: App<Element>) => {
    directives.forEach((directive) => directive(app))
  }
}

export default directive

main.ts中use一下:

import directive from './directive'
app.use(directive)

这样,我们的全屏指令v-fullscreen就可以使用了,现在我们去定义一个app模块,存放一下控制全屏的变量。在src/store/modules目录下新建app.ts

import { Module } from 'vuex'
import { AppStateProps, RootStateProps } from '../typings'

const appModule: Module<AppStateProps, RootStateProps> = {
  namespaced: true,
  state: {
    fullscreen: Boolean(document.fullscreenElement) || false
  },
  mutations: {
    SET_FULLSCREEN(state, status: boolean) {
      state.fullscreen = status
    }
  }
}

export default appModule

由于我们之前已经实现过模块自顶导入了,所以现在我们就可以去Layout中使用v-fullscreen了:

<template>
  <div v-fullscreen="fullscreen" class="wrapper">
    <!-- 中间的内容省略 -->
  </div>
</template>

<script>
/* import部分省略 */
export default defineComponent({
  name: 'Index',
  /* 其他内容省略 */
  setup() {
    const store = useStore()
    const fullscreen = computed({
      get: () => store.state.appModule.fullscreen,
      set: (value) => store.commit(Consts.MutationKey.SET_FULLSCREEN, value)
    })
    return {
      fullscreen
    }
  }
})
</script>

至此,我们的全屏指令就完成了,想要修改全屏状态的时候,只需要调用app模块中名叫的SET_FULLSCREEN的mutation方法就可以了,也就是fullscreen.value = !fullscreen.value


标签栏与页面刷新将会在下一章中讲解,敬请关注!

下一篇预告:vue3.0+ts+element-plus多页签应用模板:头部工具栏(中)

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

W先生-SirW

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值