Skip to content

Tabs 标签页

内容分组切换。标签由子组件自动收集,不需要额外维护一份标签数据。

基础用法

vue
<script setup>
import { ref } from 'vue'
const active = ref('a')
</script>

<template>
  <SrTabs v-model="active">
    <SrTabPane name="a" label="标签一">内容 A</SrTabPane>
    <SrTabPane name="b" label="标签二">内容 B</SrTabPane>
  </SrTabs>
</template>

标签是自动收集的

不需要传一份 tabs 数组。 SrTabPanesetup 阶段向父级注册自己, 标签栏据此渲染。

vue
<!-- 面板增删由宿主用 v-if / v-for 控制,标签栏自动同步 -->
<SrTabs v-model="active">
  <SrTabPane v-for="tab in tabs" :key="tab.name" :name="tab.name" :label="tab.label">
    {{ tab.content }}
  </SrTabPane>
</SrTabs>

为什么不用数据驱动

tabs prop 加插槽是另一种常见设计,但它要求两处数据保持一致—— 面板的顺序、数量、标题都得和数组对得上。

自动收集没有这个问题:DOM 里有什么标签就是什么标签, 条件渲染、按权限过滤、动态增删都不需要额外同步。

视觉样式与位置

vue
<SrTabs type="card">…</SrTabs>
<SrTabs type="line">…</SrTabs>
<SrTabs position="left">…</SrTabs>
<SrTabs position="right">…</SrTabs>
<SrTabs position="bottom">…</SrTabs>

动态增删

closableaddable 提供关闭与新增按钮。 面板的实际移除由宿主决定——组件不知道你的数据从哪来。

vue
<script setup>
import { ref } from 'vue'
const tabs = ref(['标签一', '标签二'])
const active = ref('标签一')

function onAdd() {
  const name = `标签${tabs.value.length + 1}`
  tabs.value.push(name)
  active.value = name   // 新增后自动切过去
}

function onClose(name) {
  const i = tabs.value.indexOf(name)
  if (i > -1) tabs.value.splice(i, 1)

  // 关掉当前标签后要切到邻近的一个,否则内容区会空着
  if (active.value === name) {
    active.value = tabs.value[Math.max(0, i - 1)] ?? ''
  }
}
</script>

<template>
  <SrTabs v-model="active" closable addable @tab-add="onAdd" @tab-close="onClose">
    <SrTabPane v-for="t in tabs" :key="t" :name="t" :label="t">{{ t }} 的内容</SrTabPane>
  </SrTabs>
</template>

关闭当前标签后要处理激活项

直接删掉数组元素的话,active 会指向一个不存在的 name, 内容区变空。上面示例里的 onClose 演示了正确的处理方式。

组件本身会兜底:传入的 active 不在现有面板中时回退到第一个。 但显式切换更好——「回退到第一个」未必是用户期望的位置。

等宽与过渡

vue
<SrTabs stretch>…</SrTabs>
<SrTabs :animated="false">…</SrTabs>

animated 默认开启,切换时有内容过渡。若面板内容较重(大表格、图表), 关掉可以避免过渡期间的布局抖动。

历史缺陷:曾少导入 ref

如果你在纯 Vue 项目里遇到白屏

早期版本的 SrTabs 漏写了 import { ref } from 'vue', 但组件在 setup 顶层就调用了 ref()

  • Nuxt 项目里完全正常——Nuxt 自动导入 Vue 的 API,把它兜住了;
  • 纯 Vue 项目里会直接 ReferenceError,整个组件无法初始化。

这个缺陷是接上 vue-tsc 类型检查后才发现的——此前只跑 tsc, 而 tsc 不认识 .vue 文件,等于全部组件的 TS 代码从未被检查过。

Props

名称类型默认值说明
modelValuestring | number当前激活的标签 name
type'line' | 'card''line'视觉样式
position'top' | 'bottom' | 'left' | 'right''top'标签栏位置
closablebooleanfalse是否显示关闭按钮
addablebooleanfalse是否显示新增按钮
stretchbooleanfalse标签是否等宽铺满
animatedbooleantrue切换时是否播放内容过渡

SrTabPane Props

名称类型默认值说明
namestring | number必填,标签唯一标识
labelstring标签文本
disabledbooleanfalse是否禁用
closableboolean覆盖容器的 closable

事件

事件参数说明
update:modelValuename激活项变化
changename同上(语义化事件)
tab-closename请求关闭某个标签(不移除面板
tab-add点击新增按钮

无障碍

  • 标签栏用 role="tablist",每个标签 role="tab" 并带 aria-selected
  • 内容面板 role="tabpanel",通过 aria-labelledby 关联到对应标签
  • 键盘操作:← → 在标签间移动,Home / End 跳到首尾—— 这些是 ARIA 标签页模式的标准交互

自用组件库 · 源码分发 · 不发布 npm