当前位置: 首页 > news >正文

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

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.hskmm.com/?act=detail&tid=37118

相关文章:

  • 2025年10月AI搜索优化推荐:主流榜单对比与避坑指南
  • 排序算法学习笔记
  • 异常值检测算法学习
  • 2025 年国内喷雾干燥机最新推荐排行榜:聚焦优质品牌,助力企业精准选设备造粒/工业喷雾/陶瓷喷雾/制粒/奶粉喷雾干燥机厂家推荐
  • Python环境教程(一)-环境入门之pip conda
  • AI股票预测分析报告 - 2025年10月23日
  • SQL Server 2008 R2 升级补丁需要注意的问题
  • Maven的使用(Leo)
  • 标题
  • 数字化实战:医疗器械行业售后工程师如何借CRM实现高效运维​
  • 20251020_QQ_Cipher
  • 2025年10月geo优化服务商推荐:知名机构评测列表
  • 高压差分探头PKDV508E使用常见问题与解决方案
  • 好拼|免费在线拼图工具上架谷歌商店啦 - ops
  • 基于MATLAB/Simulink的光照强度模型构建方法
  • 地中海、双肩包、格子衫?从业9年程序员聊聊真实的程序员是什么样子
  • 2025年10月又红又痒用什么产品推荐:口碑排行五款精华评价
  • 卫星遥感技术在河湖监管中的应用
  • VonaJS AOP编程:魔术方法
  • windows11关闭自动更新,通用解决方法
  • 2025年10月海南监理公司评测榜:五家实力排名全览
  • 2025年10月geo服务商推荐:主流品牌全维度对比排行榜
  • 2025年10月geo服务商推荐:权威评测列表助您精准避坑
  • 推动教育质量,布谷鸟网络科技定制K12在线教育在线教育网校软件服务
  • 使用vscode进行linux 服务器远程管理
  • 深入解析:Unity避坑——继承了MonoBehaviour的对象不能通过new来创建
  • 网页
  • 2025年10月geo优化公司推荐:主流口碑排行榜全解析
  • 2025年10月geo优化公司推荐:知名机构评测列表
  • 2025年沈阳酒店联系电话推荐:地铁直达景点合集