# Select 选择器

从固定选项集合中取值。单选用单值 `v-model`，多选改用数组；面板定位自带视口夹紧、上下翻转与滚动跟随，长列表在面板内部滚动，不会把页面顶开。

## 代码演示

### 基础用法

`options` 是 `{ label, value }` 组成的数组，未选中时显示 `placeholder`。

**演示说明：** 点击触发区展开面板，选择后单选面板自动收起。

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

const city = ref<string | number | (string | number)[] | undefined>(undefined)

const options = [
  { label: '北京', value: 'beijing' },
  { label: '上海', value: 'shanghai' },
  { label: '广州', value: 'guangzhou' },
  { label: '深圳', value: 'shenzhen' },
]
</script>

<template>
  <div class="m-row-demo">
    <MSelect v-model="city" :options="options" placeholder="请选择城市" clearable width="200px" />
  </div>
</template>
```

### 多选

`multiple` 打开后 `modelValue` 必须改为数组，触发区把已选项的 `label` 用逗号拼接展示。

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

const skills = ref<(string | number)[]>(['vue'])

const options = [
  { label: 'Vue', value: 'vue' },
  { label: 'React', value: 'react' },
  { label: 'Svelte', value: 'svelte' },
  { label: 'Solid', value: 'solid' },
]
</script>

<template>
  <div class="m-row-demo">
    <MSelect v-model="skills" :options="options" multiple clearable placeholder="可多选" width="240px" />
  </div>
</template>
```

### 选项图标与禁用项

`SelectOption.icon` 是文本图标（emoji / 字符），选中后会一并出现在触发区；`disabled` 的选项不可点击，键盘导航也会跳过。

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

const status = ref<string | number | (string | number)[] | undefined>('running')

const options = [
  { label: '运行中', value: 'running', icon: '🚀' },
  { label: '排队中', value: 'pending', icon: '⏳' },
  { label: '已失败', value: 'failed', icon: '⛔' },
  { label: '已归档（不可选）', value: 'archived', disabled: true },
]
</script>

<template>
  <div class="m-row-demo">
    <MSelect v-model="status" :options="options" width="240px" />
  </div>
</template>
```

### 尺寸与圆角

`size` 控制触发区高度，`radius` 只作用于触发区（面板圆角是 `panelRadius`）。

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

const small = ref<string | number | (string | number)[] | undefined>('small')
const normal = ref<string | number | (string | number)[] | undefined>('default')
const large = ref<string | number | (string | number)[] | undefined>('large')
const pill = ref<string | number | (string | number)[] | undefined>(undefined)

const options = [
  { label: 'small 28px', value: 'small' },
  { label: 'default 36px', value: 'default' },
  { label: 'large 48px', value: 'large' },
]
</script>

<template>
  <div class="m-row-demo--col">
    <MSelect v-model="small" :options="options" size="small" width="240px" />
    <MSelect v-model="normal" :options="options" size="default" width="240px" />
    <MSelect v-model="large" :options="options" size="large" width="240px" />
    <MSelect
      v-model="pill"
      :options="options"
      radius="999px"
      placeholder="radius=999px"
      width="240px" />
  </div>
</template>
```

### 面板宽度与圆角

面板宽度以触发区为下限、随最长选项自动撑开，视口右侧放不下时会压缩；高度上限 280px，超出后内部滚动。

**演示说明：** 左侧 14 个选项触发内部滚动，右侧的长 label 会把面板撑得比触发区宽。

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

const short = ref<string | number | (string | number)[] | undefined>(undefined)
const long = ref<string | number | (string | number)[] | undefined>(undefined)

const many = Array.from({ length: 14 }, (_, i) => ({
  label: `选项 ${i + 1}`,
  value: i + 1,
}))

const verbose = [
  { label: '只在工作日 09:00 - 18:00 接收通知', value: 'workday' },
  { label: '任何时间都接收通知（含节假日）', value: 'always' },
  { label: '完全不接收通知', value: 'never' },
]
</script>

<template>
  <div class="m-row-demo">
    <MSelect
      v-model="short"
      :options="many"
      panel-radius="0"
      placeholder="panelRadius=0"
      width="140px" />

    <MSelect
      v-model="long"
      :options="verbose"
      panel-radius="18px"
      placeholder="label 比触发区宽"
      width="180px" />
  </div>
</template>
```

### 清空与禁用

清除按钮在「有值、未禁用」时出现；单选清除抛 `undefined`，多选清除抛 `[]`。

**演示说明：** 两个 `change` 处理器会把抛出的值弹成 toast。

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

const options = [
  { label: '北京', value: 'beijing' },
  { label: '上海', value: 'shanghai' },
  { label: '广州', value: 'guangzhou' },
]

const single = ref<string | number | (string | number)[] | undefined>('beijing')
const multi = ref<(string | number)[]>(['beijing'])

function onSingle(value: string | number | (string | number)[] | undefined) {
  toast.info(value === undefined ? '清除后收到 undefined' : `选中 ${String(value)}`)
}

function onMulti(value: string | number | (string | number)[] | undefined) {
  const empty = Array.isArray(value) && value.length === 0
  toast.info(empty ? '清除后收到空数组 []' : `选中 ${String(value)}`)
}
</script>

<template>
  <div class="m-row-demo--col">
    <MSelect
      v-model="single"
      :options="options"
      clearable
      width="240px"
      @change="onSingle" />

    <MSelect
      v-model="multi"
      :options="options"
      multiple
      clearable
      width="240px"
      @change="onMulti" />

    <MSelect
      :model-value="'beijing'"
      :options="options"
      disabled
      width="240px" />
  </div>
</template>
```

## API

### Props

| 属性 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `modelValue` | `string \| number \| (string \| number)[] \| undefined` | `undefined`（未提供） | 当前值（单选为单值，多选为数组） |
| `options` | `SelectOption[]` | `[]`（工厂函数） | 选项列表 |
| `placeholder` | `string` | `undefined`（未提供） | 占位文本 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `clearable` | `boolean` | `false` | 有值时显示清除按钮 |
| `multiple` | `boolean` | `false` | 多选模式：`modelValue` 为数组 |
| `size` | `'small' \| 'default' \| 'large'` | `'default'` | 高度取全局 `--m-size-*` 令牌：small 28px / default 36px / large 48px |
| `width` | `string` | `undefined`（未提供；样式默认 220px） | 触发区宽度 |
| `radius` | `string` | `undefined`（未提供；默认 8px） | 触发区圆角（任意 CSS 长度，如 `'8px'`） |
| `panelRadius` | `string` | `undefined`（未提供；默认 8px） | 弹出面板圆角（任意 CSS 长度） |
| `popupClass` | `string` | `undefined`（未提供） | 弹出面板额外类名（面板 Teleport 到 body，需配合全局样式使用） |
| `appendToBody` | `boolean` | `true` | 是否将面板挂载到 body；在 Chrome 扩展 popup 等受限容器中建议设为 `false` |
| `lockScroll` | `boolean` | `true` | 打开面板时是否锁定 body 滚动 |

### Events

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `value: SelectValue` | 选中变化（多选为数组；单选清除为 `undefined`，多选清除为 `[]`） |
| `change` | `value: SelectValue` | 选中变化，与 `update:modelValue` 同值同刻抛出 |

### Slots

无。

### 类型定义

```ts
export interface SelectOption {
  label: string;
  value: string | number;
  icon?: string;
  iconUrl?: string;
  disabled?: boolean;
}
export type SelectValue = string | number | (string | number)[] | undefined;
export type SelectSize = InputSize; // 'small' | 'default' | 'large'
```

`Expose`：源码未暴露实例方法。

### 面板行为

- **定位**：默认 Teleport 到 `body`，用 `fixed` 定位；下方空间不足时向上翻转，两侧都不够时选空间较大的一侧并把高度锁在该侧空间内（面板始终完整落在视口内）。
- **跟随**：滚动或缩放时重新读取触发区位置并重算定位；触发区整体滚出视口后面板收起。
- **inline 模式**：`appendToBody="false"` 时面板 `absolute` 定位在组件根节点内，宽度固定等于触发区宽度，且不转移焦点，适合受限容器。
- **键盘**：未展开时 `Enter` / `Space` / `ArrowUp` / `ArrowDown` 展开；展开后上下键移动、`Home` / `End` 跳首尾、`Enter` 选中、`Escape` 关闭并归还焦点、`Tab` 关闭面板继续走焦。

## 使用建议

::: tip 多选时 modelValue 必须是数组
`multiple` 与 `modelValue` 的类型必须成对出现。多选时传了单值，组件会按空数组处理，表现为「选了不显示」。
:::

::: warning 面板内容不可自定义
面板由 `options` 数据驱动，**没有**默认插槽、选项插槽或空状态插槽，也不支持选项分组与多列布局。空列表会渲染内置的「无匹配选项」文案。需要完全自定义的下拉内容时请改用 [Dropdown 下拉菜单](/components/dropdown)。
:::

::: warning icon 与 iconUrl 的区别
`icon` 是直接渲染在选项里的**文本**（emoji、字符），不是 `MIcon` 的 path；要放图片请用 `iconUrl`（作为 `background-image`），两者同时存在时 `iconUrl` 优先。
:::

## 相关组件

- [Dropdown 下拉菜单](/components/dropdown)：需要自定义面板内容时使用。
- [Radio 单选框](/components/radio) / [Checkbox 复选框](/components/checkbox)：选项数量少且需要全量可见时，比下拉更省一次点击。
- [Input 输入框](/components/input)：候选项不固定、需要自由输入时使用。
