# Tabs 标签页

标签页由四个组件分工协作：`MTabs` / `MVTabs` 只负责画标签栏，`MTabPanes` / `MTabPane` 只负责画内容区。

这套拆分是刻意的：**标签栏不渲染内容，内容区不渲染标签**。两者之间没有父子关系，把同一份数据分成 `items`（给标签栏）和 `MTabPane`（给内容区）两份描述，再用一个共享的 `v-model` 把激活名对齐。好处是标签栏可以独立放在任何位置——比如贴近页面顶部的 sticky 区域——而内容区留在卡片里。

## 代码演示

### 基础用法

`MTabs` 用 `items` 描述标签，`MTabPanes` 里放对应的 `MTabPane`，两者绑定同一个 `active`。

**演示说明：** 注意这是两个平级组件，`MTabs` 并没有包住 `MTabPanes`。

```vue
<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 形态。

```vue
<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`。

```vue
<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` 会自动把激活名换成相邻项并回写。

```vue
<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` 锁住全部。

```vue
<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` 决定激活文字与指示条的颜色。

```vue
<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` | 无 | 面板内容 |

### 类型定义

```ts
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;
}
```

### 组合关系

```vue
<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` | 无任何直接关系，标签栏不认识面板组件 |

## 使用建议

::: tip 为什么不能写 `MTabs` 包住 `MTabPane`
`MTabs` 是纯标签栏组件，它的渲染完全由 `items` 决定，模板里没有默认插槽。写成「标签栏包住面板」不会报错，但里面的 `MTabPane` 会被直接丢弃。正确写法是两者平级、共享一个激活变量。
:::

::: warning `close` / `add` 不会改动 `items`
组件只负责抛事件，不持有数据源。删除标签要自己在 `@close` 里过滤数组，新增标签要在 `@add` 里往数组里 push。`MTabPane` 在卸载时会通过 `onBeforeUnmount` 自动注销注册表，所以用 `v-for` 渲染面板时不需要手工清理。
:::

::: warning `closable` 是「或」不是「覆盖」
渲染条件是 `item.closable || 标签栏的 closable`。所以标签栏上传 `closable` 会让**所有**标签都出现关闭按钮，item 上的 `closable: false` 也压不住它；反过来，标签栏不传 `closable` 时可以靠单个 item 的 `closable: true` 只给某些标签加关闭按钮。
:::

::: tip 只有激活的面板在 DOM 里
`MTabPane` 用 `v-if="isActive"` 控制渲染，非激活面板完全不存在于 DOM 中，因此面板里放 `MDatePicker` 这类需要初始化尺寸的组件不会在隐藏状态下量错。代价是切换时状态会被重建——需要保留的状态提到父级，或用 `v-show` 自己控制内容。

离场面板会被临时绝对定位在原来的位置和宽度上，配合切换方向（新旧索引比较得出的 `forward` / `backward`）播偏移动画；`MTabPanes` 同时会做一次容器高度过渡，避免高矮面板切换时跳变。`prefers-reduced-motion: reduce` 下会跳过高度过渡。
:::

::: tip 面板不传 `name` 会被自动编号
`MTabPane` 省略 `name` 时，`MTabPanes` 按注册（渲染）顺序分配 `pane-{index}`，从 `pane-0` 开始。所以在 `MTabPanes` 外部无法预知它的名字，只适合「内容区与标签栏无关、不需要点名激活面板」的场景。要和标签栏联动，请显式写 `name`，并保证它与 `items[].name` 一一对应。
:::

::: tip 标签失效时会自动回退
`MTabs` / `MVTabs` 的 `modelValue` 指向一个不存在或被禁用的标签时，组件会回退到第一个可用项并回写 `update:modelValue`；`MTabPanes` 在激活项被关闭时也会把激活名换成相邻项。外部状态因此始终是有效的。
:::

## 相关组件

- [Nav 导航](/components/nav)：同样由 `items` 驱动，但用于主导航而非内容切换。
- [RadioButtonGroup 按钮单选](/components/radio-button-group)：轻量的分段式视图切换。
