拜托啦,能自己写一个标签页展示真的好酷的!!!

6,935 阅读7分钟

前言:

这是我个人学习的笔记,可能会有很多做的不好的地方,麻烦各位大神指点指点。
功能前期准备需要搭建好vue + ts + vite + element-plus环境

效果图先贴上

f5jxr-u6rmo.gif

目标效果

1、左侧导航岚点击进行路由跳转同时右侧如果没有同名的标签便新增一个标签,有则跳转到该标签
2、点击标签栏跳转路由,同时左侧菜单栏跳转到对应的选项
3、左侧菜单栏从后台返回数据获取。 -- 此功能可以用于进行权限控制
4、刷新后保留当前阅览的标签栏,其余标签栏清除掉 -- 首页标签会被清除和关闭

使用的技术栈

技术栈官网
vuecn.vuejs.org/
vue-routerrouter.vuejs.org/zh/
piniapinia.vuejs.org/
vitevitejs.cn/
element-plueelement-plus.org/#/zh-CN

element-plus组件使用

el-muem
el-tabs

代码结构

1661081476799.jpg

一、建一个pinia仓库 useMuem.ts

在仓库中创建一个对象数组:MuenInfo:[
{
    title:菜单的标题
    index:菜单的路由
    icon:菜单的icon
}]

我这边没有去做后端接口,所以就直接使用一些默认值去使用,如果你的菜单数据是后端返回的,直接往这个数组中push就行了

二、创建useTabs仓库

 在仓库中创建一个对象数组:MuenInfo:[
    {
        title: tabs标签的名字
        index: tabs标签的路由
        closable: tabs是否可关闭
    }]
   
   创建一个当前路由的标识,用于路由跳转时切换标签栏
   activeIndex:string,
   

创建新增标签的方法

   这个方法需要三个参数传入,具体传入什么看代码注释
    /**
     * 用于新增标签
     * @param title - 标签页标题
     * @param name - 标签页路由
     * @param closable - 是否关闭标签页
     */
    add(title: string, name: string, closable:boolean) {
      this.tabsInfo.push({
        title:title,
        name: name,
        closable: closable
      })
    },

创建删除标签的方法

这个方法需要一个传入一个参数,参数是需要删除的标签的路由
/**
     * 用于删除标签页
     * @param url 需要删除的路由
     */
    delTabs(url:any){
      let index = 0;
      for (let tab of this.tabsInfo) {
        if (tab.name === url) {
          break;
        }
        index++;
      }
      this.tabsInfo.splice(index, 1)
      this.setActiveIndex('/Index/Test')
    }
  }

切换标签的方法

这个方法需要一个传入一个参数,参数是需要切换的标签的路由
/**
     * 用于更新当前的activeIndex
     * @param url - 更新activeIndex
     */
    setActiveIndex(url:any) {
      this.activeIndex = url
    },

完整的代码

import { defineStore } from 'pinia'

export const usetabsInfo = defineStore('tabsInfo', {
  state: () => ({
   
    tabsInfo: [{
    /**
     * tabs标签名
     */
      title: '首页',
      /**
       * tabx标签路径
       */
      name: '/Index/Test',
      /**
       * tabs标签是否可关闭
       */
      closable:false
    }],
    /**
     * 当前路由
     */
    activeIndex:'/Index/Test',
  }),
  actions: {
    /**
     * 
     * @param title - 标签页标题
     * @param name - 标签页路由
     * @param closable - 是否关闭标签页
     */
    add(title: string, name: string, closable:boolean) {
      this.tabsInfo.push({
        title:title,
        name: name,
        closable: closable
      })
    },
    /**
     * 用于更新当前的activeIndex
     * @param url - 更新activeIndex
     */
    setActiveIndex(url:any) {
      this.activeIndex = url
    },
    /**
     * 用于删除标签页
     * @param url 需要删除的路由
     */
    delTabs(url:any){
      let index = 0;
      for (let tab of this.tabsInfo) {
        if (tab.name === url) {
          break;
        }
        index++;
      }
      this.tabsInfo.splice(index, 1)
      this.setActiveIndex('/Index/Test')
    }
  }
})

三、创建navMuem.vue组件

在这组件中引入文件el-muem组件 -- 我对项目做了element-plus的全局按需引入,所以可以直接使用

首先引入useMuem仓库数据
  import { useMuem } from '../store/useMuem'
  
 /**
 * 引入muem仓库
 */
let muemInfo = useMuem()

/**
 * 获取muem仓库中的state信息
 */
let editMuem = muemInfo.muemInfo
引入useTabs.ts仓库数据
import { usetabsInfo } from '../store/useTabs'

**
 * tabs信息
 */
const tabsInfo = usetabsInfo()
/**
 * 菜单导航index路由
 */
let activeIndex = tabsInfo.activeIndex
引入之后将代码信息展示在template之中
    <el-menu
    // default-cative是选中的tabs标签
    :default-active="tabsInfo.activeIndex"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
    //自定义菜单点击事件
    @select="updateActiveIndex"
    // 一定要设置该属性为true,不然不会进行路由跳转。具体原因请看element官网组件属性
    :router="true"
  >
    // 将获取到的muem数据在menu-item中遍历展示数据
    // 便且给menu-itemc添加点击事件,addTabs
    <el-menu-item @click="addTabs(muem.title)" 
    v-for="muem in editMuem" 
    :key="muem.index" 
    :index="muem.index">
      <el-icon><component :is="muem.icon"></component></el-icon>
      <template #title>{{muem.title}}</template>
    </el-menu-item>
  </el-menu>
**************************************************
点击事件的原理显示获取tabs仓库的标签信息,
然后创建一个isExist(做flag),
遍历tabs标签信息,比较tabs中是否已经有了该标签
如果有该标签便进行切换标签
如果没有则新增标签
调用tabsInfo.add方法
/**
 * 增加新的标签
 * @param name 传入新增标签的名字
 */
const addTabs = (name: any) => {
  let tabs = tabsInfo.tabsInfo
  sessionStorage.setItem('name',name)
  //判断路径是否存在相同路径的标识
  let isExist = false
  //循环匹配该路径是否在tabs标签也中
  tabs.forEach((tab,index) => {
    if (tab.name === activeIndex) {
      isExist = true
    }
  })
  if (isExist) {
    //isExist true时,更改当前路径
    tabsInfo.setActiveIndex(activeIndex)
  } else {
    //isExist false时添加tabs标签页,并同时修改当前路径
    tabsInfo.add(name, activeIndex,true)
    tabsInfo.setActiveIndex(activeIndex)
  }
}
    

四、创建Index.vue文件

这个组件用于展示tabs标签栏

引入usetabs仓库数据和router路由信息

import { usetabsInfo } from '../store/useTabs'
import router from '../router/index'

/**
 * 获取tabsinfo的信息
 */
const tabsInfo = usetabsInfo()
/**
 * 获取tabsinfo中state的信息
 */
const editableTabs = tabsInfo.tabsInfo

创建关闭标签的方法

 该方法用于关闭标签之后跳转路由
/**
 * 关闭当前标签,并同时跳转到首页路由
 * @param name 要关闭的标签的路径
 */
const tabRemove = (name:string ) => {
  tabsInfo.delTabs(name)
  tabsInfo.setActiveIndex('/Index/Test')
  router.push('/Index/Test')
}

创建点击标签跳转路由的方法

/**
 * 点击标签更改当前的路径,并跳转路由,向sessionStorage中添加name路径
   想sessionStorage中存储数据是为了在页面刷新之后还能保持数据信息
 * @param pane - tabs的信息
 * @param ev 
 */
const check = (pane: any, ev: any) => {
  tabsInfo.setActiveIndex(pane.props.name)
  router.push(pane.props.name)
  sessionStorage.setItem('name',pane.props.label)
}

在template中展示页面

<el-tabs type="card" class="demo-tabs" 
    //model-value 表示当前选中的标签
  :model-value="tabsInfo.activeIndex" 
    //添加点击事件,使页面点击之后可以进行路由跳转
  @tab-click="check"
    //添加页面关闭事件
  @tab-remove = "tabRemove">
    <el-tab-pane v-for="item in editableTabs"
    :key="item.name" 
    :label="item.title" 
    :closable= "item.closable"
    :name="item.name">
    </el-tab-pane>
    // 使用keep-alive的目的是为了在页面切换之后数据不会被清除掉
    // 便于表单信息的存储
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </router-view>
  </el-tabs>

在上面的都布置完成之后可以完成标签展示了 但是有个小bug就是在页面书信之后,新增的标签栏都被关闭了,数据都不会被存储住 所以得做一些优化

五、做刷新之后保持页面的功能

首先在mian.ts主文件中添加路由守卫

该路由守卫的作用是往sessionStorage中存入当前的路由路径
/**
 * 全局路由守卫,在路由跳转的时候像sessionStorage中存入当前路径
 * @param to - 要前往的路由
 * @param from - 当前的路由
 */
router.beforeEach((to,from) => {
  sessionStorage.setItem('path',to.fullPath)
})

然后在navMuem.vue中添加初始化tabs标签的代码

该代码的功能和dadtabs功能效果一样,只是多一个标签名字的参数
/**
 * 初始化标签的函数,与addTabs同理
 * @param name 标签的名字
 * @param newIndex 标签的路径
 */
function initTabs(name:any,newIndex:any){
    let tabs = tabsInfo.tabsInfo
    let isExist = false
    tabs.forEach((tab,index) => {
      if (tab.name === newIndex) {
        isExist = true
      }
    })
    if (isExist) {
      tabsInfo.setActiveIndex(newIndex)
    } else {
      tabsInfo.add(name, newIndex,true)
      tabsInfo.setActiveIndex(newIndex)
  }
  }

最后在onBeforeMount钩子中添加初始化代码

onBeforeMount(() => {
  /**
   * 获取sessionStorage中的path
   */
  const initTab: any = sessionStorage.getItem('path')
  /**
   * 获取sessionStorage中的name
   */
  const inittabsName: any = sessionStorage.getItem('name')
  if (inittabsName) {
    initTabs(inittabsName,initTab) // -- 初始化函数
  }
})

以上完成便可以在实现标签页展示的功能了

六、完整代码

NavMuem.vue

<template>
  <el-menu
    :default-active="tabsInfo.activeIndex"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
    @select="updateActiveIndex"
    :router="true"
  >
    <el-menu-item @click="addTabs(muem.title)" 
    v-for="muem in editMuem" 
    :key="muem.index" 
    :index="muem.index">
      <el-icon><component :is="muem.icon"></component></el-icon>
      <template #title>{{muem.title}}</template>
    </el-menu-item>
  </el-menu>
</template>

<script lang="ts" setup>
import { markRaw, onBeforeMount, ref } from 'vue'
import {
  Document,
  Menu as IconMenu,
  Location,
  Setting,
  User,
  House
} from '@element-plus/icons-vue'

// 引入路由
import { useRoute } from 'vue-router'
import { usetabsInfo } from '../store/useTabs'
import { useMuem } from '../store/useMuem'

/**
 * 变为不可更改的属性
 */
markRaw(User)
markRaw(House)
/**
 * tabs信息
 */
const tabsInfo = usetabsInfo()
/**
 * 菜单导航index路由
 */
let activeIndex = tabsInfo.activeIndex
/**
 * 引入muem仓库
 */
let muemInfo = useMuem()
/**
 * 获取muem仓库中的state信息
 */
let editMuem = muemInfo.muemInfo

/**
 * 该方法用于点击标签页时更新activeIndex
 * 参数详情请参考elementplus官网
 * 
 * @param index 
 * @param indexPath 
 * @param item 
 * @param routeResult 
 * 
 */
function updateActiveIndex(index:any,indexPath:any,item:any,routeResult:any){
  activeIndex = index
}
const isCollapse = ref(false)

/**
 * 增加新的标签
 * @param name 传入新增标签的名字
 */
const addTabs = (name: any) => {
  let tabs = tabsInfo.tabsInfo
  sessionStorage.setItem('name',name)
  //判断路径是否存在相同路径的标识
  let isExist = false
  //循环匹配该路径是否在tabs标签也中
  tabs.forEach((tab,index) => {
    if (tab.name === activeIndex) {
      isExist = true
    }
  })
  if (isExist) {
    //isExist true时,更改当前路径
    tabsInfo.setActiveIndex(activeIndex)
  } else {
    //isExist false时添加tabs标签页,并同时修改当前路径
    tabsInfo.add(name, activeIndex,true)
    tabsInfo.setActiveIndex(activeIndex)
  }
}

/**
 * 初始化标签的函数,与addTabs同理
 * @param name 标签的名字
 * @param newIndex 标签的路径
 */
function initTabs(name:any,newIndex:any){
    let tabs = tabsInfo.tabsInfo
    let isExist = false
    tabs.forEach((tab,index) => {
      if (tab.name === newIndex) {
        isExist = true
      }
    })
    if (isExist) {
      tabsInfo.setActiveIndex(newIndex)
    } else {
      tabsInfo.add(name, newIndex,true)
      tabsInfo.setActiveIndex(newIndex)
  }
  } 

onBeforeMount(() => {
  /**
   * 获取sessionStorage中的path
   */
  const initTab: any = sessionStorage.getItem('path')
  /**
   * 获取sessionStorage中的name
   */
  const inittabsName: any = sessionStorage.getItem('name')
  if (inittabsName) {
    initTabs(inittabsName,initTab) // -- 初始化函数
  }
})

</script>

<style>
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
  min-height: 400px;
}
</style>

router/index.ts

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { defineAsyncComponent } from 'vue'

const routes: Array<RouteRecordRaw> = [
  {
    /**
     * 路由重定向,当路由为 / 时,会跳转难道登录路由上
     */
    path: '/',
    redirect: '/Index/Test'
  },
  {
    path: '/Index',
    name: 'Index',
    component: () => import(`../view/Index.vue`),
    children: [{
      path: 'Test',
      name: 'Test',
      component:()=>import('../view/Test.vue')
    },{
      path: 'Test2',
      name: 'Test2',
      component: () => import('../view/Test2.vue')
    }]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})



export default router

userMuem.ts

import { defineStore } from 'pinia'
import {
  House,
  User
} from '@element-plus/icons-vue'
/**
 * 菜单仓库
 * @return {object[]} muemInfo - 返回菜单信息
 * @return title - 菜单标题
 * @return index - 菜单路由
 * @return icon  - 菜单icon
 */
export const useMuem = defineStore('muemInfo', {
  state: (() => ({
    muemInfo:[
      {
        title: '首页',
        index: '/Index/Test',
        icon: House
      }, {
        title: '第二页',
        index: '/Index/Test2',
        icon: User
      }
    ]
  }))
})

useTabs.ts

import { defineStore } from 'pinia'

export const usetabsInfo = defineStore('tabsInfo', {
  state: () => ({
   
    tabsInfo: [{
    /**
     * tabs标签名
     */
      title: '首页',
      /**
       * tabx标签路径
       */
      name: '/Index/Test',
      /**
       * tabs标签是否可关闭
       */
      closable:false
    }],
    /**
     * 当前路由
     */
    activeIndex:'/Index/Test',
  }),
  actions: {
    /**
     * 用于新增标签
     * @param title - 标签页标题
     * @param name - 标签页路由
     * @param closable - 是否关闭标签页
     */
    add(title: string, name: string, closable:boolean) {
      this.tabsInfo.push({
        title:title,
        name: name,
        closable: closable
      })
    },
    /**
     * 用于更新当前的activeIndex
     * @param url - 更新activeIndex
     */
    setActiveIndex(url:any) {
      this.activeIndex = url
    },
    /**
     * 用于删除标签页
     * @param url 需要删除的路由
     */
    delTabs(url:any){
      let index = 0;
      for (let tab of this.tabsInfo) {
        if (tab.name === url) {
          break;
        }
        index++;
      }
      this.tabsInfo.splice(index, 1)
      this.setActiveIndex('/Index/Test')
    }
  }
})

view/Index.vue

<template>
  <el-tabs type="card" class="demo-tabs" 
  :model-value="tabsInfo.activeIndex" 
  @tab-click="check"
  @tab-remove = "tabRemove">
    <el-tab-pane v-for="item in editableTabs"
    :key="item.name" 
    :label="item.title" 
    :closable= "item.closable"
    :name="item.name">
    </el-tab-pane>
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </router-view>
  </el-tabs>
</template>


<script lang="ts" setup>
import {  onBeforeMount, onUpdated, ref} from 'vue'
import { usetabsInfo } from '../store/useTabs'
import router from '../router/index'

/**
 * 获取tabsinfo的信息
 */
const tabsInfo = usetabsInfo()
/**
 * 获取tabsinfo中state的信息
 */
const editableTabs = tabsInfo.tabsInfo

/**
 * 关闭当前标签,并同时跳转到首页路由
 * @param name 要关闭的标签的路径
 */
const tabRemove = (name:string ) => {
  tabsInfo.delTabs(name)
  tabsInfo.setActiveIndex('/Index/Test')
  router.push('/Index/Test')
}

/**
 * 点击标签更改当前的路径,并跳转路由,向sessionStorage中添加name路径
 * @param pane - tabs的信息
 * @param ev 
 */
const check = (pane: any, ev: any) => {
  tabsInfo.setActiveIndex(pane.props.name)
  router.push(pane.props.name)
  sessionStorage.setItem('name',pane.props.label)
}

</script>
<style>
.demo-tabs > .el-tabs__content {
  padding: 32px;
  color: #6b778c;
  font-size: 32px;
  font-weight: 600;
}
.demo-tabs{
  width: 100%;
}
</style>

view/test.vue

<template>
<input type="text">
  <button>jksjdksjk</button>
</template>

<script></script>

<style></style>

view/test.vue

<template>
  <button>乌拉无阿里</button>
</template>

<script></script>

<style></style>

App.vue

<script setup lang="ts">
import NavMuem from './components/NavMuem.vue'
import Header from './components/Header.vue'
import Index from './view/Index.vue'
</script>

<template>
  <div class="common-layout">
    <Header />
    <div class="main">
      <NavMuem />
      <router-view>
      </router-view>>
    </div>
    
  </div>
</template>

<style scoped>
.common-layout {
  width: 98vw;
  height: 100vh;
  color: aliceblue;
}
.header {
  height: 60px;
  width: 100%;
  background: green !important;
}
</style>

<style lang="less" scoped>
.main {
  display: flex;
  justify-content: flex-start;
}
.index{
  width: 100vw;
}
</style>

main.ts

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

// pinia状态管理器
import { createPinia } from 'pinia';
// 路由组件
import router from './router/index'



/**
 * 全局路由守卫,在路由跳转的时候像sessionStorage中存入当前路径
 * @param to - 要前往的路由
 * @param from - 当前的路由
 */
router.beforeEach((to,from) => {
  sessionStorage.setItem('path',to.fullPath)
})

const app = createApp(App)

const pinia = createPinia();
app.use(pinia)
app.use(router)
app.mount('#app')

七、结语

本文只是一个菜鸟前端写的,只为了记录自己的成长,如有错误的地方请大佬指正。欢迎大佬们批评

本文代码仓库暂未开源,如有需要可私我,我可以进行开源