Vue3+ts+pinia实现活跃的tab栏

news/2025/10/23 9:38:46/文章来源:https://www.cnblogs.com/hdc-web/p/19159632

pinia 部分

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'export interface TabItem {id: stringtitle: stringpath: stringicon?: stringclosable?: boolean
}export const useTabsStore = defineStore('tabs', () => {// 标签页列表const tabs = ref<TabItem[]>([])// 当前活跃标签页const activeTabId = ref<string>('')// 获取当前活跃标签页const activeTab = computed(() => {return tabs.value.find((tab) => tab.id === activeTabId.value)})// 添加标签页const addTab = (tab: TabItem) => {// 检查是否已存在相同路径的标签页const existingTab = tabs.value.find((t) => t.path === tab.path)if (existingTab) {// 如果已存在,则激活该标签页activeTabId.value = existingTab.idreturn}// 添加新标签页tabs.value.push(tab)activeTabId.value = tab.id}// 关闭标签页const closeTab = (tabId: string) => {const index = tabs.value.findIndex((tab) => tab.id === tabId)if (index === -1) return// 如果只有一个标签页,不允许关闭if (tabs.value.length <= 1) {ElMessage.error('至少保留一个标签页')return}// 如果关闭的是当前活跃标签页,需要切换到其他标签页if (tabId === activeTabId.value) {// 优先切换到右侧标签页,如果没有则切换到左侧if (index < tabs.value.length - 1) {activeTabId.value = tabs.value[index + 1]?.id || ''} else if (index > 0) {activeTabId.value = tabs.value[index - 1]?.id || ''} else {activeTabId.value = ''}}// 移除标签页tabs.value.splice(index, 1)}// 设置活跃标签页const setActiveTab = (tabId: string) => {const tab = tabs.value.find((t) => t.id === tabId)if (tab) {activeTabId.value = tabId}}// 关闭其他标签页const closeOtherTabs = (keepTabId: string) => {tabs.value = tabs.value.filter((tab) => tab.id === keepTabId)activeTabId.value = keepTabId}// 关闭所有标签页const closeAllTabs = () => {tabs.value = []activeTabId.value = ''}// 关闭左侧标签页const closeLeftTabs = (tabId: string) => {const index = tabs.value.findIndex((tab) => tab.id === tabId)if (index > 0) {tabs.value.splice(0, index)}}// 关闭右侧标签页const closeRightTabs = (tabId: string) => {const index = tabs.value.findIndex((tab) => tab.id === tabId)if (index < tabs.value.length - 1) {tabs.value.splice(index + 1)}}return {tabs,activeTabId,activeTab,addTab,closeTab,setActiveTab,closeOtherTabs,closeAllTabs,closeLeftTabs,closeRightTabs,}
})

tab 标签页

<template><div class="tabs-container"><!-- 标签页列表 --><div class="tabs-list" ref="tabsListRef"><divv-for="tab in tabsStore.tabs":key="tab.id":class="['tab-item', { active: tab.id === tabsStore.activeTabId }]"@click="handleTabClick(tab)"@contextmenu.prevent="handleContextMenu($event, tab)"><component v-if="tab.icon" :is="tab.icon" class="tab-icon" /><span class="tab-title">{{ tab.title }}</span><buttonv-if="tab.closable !== false"class="tab-close"@click.stop="handleCloseTab(tab.id)">×</button></div></div><!-- 右键菜单 --><divv-if="contextMenu.visible"class="context-menu":style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"@click.stop><div class="menu-item" @click="handleCloseTab(contextMenu.tabId)">关闭标签页</div><div class="menu-item" @click="handleCloseOtherTabs(contextMenu.tabId)">关闭其他标签页</div><div class="menu-item" @click="handleCloseLeftTabs(contextMenu.tabId)">关闭左侧标签页</div><div class="menu-item" @click="handleCloseRightTabs(contextMenu.tabId)">关闭右侧标签页</div><div class="menu-item" @click="handleCloseAllTabs">关闭所有标签页</div></div></div>
</template><script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useTabsStore, type TabItem } from '@/stores/tabs'const router = useRouter()
const tabsStore = useTabsStore()const tabsListRef = ref<HTMLElement>()// 右键菜单状态
const contextMenu = ref({visible: false,x: 0,y: 0,tabId: '',
})// 处理标签页点击
const handleTabClick = (tab: TabItem) => {tabsStore.setActiveTab(tab.id)router.push(tab.path)
}// 处理关闭标签页
const handleCloseTab = (tabId: string) => {const tab = tabsStore.tabs.find((t) => t.id === tabId)if (!tab) returntabsStore.closeTab(tabId)// 如果关闭后还有标签页,导航到当前活跃标签页if (tabsStore.activeTabId && tabsStore.activeTab) {router.push(tabsStore.activeTab.path)} else {// 如果没有标签页了,导航到首页router.push('/')}
}// 处理右键菜单
const handleContextMenu = (event: MouseEvent, tab: TabItem) => {contextMenu.value = {visible: true,x: event.clientX,y: event.clientY,tabId: tab.id,}
}// 关闭其他标签页
const handleCloseOtherTabs = (tabId: string) => {tabsStore.closeOtherTabs(tabId)const tab = tabsStore.tabs.find((t) => t.id === tabId)if (tab) {router.push(tab.path)}hideContextMenu()
}// 关闭左侧标签页
const handleCloseLeftTabs = (tabId: string) => {tabsStore.closeLeftTabs(tabId)hideContextMenu()
}// 关闭右侧标签页
const handleCloseRightTabs = (tabId: string) => {tabsStore.closeRightTabs(tabId)hideContextMenu()
}// 关闭所有标签页
const handleCloseAllTabs = () => {tabsStore.closeAllTabs()router.push('/')hideContextMenu()
}// 隐藏右键菜单
const hideContextMenu = () => {contextMenu.value.visible = false
}// 点击其他地方隐藏右键菜单
const handleClickOutside = () => {hideContextMenu()
}onMounted(() => {document.addEventListener('click', handleClickOutside)
})onUnmounted(() => {document.removeEventListener('click', handleClickOutside)
})
</script><style scoped>
.tabs-container {position: relative;background: #fff;border-bottom: 1px solid #e5e7eb;
}.tabs-list {display: flex;overflow-x: auto;scrollbar-width: none;-ms-overflow-style: none;
}.tabs-list::-webkit-scrollbar {display: none;
}.tab-item {display: flex;align-items: center;padding: 8px 16px;min-width: 120px;max-width: 200px;border-right: 1px solid #e5e7eb;border-bottom: 2px solid transparent;cursor: pointer;transition: all 0.2s;position: relative;white-space: nowrap;background: #f9fafb;
}.tab-item:hover {background: #f3f4f6;
}.tab-item.active {background: #fff;border-bottom: 2px solid #3b82f6;color: #3b82f6;
}.tab-icon {width: 16px;height: 16px;margin-right: 8px;flex-shrink: 0;
}.tab-title {flex: 1;overflow: hidden;text-overflow: ellipsis;font-size: 14px;
}.tab-close {width: 18px;height: 18px;border: none;background: none;cursor: pointer;border-radius: 50%;display: flex;align-items: center;justify-content: center;margin-left: 8px;font-size: 16px;color: #6b7280;transition: all 0.2s;flex-shrink: 0;
}.tab-close:hover {background: #e5e7eb;color: #374151;
}.context-menu {position: fixed;background: #fff;border: 1px solid #e5e7eb;border-radius: 6px;box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);z-index: 1000;min-width: 150px;
}.menu-item {padding: 8px 12px;cursor: pointer;font-size: 14px;color: #374151;transition: background-color 0.2s;
}.menu-item:hover {background: #f3f4f6;
}.menu-item:first-child {border-radius: 6px 6px 0 0;
}.menu-item:last-child {border-radius: 0 0 6px 6px;
}
</style>

router

import { createRouter, createWebHistory } from 'vue-router'
import { useTabsStore } from '@/stores/tabs'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',redirect: '/design-workspace',},{path: '/design-workspace',component: () => import('@/view/DesignWorkspace/DesignWorkspace.vue'),meta: {title: '设计工作区',icon: 'BrushFilled',keepAlive: true,},},{path: '/user-test',component: () => import('@/view/UserTest/UserTest.vue'),meta: {title: '用户测试',icon: 'Platform',keepAlive: true,},},{path: '/design-specification',component: () => import('@/view/DesignSpecification/DesignSpecification.vue'),meta: {title: '设计规范',icon: 'List',keepAlive: true,},},{path: '/audit-and-cooperation',component: () => import('@/view/AuditAndCooperation/AuditAndCooperation.vue'),meta: {title: '审核对接',icon: 'CircleCheckFilled',keepAlive: true,},},],
})// 路由守卫 - 自动管理标签页
router.beforeEach((to, from, next) => {// 跳过重定向路由if (to.path === '/') {next()return}// 如果路由有 meta 信息,自动添加标签页if (to.meta && to.meta.title) {const tabsStore = useTabsStore()const tabId = `tab-${to.path.replace(/\//g, '-')}`tabsStore.addTab({id: tabId,title: to.meta.title as string,path: to.path,icon: to.meta.icon as string,closable: true,})}next()
})export default router

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/943958.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年10月AI搜索优化推荐:主流榜单对比与避坑指南

引言与现状分析 当品牌主在2025年重新分配数字预算时,“AI搜索优化”已不再是可选项。DeepSeek、豆包、通义千问、元宝、Kimi五大生成式引擎每天新增问答量超8.3亿次(QuestMobile 2025Q3),其中35%的查询隐含商业意…

2025 年国内喷雾干燥机最新推荐排行榜:聚焦优质品牌,助力企业精准选设备造粒/工业喷雾/陶瓷喷雾/制粒/奶粉喷雾干燥机厂家推荐

引言 当前喷雾干燥技术广泛应用于新能源材料、精密陶瓷、化工、医药等多领域,成为工业生产中物料干燥处理的关键环节。但市场上品牌众多,部分品牌核心技术依赖进口导致成本高、维修难,部分设备在稳定性、能耗、智能…

Python环境教程(一)-环境入门之pip conda

环境入门之pip conda Pip # 查看版本 pip --version # 安装包 pip install SomePackage # 安装最新版本 pip install SomePackage==1.0.4 # 安装指定版本 pip install SomePackage>=1.0.4 # 安装最低版本 # 升级…

Datawhale 春训营新能源预测(数据处理)

[!NOTE] 数据背景介绍 数据来自 比赛举办方: 主要数据是 三个天气数据源nwp1 nwp2 nwp3,以及历史发电功率数据新能源预测(数据处理) 1. NWP 数据 1.1 nwp数据 nwp 数据 -- NWP代表数值天气预报(Numerical Weather …

权威调研榜单:实验用超细粉碎机实力厂家TOP7榜单好评深度解析

在科研实验与工业研发领域,实验用超细粉碎机作为材料前处理的核心设备,其性能优劣直接关系到研究成果的准确性与可靠性。本文基于专业市场调研数据,从企业规模、技术专利、品质管控、行业应用案例等多维度进行深度解…

AI股票预测分析报告 - 2025年10月23日

AI股票预测分析报告 - 2025年10月23日body { font-family: "Microsoft YaHei", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: rgba(51, 51, 51, 1); max-width: 1…

智能化时代下,企业DevOps平台的选型突围:谁在真正驱动业务价值?

数字化转型中,DevOps平台从工具自动化转向价值赋能。本文对比主流DevOps产品,国产DevOps平台具备价值流可视化、AI赋能及安全合规能力,适配云原生趋势,契合信创DevOps需求,助力企业提升研发效能。在数字化转型的深…

2025年10月deepseek排名优化推荐:主流机构对比排行榜

引言与现状分析 当用户在搜索框输入“deepseek排名优化”时,往往面临三重焦虑:一是生成式引擎迭代快,上周有效的方法本周可能失效;二是服务商宣传口径趋同,难以判断真实技术深度;三是预算有限,却担心低价方案留…

异常值检测算法学习

1. 基于分布的异常检测 1.1 3σ准则 (3-Sigma Rule) 原理:基于正态分布假设,认为距离均值3个标准差之外的数据点为异常值 数学表达式: python def three_sigma_detection(data):mean = np.mean(data)std = np.std(d…

取方案

取方案对于取方案: 跑两遍,第一遍取值,第二遍取方案

SQL Server 2008 R2 升级补丁需要注意的问题

安装了sqlserver2008r2-kb3045314-x64.exe后无法再安装sqlserver2008r2-kb3045316-x64,并且sqlserver2008r2-kb3045314-x64.exe安装后的版本高于sqlserver2008r2-kb3045316-x64, 我猜测是微软将两个补丁的名称顺序弄…

Maven的使用(Leo)

Maven Maven构建生命周期的核心阶段clean:清理项目编译、打包生成的输出文件(如 target 目录 ) validate:校验项目必要信息、依赖是否完整 compile:编译项目主代码(一般是 src/main/java 里的 Java 文件 ) test…

数字化实战:医疗器械行业售后工程师如何借CRM实现高效运维​

北京某三甲医院手术室走廊,晨光透过玻璃窗洒在消毒设备上。赵工抓起工牌走向电梯,口袋里的手机震动了一下,这是他今天收到的第一条设备预警通知。1、7:30 AM | 出发前的设备体检 作为一家国内头部医疗器械企业的售后…

2025年10月geo优化服务商推荐:知名机构评测列表

引言与现状分析 当品牌方在2025年第四季度规划来年预算时,“如何在生成式引擎里被看见”成为CMO例会的高频议题。DeepSeek、豆包、通义千问、元宝、Kimi的日活总和已突破4.3亿,传统SEO流量出现两位数的环比下滑,而G…

pg数据库表的大小

SELECT table_schema || . || table_name AS table_full_name, pg_size_pretty(pg_total_relation_size(" || table_schema || "." || table_name || ")) AS sizeFROM information_sch…

20251020_QQ_Cipher

倒序Rev,Base64,异或XOR,字符串Tags:倒序Rev,Base64,异或XOR,字符串 0x00. 题目 题目表述 A3O9Uzb1gzbox2O5kDNoVDOo1Db6kWao5mb8oDP8Qza4YnasF2a 如果能够倒带到最初的起点 如果能够补全不圆满 如果能有128种选择 总有…

高压差分探头PKDV508E使用常见问题与解决方案

高压差分探头在电力电子、开关电源、变频器等众多领域是必不可少的测量工具,尤其在浮地测量和高共模噪声抑制等场景下表现出色。PKDV508E作为一款具有100MHz带宽、800Vpk高压测量能力的差分探头,被广泛应用于研发、调…

好拼|免费在线拼图工具上架谷歌商店啦 - ops

在刚刚过去的两个月,我沿着中国东南沿海自驾了一大圈,那段时间几乎所有的精力都投入到了山川大海与人文美食之中,没怎么更新我的免费在线拼图工具,后台有不少朋友催更。这不,一回到家我就马不停蹄地开始了新一轮更…

基于MATLAB/Simulink的光照强度模型构建方法

一、基础光照模型实现 1. 恒定光照模型适用场景:简化分析或基准测试实现步骤: % 添加常量模块 add_block(simulink/Sources/Constant, Solar_Irradiance); set_param(Solar_Irradiance, Value, 1000); % 设置为标准光…

地中海、双肩包、格子衫?从业9年程序员聊聊真实的程序员是什么样子

你印象中的程序员,是不是这样的?不可否认,这确实是程序员的一种状态,并且现在依然存在。但其实这并不能代表大多数程序员,作为一名工作了 9 年的程序员,有必要跟大家聊聊真实的程序员是什么样子。 其实每个行业都…