BCVP.VUE3系列第九课:实现tabs标签栏

科技   科技   2024-10-05 09:26   河南  
BCVP 开发者社区出品

BCVP V3开发

数字化
服务化
绿色化



放假不停歇,趁着假期学习下VUE3相关的内容,一方面是自己保持活力,另一方面也是工作需要,本系列是我的自学教程,如果有从0开始学习VUE3的,可以跟着一起练习下,毕竟前端我也是泥腿子出身,这一系列会使用Vite、TS、Pinia、Element-Plus等新知识点,既是查漏补缺,也是知识分享。

代码地址:

https://github.com/anjoy8/bcvp.vue3.git

这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore。

系列文章:

第一课:项目初始化与核心知识点说明

第二课:基于泛型基类封装Axios请求

第三课:封装Axios拦截器

第四课:登录页设计

第五课:获取用户信息

第六课:获取动态菜单接口

第七课:基于布局模式实现动态菜单渲染

第八课:丰富面包屑组件



0、本文介绍



本文参考的是开源项目

https://gitee.com/HalseySpicy/Geeker-Admin/tree/template

分步骤讲解框架核心逻辑,今天的内容是:实现tabs标签栏功能,效果图:



1、设计标签数据的状态管理


还是老规矩,只要是对数据的管理和控制,就想到使用状态管理器Pinia来处理,新建文件src\stores\modules\tabs.ts


import router from "@/router";import { defineStore } from "pinia";import type { TabsState, TabsMenuProps } from "@/stores/interface";import { useKeepAliveStore } from "./keepAlive";
const keepAliveStore = useKeepAliveStore();
export const useTabsStore = defineStore({ id: "blogvue3-tabs", state: (): TabsState => ({ tabsMenuList: [] }), actions: { // Add Tabs async addTabs(tabItem: TabsMenuProps) { if (this.tabsMenuList.every(item => item.path !== tabItem.path)) { this.tabsMenuList.push(tabItem); } }, // Remove Tabs async removeTabs(tabPath: string, isCurrent: boolean = true) { const tabsMenuList = this.tabsMenuList; // 如果是删除当前路由 if (isCurrent) { tabsMenuList.forEach((item, index) => { if (item.path !== tabPath) return; // 让页面自动加载前一个或者后一个路由页面 const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1]; if (!nextTab) return; router.push(nextTab.path); }); } // 数据清理 this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabPath); }, // Close Tabs On Side async closeTabsOnSide(path: string, type: "left" | "right") { // 关闭左侧、右侧 const currentIndex = this.tabsMenuList.findIndex(item => item.path === path); if (currentIndex !== -1) { const range = type === "left" ? [0, currentIndex] : [currentIndex + 1, this.tabsMenuList.length]; this.tabsMenuList = this.tabsMenuList.filter((item, index) => { return index < range[0] || index >= range[1] || !item.close; }); } keepAliveStore.setKeepAliveName(this.tabsMenuList.map(item => item.name)); }, // Close MultipleTab async closeMultipleTab(tabsMenuValue?: string) { // 关闭其他 this.tabsMenuList = this.tabsMenuList.filter(item => { return item.path === tabsMenuValue || !item.close; }); keepAliveStore.setKeepAliveName(this.tabsMenuList.map(item => item.name)); }, // Set Tabs async setTabs(tabsMenuList: TabsMenuProps[]) { this.tabsMenuList = tabsMenuList; }, // Set Tabs Title async setTabsTitle(title: string) { const nowFullPath = location.hash.substring(1); this.tabsMenuList.forEach(item => { if (item.path == nowFullPath) item.title = title; }); } },});


内容比较简单,应该都能看的懂,就是实现了对tabsMenuList的增删改查操作,借助Pinia可以很简单的实现数据管理和维护,同时也能实现响应式。


2、定义tabs组件


标签组件设计比较常规,就是将用户点击的路由给存下来,然后借助element-plus官方提供的el-tabs组件,实现渲染,再增加几个小操作而已。
新建src\layouts\components\Tabs\index.vue,添加内容
<template>  <div class="tabs-box">    <div class="tabs-menu">      <el-tabs v-model="tabsMenuValue" type="card" @tab-click="tabClick" @tab-remove="tabRemove">        <el-tab-pane v-for="item in tabsMenuList" :key="item.path" :label="item.title" :name="item.path"          :closable="item.close">          <template #label>            <el-icon v-show="item.icon && tabsIcon" class="tabs-icon">              <component :is="item.icon"></component>            </el-icon>            {{ item.title }}          </template>        </el-tab-pane>      </el-tabs>      <MoreButton />    </div>  </div></template>
<script setup lang="ts">import { ref, computed, watch, onMounted } from "vue";import { useRoute, useRouter } from "vue-router";import { useGlobalStore } from "@/stores/modules/global";import { useTabsStore } from "@/stores/modules/tabs";import { useAuthMenuStore } from "@/stores/modules/authMenu";import { useKeepAliveStore } from "@/stores/modules/keepAlive";import type { TabsPaneContext, TabPaneName } from "element-plus";import MoreButton from "./components/MoreButton.vue";import type { TabsMenuProps } from "@/stores/interface";
const route = useRoute();const router = useRouter();const tabStore = useTabsStore();const authStore = useAuthMenuStore();const globalStore = useGlobalStore();const keepAliveStore = useKeepAliveStore();
const tabsMenuValue = ref(route.fullPath);const tabsMenuList = computed(() => tabStore.tabsMenuList);const tabsIcon = computed(() => globalStore.tabsIcon);
onMounted(() => { // initTabs();});
// 监听路由的变化(防止浏览器后退/前进不变化 tabsMenuValue)watch( () => route.fullPath, () => { if (route.meta.isFull) return; tabsMenuValue.value = route.fullPath; const tabsParams = { icon: route.meta.icon as string, title: route.meta.title as string, path: route.fullPath, name: route.name as string, close: route.path != '/' }; tabStore.addTabs(tabsParams); route.meta.isKeepAlive && keepAliveStore.addKeepAliveName(route.name as string); }, { immediate: true });
// 初始化需要固定的 tabsconst initTabs = () => { authStore.flatMenuListGet.forEach(item => { if (!item.IsButton) { const tabsParams = { icon: item.meta.icon, title: item.meta.title, path: item.path, name: item.name, close: route.path != '/' // 可以固定某些路由不被删除 }; tabStore.addTabs(tabsParams as TabsMenuProps); } });};
// Tab Clickconst tabClick = (tabItem: TabsPaneContext) => { const fullPath = tabItem.props.name as string; router.push(fullPath);};
// Remove Tabconst tabRemove = (fullPath: TabPaneName) => { const name = tabStore.tabsMenuList.filter(item => item.path == fullPath)[0].name || ""; keepAliveStore.removeKeepAliveName(name); tabStore.removeTabs(fullPath as string, fullPath == route.fullPath);};</script>
<style scoped lang="scss">@import "./index.scss";</style>

可以看到,核心逻辑就是有一个watch监听,将数据给放到状态管理器Pinia中。

然后再来个更多操作的按钮组件,将标签的增删改查操作同样在Pinia中处理:

新建src\layouts\components\Tabs\components\MoreButton.vue,

<template>  <el-dropdown trigger="click" :teleported="false">    <div class="more-button">      <i :class="'iconfont icon-xiala'"></i>    </div>    <template #dropdown>      <el-dropdown-menu>        <el-dropdown-item @click="refresh">          <el-icon>            <Refresh />          </el-icon>刷新        </el-dropdown-item>        <el-dropdown-item @click="maximize">          <el-icon>            <FullScreen />          </el-icon>最大化        </el-dropdown-item>        <el-dropdown-item divided @click="closeCurrentTab">          <el-icon>            <Remove />          </el-icon>关闭当前        </el-dropdown-item>        <el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'left')">          <el-icon>            <DArrowLeft />          </el-icon>关闭左侧        </el-dropdown-item>        <el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'right')">          <el-icon>            <DArrowRight />          </el-icon>关闭右侧        </el-dropdown-item>        <el-dropdown-item @click="closeOtherTab">          <el-icon>            <CircleClose />          </el-icon>关闭其它        </el-dropdown-item>        <el-dropdown-item @click="closeAllTab">          <el-icon>            <FolderDelete />          </el-icon>关闭所有        </el-dropdown-item>      </el-dropdown-menu>    </template>  </el-dropdown></template>
<script setup lang="ts">import { inject, nextTick } from "vue";import { useTabsStore } from "@/stores/modules/tabs";import { useGlobalStore } from "@/stores/modules/global";import { useKeepAliveStore } from "@/stores/modules/keepAlive";import { useRoute, useRouter } from "vue-router";
const route = useRoute();const router = useRouter();const tabStore = useTabsStore();const globalStore = useGlobalStore();const keepAliveStore = useKeepAliveStore();
// refresh current pageconst refreshCurrentPage: Function = inject("refresh") as Function;const refresh = () => { setTimeout(() => { keepAliveStore.removeKeepAliveName(route.name as string); refreshCurrentPage(false); nextTick(() => { keepAliveStore.addKeepAliveName(route.name as string); refreshCurrentPage(true); }); }, 0);};
// maximize current pageconst maximize = () => { globalStore.setGlobalState("maximize", true);};
// Close Currentconst closeCurrentTab = () => { if (route.path == '/') return; tabStore.removeTabs(route.fullPath); keepAliveStore.removeKeepAliveName(route.name as string);};
// Close Otherconst closeOtherTab = () => { tabStore.closeMultipleTab(route.fullPath);};
// Close Allconst closeAllTab = () => { tabStore.closeMultipleTab(); router.push('/');};</script>


3、Main中引用Tabs组件



在文件src\layouts\components\Main\index.vue,增加内容

这种组件封装还是比较简单的,通过Pinia也能实现响应式,不用太关心数据通讯,一劳永逸。

到了这里,其实已经开发完成了,不过会有一个问题,就是刷新页面的时候,之前点击后保存的数据会丢失掉,但是你可能好奇,为何左侧的菜单没有丢失,是因为每次刷新页面,就重新拉取了一次动态菜单的权限接口,又重新渲染了一次,所以左侧菜单就不受影响。

但是tabs标签栏不行,这数据本来就不是数据库控制的,所以就需要借助前端的持久化工具了——比如localstorage,传统的做法,在vue2的时候,就是用的vuex配合localstorage,实现数据的持久化,其实在第五课的时候,也是这么操作用户数据的,那有没有更简单的办法呢,答案是肯定的。


4、自定义配置Pinia持久化


有一个Pinia的插件pinia-plugin-persistedstate,可以帮助我们很简单的将状态管理中的数据做持久化处理。

首先需要安装这个依赖,直接npm install即可,

然后新增src/stores/index.ts,使用这个插件:

import { createPinia } from "pinia";import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
// pinia persistconst pinia = createPinia();pinia.use(piniaPluginPersistedstate);
export default pinia;


接下来对持久化方案进行配置,可以自定义存储位置,

新增src\stores\config\piniaPersist.ts:

import type { PersistedStateOptions } from "pinia-plugin-persistedstate";
/** * @description pinia 持久化参数配置 * @param {String} key 存储到持久化的 name * @param {Array} paths 需要持久化的 state name * @return persist * */const piniaPersistConfig = (key: string, paths?: string[]) => { const persist: PersistedStateOptions = { key, storage: localStorage, // storage: sessionStorage, paths }; return persist;};
export default piniaPersistConfig;

这里直接配置到localstorage中了,然后在main.ts主程序入口中,更新Pinia的注册方式:


最后在tabs.ts的标签状态管理中,引入这个配置:

import router from "@/router";import { defineStore } from "pinia";import piniaPersistConfig from "@/stores/config/piniaPersist";
export const useTabsStore = defineStore({ id: "blogvue3-tabs", state: (): TabsState => ({ tabsMenuList: [] }), actions: { // Add Tabs // .... 更多逻辑 }, persist: piniaPersistConfig("blogvue3-tabs")});


现在可以试试路由跳转和页面刷新都没问题


最后的最后,需要引用两个样式scss文件,分别是iconfont和element的。

本文全部的提交就如图:


下篇文章我们继续对页面优化,增加Header顶部右侧的功能ToolBar和个人信息配置,敬请期待。

BCVP代码创新社
专注于 NetCore 相关技术栈的推广,致力于前后端之间的完全分离,从壹开始,让每一个程序员都能从这里学有所成。
 最新文章