<script setup lang="ts">
import { ref } from 'vue'
import { MultiSelect } from '@hina-ui/vue'
const tags = ref(['school', 'sf'])
const options = [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
{ value: 'romance', label: '恋爱' },
{ value: 'mystery', label: '悬疑' },
{ value: 'fantasy', label: '奇幻' },
]
</script>
<template>
<MultiSelect v-model="tags" :options="options" aria-label="标签" class="w-72" />
</template>
vue
用法
import { MultiSelect } from '@hina-ui/vue'
ts
多选框与 Select 共用同一套选项数据与列表,v-model 绑定选中值的数组。已选项以标签显示在触发器里,每个标签可以单独移除;设置 clearable 后末尾出现清除按钮,一次清空全部。选择后列表保持展开,方便连续勾选。
当前值:无
<script setup lang="ts">
import { ref } from 'vue'
import { MultiSelect, Stack, Text } from '@hina-ui/vue'
const platforms = ref<string[]>([])
const options = [
{ value: 'windows', label: 'Windows' },
{ value: 'switch', label: 'Switch' },
{ value: 'ps5', label: 'PS5' },
{ value: 'android', label: 'Android' },
{ value: 'ios', label: 'iOS' },
]
</script>
<template>
<Stack class="w-72">
<MultiSelect v-model="platforms" :options="options" placeholder="选择平台" aria-label="平台" />
<Text tone="muted">当前值:{{ platforms.length ? platforms.join('、') : '无' }}</Text>
</Stack>
</template>
vue
示例
分组
分组项带 label 与 options,可以与普通选项混排。
<script setup lang="ts">
import { ref } from 'vue'
import { MultiSelect } from '@hina-ui/vue'
const languages = ref(['zh'])
const options = [
{ value: 'zh', label: '简体中文' },
{
label: '日文',
options: [
{ value: 'ja', label: '日文原版' },
{ value: 'ja-tl', label: '日文(附翻译)' },
],
},
{
label: '其他',
options: [
{ value: 'en', label: '英文' },
{ value: 'ko', label: '韩文' },
],
},
]
</script>
<template>
<MultiSelect v-model="languages" :options="options" aria-label="语言" class="w-72" />
</template>
vue
显示数量
触发器高度固定,只显示前 maxVisible 个标签,其余折成「+N」。
<script setup lang="ts">
import { ref } from 'vue'
import { MultiSelect, Stack } from '@hina-ui/vue'
const tags = ref(['school', 'sf', 'romance', 'mystery'])
const options = [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
{ value: 'romance', label: '恋爱' },
{ value: 'mystery', label: '悬疑' },
{ value: 'fantasy', label: '奇幻' },
]
</script>
<template>
<Stack class="w-96">
<MultiSelect v-model="tags" :options="options" :max-visible="1" aria-label="最多一个" />
<MultiSelect v-model="tags" :options="options" :max-visible="3" aria-label="最多三个" />
</Stack>
</template>
vue
定制内容
option 插槽定制列表中每一项的内容。
组件从 options 推断完整选项类型,插槽中的 option 保留额外字段及其类型;v-model 仍绑定选项的 value 数组。类型定义见 Select。
<script setup lang="ts">
import { ref, type Component } from 'vue'
import { BookOpen, Clapperboard, Gamepad2 } from '@lucide/vue'
import { Inline, MultiSelect } from '@hina-ui/vue'
const types = ref(['gal'])
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: '轻小说' },
{ value: 'anime', label: '动画' },
]
const icons: Record<string, Component> = { gal: Gamepad2, ln: BookOpen, anime: Clapperboard }
</script>
<template>
<MultiSelect v-model="types" :options="options" aria-label="作品类型" class="w-72">
<template #option="{ option }">
<Inline as="span" gap="sm" :wrap="false">
<component :is="icons[option.value]" />
{{ option.label }}
</Inline>
</template>
</MultiSelect>
</template>
vue
可清除
clearable 在末尾加一个按钮,点击后清空全部。每个标签自带移除按钮,所以默认不显示它。
<script setup lang="ts">
import { ref } from 'vue'
import { MultiSelect } from '@hina-ui/vue'
const tags = ref(['school', 'sf'])
const options = [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
{ value: 'romance', label: '恋爱' },
{ value: 'mystery', label: '悬疑' },
]
</script>
<template>
<MultiSelect v-model="tags" :options="options" clearable aria-label="标签" class="w-72" />
</template>
vue
尺寸
三档尺寸与输入框相同,标签随之缩放。
<script setup lang="ts">
import { MultiSelect, Stack } from '@hina-ui/vue'
const options = [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
{ value: 'romance', label: '恋爱' },
]
</script>
<template>
<Stack class="w-72">
<MultiSelect size="sm" :options="options" :model-value="['school', 'sf']" aria-label="小号" />
<MultiSelect size="md" :options="options" :model-value="['school', 'sf']" aria-label="中号" />
<MultiSelect size="lg" :options="options" :model-value="['school', 'sf']" aria-label="大号" />
</Stack>
</template>
vue
形态
primary 直接放在页面底色上,带边框与阴影;secondary 放在卡片等表面内,只有一层浅色底。
<script setup lang="ts">
import { Card, MultiSelect, Stack } from '@hina-ui/vue'
const options = [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
]
</script>
<template>
<Stack class="w-72">
<MultiSelect :options="options" placeholder="直接放在页面上" aria-label="页面上的多选框" />
<Card>
<MultiSelect
variant="secondary"
:options="options"
placeholder="放在卡片内"
aria-label="卡片内的多选框"
/>
</Card>
</Stack>
</template>
vue
状态
invalid 标出校验未通过,disabled 禁用整个多选框。
<script setup lang="ts">
import { MultiSelect, Stack } from '@hina-ui/vue'
const options = [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
{ value: 'romance', label: '恋爱' },
]
</script>
<template>
<Stack class="w-72">
<MultiSelect
invalid
:options="options"
placeholder="至少选择一个标签"
aria-label="校验未通过"
/>
<MultiSelect disabled :options="options" :model-value="['school']" aria-label="已禁用" />
</Stack>
</template>
vue
在表单中
放进 FormField 后,标签指向触发器,说明与错误信息由字段渲染;校验规则与提交交给 Form。值是数组,数量限制写在数组层。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, Form, FormField, MultiSelect, Text } from '@hina-ui/vue'
const genres = [
{ label: '恋爱', value: 'romance' },
{ label: '悬疑', value: 'mystery' },
{ label: '奇幻', value: 'fantasy' },
{ label: '日常', value: 'slice-of-life' },
{ label: '科幻', value: 'sci-fi' },
]
const schema = v.object({
genres: v.pipe(
v.array(v.string('请选择题材')),
v.minLength(1, '至少选择一个题材'),
v.maxLength(3, '最多选择三个题材'),
),
})
const values = reactive({ genres: [] as Array<string | number> })
const saved = ref('')
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = JSON.stringify(data)
}
</script>
<template>
<Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
<FormField name="genres" label="题材" description="一到三个" required>
<MultiSelect v-model="values.genres" :options="genres" />
</FormField>
<Button type="submit" :loading="submitting" class="self-start">保存</Button>
<Text v-if="saved" tone="muted" size="sm">已保存:{{ saved }}</Text>
</Form>
</template>
vue
虚拟滚动
virtualize 按需渲染可见范围附近的条目,与 VirtualList 共用测量与滚动底层。默认关闭;可传 { estimateSize, overscan } 调整预估行高和两侧预渲染数量,行高会按实际内容测量。键盘导航覆盖完整数据,禁用项会跳过。 条目离开渲染范围后会卸载;插槽内需要持久保留的状态应按唯一 value 存在外部。
已选: 1
<script setup lang="ts">
import { ref } from 'vue'
import { MultiSelect, Stack, Text } from '@hina-ui/vue'
const selected = ref<Array<string | number>>([7890])
const options = Array.from({ length: 10000 }, (_, index) => ({
value: index,
label: `条目 ${String(index + 1).padStart(5, '0')}`,
disabled: index % 97 === 0,
}))
</script>
<template>
<Stack gap="sm" class="w-72 max-w-full">
<MultiSelect
v-model="selected"
:options="options"
:virtualize="{ estimateSize: 36, overscan: 6 }"
aria-label="一万项"
/>
<Text size="sm" tone="muted">已选: {{ selected.length }}</Text>
</Stack>
</template>
vue
行为
- 点选或者按 Enter 勾选一项后列表保持展开,再次点选取消勾选。
- 点击标签上的移除按钮与末尾的清除按钮不会打开列表。
- 清空按钮的出现与消失有过渡。
- 浮层贴着触发器展开,宽度与触发器相同,列表超出高度时在浮层内滚动。
- 打开期间页面锁定滚动,点击外部或者按 Esc 关闭。
无障碍
- 触发器为
role="combobox",列表为role="listbox",选项为role="option"并带aria-selected。 - 标签的移除按钮与清除按钮都有本地化名称,可以用 Tab 到达。
- 应当配合
label元素或者aria-label提供名称。invalid同时设置aria-invalid。
API
Props
T extends SelectOption 从 options 推断,默认是 SelectOption。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | Array<string | number> | [] | 选中的值 |
options | SelectItems<T> | — | 选项,类型见 Select |
virtualize | VirtualizeOptions | false | 虚拟滚动;预估行高按内容,overscan 6 |
placeholder | string | 语言包 | 无值时显示的文字 |
maxVisible | number | 2 | 触发器里最多显示的标签数 |
clearable | boolean | false | 是否显示清空全部的按钮 |
open | boolean | false | 浮层是否打开,支持 v-model:open |
variant | 'primary' | 'secondary' | 'primary' | 形态 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
invalid | boolean | false | 是否校验未通过 |
name | string | — | 原生表单字段名 |
required | boolean | false | 设置 name 后启用原生必填校验 |
autocomplete | string | — | 原生表单自动填充提示 |
disabled | boolean | false | 是否禁用 |
class | string | — | 追加至触发器的类名 |
插槽
| 插槽 | 参数 | 说明 |
|---|---|---|
option | { option: T } | 列表中每一项的内容 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue | value: Array<string | number> | 选中值变化 |
update:open | open: boolean | 浮层开合变化 |
clear | — | 全部清空 |
type VirtualizeOptions = boolean | { estimateSize?: number; overscan?: number }
ts