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 数组。 SrTabPane 在 setup 阶段向父级注册自己, 标签栏据此渲染。
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>动态增删
closable 与 addable 提供关闭与新增按钮。 面板的实际移除由宿主决定——组件不知道你的数据从哪来。
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
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | string | number | — | 当前激活的标签 name |
type | 'line' | 'card' | 'line' | 视觉样式 |
position | 'top' | 'bottom' | 'left' | 'right' | 'top' | 标签栏位置 |
closable | boolean | false | 是否显示关闭按钮 |
addable | boolean | false | 是否显示新增按钮 |
stretch | boolean | false | 标签是否等宽铺满 |
animated | boolean | true | 切换时是否播放内容过渡 |
SrTabPane Props
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | string | number | — | 必填,标签唯一标识 |
label | string | — | 标签文本 |
disabled | boolean | false | 是否禁用 |
closable | boolean | — | 覆盖容器的 closable |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue | name | 激活项变化 |
change | name | 同上(语义化事件) |
tab-close | name | 请求关闭某个标签(不移除面板) |
tab-add | — | 点击新增按钮 |
无障碍
- 标签栏用
role="tablist",每个标签role="tab"并带aria-selected - 内容面板
role="tabpanel",通过aria-labelledby关联到对应标签 - 键盘操作:← → 在标签间移动,Home / End 跳到首尾—— 这些是 ARIA 标签页模式的标准交互