输入 sta,选择 status: 后继续选择 error。没有高亮候选时,回车应用整条查询。
已应用:entry:http
<script setup lang="ts">
import { ref } from 'vue'
import { Search } from '@lucide/vue'
import {
Autocomplete,
Tag,
Kbd,
Stack,
Text,
type AutocompleteOption,
type CompletionContext,
type CompletionEdit,
} from '@hina-ui/vue'
import { queryToken, queryValues } from '../../autocomplete'
const text = ref('entry:http ')
const applied = ref('entry:http ')
const options = ref<AutocompleteOption[]>([])
const descriptions: Record<string, string> = {
entry: '入口类型',
status: '请求状态',
duration: '请求耗时',
service: '服务名称',
}
function query(context: CompletionContext) {
const token = queryToken(context)
const values = token.key ? (queryValues[token.key] ?? []) : Object.keys(queryValues)
options.value = values
.filter(value => value.startsWith(token.prefix))
.map(value => ({
value,
label: token.key ? value : `${value}:`,
description: token.key ? undefined : descriptions[value],
}))
}
function complete(option: AutocompleteOption, context: CompletionContext): CompletionEdit {
const token = queryToken(context)
return { range: token.range, text: option.label, keepOpen: !token.key }
}
</script>
<template>
<Stack class="w-full max-w-lg">
<Autocomplete
v-model="text"
:options="options"
:get-completion="complete"
select-on-tab
aria-label="请求查询"
placeholder="entry:http status:error duration:>500ms"
@query="query"
@submit="applied = $event"
>
<template #leading><Search /></template>
<template #trailing>
<Tag v-if="text !== applied" size="sm" tone="warning" class="mx-2 whitespace-nowrap">
待应用
</Tag>
<Kbd v-else class="mx-2">Enter</Kbd>
</template>
</Autocomplete>
<Text tone="muted" size="sm">
输入 sta,选择 status: 后继续选择 error。没有高亮候选时,回车应用整条查询。
</Text>
<Text size="sm" class="break-all">已应用:{{ applied || '—' }}</Text>
</Stack>
</template>
用法
import { Autocomplete } from '@hina-ui/vue'
v-model 始终是输入框里的完整字符串。选择候选可以修改这段文本,失焦不会回滚或清空。需要把值限定在选项集合内时,使用 Combobox。
options 原样作为候选列表,组件不额外筛选。未提供 getCompletion 时,选择候选会用 option.label 替换整段文本并关闭列表。键入任意内容也有效。
选择建议,也可以保留自己输入的文字。
文本:—
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Autocomplete, FormField, Stack, Text } from '@hina-ui/vue'
const text = ref('')
const candidates = ['Vue', 'React', 'Svelte', 'Solid', 'Angular']
const options = computed(() =>
candidates
.filter(label => label.toLowerCase().includes(text.value.toLowerCase()))
.map(label => ({ value: label, label })),
)
</script>
<template>
<Stack class="w-full max-w-sm">
<FormField label="框架" description="选择建议,也可以保留自己输入的文字。">
<Autocomplete v-model="text" :options="options" placeholder="输入框架名称" />
</FormField>
<Text tone="muted" size="sm">文本:{{ text || '—' }}</Text>
</Stack>
</template>
示例
光标处连续补全
首个示例演示查询片段补全。输入 sta 后用方向键选中 status:,回车确认;列表保持打开,继续提供 ok、error、timeout。选择值后关闭列表,再按回车应用整条查询。也可以把光标移回已有片段进行替换,后面的文本会保留。
@query 提供当前文本与选区,输入、移动光标、改变选区和完成补全时都会通知。getCompletion(option, context) 同步返回替换范围与插入文本;范围采用原生输入框的 UTF-16 偏移,左闭右开。组件完成替换后把光标放到插入文本末尾。
function complete(option, context) {
const range = locateToken(context)
return {
range,
text: option.label,
keepOpen: option.kind === 'key',
}
}
locateToken、键值判断和候选生成由应用提供。keepOpen: true 可连续补全,完成后新的 query 事件携带更新后的文本与光标。不要缓存旧选区后再用于替换,应使用 getCompletion 本次收到的 context。范围超出当前文本时抛出 RangeError。
远程候选
loading 显示加载指示和列表中的状态提示,保留输入文字。异步替换 options 会清除旧高亮,新结果不会自动选中第一项。无候选时可以通过 #empty 自定义提示;加载文案使用 #loading。
请求、防抖、取消和过期响应处理由调用方负责。示例请求随文档部署的静态 JSON,在调用方筛选结果;改为实际搜索接口时保留相同的数据流即可。
输入时请求候选,也允许保留候选之外的文本。
文本:—
<script setup lang="ts">
import { ref, watch } from 'vue'
import { refDebounced } from '@vueuse/core'
import { useRuntimeConfig } from '#app'
import { Search } from '@lucide/vue'
import {
Autocomplete,
FormField,
Stack,
Text,
type AutocompleteOption,
type CompletionContext,
} from '@hina-ui/vue'
const text = ref('')
const search = ref('')
const keyword = refDebounced(search, 250)
const options = ref<AutocompleteOption[]>([])
const loading = ref(false)
const error = ref(false)
const baseURL = useRuntimeConfig().app.baseURL
function query(context: CompletionContext) {
if (search.value === context.text) return
search.value = context.text
options.value = []
loading.value = !!context.text.trim()
error.value = false
}
watch(keyword, async (value, _, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
if (!value.trim()) {
options.value = []
loading.value = false
return
}
loading.value = true
error.value = false
try {
const response = await fetch(`${baseURL}demo/tags.json`, { signal: controller.signal })
if (!response.ok) throw new Error(String(response.status))
const result = (await response.json()) as {
data: { items: { id: number; name: string; nameEn: string }[] }
}
if (controller.signal.aborted || search.value !== value) return
options.value = result.data.items
.filter(tag => tag.name.toLowerCase().includes(value.trim().toLowerCase()))
.slice(0, 10)
.map(tag => ({ value: tag.id, label: tag.name }))
} catch {
if (!controller.signal.aborted && search.value === value) error.value = true
} finally {
if (!controller.signal.aborted && search.value === value) loading.value = false
}
})
</script>
<template>
<Stack class="w-full max-w-sm">
<FormField label="标签搜索" description="输入时请求候选,也允许保留候选之外的文本。">
<Autocomplete
v-model="text"
:options="options"
:loading="loading"
placeholder="试试「小说」或「音乐」"
@query="query"
>
<template #leading><Search /></template>
<template #empty>
{{
error
? '候选加载失败,修改文字后重试。'
: text.trim()
? '没有建议,仍可使用当前文本。'
: '输入文字搜索标签。'
}}
</template>
</Autocomplete>
</FormField>
<Text tone="muted" size="sm">文本:{{ text || '—' }}</Text>
</Stack>
</template>
尺寸与状态
输入面沿用 Input 的尺寸与形态。放进 FormField 后自动关联标签、说明、错误和禁用状态。未声明的属性(如 name、maxlength、aria-label)传给内部输入框。
请输入状态值。
<script setup lang="ts">
import { Autocomplete, FormField, Stack } from '@hina-ui/vue'
const options = [
{ value: 'http', label: 'entry:http' },
{ value: 'rpc', label: 'entry:rpc' },
]
</script>
<template>
<Stack class="w-full max-w-sm" gap="lg">
<FormField label="小号">
<Autocomplete :options="options" size="sm" placeholder="输入查询" />
</FormField>
<FormField label="次级形态">
<Autocomplete :options="options" variant="secondary" placeholder="输入查询" />
</FormField>
<FormField label="大号">
<Autocomplete :options="options" size="lg" placeholder="输入查询" />
</FormField>
<FormField label="校验失败" error="请输入状态值。">
<Autocomplete :options="[]" model-value="status:" />
</FormField>
<FormField label="只读">
<Autocomplete :options="options" model-value="entry:http" readonly />
</FormField>
<FormField label="禁用" disabled>
<Autocomplete :options="options" model-value="entry:http" />
</FormField>
</Stack>
</template>
在表单中
通过 FormField 的 name 关联校验规则,错误与提交期间的禁用状态会自动传给输入框。候选只提供补全建议,用户输入的其他文本也可以提交。
将 @submit 接到 Form 实例的 submit():回车有高亮候选时只完成补全,没有高亮候选时才触发表单校验与提交;保存按钮也走同一套校验。
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import * as v from 'valibot'
import { Autocomplete, Button, Form, FormField, Text } from '@hina-ui/vue'
const form = ref<InstanceType<typeof Form>>()
const values = reactive({ framework: '' })
const saved = ref('')
const candidates = ['Vue', 'React', 'Svelte', 'Solid', 'Angular']
const options = computed(() =>
candidates
.filter(label => label.toLowerCase().includes(values.framework.toLowerCase()))
.map(label => ({ value: label, label })),
)
const schema = v.object({
framework: v.pipe(v.string(), v.trim(), v.nonEmpty('请输入框架名称')),
})
async function save(data: unknown) {
await new Promise(resolve => setTimeout(resolve, 600))
saved.value = (data as { framework: string }).framework
}
</script>
<template>
<Form
ref="form"
v-slot="{ submitting }"
:values="values"
:rules="schema"
class="w-full max-w-sm"
@submit="save"
>
<FormField
name="framework"
label="主要框架"
description="选择建议,也可以填写其他框架。"
required
>
<Autocomplete
v-model="values.framework"
:options="options"
name="framework"
placeholder="选择或输入框架名称"
@submit="form?.submit()"
/>
</FormField>
<Button type="submit" :loading="submitting" class="self-start">保存</Button>
<Text v-if="saved" role="status" tone="muted" size="sm">已保存:{{ saved }}</Text>
</Form>
</template>
键盘与焦点
- 聚焦、点击或输入时打开列表,保持输入框焦点,不自动高亮首项。
- 上下方向键移动高亮并跳过禁用项,列表内部滚动到当前项,不滚动外层页面。
- 回车有高亮时仅接受候选;没有高亮时关闭列表并触发
submit(text),不会同时触发浏览器表单提交。 selectOnTab默认关闭。开启后,Tab 只在有高亮候选时接受它并保留输入焦点;没有高亮、Shift+Tab 仍正常移动焦点。- Esc 先关闭列表;列表已关闭时清空文本并触发
clear。关闭和清空都不会应用查询。 - 中文等输入法组词期间不处理候选选择、提交或清除,组词完成后再更新建议。
- 失焦保留文本。
readonly可聚焦、选择和复制文本,但不打开建议;disabled禁止交互。
输入框、列表和候选分别使用 combobox、listbox、option 语义,活动项由 aria-activedescendant 关联。为组件提供 FormField、关联的标签或 aria-label。候选内容插槽用于展示,不应嵌入按钮、链接等独立交互控件。
API
Props
T extends AutocompleteOption 从 options 推断,额外业务字段在回调和插槽中保留类型。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | string | '' | 完整文本,支持 v-model |
open | boolean | false | 浮层状态,支持 v-model:open |
options | readonly T[] | — | 当前候选, value 在列表中唯一且稳定 |
getCompletion | (option: T, context: CompletionContext) => CompletionEdit | — | 返回同步文本编辑;默认整段替换为 label |
loading | boolean | false | 加载状态;已有候选仍可选,新请求需清除过期候选时由调用方置空 |
selectOnTab | boolean | false | Tab 接受高亮候选 |
placeholder | string | — | 占位文字 |
variant | 'primary' | 'secondary' | 'primary' | 输入面形态 |
size | 'sm' | 'md' | 'lg' | 'md' | 尺寸 |
disabled | boolean | false | 禁用 |
readonly | boolean | false | 只读 |
invalid | boolean | false | 校验失败 |
class | string | — | 输入面样式 |
插槽
| 插槽 | 参数 | 说明 |
|---|---|---|
leading | — | 左侧图标等附加内容 |
trailing | — | 右侧快捷键提示或状态标记;RTL 下随逻辑方向排列 |
option | { option: T, active: boolean } | 候选展示内容,行的交互与无障碍由组件负责 |
empty | — | 无候选提示 |
loading | — | 列表内加载提示 |
事件
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue | string | 文本变化 |
update:open | boolean | 浮层开关变化 |
query | CompletionContext | 聚焦或重新打开、文本或选区变化、补全完成 |
select | AutocompleteSelection<T> | 候选被接受,包含编辑前上下文和所应用的编辑 |
submit | string | 没有高亮候选时回车应用完整文本 |
clear | — | 列表关闭时按 Esc 清空非空文本 |
实例
input 暴露内部 HTMLInputElement,可使用 setSelectionRange() 操作选区;focus()、blur() 分别聚焦和失焦。实例仅在挂载后可用。
类型
interface AutocompleteOption {
value: string | number
label: string
description?: string
disabled?: boolean
}
interface CompletionContext {
text: string
selectionStart: number
selectionEnd: number
}
interface CompletionEdit {
range: [number, number]
text: string
keepOpen?: boolean
}
interface AutocompleteSelection<T extends AutocompleteOption = AutocompleteOption> {
option: T
context: CompletionContext
edit: CompletionEdit
}