# RadioButtonGroup 按钮单选

把一组互斥选项拼成整块的分段控件，选中的一段以语义色高亮。它是独立组件，内部自带隐藏原生 `input` 与同名互斥逻辑，**不使用 `MRadio` / `MRadioGroup`**，因此与单选框那套的 API、DOM 与样式互不牵连。数据由 `options` 数组驱动，选项可等宽（`block`）或按内容自适应。

## 代码演示

### 基础用法

`options` 描述选项，`v-model` 绑定选中值。加 `block` 后各组等宽平分父容器宽度，是视图切换最常见的形态。

**演示说明：** `modelValue` 支持 `string | number | boolean` 三种类型。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const view = ref<string | number | boolean>('list')
const options = [
  { value: 'list', label: '列表' },
  { value: 'grid', label: '网格' },
  { value: 'board', label: '看板' },
]
</script>

<template>
  <div class="m-row-demo--stretch">
    <MRadioButtonGroup v-model="view" :options="options" block aria-label="视图切换" />
    <span>当前视图：{{ view }}</span>
  </div>
</template>
```

### 受控切换视图

组件完全是受控的：选中值变化即驱动外部内容切换，适合「列表 / 网格 / 看板」这类视图切换器。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const view = ref<string | number | boolean>('list')
const options = [
  { value: 'list', label: '列表' },
  { value: 'grid', label: '网格' },
  { value: 'board', label: '看板' },
]
const hints: Record<string, string> = {
  list: '每行一条记录，适合字段较少的列表。',
  grid: '宫格平铺，适合封面 / 头像类内容。',
  board: '按状态分列，适合拖拽流转。',
}
</script>

<template>
  <div class="m-row-demo--col">
    <MRadioButtonGroup v-model="view" :options="options" color="secondary" aria-label="切换内容视图" />
    <span>{{ hints[String(view)] }}</span>
  </div>
</template>
```

### 禁用项

`option.disabled` 只禁用单个选项，组件的 `disabled` 禁用整组，两者可叠加。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const value = ref<string | number | boolean>('grid')
const locked = ref<string | number | boolean>('list')
const options = [
  { value: 'list', label: '列表' },
  { value: 'grid', label: '网格' },
  { value: 'board', label: '看板', disabled: true },
]
</script>

<template>
  <div class="m-row-demo--col">
    <MRadioButtonGroup v-model="value" :options="options" aria-label="含禁用项的视图切换" />
    <MRadioButtonGroup v-model="locked" :options="options" disabled aria-label="整体禁用" />
  </div>
</template>
```

### 尺寸

`small` / `default` / `large` 三档，高度取自全局令牌 `--m-size-small`（28px）、`--m-size-default`（36px）、`--m-size-large`（48px）。

```vue
<script setup lang="ts">
import { ref } from 'vue'

const small = ref<string | number | boolean>('a')
const normal = ref<string | number | boolean>('a')
const large = ref<string | number | boolean>('a')
const options = [
  { value: 'a', label: '选项一' },
  { value: 'b', label: '选项二' },
  { value: 'c', label: '选项三' },
]
</script>

<template>
  <div class="m-row-demo--col">
    <MRadioButtonGroup v-model="small" :options="options" size="small" aria-label="小尺寸" />
    <MRadioButtonGroup v-model="normal" :options="options" size="default" aria-label="默认尺寸" />
    <MRadioButtonGroup v-model="large" :options="options" size="large" aria-label="大尺寸" />
  </div>
</template>
```

### 图标与纵向排列

`option.icon` 传入 `@mdi/js` 的 SVG path（与 `MIcon` 同源）。只给 `icon` 不给 `label` 时，会用 `String(value)` 生成可访问名称。`vertical` 让选项纵向排列。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { mdiViewListOutline, mdiViewGridOutline, mdiViewDashboardOutline, mdiFormatAlignLeft, mdiFormatAlignCenter, mdiFormatAlignRight } from '@mdi/js'

const view = ref<string | number | boolean>('list')
const align = ref<string | number | boolean>('left')
const iconOptions = [
  { value: 'list', label: '列表', icon: mdiViewListOutline },
  { value: 'grid', label: '网格', icon: mdiViewGridOutline },
  { value: 'board', label: '看板', icon: mdiViewDashboardOutline },
]
const alignOptions = [
  { value: 'left', icon: mdiFormatAlignLeft },
  { value: 'center', icon: mdiFormatAlignCenter },
  { value: 'right', icon: mdiFormatAlignRight },
]
</script>

<template>
  <div class="m-row-demo">
    <MRadioButtonGroup v-model="view" :options="iconOptions" color="success" aria-label="带图标的视图切换" />
    <MRadioButtonGroup v-model="align" :options="alignOptions" vertical aria-label="纵向对齐方式" />
  </div>
</template>
```

## API

### Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `string \| number \| boolean` | `undefined` | 当前选中值（v-model） |
| `options` | `RadioButtonGroupOption[]` | `[]` | 选项列表 |
| `name` | `string` | `undefined`（自动生成 `m-radio-button-group-{uid}`） | 原生 input name：同名才互斥、方向键才在组内切换；缺省时自动生成 |
| `disabled` | `boolean` | `false` | 整体禁用 |
| `color` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | `'primary'` | 选中态颜色 |
| `size` | `'small' \| 'default' \| 'large'` | `'default'` | 尺寸 |
| `block` | `boolean` | `false` | 占满父容器宽度，各段等宽平分 |
| `vertical` | `boolean` | `false` | 纵向排列（默认横向） |
| `ariaLabel` | `string` | `undefined` | 组的可访问名称，写在 `role="radiogroup"` 容器上 |

### Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `value: string \| number \| boolean` | 选中变化（已选中项不重复抛出） |
| `change` | `value: string \| number \| boolean` | 选中变化 |

### Slots

无（纯 `options` 数据驱动）。

### 类型定义

```ts
type RadioValue = string | number | boolean

interface RadioButtonGroupOption {
  /** 选项值：与 modelValue 比对判定选中（组内应唯一） */
  value: RadioValue
  /** 选项文本 */
  label?: string
  /** 图标：@mdi/js 的 SVG path，与 MIcon 的 path 同源 */
  icon?: string
  /** 单独禁用该选项 */
  disabled?: boolean
}
```

## 使用建议

::: tip 与 MRadio 的分工
`MRadio` 用于表单里带文字说明的单选列表，`MRadioButtonGroup` 用于工具栏式的紧凑切换。两者互不依赖，可以同时出现在同一页面，不会共享样式或 `name`。
:::

::: tip label 与 icon 至少给一项
只给 `icon` 时，组件会用 `String(value)` 兜底生成可访问名称（`aria-label`）。若两者都缺，屏幕阅读器读不出该选项的语义。
:::

::: warning 选项值比较是严格相等
选中判定用 `option.value === modelValue`，`1` 与 `'1'`、`true` 与 `'true'` 不会互相命中。切换 `modelValue` 类型时要同步改 `options` 里的 `value` 类型。
:::

::: warning 不传 name 也会自动生成
`name` 缺省时会用 `useId()` 生成唯一值，保证同名互斥与方向键切换正常工作。只有当你要把该组与外部表单的字段对齐时，才需要显式传 `name`。
:::

## 相关组件

- [Radio 单选框](/components/radio)：表单场景的单选组，带文字说明与子项标签。
- [Button 按钮](/components/button)：需要的是「点击执行」而非「选中某一项」时使用。
- [Icon 图标](/components/icon)：`option.icon` 的 path 与 `MIcon` 同源。
