vue3-组件标签页

举报
林太白 发表于 2025/12/24 10:29:44 2025/12/24
【摘要】 vue3-组件标签页

outline: deep

vue3-组件标签页

源码地址,就给它个star吧!更多教学源码没有的直接找我要哦!

https://github.com/lintaibai/TG

1、布局编写

选型技术

标签部分也是我们正常需要额外关注的一个布局部分,接下来我们就开发一个标签部分,放置位置如下

src\layout\TagsView 

之前我们这部分使用的模块是

el-scrollbar

ElementPlus提示:

废弃的 directive
我们将不再维护这个 directive。 
会在 3.0.0 被移除,请使用 el-scrollbar infinite scroll 作为代替。

基础布局

所以我们迁移这部分更改为el-tabs进行使用,这里我们依旧还是先写死一个然后实现基础功能以后进行完善

<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-if="item.icon && tabsIcon" class="tabs-icon">
              <component :is="item.icon"></component>
            </el-icon>
            {{ item.title }}
          </template>
        </el-tab-pane>
      </el-tabs>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";

// 类型
import type { TabsPaneContext } from 'element-plus'


const route = useRoute();
const router = useRouter();
const tabsMenuList = [
  {
    path: '/dashboard',
    title: '仪表盘',
    icon: 'Monitor',
    close: true
  },
  {
    path: '/user',
    title: '用户管理',
    icon: 'User',
    close: true
  },
  {
    path: '/settings',
    title: '系统设置',
    icon: 'Setting',
    close: true
  },
  {
    path: '/home',
    title: '首页',
    icon: 'House',
    close: false  // 首页通常不允许关闭
  },
  {
    path: '/data-analysis',
    title: '数据分析',
    icon: 'DataAnalysis',
    close: true
  }
]

// 对应的 tabsMenuValue 初始值可以是:
const tabsMenuValue = ref('/dashboard')// 默认选中
const tabsIcon = ref(true);
const tabClick = (tab: TabsPaneContext, event: Event) => {
  console.log(tab, event,'tabClick')
}
// Remove Tab
const tabRemove = (fullPath) => {
  console.log(fullPath,'tabRemove');
};
</script>

2、逻辑编写

src\store\modules\tabs.ts我们的缓存部分放置到这里

// 布局-标签页缓存
import { defineStore } from "pinia";
/* tabsMenuProps */
export interface TabsMenuProps {
  icon: string;
  title: string;
  path: string;
  name: string;
  close: boolean;
  isKeepAlive: boolean;
}

/* TabsState */
export interface TabsState {
  tabsMenuList: TabsMenuProps[];
}
export const useTabsStore = defineStore("layout-tabs",{
  state:(): TabsState  => ({
    tabsMenuList: []
  }),
  actions: {
    // Add Tabs
    async addTabs(tabItem) {},
    // Remove Tabs
    async removeTabs(tabPath: string, isCurrent: boolean = true) {},
    // Close Tabs On Side
    async closeTabsOnSide(path: string, type: "left" | "right") {},
    // Close MultipleTab
    async closeMultipleTab(tabsMenuValue?: string) {},
    // Set Tabs
    async setTabs(tabsMenuList: TabsMenuProps[]) {},
    // Set Tabs Title
    async setTabsTitle(title: string) {}
  },
});

默认标签页面

这个时候我们就可以设置默认的标签显示页面,使用Pinia管理标签组件的默认首页

// 默认显示首页 
const DEFAULT_TAB: TabsMenuProps = {
  icon: "HomeFilled",
  title: "首页",
  path: "/dashboard",
  name: "dashboard",
  close: false,
  isKeepAlive: true
};

export interface TabsState {
  tabsMenuList: TabsMenuProps[];
}

项目中引入使用,这个时候我们首页就已经设置好了

// store
import { useTabsStore } from "@/store/modules/tabs";
// tabs
const tabStore = useTabsStore();
// tabs列表
const tabsMenuList = computed(() => tabStore.tabsMenuList); 

添加标签

监听一下我们路由变化,就是改变的时候加入我们的标签之中

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.meta.isAffix,
      isKeepAlive: route.meta.isKeepAlive as boolean
    };
    tabStore.addTabs(tabsParams);
  },
  { immediate: true }
);

这个时候输出就可以看到每次我们点击路由切换的时候看到的路由信息

async addTabs(tabItem) {
  console.log(tabItem,'store-tabItem');
},

完善Store方法,这个时候我们添加标签已经好了

 // Add Tabs
async addTabs(tabItem) {
  console.log(tabItem,'store-tabItem');
  // 1. 检查并添加标签页
  if (this.tabsMenuList.every(item => item.path !== tabItem.path)) {
    this.tabsMenuList.push(tabItem);
  }
},

标签点击

添加一下标签的点击事件

// Tab Click
const tabClick = (tabItem: TabsPaneContext, event: Event) => {
  console.log(tabItem, event,'tabClick');
  const fullPath = tabItem.props.name as string;
  router.push(fullPath);
}

移除标签

完善移除标签的方法,这个时候我们已经实现了最基本的移除标签的组件以及方法

 async removeTabs(tabPath: string, isCurrent: boolean = true) {
  console.log(tabPath,'store-tabPath');
  tabStore.removeTabs(fullPath as string, fullPath == route.fullPath);
},

这里写的时候有时候突然出现输出无效的时候,这个时候就重新启动项目清理缓存尝试

// Remove Tabs
async removeTabs(tabPath: string, isCurrent: boolean = true) {
  console.log(tabPath,'store-removeTabs');
  console.log(isCurrent,'store-isCurrent');
  // 1. 移除标签页
  if (isCurrent) {
    this.tabsMenuList.forEach((item, index) => {
      if (item.path !== tabPath) return;
      const nextTab = this.tabsMenuList[index + 1] || this.tabsMenuList[index - 1];
      if (!nextTab) return;
      router.push(nextTab.path);
    });
  }
  // 过滤掉要删除的标签页
  this.tabsMenuList = this.tabsMenuList.filter(item => item.path !== tabPath);
  console.log(this.tabsMenuList,'store-tabsMenuList');
},

3、优化标签

缓存提升性能Keep-Alive

我们点击标签的时候会频繁的重新创建,这个时候可以借助Keep-Alive进行缓存

Vue 的 Keep-Alive 功能,它允许你动态地控制哪些组件/页面需要被缓存。当组件被 Keep-Alive 缓存后,它的状态会被保留,而不是每次切换时都重新创建,这对于保持用户操作状态、提高性能很有帮助。

编写缓存模块

src\store\modules\keepAlive.ts
import { defineStore } from "pinia";
/* KeepAliveState */
interface KeepAliveState {
  keepAliveName: string[];
}
export const useKeepAliveStore = defineStore("keepAlive",{
  state: (): KeepAliveState => ({
    keepAliveName: []
  }),
  actions: {
    // Add KeepAliveName
    async addKeepAliveName(name: string) {
      !this.keepAliveName.includes(name) && this.keepAliveName.push(name);
    },
    // Remove KeepAliveName
    async removeKeepAliveName(name: string) {
      this.keepAliveName = this.keepAliveName.filter(item => item !== name);
    },
    // Set KeepAliveName
    async setKeepAliveName(keepAliveName: string[] = []) {
      this.keepAliveName = keepAliveName;
    }
  }
});

引入使用

// 标签页缓存
import { useKeepAliveStore } from "./keepAlive";
const keepAliveStore = useKeepAliveStore();

添加时候管理

async addTabs(tabItem) {
  // 1. 检查并添加标签页
  if (this.tabsMenuList.every(item => item.path !== tabItem.path)) {
    this.tabsMenuList.push(tabItem);
  }
  // 2. 管理页面缓存
  if (!keepAliveStore.keepAliveName.includes(tabItem.name) && tabItem.isKeepAlive) {
    keepAliveStore.addKeepAliveName(tabItem.path);
  }
},

移除时候管理

// Remove Tabs
async removeTabs(tabPath: string, isCurrent: boolean = true) {
  // 1. 移除标签页
  if (isCurrent) {
    this.tabsMenuList.forEach((item, index) => {
      if (item.path !== tabPath) return;
      const nextTab = this.tabsMenuList[index + 1] || this.tabsMenuList[index - 1];
      if (!nextTab) return;
      router.push(nextTab.path);
    });
  }
 
  // 2. 管理页面缓存
  const tabItem = this.tabsMenuList.find(item => item.path === tabPath);
  tabItem?.isKeepAlive && keepAliveStore.removeKeepAliveName(tabItem.path);
  
  // 过滤掉要删除的标签页
  this.tabsMenuList = this.tabsMenuList.filter(item => item.path !== tabPath);
},

固定标签

有时候,我们需要固定一些标签,这里我们就以菜单为例

// 初始化需要固定的 tabs
const initTabs = () => {};

这个时候我们需要扁平化的菜单路由数据

// 静态+动态路由
import usePermissionStore from '@/store/modules/usePermissionStore'
const permissionStore = usePermissionStore()

//扁平侧边路由
const flattenMenuList = computed(() => permissionStore.flattenMenuList)

根据条件扁平化固定标签,这个时候我们固定标签就ok了

// 初始化需要固定的 tabs
const initTabs = () => {
  console.log(flattenMenuList.value,'flattenMenuList.value');
  console.log('固定tabs');
  flattenMenuList.value.forEach(item => {
    //  && !item.meta.isHide && !item.meta.isFull
    if (item.meta?.isAffix) {
      console.log(item,'item')
      const tabsParams = {
        // icon: item.meta.icon? item.meta.icon : '',
        title: item.meta.title,
        path: item.path,
        name: item.name,
        close: !item.meta.isAffix,
        isKeepAlive: item.meta.isKeepAlive
      };
      tabStore.addTabs(tabsParams);
    }
  });
};

添加标签ICON

添加我们的动态icon,这个时候进行使用

<el-icon v-if="item.icon && tabsIcon" class="tabs-icon">
  <component :is="item.icon"></component>
</el-icon>

// 使用 -- 
meta: { title: '用户管理', icon: 'User',show: true,hidden: true ,isAffix: true},

4、下拉组件编写

接下来我们往右侧添加一个下拉组件,进行我们日常标签组件的使用,这时候我们就来编写一下

src\layout\TabsMoreButton\index.vue

布局编写

<template>
  <el-dropdown trigger="click" :teleported="false">
    <div class="more-button icon-xiala flex items-center justify-center font12">
      <el-icon><ArrowDownBold /></el-icon>
    </div>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item @click="refresh">
          <el-icon><Refresh /></el-icon>refresh
        </el-dropdown-item>
        <el-dropdown-item @click="maximize">
          <el-icon><FullScreen /></el-icon>maximize
        </el-dropdown-item>
        <el-dropdown-item divided @click="closeCurrentTab">
          <el-icon><Remove /></el-icon>closeCurrent
        </el-dropdown-item>
        <el-dropdown-item>
          <el-icon><DArrowLeft /></el-icon>closeLeft
        </el-dropdown-item>
        <el-dropdown-item>
          <el-icon><DArrowRight /></el-icon>closeRight
        </el-dropdown-item>
        <el-dropdown-item divided>
          <el-icon><CircleClose /></el-icon>closeOther
        </el-dropdown-item>
        <el-dropdown-item @click="closeAllTab">
          <el-icon><FolderDelete /></el-icon>closeAll
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup lang="ts">
const refresh = () => {
};

// maximize current page
const maximize = () => {
};

// Close Current
const closeCurrentTab = () => {
};

// Close All
const closeAllTab = () => {
};
</script>

<style>
.tabs-menu .el-dropdown .more-button {
  width: 43px;
  cursor: pointer;
  border-left: 1px solid var(--el-border-color-light);
  transition: all 0.3s;
}
.tabs-menu .el-dropdown .more-button:hover {
  background-color: var(--el-color-info-light-9);
}
</style>

功能实现

完善一下其中的功能,其实就是控制页面的刷新,这里我们简单编写一下

刷新页面

// 子组件
import { inject } from 'vue'
// 注入refresh
const refreshvalue = inject('refresh');
const refresh = () => {
  refreshvalue.value = true;
  setTimeout(() => {refreshvalue.value = false;}, 100);
};

在我们需要刷新的地方提供函数方法,这个时候在刷新页面中就可以使用我们的方法,后面可以继续完善,原理其实都一样

import { ref, onMounted, onUnmounted,computed,getCurrentInstance,provide} from 'vue'

const refreshvalue = ref(false);
provide("refresh", refreshvalue);

关闭当前标签页

import { useTabsStore } from "@/store/modules/tabs";
// 标签页仓库
const tabStore = useTabsStore();

// 关闭当前页
const closeCurrentTab = () => {
  if (route.meta.isAffix) return;
  tabStore.removeTabs(route.fullPath);
};

完善仓库方法

// Remove Tabs
async removeTabs(tabPath: string, isCurrent: boolean = true) {
    // 1. 移除标签页
  if (isCurrent) {
    this.tabsMenuList.forEach((item, index) => {
      if (item.path !== tabPath) return;
      const nextTab = this.tabsMenuList[index + 1] || this.tabsMenuList[index - 1];
      if (!nextTab) return;
      router.push(nextTab.path);
    });
  }
 
  // 2. 管理页面缓存
  const tabItem = this.tabsMenuList.find(item => item.path === tabPath);
  tabItem?.isKeepAlive && keepAliveStore.removeKeepAliveName(tabItem.path);
  
  // 过滤掉要删除的标签页
  this.tabsMenuList = this.tabsMenuList.filter(item => item.path !== tabPath);
},

关闭左右侧标签页

关闭左侧和右侧其实都差不多,原理也很简单,拿历史然后跳过去

<el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'left')">
  <el-icon><DArrowLeft /></el-icon>closeLeft
</el-dropdown-item>

关闭右侧标签页

@click="tabStore.closeTabsOnSide(route.fullPath, 'right')"

方法完善

async closeTabsOnSide(path: string, type: "left" | "right") {
  console.log(path, type,'store-path, type');
  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;
    });
  }
  // set keepalive
  const KeepAliveList = this.tabsMenuList.filter(item => item.isKeepAlive);
  keepAliveStore.setKeepAliveName(KeepAliveList.map(item => item.path));
},

关闭其他标签页

@click="tabStore.closeMultipleTab(route.fullPath)"

 async closeMultipleTab(tabsMenuValue?: string) {
  this.tabsMenuList = this.tabsMenuList.filter(item => {
    return item.path === tabsMenuValue || !item.close;
  });
  // set keepalive
  const KeepAliveList = this.tabsMenuList.filter(item => item.isKeepAlive);
  keepAliveStore.setKeepAliveName(KeepAliveList.map(item => item.path));
},

关闭所有

@click="closeAllTab"

// 关闭所有标签页
const closeAllTab = () => {
  tabStore.closeMultipleTab();
  // 首页
  router.push(SettingTab.tabsMenuValue);
};

方法完善,ok,到这里我们的所有功能就完善好了

// 关闭其他标签页
async closeMultipleTab(tabsMenuValue?: string) {
  this.tabsMenuList = this.tabsMenuList.filter(item => {
    return item.path === tabsMenuValue || !item.close;
  });
  // set keepalive
  const KeepAliveList = this.tabsMenuList.filter(item => item.isKeepAlive);
  keepAliveStore.setKeepAliveName(KeepAliveList.map(item => item.path));
},
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。