<script setup lang="ts">
import { ref } from 'vue'
import { Combobox } from '@hina-ui/vue'
const work = ref<string | null>(null)
const works = [
{ value: 'summer-pockets', label: 'Summer Pockets' },
{ value: 'clannad', label: 'CLANNAD' },
{ value: 'little-busters', label: 'Little Busters!' },
{ value: 'rewrite', label: 'Rewrite' },
{ value: 'air', label: 'AIR' },
{ value: 'kanon', label: 'Kanon' },
]
</script>
<template>
<Combobox v-model="work" :options="works" placeholder="搜索作品" aria-label="作品" class="w-64" />
</template>
用法
import { Combobox } from '@hina-ui/vue'
组合框由输入框与浮层列表组成,与 Select 共用同一套选项数据。v-model 绑定选中的值,输入文字即按选项文字筛选列表,末尾的按钮展开或者收起列表。输入区与输入框共用同一副输入面,未声明的属性都会传给内部的 input。
当前值:key
<script setup lang="ts">
import { ref } from 'vue'
import { Combobox, Stack, Text } from '@hina-ui/vue'
const studio = ref<string | null>('key')
const studios = [
{ value: 'key', label: 'Key' },
{ value: 'type-moon', label: 'TYPE-MOON' },
{ value: 'august', label: 'August' },
{ value: 'saga-planets', label: 'SAGA PLANETS' },
{ value: 'yuzusoft', label: 'ゆずソフト' },
]
</script>
<template>
<Stack class="w-64">
<Combobox v-model="studio" :options="studios" aria-label="制作商" />
<Text tone="muted">当前值:{{ studio ?? '无' }}</Text>
</Stack>
</template>
组件从 options 推断完整选项类型,插槽中的 option 保留额外字段及其类型;v-model 仍绑定 value。类型定义见 Select。
示例
分组
分组项带 label 与 options,筛选时空的分组会一并隐藏。
<script setup lang="ts">
import { ref } from 'vue'
import { Combobox } from '@hina-ui/vue'
const tag = ref<string | null>(null)
const tags = [
{
label: '题材',
options: [
{ value: 'school', label: '校园' },
{ value: 'sf', label: '科幻' },
{ value: 'fantasy', label: '奇幻' },
],
},
{
label: '形式',
options: [
{ value: 'kinetic', label: '线性剧情' },
{ value: 'branch', label: '多线分支' },
],
},
]
</script>
<template>
<Combobox v-model="tag" :options="tags" placeholder="搜索标签" aria-label="标签" class="w-64" />
</template>
远程数据
ignoreFilter 关闭本地筛选,v-model:search 提供输入文字,调用方将搜索结果直接传给 options。下拉列表只渲染这份结果。
selectedOption 单独提供已选项资料,value 与 v-model 匹配时用于显示名称,不会加入候选列表,也不会改变选中值。组件会记住选项名称,搜索结果替换或清空后仍能回显。资料异步到达或名称更新时同步显示,正在输入的搜索词不会被覆盖;关闭列表后恢复最新名称。同一值同时出现在两份资料中时,回显名称以 selectedOption 为准。
loading 将展开按钮中的箭头切换为加载指示,并保留输入、选择和清除操作。请求状态与防抖由调用方控制。
示例请求随文档发布的静态 JSON,并在调用方模拟筛选,无需服务端代理。接入实际接口时替换请求地址与结果映射。
候选:0 · 选中值:31
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { refDebounced, useFetch } from '@vueuse/core'
import { useRuntimeConfig } from '#app'
import { Combobox, Stack, Text } from '@hina-ui/vue'
interface TagPage {
data: { items: Array<{ id: number; name: string; nameEn: string }> }
}
const selectedOption = { value: 31, label: '轻小说' }
const selected = ref<string | number | null>(selectedOption.value)
const baseURL = useRuntimeConfig().app.baseURL
const search = ref('')
const keyword = refDebounced(search, 300)
const url = computed(
() => `${baseURL}demo/tags.json?${new URLSearchParams({ search: keyword.value })}`,
)
const { data, isFetching, execute } = useFetch(url, {
refetch: true,
immediate: false,
}).json<TagPage>()
const options = computed(
() =>
data.value?.data.items
.filter(tag =>
tag.name.toLocaleLowerCase().includes(keyword.value.trim().toLocaleLowerCase()),
)
.slice(0, 10)
.map(tag => ({ value: tag.id, label: tag.name })) ?? [],
)
onMounted(() => execute())
</script>
<template>
<Stack class="w-64">
<Combobox
v-model="selected"
v-model:search="search"
:options="options"
:selected-option="selectedOption"
:loading="isFetching"
ignore-filter
clearable
placeholder="搜索标签"
aria-label="标签"
/>
<Text tone="muted">候选:{{ options.length }} · 选中值:{{ selected ?? '无' }}</Text>
</Stack>
</template>
可清除
clearable 在有值时显示清除按钮,点击后清空值与输入文字。
<script setup lang="ts">
import { ref } from 'vue'
import { Combobox } from '@hina-ui/vue'
const work = ref<string | null>('clannad')
const works = [
{ value: 'summer-pockets', label: 'Summer Pockets' },
{ value: 'clannad', label: 'CLANNAD' },
{ value: 'rewrite', label: 'Rewrite' },
]
</script>
<template>
<Combobox v-model="work" :options="works" clearable aria-label="作品" class="w-72" />
</template>
尺寸
三档尺寸与输入框相同。
<script setup lang="ts">
import { Combobox, Stack } from '@hina-ui/vue'
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: '轻小说' },
]
</script>
<template>
<Stack class="w-64">
<Combobox size="sm" :options="options" model-value="gal" aria-label="小号" />
<Combobox size="md" :options="options" model-value="gal" aria-label="中号" />
<Combobox size="lg" :options="options" model-value="gal" aria-label="大号" />
</Stack>
</template>
形态
primary 直接放在页面底色上,带边框与阴影;secondary 放在卡片等表面内,只有一层浅色底。
<script setup lang="ts">
import { Card, Combobox, Stack } from '@hina-ui/vue'
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: '轻小说' },
]
</script>
<template>
<Stack class="w-64">
<Combobox :options="options" placeholder="直接放在页面上" aria-label="页面上的组合框" />
<Card>
<Combobox
variant="secondary"
:options="options"
placeholder="放在卡片内"
aria-label="卡片内的组合框"
/>
</Card>
</Stack>
</template>
状态
invalid 标出校验未通过,disabled 禁用整个组合框,选项上的 disabled 只禁用该项。
<script setup lang="ts">
import { Combobox, Stack } from '@hina-ui/vue'
const options = [
{ value: 'gal', label: 'Galgame' },
{ value: 'ln', label: '轻小说' },
{ value: 'manga', label: '漫画', disabled: true },
]
</script>
<template>
<Stack class="w-64">
<Combobox invalid :options="options" placeholder="请选择类型" aria-label="校验未通过" />
<Combobox disabled :options="options" model-value="gal" aria-label="已禁用" />
</Stack>
</template>
在表单中
放进 FormField 后,标签指向输入区,说明与错误信息由字段渲染并关联到控件;校验规则与提交交给 Form。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import * as v from 'valibot'
import { Button, Combobox, Form, FormField, Text } from '@hina-ui/vue'
const cities = [
{ label: '东京', value: 'tokyo' },
{ label: '大阪', value: 'osaka' },
{ label: '京都', value: 'kyoto' },
{ label: '札幌', value: 'sapporo' },
{ label: '福冈', value: 'fukuoka' },
]
const schema = v.object({
city: v.pipe(v.string('请选择城市'), v.nonEmpty('请选择城市')),
})
const values = reactive({ city: null as string | number | null })
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="city" label="所在城市" required>
<Combobox v-model="values.city" :options="cities" />
</FormField>
<Button type="submit" :loading="submitting" class="self-start">保存</Button>
<Text v-if="saved" tone="muted" size="sm">已保存:{{ saved }}</Text>
</Form>
</template>
虚拟滚动
virtualize 按需渲染可见范围附近的条目,与 VirtualList 共用测量与滚动底层。默认关闭;可传 { estimateSize, overscan } 调整预估行高和两侧预渲染数量,行高会按实际内容测量。键盘导航覆盖完整数据,禁用项会跳过。 搜索仍处理完整数据。 条目离开渲染范围后会卸载;插槽内需要持久保留的状态应按唯一 value 存在外部。
已选: 7890
<script setup lang="ts">
import { ref } from 'vue'
import { Combobox, Stack, Text } from '@hina-ui/vue'
const selected = ref<string | number | null>(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-64 max-w-full">
<Combobox
v-model="selected"
:options="options"
:virtualize="{ estimateSize: 36, overscan: 6 }"
aria-label="一万项"
/>
<Text size="sm" tone="muted">已选: {{ selected }}</Text>
</Stack>
</template>
行为
- 点击输入区或者按下方向键打开列表,输入文字时按选项文字筛选,无匹配时显示提示。
- 选中后列表关闭,输入框显示选项文字;失焦时未选中的输入文字恢复为已选项,把文字删干净则清除选中值。
- 浮层贴着输入区展开,宽度与输入区相同,列表超出高度时在浮层内滚动。
- 展开按钮与清除按钮不会让输入区失焦。
无障碍
- 输入区为
role="combobox"并带aria-autocomplete="list",列表为role="listbox",选项为role="option"并带aria-selected。 - 展开按钮不进入 Tab 序列,名称随语言包本地化。
- 加载时输入面设置
aria-busy="true"。 - 应当配合
label元素或者aria-label提供名称。invalid同时设置aria-invalid。
API
Props
T extends SelectOption 从 options 与 selectedOption 推断,默认是 SelectOption。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | string | number | null | — | 选中的值 |
options | SelectItems<T> | — | 选项,类型见 Select |
virtualize | VirtualizeOptions | false | 虚拟滚动;预估行高按内容,overscan 6 |
selectedOption | T | null | — | 已选项资料,仅用于回显,不加入候选列表 |
search | string | '' | 当前输入的文字,支持 v-model:search |
placeholder | string | 语言包 | 无值时显示的文字 |
ignoreFilter | boolean | false | 是否交由调用方筛选 |
loading | boolean | false | 是否显示加载指示 |
clearable | boolean | false | 是否显示清除按钮 |
open | boolean | false | 浮层是否打开,支持 v-model:open |
variant | 'primary' | 'secondary' | 'primary' | 形态 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
invalid | boolean | false | 是否校验未通过 |
disabled | boolean | false | 是否禁用 |
class | string | — | 追加至根元素的类名 |
插槽
| 插槽 | 参数 | 说明 |
|---|---|---|
option | { option: T } | 列表中每一项的内容 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue | value: string | number | 选中值变化 |
update:search | search: string | 输入文字变化 |
update:open | open: boolean | 浮层开合变化 |
clear | — | 被清空 |
type VirtualizeOptions = boolean | { estimateSize?: number; overscan?: number }