开发预览 更新于 2026-05-10

Select 选择器

下拉选择 —— 单/多选、可搜索、分组、loading 异步占位、键盘导航、tag 标签溢出收缩。

基础用法

通过 options 数组传入选项,每项至少包含 valuelabel

背景

已选: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>
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)}
  />
);
}

视觉变体

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>
<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>
<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>
<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>

可搜索

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>

分组

给 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>

异步加载

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>

键盘交互

按键行为
/ 打开菜单 / 在选项之间移动 active
Enter / 空格打开菜单 / 选中当前 active 项
Esc关闭菜单
Tab关闭菜单并把焦点交还浏览器

API

Prop类型默认值说明
optionsSelectOption[][]选项数组,{ value, label, disabled? }
placeholderstring'请选择'未选中时显示
variant'outline' | 'filled' | 'ghost''outline'视觉变体
size'sm' | 'md' | 'lg''md'整体尺寸
clearablebooleanfalse已选中时显示清除按钮
disabledbooleanfalse整体禁用
errorbooleanfalse错误态
namestring生成 hidden input,参与原生表单提交
idstring触发按钮 id,并用于关联 listbox
multiplebooleanfalse多选模式,model 变为 Array<value>
searchablebooleanfalse菜单顶部加搜索输入框
loadingbooleanfalse异步加载中(trigger 禁用 + spinner)
emptyTextstring'无选项'自定义”无选项”文案
maxTagCountnumber多选模式下最多展示几个 tag,溢出收成 +N

Events

Vue 事件React 回调payload说明
changeonChange(value, { option })值变化,清空时 optionnull
selectonSelectoption选中某个 option
clearonClear点击清除按钮
open-changeonOpenChangeopen下拉菜单打开 / 关闭
active-changeonActiveChange(option, index)键盘或鼠标移动 active option
searchonSearchterm搜索框输入(仅 searchable=true 时)
focus / bluronFocus / onBlurFocusEvent触发按钮焦点事件

当前实现使用 position: absolute 浮层;如果父级 overflow: hidden 裁掉了菜单,后续会扩展 Portal 版本。

反馈与讨论

Select 选择器 的讨论

0
0 / 600
一键发送
正在加载评论...