/vue3-tab-demo

🗂️ 一个 Vue3 的多 Tabs 标签页切换选项卡的演示模板, 🔨支持路由表自动生成多页签、页面缓存(KeepAlive)和标签页命名空间缓存管理. 基于 Vite 5 + Pinia 2 + TypeScript + Naive UI + Vue Router 4 + UnoCSS + Unplugin + ESLint(v9) + Vitest, 开箱即用的解决方案, 快速开发中后台前端,可用于学习和参考

Primary LanguageVueMIT LicenseMIT

vue3-tab-demo

GitHub Workflow Status (branch) thanks

License NaiveUI Version Vue Version Vite Version

简介

🗂️ 一个基于 Vite5 + Vue3 + Naive UI + Pinia + TS + ESLint(v9) + Unplugin + Husky 的 Tab 切换选项卡演示项目,其内部抽象出了一个比较贴近实战项目管理系统的业务场景,虽不涉及特别复杂的业务逻辑但也不失灵活,旨在更好地理解和展示如何使用 Tab 标签页组件,项目基于原子化 UnoCSS 框架配置主题,还自带一个模块化的组件开发环境,使页面组件、路由组件、状态管理和样式等模块可以根据 Modules 目录进行解耦,它是一个开箱即用的解决方案,也适合作为快速开发中后台前端,可用于学习和参考

🌈 Live Demo 在线体验

🌱 技术栈

  • Naive UI 2.x
  • Vue 3.5.x
  • Vite 5.x + Vitest
  • Pinia 2.x
  • TypeScript 5.x
  • ESLint 9.x + Stylistic
  • Husky + lint-staged
  • Lodash
  • VueUse
  • Unplugin + UnoCSS
  • Iconify + Unplugin-Icons

🎉 Tab 核心特性

  • 支持按照一级 ID 进行划分的缓存空间集合,本示例的一级ID为 projectId,可加以自行改造和将其耦合到组件内
  • 支持命名空间的 Tab 集合
  • 支持 Caches 缓存, 用于 Keep Alive
  • 支持缓存空间的切换及内部 Tab 的添加/关闭
  • 支持一键关闭其他标签页
  • 支持多层级动态路由参数页面的缓存(如 /xxxx/:id1, /xxxx/:id1/xxx/:id2...)
  • 支持关闭 Tab 之前的 Hook 钩子, 用于手动处理阻止关闭 Tab 的逻辑
  • 支持同时打开多个 Tab
  • 支持 Tab 打开时自定义命名
  • 支持 Pinia 统一数据管理和本地 Session 存储 (可自由改造为 LocalStorage)

前置条件

  • Vue 3.5.x
  • Node >= 18.12.x
  • Pnpm 9.x
  • VS Code 插件 dbaeumer.vscode-eslint >= v3.0.5 (pre-release)

安装和运行

  • 安装依赖
pnpm i
  • 本地开发
pnpm dev

项目示例图

image image image image image

🧪 使用示例

  • 打开单个 Tab
const router = useTabRouter()
router.push({
  name: 'Xxxxxx',
  params: {
    datasetId: row.xxxId
  }
}, `自定义名称-${row.xxxId}`)
  • 同时打开多个 Tab
const router = useTabRouter()
router.pushMultiple(
  // 路由一级动态ID
  'xxxxprojectId',
  [
    {
      to: {
        name: 'Xxxx1',
        params: {
          datasetId: row.id
        },
        query: {
          query1: '123456'
        }
      },
      tabName: '自定义名称1'
    },
    {
      to: {
        name: 'Xxxx2',
      },
      tabName: '自定义名称2'
    },
  ]
)

或直接使用全路径:

// 路由一级动态ID
const prefixKey = route.params.projectId // 'xxxxprojectId'
router.pushMultiple(
  prefixKey,
  [
    `/group-project/${prefixKey}/dashboard/monitor`,
    `/group-project/${prefixKey}/work-platform`,
    `/group-project/${prefixKey}/work-platform/nested-level/level-1-2`,
  ]
)

🛠️ 缓存空间设计

由于每个 WorkTab 代表一个路由页面,所以在设计初期就已经将以下核心概念引入到路由中,以方便更好的理解组件的设计理念:

路由 Layout 布局配置

Tab 组件本身已经解耦了 TabContent 区域和 TabsController 区域,所以只需要将两者简单结合封装即可完成路由布局的配置

本示例项目主要到涉及两个 Tab 路由布局, 感兴趣的可以直接看源码: 路由布局组件1, 路由布局组件2

为确保 Tab 和路由不会有较强的耦合关系,缓存空间 Key 则以可插拔的形式绑定到路由元信息 meta 中,形如以下路由配置代码:

// src/router/frontend/test-routes.ts
export const testRoutesExample = {
  path: 'example-component',
  name: 'ExampleComponentRoot',
  component: LayoutWork, // 配置 Tab 路由布局
  redirect: {
    name: 'ExampleComponentBasic'
  },
  children: [
    {
      path: 'basic',
      name: 'ExampleComponentBasic',
      meta: {
        title: '组件示例-基础组件',
        cacheSpaceKey: CacheSpaceKeys.exampleComponent // 配置缓存空间 Key
      },
      component: () => import('@/modules/ExampleComponent/pages/basic.vue')
    },
    {
      path: 'form',
      name: 'ExampleComponentForm',
      meta: {
        title: '组件示例-表单',
        cacheSpaceKey: CacheSpaceKeys.exampleComponent // 配置缓存空间 Key
      },
      component: () => import('@/modules/ExampleComponent/pages/form.vue')
    },
    // ...
  ]
}

这样也就能确保具有相同缓存空间 Key 的路由(WorkTab)能够被归集到同一个缓存空间 CacheSpace


缓存空间与 Tab 的缓存

为尽可能地保证使用者无感知的使用体验并避免重复编码,这里通过监听 Vue Router 中 route.fullPath 的变化来实现自动触发 Tab 缓存的添加、切换等逻辑。具体的实现细节可以看源码

为了满足同时打开一个或多个自定义命名 Tab 的需求,并克服 Vue Router 自身的限制,项目中封装了 useTabRouter Hook 方法,用于替代原生的 useRouter 中的 pushreplace 方法(也可以根据需要进行扩展) 在 useTabRouter 内部,采用了单例模式来处理每个Tab的自定义命名。感兴趣的可以深究对应源码

其核心就是利用 Vue Router 的 API 解决缓存空间的创建时机、缓存空间于 Tab 的关联以及 Tab 的自定义命名等问题。

以下是 Tab 缓存空间的核心结构设计:

  • 单个缓存空间
export interface CacheSpace {
  cacheSpaceKey: string
  tabs: Array<WorkTab>
  activeTabKey: string | null
}
  • 缓存空间集合
Map<cacheSpaceKey, CacheSpace>
  • 单个 Tab 页签数据
export interface WorkTab {
  // 取自 meta.title 的 tab 标题名称
  label: string
  // 自定义 tab 标题名称
  customLabel: string
  tabKey: string
  link: string
  routeName: RouteRecordName
}


💡 注意事项

  • Vue 组件名称需要与对应路由名称保持一致,否则 Keep Alive 将会失效
  • 由于 Tab 组件自身解耦了所有的业务逻辑,所以涉及到路由一级动态 ID 这种跟业务路由强耦合的地方都需要再自行传入,嫌麻烦的可以直接修改源码中的所有 dynamicCacheSpacePrefixKey 字段或将相关 hook 二次封装一下。具体参考这两个位置:源码1源码2
  • 若 Husky 未生效,可能是由于未完成初始化,执行 pnpm run prepare 进行初始化再尝试
  • 推荐使用本项目进行二次开发和定制实际的业务项目

说明

  • 如果此开源对您有帮助,您可以点右上角 "Star" 支持一下 谢谢! ^_^ 🌹

  • 或者您可以 "follow" 一下, 我会不断开源更多有趣和实用的项目

  • 开发环境 MacOS Ventura, VSCode

  • 如有问题请直接在 Issues 中提, 或者您发现问题并有非常好的解决方案, 欢迎 PR 👍

  • 推荐一个 Vite5 + Vue3 + TS + Element Plus 开源入门项目, 对 Element Plus UI 库感兴趣的朋友可以去看看。地址在这里

  • 另外一个 Vite5 + Vue3 + Naive UI + TS 的入门项目, 比当前项目简洁很多, 非常适合入门练习和二次开发。地址在这里

License

MIT License | Copyright © 2023-PRESENT Wisdom