Tabs 标签页
标签页由四个组件分工协作:MTabs / MVTabs 只负责画标签栏,MTabPanes / MTabPane 只负责画内容区。
这套拆分是刻意的:标签栏不渲染内容,内容区不渲染标签。两者之间没有父子关系,把同一份数据分成 items(给标签栏)和 MTabPane(给内容区)两份描述,再用一个共享的 v-model 把激活名对齐。好处是标签栏可以独立放在任何位置——比如贴近页面顶部的 sticky 区域——而内容区留在卡片里。
代码演示
基础用法
MTabs 用 items 描述标签,MTabPanes 里放对应的 MTabPane,两者绑定同一个 active。
注意这是两个平级组件,MTabs 并没有包住 MTabPanes。
<script setup lang="ts">
import { ref } from 'vue'
const active = ref<string | number>('overview')
const items = [
{ name: 'overview', label: '概览' },
{ name: 'usage', label: '用量' },
{ name: 'billing', label: '账单' },
]
</script>
<template>
<div class="m-row-demo--stretch">
<MTabs v-model="active" :items="items" />
<MTabPanes v-model="active">
<MTabPane name="overview">标签栏与内容区绑定同一个 v-model。</MTabPane>
<MTabPane name="usage">只有激活的面板会挂载到 DOM。</MTabPane>
<MTabPane name="billing">切换方向决定偏移动画的方向。</MTabPane>
</MTabPanes>
</div>
</template>卡片与分段形态
type="card" 去掉下沿指示条,改用淡彩背景标记激活项,相邻标签的圆角相接,视觉上接近分段控件。
type 只有 line 与 card 两种取值,没有单独的 segmented 形态。
<script setup lang="ts">
import { ref } from 'vue'
const active = ref<string | number>('a')
const items = [
{ name: 'a', label: '全部' },
{ name: 'b', label: '进行中' },
{ name: 'c', label: '已完成' },
]
</script>
<template>
<div class="m-row-demo--stretch">
<MTabs v-model="active" :items="items" type="card" />
<MTabPanes v-model="active">
<MTabPane name="a">card 形态:去掉下沿指示条,用淡彩背景标记激活项。</MTabPane>
<MTabPane name="b">相邻标签圆角相接,视觉上接近分段控件。</MTabPane>
<MTabPane name="c">切换仍然由同一个 v-model 驱动。</MTabPane>
</MTabPanes>
</div>
</template>纵向标签栏
纵向用独立的 MVTabs,其余用法与 MTabs 一致。
align 取值变成 top / middle / bottom。
<script setup lang="ts">
import { defineComponent, h, ref, resolveComponent, type Component } from 'vue'
import { mdiChartBoxOutline, mdiAccountCogOutline, mdiBellOutline } from '@mdi/js'
const MIcon = resolveComponent('MIcon') as Component
function icon(path: string) {
return defineComponent({
name: 'DemoTabsIcon',
props: { size: { type: [Number, String], default: 16 } },
setup(props) {
return () => h(MIcon, { path, size: props.size })
},
})
}
const active = ref<string | number>('overview')
const items = [
{ name: 'overview', label: '概览', icon: icon(mdiChartBoxOutline) },
{ name: 'account', label: '账户', icon: icon(mdiAccountCogOutline) },
{ name: 'notice', label: '通知', icon: icon(mdiBellOutline) },
]
</script>
<template>
<div class="m-row-demo">
<MVTabs v-model="active" :items="items" type="line" align="top" />
<MTabPanes v-model="active" style="flex: 1; min-width: 0">
<MTabPane name="overview">纵向标签栏立在左侧,指示条沿纵向滑动。</MTabPane>
<MTabPane name="account">标签栏与内容区仍是两个平级组件。</MTabPane>
<MTabPane name="notice">方向键换成了 ↑ / ↓。</MTabPane>
</MTabPanes>
</div>
</template>可关闭与可新增
addable 在标签栏尾部加一个新增按钮。close 与 add 都只抛事件,不会自动改动 items,需要消费方自己更新数组。
关掉当前激活的标签时,MTabPanes 会自动把激活名换成相邻项并回写。
<script setup lang="ts">
import { ref } from 'vue'
const items = ref([
{ name: 'tab-1', label: '标签一', closable: true },
{ name: 'tab-2', label: '标签二', closable: true },
{ name: 'tab-3', label: '标签三' },
])
const active = ref<string | number>('tab-1')
let seq = 3
function handleClose(name: string | number) {
// close 只抛事件,数据由消费方自己维护
items.value = items.value.filter((item) => item.name !== name)
}
function handleAdd() {
seq += 1
const name = `tab-${seq}`
items.value = [...items.value, { name, label: `标签${seq}`, closable: true }]
active.value = name
}
</script>
<template>
<div class="m-row-demo--stretch">
<MTabs
v-model="active"
:items="items"
addable
@close="handleClose"
@add="handleAdd" />
<MTabPanes v-model="active">
<MTabPane v-for="item in items" :key="item.name" :name="item.name">
「{{ item.label }}」的内容
</MTabPane>
</MTabPanes>
</div>
</template>禁用项
MTabPane / items 上的 disabled 只锁住单项,标签栏顶层的 disabled 锁住全部。
<script setup lang="ts">
import { ref } from 'vue'
const active = ref<string | number>('a')
const items = [
{ name: 'a', label: '概览' },
{ name: 'b', label: '用量(禁用)', disabled: true },
{ name: 'c', label: '账单' },
]
</script>
<template>
<div class="m-row-demo--stretch">
<MTabs v-model="active" :items="items" />
<MTabPanes v-model="active">
<MTabPane name="a">中间那一项被 item 上的 `disabled` 锁住,点击无响应。</MTabPane>
<MTabPane name="b">这一项无法被激活。</MTabPane>
<MTabPane name="c">其余项正常切换。</MTabPane>
</MTabPanes>
<MTabs v-model="active" :items="items" disabled />
</div>
</template>尺寸、对齐与语义色
size 影响标签高度与字号(32 / 40 / 48 px),align 控制标签在栏内的对齐,color 决定激活文字与指示条的颜色。
<script setup lang="ts">
import { ref } from 'vue'
const active = ref<string | number>('a')
const items = [
{ name: 'a', label: '标签一' },
{ name: 'b', label: '标签二' },
{ name: 'c', label: '标签三' },
]
</script>
<template>
<div class="m-row-demo--stretch">
<MTabs v-model="active" :items="items" size="small" />
<MTabs v-model="active" :items="items" size="default" />
<MTabs v-model="active" :items="items" size="large" />
<MTabs v-model="active" :items="items" size="default" align="center" color="success" />
<MTabs v-model="active" :items="items" size="default" align="right" color="error" />
</div>
</template>API
MTabs Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | string | number | undefined | 当前激活的标签 name(v-model) |
items | TabsItem[] | [] | 标签项数据;MTabs 是纯标签栏,标签只能通过 items 传入 |
type | 'line' | 'card' | 'line' | 标签形态:下划线 / 卡片 |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | undefined | 主题色,决定激活文字、指示条、hover 底色与卡片激活背景;未设置时按 primary 渲染 |
size | 'small' | 'default' | 'large' | 'default' | 尺寸,影响标签高度与字号 |
align | 'left' | 'center' | 'right' | 'left' | 标签在栏内的对齐方式 |
disabled | boolean | false | 是否禁用全部标签 |
closable | boolean | false | 所有标签是否显示关闭按钮,与 item 自身的 closable 取「或」 |
addable | boolean | false | 是否在标签栏尾部显示「新增」按钮,点击触发 add |
MVTabs Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | string | number | undefined | 当前激活的标签 name(v-model) |
items | TabsItem[] | [] | 标签项数据 |
type | 'line' | 'card' | 'line' | 标签形态:右沿指示条 / 卡片 |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | undefined | 主题色,未设置时按 primary 渲染 |
size | 'small' | 'default' | 'large' | 'default' | 尺寸,影响标签高度与字号 |
align | 'top' | 'middle' | 'bottom' | 'top' | 标签在栏内的对齐方式 |
disabled | boolean | false | 是否禁用全部标签 |
closable | boolean | false | 所有标签是否显示关闭按钮,与 item 自身的 closable 取「或」 |
addable | boolean | false | 是否在标签栏底部显示「新增」按钮,点击触发 add |
MTabPane Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | string | number | undefined | 唯一标识;不传时由 MTabPanes 按注册顺序分配 pane-{index},独立使用时为 0 |
label | string | undefined | 标签文本(纯文本) |
icon | Component | undefined | 标签图标组件 |
disabled | boolean | false | 是否禁用该标签 |
closable | boolean | undefined | 是否显示关闭按钮,未显式设置时继承标签栏的 closable |
MTabPanes Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | string | number | undefined | 当前激活的 MTabPane 的 name;与标签栏共享同一个变量即可联动 |
Events
| 组件 | 事件 | 参数 | 说明 |
|---|---|---|---|
MTabs / MVTabs | update:modelValue | value: string | number | 激活项变化,包含失效回退时的回写 |
MTabs / MVTabs | change | name: string | number | 用户切换激活标签 |
MTabs / MVTabs | close | name: string | number | 点击标签关闭按钮;不自动移除数据 |
MTabs / MVTabs | add | 无 | 点击「新增」按钮 |
MTabPanes | update:modelValue | value: string | number | 激活项变化,包含关闭激活项后自动激活相邻项的回写 |
MTabPane | 无 |
Slots
| 组件 | 插槽 | 参数 | 说明 |
|---|---|---|---|
MTabs / MVTabs | 无 | ||
MTabPanes | default | 无 | 内部的 MTabPane 列表 |
MTabPane | default | 无 | 面板内容 |
类型定义
export type TabsType = 'line' | 'card';
export type TabsSize = 'small' | 'default' | 'large';
export type TabsAlign = 'left' | 'center' | 'right';
export type VTabsAlign = 'top' | 'middle' | 'bottom';
export type TabsColor =
| 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info';
export interface TabsItem {
name: string | number;
label: string;
icon?: Component;
disabled?: boolean;
closable?: boolean;
}组合关系
<script setup lang="ts">
import { ref } from 'vue';
const active = ref<string | number>('overview');
const items = [
{ name: 'overview', label: '概览' },
{ name: 'usage', label: '用量' },
];
</script>
<template>
<!-- 标签栏:纯数据驱动,不收集子组件 -->
<MTabs v-model="active" :items="items" type="line" />
<!-- 内容区:平级组件,靠上面同一个 active 联动 -->
<MTabPanes v-model="active">
<!-- 只有 MTabPanes ↔ MTabPane 之间是父子关系 -->
<MTabPane name="overview">概览内容</MTabPane>
<MTabPane name="usage">用量内容</MTabPane>
</MTabPanes>
</template>| 关系 | 说明 |
|---|---|
MTabs / MVTabs ↔ MTabPanes | 平级,没有 provide / inject,只靠绑定同一个 v-model 联动 |
MTabPanes ↔ MTabPane | 父子,MTabPanes 通过 provide 下发注册表,MTabPane 在 setup 里 inject 并注册自己 |
MTabs / MVTabs ↔ MTabPane | 无任何直接关系,标签栏不认识面板组件 |
使用建议
为什么不能写 MTabs 包住 MTabPane
MTabs 是纯标签栏组件,它的渲染完全由 items 决定,模板里没有默认插槽。写成「标签栏包住面板」不会报错,但里面的 MTabPane 会被直接丢弃。正确写法是两者平级、共享一个激活变量。
close / add 不会改动 items
组件只负责抛事件,不持有数据源。删除标签要自己在 @close 里过滤数组,新增标签要在 @add 里往数组里 push。MTabPane 在卸载时会通过 onBeforeUnmount 自动注销注册表,所以用 v-for 渲染面板时不需要手工清理。
closable 是「或」不是「覆盖」
渲染条件是 item.closable || 标签栏的 closable。所以标签栏上传 closable 会让所有标签都出现关闭按钮,item 上的 closable: false 也压不住它;反过来,标签栏不传 closable 时可以靠单个 item 的 closable: true 只给某些标签加关闭按钮。
只有激活的面板在 DOM 里
MTabPane 用 v-if="isActive" 控制渲染,非激活面板完全不存在于 DOM 中,因此面板里放 MDatePicker 这类需要初始化尺寸的组件不会在隐藏状态下量错。代价是切换时状态会被重建——需要保留的状态提到父级,或用 v-show 自己控制内容。
离场面板会被临时绝对定位在原来的位置和宽度上,配合切换方向(新旧索引比较得出的 forward / backward)播偏移动画;MTabPanes 同时会做一次容器高度过渡,避免高矮面板切换时跳变。prefers-reduced-motion: reduce 下会跳过高度过渡。
面板不传 name 会被自动编号
MTabPane 省略 name 时,MTabPanes 按注册(渲染)顺序分配 pane-{index},从 pane-0 开始。所以在 MTabPanes 外部无法预知它的名字,只适合「内容区与标签栏无关、不需要点名激活面板」的场景。要和标签栏联动,请显式写 name,并保证它与 items[].name 一一对应。
标签失效时会自动回退
MTabs / MVTabs 的 modelValue 指向一个不存在或被禁用的标签时,组件会回退到第一个可用项并回写 update:modelValue;MTabPanes 在激活项被关闭时也会把激活名换成相邻项。外部状态因此始终是有效的。
相关组件
- Nav 导航:同样由
items驱动,但用于主导航而非内容切换。 - RadioButtonGroup 按钮单选:轻量的分段式视图切换。