xn-vue3-router-tabs
v0.4.0
Published
Multi-tab management plugin for Vue 3 + Vue Router 4
Maintainers
Readme
xn-vue3-router-tabs
为 Vue 3 + Vue Router 4 提供多页签能力的插件。
- 零额外依赖(仅 peerDep: vue + vue-router)
- 配置全部收口在路由
meta中,不侵入业务代码 - 内置 KeepAlive 缓存,切换 tab 不重新加载页面
- 提供无样式
<TabBar>组件,UI 完全由你控制
安装
npm install xn-vue3-router-tabs
# 或
pnpm add xn-vue3-router-tabs快速开始
1. 安装插件(main.ts)
必须在 app.use(router) 之后安装。
import { createApp } from 'vue'
import { createTabPlugin } from 'xn-vue3-router-tabs'
import App from './App.vue'
import { router } from './router'
const app = createApp(App)
app.use(router)
app.use(createTabPlugin({
homePath: '/dashboard', // 关闭最后一个 tab 时的兜底页
maxTabs: 10, // 最大 tab 数(可选,默认不限)
closeStrategy: 'last-visited', // 关闭当前 tab 后的跳转策略(默认)
}))
app.mount('#app')2. 配置路由 meta
所有路由默认纳入多页签管理,只有以下情况需要配置 meta:
const routes = [
// 不纳入管理(登录页、404 页等)
{ path: '/login', meta: { tab: false } },
// 自定义标题(字符串)
{ path: '/list', meta: { title: '采购列表' } },
// 动态标题(函数)—— path 传参,不同 id 自动产生独立 tab
{
path: '/detail/:id',
meta: { title: (route) => `单据 ${route.params.id}` },
},
// query 传参,不同 id 产生独立 tab(依赖 fullPath 作为默认 key)
{
path: '/order',
meta: { title: (route) => `订单 ${route.query.id}` },
},
// 两条路由共用同一个 tab(互相替换,而非新建)
{
path: '/purchase/new',
meta: { tabKey: '/purchase/list', title: '采购列表' },
},
{
path: '/purchase/list',
meta: { title: '采购列表' },
},
]3. 布局组件(Layout.vue)
用 <RouterViewTab> 替换 <RouterView>,用 <TabBar> 渲染标签栏:
<template>
<!-- 标签栏:通过 slot 完全自定义每个 tab 的渲染 -->
<div class="tab-bar">
<TabBar v-slot="{ tab, isActive, close, refresh }">
<div
class="tab"
:class="{ active: isActive }"
@click="router.push(tab.route.fullPath)"
>
<span>{{ tab.title }}</span>
<button @click.stop="refresh()">刷新</button>
<button @click.stop="close()">关闭</button>
</div>
</TabBar>
</div>
<!-- 页面内容:内置 KeepAlive,切换 tab 不重新加载 -->
<RouterViewTab />
</template>
<script setup lang="ts">
import { RouterViewTab, TabBar } from 'xn-vue3-router-tabs'
import { useRouter } from 'vue-router'
const router = useRouter()
</script>API
createTabPlugin(options?)
创建插件实例。
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| homePath | string | '/' | 关闭最后一个 tab 或 closeAll() 后的跳转页 |
| maxTabs | number | 不限 | 最大 tab 数,超出时自动移除最久未访问的 |
| closeStrategy | CloseStrategy | 'last-visited' | 关闭当前 tab 后的跳转策略 |
| getTabKey | (route) => string | — | 全局 tabKey 计算函数(meta.tabKey 优先) |
| getTitle | (route) => string | — | 全局 title 计算函数(meta.title 优先) |
| beforeClose | (tab: Tab) => boolean \| Promise<boolean> | — | 全局关闭守卫,返回 false 阻止关闭 |
CloseStrategy 可选值:
| 值 | 行为 |
|----|------|
| 'last-visited' | 跳转到最近访问的其他 tab |
| 'previous' | 跳转到列表中左侧相邻的 tab |
| 'next' | 跳转到列表中右侧相邻的 tab |
| 'home' | 跳转到 homePath |
useTabs()
在组件中调用,返回标签状态和操作方法。
const {
tabs, // Readonly<Tab[]> — 响应式标签列表
activeKey, // Readonly<Ref<string>> — 当前激活 tab 的 key
activeTab, // ComputedRef<Tab | undefined> — 当前激活的 tab 对象
close, // (key: string) => Promise<void>
refresh, // (key?: string) => void
closeOthers, // (key?: string) => Promise<void>
closeAll, // () => Promise<void>
onBeforeClose, // (guard: BeforeCloseGuard) => void
} = useTabs()Tab 对象结构:
interface Tab {
key: string // 唯一键
title: string // 显示标题
route: RouteLocationNormalizedLoaded // 对应路由快照
refreshCount: number // 内部刷新计数
createdAt: number // 创建时间戳
visitedAt: number // 最后访问时间戳
}<RouterViewTab />
替代 <RouterView> 使用。内部封装了 <KeepAlive>,保证:
- 切换 tab 时页面状态保留(不重新加载)
- 不同参数的同路由各自拥有独立缓存实例
- 调用
refresh()时仅重建当前 tab 的组件,不影响其他 tab
<!-- 只需替换 RouterView,无需其他配置 -->
<RouterViewTab /><TabBar> 无样式组件
通过 scoped slot 渲染每个 tab,插件不提供任何样式:
<TabBar v-slot="{ tab, isActive, close, refresh }">
<!-- tab: Tab 对象 -->
<!-- isActive: boolean 是否为当前激活 tab -->
<!-- close: () => void 关闭该 tab -->
<!-- refresh: () => void 刷新该 tab -->
<div :class="{ active: isActive }">
{{ tab.title }}
</div>
</TabBar>路由 meta 配置
| 字段 | 类型 | 说明 |
|------|------|------|
| tab | boolean | false 时排除该路由,默认 true(纳入管理) |
| tabKey | string \| (route) => string | 自定义 tab 唯一键 |
| title | string \| (route) => string | 自定义 tab 标题 |
tabKey 优先级: meta.tabKey → options.getTabKey → route.fullPath
title 优先级: meta.title → options.getTitle → route.path
使用场景示例
path 传参 — 不同 id 独立 tab(默认行为)
// 路由配置
{ path: '/detail/:id', meta: { title: (r) => `单据 ${r.params.id}` } }
// 访问以下路由会产生 2 个独立 tab
router.push('/detail/1001')
router.push('/detail/1002')query 传参 — 不同 id 独立 tab(默认行为)
// 路由配置(无需额外配置,fullPath 天然不同)
{ path: '/order', meta: { title: (r) => `订单 ${r.query.id}` } }
// 访问以下路由会产生 2 个独立 tab
router.push('/order?id=A001')
router.push('/order?id=A002')强制单实例 — 任意参数共用同一个 tab
// 所有详情页共用一个 tab,后打开的替换前一个
{ path: '/detail/:id', meta: { tabKey: '/detail', title: (r) => `单据 ${r.params.id}` } }两条路由共用同一个 tab
// /purchase/new 和 /purchase/list 共用同一个 tab
{ path: '/purchase/new', meta: { tabKey: '/purchase/list', title: '新建采购单' } }
{ path: '/purchase/list', meta: { title: '采购列表' } }编程式操作
const { close, refresh, closeOthers, closeAll, activeKey } = useTabs()
// 关闭当前 tab
close(activeKey.value)
// 刷新当前 tab
refresh()
// 关闭其他所有 tab,保留当前
closeOthers()
// 关闭全部并跳转 homePath
closeAll()关闭守卫(beforeClose)
在页面有未保存修改时拦截 tab 关闭,支持组件级和全局级两种方式。
组件级守卫 — onBeforeClose
在页面组件内注册,组件销毁时自动解除:
<script setup>
import { ref } from 'vue'
import { useTabs } from 'xn-vue3-router-tabs'
const { onBeforeClose } = useTabs()
const hasUnsavedChanges = ref(false)
onBeforeClose(() => {
if (hasUnsavedChanges.value) {
return window.confirm('有未保存的修改,确定关闭?')
}
return true
})
</script>全局守卫 — 插件选项 beforeClose
在插件安装时配置,应用于所有 tab:
app.use(createTabPlugin({
beforeClose: async (tab) => {
// 可结合后端接口判断
return true
},
}))执行规则
- 返回值:
true(允许关闭)、false(阻止关闭),支持Promise - 执行顺序:组件级守卫先执行 → 通过后 → 全局守卫再执行。任一返回
false即阻止 - 批量关闭(
closeOthers/closeAll):逐个检查每个待关闭 tab,拒绝的保留,通过的关闭
TypeScript 支持
插件自动扩展 vue-router 的 RouteMeta 类型,路由配置中 meta.tab、meta.tabKey、meta.title 均有完整类型提示,无需手动声明。
