npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

xn-vue3-router-tabs

v0.4.0

Published

Multi-tab management plugin for Vue 3 + Vue Router 4

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.tabKeyoptions.getTabKeyroute.fullPath

title 优先级: meta.titleoptions.getTitleroute.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-routerRouteMeta 类型,路由配置中 meta.tabmeta.tabKeymeta.title 均有完整类型提示,无需手动声明。