Select 选择器
下拉选择 —— 单/多选、可搜索、分组、loading 异步占位、键盘导航、tag 标签溢出收缩。
基础用法
通过 options 数组传入选项,每项至少包含 value 与 label。
背景
已选:shanghai
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
];
const city = ref<string | null>('shanghai');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="city" :options="options" placeholder="选一个城市" />
<p class="demo-hint">已选:<code>{{ city ?? 'null' }}</code></p>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
];
const city = ref<string | null>('shanghai');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="city" :options="options" placeholder="选一个城市" />
<p class="demo-hint">已选:<code>{{ city ?? 'null' }}</code></p>
</div>
</template> import { useState } from 'react';
import { CfSelect, type SelectOption } from '@chufix-design/react';
const options: SelectOption[] = [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
];
export default function Demo() {
const [city, setCity] = useState<string | null>('shanghai');
return (
<CfSelect
value={city}
options={options}
placeholder="选一个城市"
onChange={(v) => setCity(v as string | null)}
/>
);
} import { useState } from 'react';
import { CfSelect } from '@chufix-design/react';
const options= [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
];
export default function Demo() {
const [city, setCity] = useState<string | null>('shanghai');
return (
<CfSelect
value={city}
options={options}
placeholder="选一个城市"
onChange={(v) => setCity(v)}
/>
);
} 视觉变体
3 种 variant 与 Input 一致:outline(默认)/ filled / ghost。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'a', label: '选项 A' },
{ value: 'b', label: '选项 B' },
{ value: 'c', label: '选项 C' },
];
const v1 = ref<string | null>('a');
const v2 = ref<string | null>('b');
const v3 = ref<string | null>('c');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="v1" :options="options" variant="outline" />
<CfSelect v-model="v2" :options="options" variant="filled" />
<CfSelect v-model="v3" :options="options" variant="ghost" />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'a', label: '选项 A' },
{ value: 'b', label: '选项 B' },
{ value: 'c', label: '选项 C' },
];
const v1 = ref<string | null>('a');
const v2 = ref<string | null>('b');
const v3 = ref<string | null>('c');
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="v1" :options="options" variant="outline" />
<CfSelect v-model="v2" :options="options" variant="filled" />
<CfSelect v-model="v3" :options="options" variant="ghost" />
</div>
</template> <CfSelect value={v1} options={options} variant="outline" onChange={setV1} />
<CfSelect value={v2} options={options} variant="filled" onChange={setV2} />
<CfSelect value={v3} options={options} variant="ghost" onChange={setV3} /> <CfSelect value={v1} options={options} variant="outline" onChange={setV1} />
<CfSelect value={v2} options={options} variant="filled" onChange={setV2} />
<CfSelect value={v3} options={options} variant="ghost" onChange={setV3} /> 状态与可清空
clearable 在已选中时显示 ×;disabled 整体禁用;error 切到错误色边框。单个 option 可以加 disabled: true 跳过。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'chengdu', label: '成都(暂不可选)', disabled: true },
];
const a = ref<string | null>('shanghai');
const b = ref<string | null>(null);
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="a" :options="options" placeholder="可清空(输入后出现 ×)" clearable />
<CfSelect v-model="b" :options="options" placeholder="禁用" disabled />
<CfSelect v-model="b" :options="options" placeholder="错误状态" error />
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' },
{ value: 'chengdu', label: '成都(暂不可选)', disabled: true },
];
const a = ref<string | null>('shanghai');
const b = ref<string | null>(null);
</script>
<template>
<div class="demo-stack">
<CfSelect v-model="a" :options="options" placeholder="可清空(输入后出现 ×)" clearable />
<CfSelect v-model="b" :options="options" placeholder="禁用" disabled />
<CfSelect v-model="b" :options="options" placeholder="错误状态" error />
</div>
</template> <CfSelect value={a} options={options} clearable onChange={setA} />
<CfSelect value={b} options={options} disabled />
<CfSelect value={b} options={options} error /> <CfSelect value={a} options={options} clearable onChange={setA} />
<CfSelect value={b} options={options} disabled />
<CfSelect value={b} options={options} error /> 事件与表单
Select 除了值变化,也会暴露打开状态、active 选项、清空、焦点等事件;传入 name 时会同步一个 hidden input,方便在原生表单里提交当前值。
背景
api
等待交互:打开菜单、移动 active、选择或清空。<script setup lang="ts">
import { ref } from 'vue';
import {
CfBadge,
CfSelect,
type SelectChangeMeta,
type SelectOption,
type SelectValue,
} from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'api', label: 'API 事件' },
{ value: 'audit', label: '审计筛选' },
{ value: 'release', label: '发布分组' },
{ value: 'locked', label: '锁定项(不可选)', disabled: true },
];
const value = ref<SelectValue>('api');
const active = ref('api');
const logs = ref(['等待交互:打开菜单、移动 active、选择或清空。']);
function record(name: string, detail: string) {
logs.value = [`${name}: ${detail}`, ...logs.value].slice(0, 4);
}
function onChange(next: SelectValue, meta: SelectChangeMeta) {
value.value = next;
record('change', `${String(next ?? 'null')} / ${meta.option?.label ?? '无选项'}`);
}
function onSelect(option: SelectOption) {
active.value = String(option.value ?? '');
record('select', option.label);
}
function onActiveChange(option: SelectOption | null, index: number) {
active.value = option ? String(option.value) : 'none';
record('active-change', `${index} / ${option?.label ?? '无'}`);
}
</script>
<template>
<div class="select-events">
<CfSelect
:model-value="value"
:options="options"
placeholder="选择记录类型"
clearable
name="select-event-demo"
@change="onChange"
@select="onSelect"
@clear="record('clear', 'value 已重置为 null')"
@open-change="(open) => record('open-change', open ? 'opened' : 'closed')"
@active-change="onActiveChange"
@focus="record('focus', 'trigger focused')"
@blur="record('blur', 'trigger blurred')"
/>
<div class="select-events__status">
<CfBadge tone="info" :content="active || 'none'" />
<div class="select-events__log" aria-live="polite">
<code v-for="entry in logs" :key="entry">{{ entry }}</code>
</div>
</div>
</div>
</template>
<style scoped>
.select-events {
display: grid;
gap: 12px;
width: min(100%, 420px);
}
.select-events__status {
display: flex;
align-items: flex-start;
gap: 10px;
}
.select-events__log {
display: grid;
gap: 4px;
min-width: 0;
}
.select-events__log code {
white-space: normal;
}
</style> <script setup>
import { ref } from 'vue';
import {
CfBadge,
CfSelect,
} from '@chufix-design/vue';
const options= [
{ value: 'api', label: 'API 事件' },
{ value: 'audit', label: '审计筛选' },
{ value: 'release', label: '发布分组' },
{ value: 'locked', label: '锁定项(不可选)', disabled: true },
];
const value = ref<SelectValue>('api');
const active = ref('api');
const logs = ref(['等待交互:打开菜单、移动 active、选择或清空。']);
function record(name, detail) {
logs.value = [`${name}: ${detail}`, ...logs.value].slice(0, 4);
}
function onChange(next, meta) {
value.value = next;
record('change', `${String(next ?? 'null')} / ${meta.option?.label ?? '无选项'}`);
}
function onSelect(option) {
active.value = String(option.value ?? '');
record('select', option.label);
}
function onActiveChange(option, index) {
active.value = option ? String(option.value) : 'none';
record('active-change', `${index} / ${option?.label ?? '无'}`);
}
</script>
<template>
<div class="select-events">
<CfSelect
:model-value="value"
:options="options"
placeholder="选择记录类型"
clearable
name="select-event-demo"
@change="onChange"
@select="onSelect"
@clear="record('clear', 'value 已重置为 null')"
@open-change="(open) => record('open-change', open ? 'opened' : 'closed')"
@active-change="onActiveChange"
@focus="record('focus', 'trigger focused')"
@blur="record('blur', 'trigger blurred')"
/>
<div class="select-events__status">
<CfBadge tone="info" :content="active || 'none'" />
<div class="select-events__log" aria-live="polite">
<code v-for="entry in logs" :key="entry">{{ entry }}</code>
</div>
</div>
</div>
</template>
<style scoped>
.select-events {
display: grid;
gap: 12px;
width: min(100%, 420px);
}
.select-events__status {
display: flex;
align-items: flex-start;
gap: 10px;
}
.select-events__log {
display: grid;
gap: 4px;
min-width: 0;
}
.select-events__log code {
white-space: normal;
}
</style> <CfSelect
value={value}
options={options}
clearable
name="status"
onChange={(value, meta) => console.log(value, meta.option)}
onSelect={(option) => console.log('select', option)}
onClear={() => console.log('clear')}
onOpenChange={(open) => console.log('open', open)}
onActiveChange={(option, index) => console.log(option, index)}
/> <CfSelect
value={value}
options={options}
clearable
name="status"
onChange={(value, meta) => console.log(value, meta.option)}
onSelect={(option) => console.log('select', option)}
onClear={() => console.log('clear')}
onOpenChange={(open) => console.log('open', open)}
onActiveChange={(option, index) => console.log(option, index)}
/> 多选
multiple 让 model 变成数组,触发器渲染为可移除的 tag 组。maxTagCount 把多余的 tag 收成 +N 胶囊。退格键删除最后一个 tag。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'go', label: 'Go' },
{ value: 'rs', label: 'Rust' },
{ value: 'py', label: 'Python' },
{ value: 'rb', label: 'Ruby' },
];
const value = ref<Array<string>>(['ts', 'go']);
</script>
<template>
<CfSelect
v-model="value"
multiple
clearable
:options="options"
placeholder="选一种或多种语言"
style="max-width: 360px;"
/>
<p style="margin-top: 8px; color: var(--fg-3); font-size: 12px;">
当前:{{ value.length ? value.join(' / ') : '空' }} · 用退格键删除最近的标签
</p>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'go', label: 'Go' },
{ value: 'rs', label: 'Rust' },
{ value: 'py', label: 'Python' },
{ value: 'rb', label: 'Ruby' },
];
const value = ref<Array<string>>(['ts', 'go']);
</script>
<template>
<CfSelect
v-model="value"
multiple
clearable
:options="options"
placeholder="选一种或多种语言"
style="max-width: 360px;"
/>
<p style="margin-top: 8px; color: var(--fg-3); font-size: 12px;">
当前:{{ value.length ? value.join(' / ') : '空' }} · 用退格键删除最近的标签
</p>
</template> 可搜索
searchable 在菜单顶部加一个输入框,按 label 实时过滤。和 multiple 可叠加 —— 像传统 multi-select 一样组合使用。
背景
<script setup lang="ts">
import { computed, ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const COUNTRIES = [
'中国', '日本', '韩国', '美国', '加拿大', '英国', '法国', '德国',
'意大利', '西班牙', '葡萄牙', '荷兰', '比利时', '瑞士', '瑞典',
'挪威', '丹麦', '芬兰', '俄罗斯', '巴西', '墨西哥', '阿根廷',
'澳大利亚', '新西兰', '南非', '埃及', '印度', '新加坡', '马来西亚',
'泰国', '越南', '印度尼西亚', '土耳其', '希腊', '波兰', '捷克',
];
const options: SelectOption[] = COUNTRIES.map((c) => ({ value: c, label: c }));
const value = ref<string | null>(null);
</script>
<template>
<CfSelect
v-model="value"
searchable
clearable
:options="options"
placeholder="选择国家 / 地区"
style="max-width: 280px;"
/>
</template> <script setup>
import { computed, ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const COUNTRIES = [
'中国', '日本', '韩国', '美国', '加拿大', '英国', '法国', '德国',
'意大利', '西班牙', '葡萄牙', '荷兰', '比利时', '瑞士', '瑞典',
'挪威', '丹麦', '芬兰', '俄罗斯', '巴西', '墨西哥', '阿根廷',
'澳大利亚', '新西兰', '南非', '埃及', '印度', '新加坡', '马来西亚',
'泰国', '越南', '印度尼西亚', '土耳其', '希腊', '波兰', '捷克',
];
const options= COUNTRIES.map((c) => ({ value: c, label: c }));
const value = ref<string | null>(null);
</script>
<template>
<CfSelect
v-model="value"
searchable
clearable
:options="options"
placeholder="选择国家 / 地区"
style="max-width: 280px;"
/>
</template> 分组
给 option 加 group: string,组件自动按 group 渲染分组标题(顺序按 group 首次出现的次序)。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const options: SelectOption[] = [
{ value: 'us-east', label: 'US East', group: '美洲' },
{ value: 'us-west', label: 'US West', group: '美洲' },
{ value: 'sa-east', label: 'SA East', group: '美洲' },
{ value: 'eu-west', label: 'EU West', group: '欧洲' },
{ value: 'eu-central', label: 'EU Central', group: '欧洲' },
{ value: 'ap-tokyo', label: 'AP Tokyo', group: '亚太' },
{ value: 'ap-singapore', label: 'AP Singapore', group: '亚太' },
{ value: 'ap-shanghai', label: 'AP Shanghai', group: '亚太' },
];
const value = ref<string | null>('eu-west');
</script>
<template>
<CfSelect
v-model="value"
searchable
:options="options"
placeholder="选择 region"
style="max-width: 280px;"
/>
</template> <script setup>
import { ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const options= [
{ value: 'us-east', label: 'US East', group: '美洲' },
{ value: 'us-west', label: 'US West', group: '美洲' },
{ value: 'sa-east', label: 'SA East', group: '美洲' },
{ value: 'eu-west', label: 'EU West', group: '欧洲' },
{ value: 'eu-central', label: 'EU Central', group: '欧洲' },
{ value: 'ap-tokyo', label: 'AP Tokyo', group: '亚太' },
{ value: 'ap-singapore', label: 'AP Singapore', group: '亚太' },
{ value: 'ap-shanghai', label: 'AP Shanghai', group: '亚太' },
];
const value = ref<string | null>('eu-west');
</script>
<template>
<CfSelect
v-model="value"
searchable
:options="options"
placeholder="选择 region"
style="max-width: 280px;"
/>
</template> 异步加载
loading=true 时触发器变为禁用并展示 spinner,菜单内显示 “加载中…”。结合 onMounted / SWR / TanStack Query 等数据源使用。
背景
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { CfSelect, type SelectOption } from '@chufix-design/vue';
const loading = ref(true);
const options = ref<SelectOption[]>([]);
const value = ref<string | null>(null);
onMounted(() => {
setTimeout(() => {
options.value = [
{ value: 'a', label: '从远端加载的选项 A' },
{ value: 'b', label: '从远端加载的选项 B' },
{ value: 'c', label: '从远端加载的选项 C' },
];
loading.value = false;
}, 1200);
});
</script>
<template>
<CfSelect
v-model="value"
:options="options"
:loading="loading"
placeholder="loading=true 时显示 spinner"
style="max-width: 280px;"
/>
</template> <script setup>
import { onMounted, ref } from 'vue';
import { CfSelect } from '@chufix-design/vue';
const loading = ref(true);
const options = ref<SelectOption[]>([]);
const value = ref<string | null>(null);
onMounted(() => {
setTimeout(() => {
options.value = [
{ value: 'a', label: '从远端加载的选项 A' },
{ value: 'b', label: '从远端加载的选项 B' },
{ value: 'c', label: '从远端加载的选项 C' },
];
loading.value = false;
}, 1200);
});
</script>
<template>
<CfSelect
v-model="value"
:options="options"
:loading="loading"
placeholder="loading=true 时显示 spinner"
style="max-width: 280px;"
/>
</template> 键盘交互
| 按键 | 行为 |
|---|---|
↓ / ↑ | 打开菜单 / 在选项之间移动 active |
Enter / 空格 | 打开菜单 / 选中当前 active 项 |
Esc | 关闭菜单 |
Tab | 关闭菜单并把焦点交还浏览器 |
API
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
options | SelectOption[] | [] | 选项数组,{ value, label, disabled? } |
placeholder | string | '请选择' | 未选中时显示 |
variant | 'outline' | 'filled' | 'ghost' | 'outline' | 视觉变体 |
size | 'sm' | 'md' | 'lg' | 'md' | 整体尺寸 |
clearable | boolean | false | 已选中时显示清除按钮 |
disabled | boolean | false | 整体禁用 |
error | boolean | false | 错误态 |
name | string | — | 生成 hidden input,参与原生表单提交 |
id | string | — | 触发按钮 id,并用于关联 listbox |
multiple | boolean | false | 多选模式,model 变为 Array<value> |
searchable | boolean | false | 菜单顶部加搜索输入框 |
loading | boolean | false | 异步加载中(trigger 禁用 + spinner) |
emptyText | string | '无选项' | 自定义”无选项”文案 |
maxTagCount | number | — | 多选模式下最多展示几个 tag,溢出收成 +N |
Events
| Vue 事件 | React 回调 | payload | 说明 |
|---|---|---|---|
change | onChange | (value, { option }) | 值变化,清空时 option 为 null |
select | onSelect | option | 选中某个 option |
clear | onClear | — | 点击清除按钮 |
open-change | onOpenChange | open | 下拉菜单打开 / 关闭 |
active-change | onActiveChange | (option, index) | 键盘或鼠标移动 active option |
search | onSearch | term | 搜索框输入(仅 searchable=true 时) |
focus / blur | onFocus / onBlur | FocusEvent | 触发按钮焦点事件 |
当前实现使用
position: absolute浮层;如果父级overflow: hidden裁掉了菜单,后续会扩展 Portal 版本。
反馈与讨论
Select 选择器 的讨论