Appearance
Select 选择器
用于从选项集合中选择单个或多个值,适合枚举、状态、人员、城市、分类等字段。
基础用法
当前值:pending,清除次数:0
vue
<template>
<x-select v-model="value" placeholder="请选择" allow-clear @clear="clearCount++">
<x-select-option value="pending">待处理</x-select-option>
<x-select-option value="processing">处理中</x-select-option>
<x-select-option value="done">已完成</x-select-option>
<x-select-option value="disabled" disabled>禁用项</x-select-option>
</x-select>
<p>当前值:{{ value || '未选择' }},清除次数:{{ clearCount }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('pending');
const clearCount = ref(0);
</script>多选
请选择成员当前值:design, frontend
vue
<template>
<x-select
v-model="values"
multiple
allow-clear
:max-tag-count="2"
placeholder="请选择成员"
>
<x-select-option value="design" :tag-props="{ color: 'blue' }">设计</x-select-option>
<x-select-option value="frontend" :tag-props="{ color: 'green' }">前端</x-select-option>
<x-select-option value="backend">后端</x-select-option>
<x-select-option value="qa">测试</x-select-option>
</x-select>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const values = ref(['design', 'frontend']);
</script>搜索和自定义过滤
支持中文和英文拼写过滤
vue
<template>
<x-select
v-model="city"
allow-search
:options="cities"
:filter-option="filterCity"
placeholder="搜索城市"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const city = ref('');
const cities = [
{ label: '北京 Beijing', value: 'beijing' },
{ label: '上海 Shanghai', value: 'shanghai' },
{ label: '广州 Guangzhou', value: 'guangzhou' },
{ label: '深圳 Shenzhen', value: 'shenzhen' },
];
const filterCity = (input: string, option: { label?: string }) =>
option.label?.toLowerCase().includes(input.toLowerCase()) ?? false;
</script>允许创建
输入后选择新标签当前标签:vue
vue
<template>
<x-select v-model="tags" multiple allow-create placeholder="输入后选择新标签">
<x-select-option value="vue">Vue</x-select-option>
<x-select-option value="typescript">TypeScript</x-select-option>
<x-select-option value="vite">Vite</x-select-option>
</x-select>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const tags = ref(['vue']);
</script>选择数量限制
产品尝试选择第 3 项触发限制
vue
<template>
<x-select
v-model="members"
multiple
:limit="2"
:options="memberOptions"
placeholder="最多选择 2 项"
@exceed-limit="message = '最多只能选择 2 项'"
/>
<p>{{ message }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const members = ref(['pm']);
const message = ref('');
const memberOptions = [
{ label: '产品', value: 'pm' },
{ label: '设计', value: 'design' },
{ label: '前端', value: 'frontend' },
{ label: '后端', value: 'backend' },
];
</script>通过 options 渲染
北京 Beijing当前值:beijing
vue
<template>
<x-select v-model="value" :options="options" placeholder="请选择城市" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('beijing');
const options = [
{ label: '北京', value: 'beijing' },
{ label: '上海', value: 'shanghai' },
{ label: '禁用项', value: 'disabled', disabled: true },
];
</script>自定义字段名
北京
vue
<template>
<x-select
v-model="city"
:options="options"
:field-names="{ value: 'id', label: 'name', disabled: 'locked' }"
placeholder="请选择城市"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const city = ref('bj');
const options = [
{ id: 'bj', name: '北京' },
{ id: 'sh', name: '上海' },
{ id: 'gz', name: '广州', locked: true },
];
</script>选项分组
vue
<template>
<x-select v-model="city" placeholder="请选择城市">
<x-select-option-group label="华北">
<x-select-option value="beijing">北京</x-select-option>
<x-select-option value="tianjin">天津</x-select-option>
</x-select-option-group>
<x-select-option-group label="华东">
<x-select-option value="shanghai">上海</x-select-option>
<x-select-option value="hangzhou">杭州</x-select-option>
</x-select-option-group>
</x-select>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const city = ref('');
</script>状态和尺寸
vue
<template>
<x-space direction="vertical">
<x-select v-model="value" size="small" :options="options" placeholder="小尺寸" />
<x-select v-model="value" :options="options" disabled placeholder="禁用状态" />
<x-select v-model="value" :options="options" error placeholder="错误状态" />
<x-select v-model="value" :options="options" loading placeholder="加载中" />
<x-select v-model="value" :options="options" :bordered="false" placeholder="无边框" />
</x-space>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('');
const options = [
{ label: '选项 A', value: 'a' },
{ label: '选项 B', value: 'b' },
];
</script>自定义内容和插槽
状态
vue
<template>
<x-select v-model="value" placeholder="请选择">
<template #prefix>状态</template>
<template #header>
<div style="padding: 8px 12px">常用状态</div>
</template>
<template #footer>
<div style="padding: 8px 12px">没有合适选项时可联系管理员</div>
</template>
<template #label="{ data }">
{{ data.label }} / {{ data.value }}
</template>
<x-select-option value="pending" label="待处理">
待处理
<template #suffix>3</template>
</x-select-option>
<x-select-option value="done" label="已完成">
已完成
<template #suffix>8</template>
</x-select-option>
</x-select>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('pending');
</script>虚拟列表和滚动事件
虚拟与非虚拟列表共用 scrollbar 配置,并触发相同的滚动事件;传 scrollbar=false 可使用原生滚动条。
继续滚动查看更多
vue
<template>
<x-select
v-model="value"
:options="options"
:virtual-list-props="{ height: 200 }"
placeholder="大量数据"
@dropdown-reach-bottom="reached = true"
/>
<p>{{ reached ? '已滚动到底部' : '继续滚动查看更多' }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('');
const reached = ref(false);
const options = Array.from({ length: 100 }, (_, index) => ({
label: `选项 ${index + 1}`,
value: index + 1,
}));
</script>局部容器滚动
打开下拉后滚动下面的容器:默认跟随触发器。开启「滚动关闭」后,从打开时的位置滚动 24px 即收起;不需要同时设置 updateAtScroll。若需要固定在原位置,可传 triggerProps: { updateAtScroll: false }。
滚动关闭 · 下拉已关闭
选项 A
vue
<script setup lang="ts">
import { ref } from 'vue';
import { Select, Switch } from 'x-next';
const value = ref('a');
const popup = ref(false);
const closeOnScroll = ref(false);
const host = ref<HTMLElement>();
const switchAttrs = { 'aria-label': '滚动关闭' };
const options = [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }];
</script>
<template>
<section class="scroll-scope">
<Switch v-model="closeOnScroll" v-bind="switchAttrs" />
<span>{{ popup ? '下拉已打开' : '下拉已关闭' }}</span>
<div class="scroll-region">
<div class="scroll-content">
<Select
v-if="host"
v-model="value"
v-model:popup-visible="popup"
:options="options"
:popup-container="host"
:trigger-props="{ scrollToClose: closeOnScroll, scrollToCloseDistance: 24 }"
style="width: 220px"
/>
</div>
</div>
<div ref="host" class="scroll-overlay" />
</section>
</template>
<style scoped>
.scroll-region { height: 200px; overflow: auto; }
.scroll-content { min-height: 480px; padding: 80px 16px 0; }
.scroll-overlay { position: fixed; inset: 0; pointer-events: none; }
</style>键盘、焦点与父级 Escape
用 Tab 聚焦下面的选择器,再按 ↓ / ↑ 打开面板。没有已选的启用项时,↓ 激活首个启用项,↑ 激活最后一个启用项;已有选中项时优先激活该项。展开后上下键跳过禁用项并循环,Enter 选择活动项。通过方向键打开时会激活选项,即使设置了 defaultActiveFirstOption=false。
Enter 单选或 Esc 关闭面板后,焦点保留在选择器上。展开时第一次 Esc 只关闭选项面板,继续按 Esc 才到达父级;闭合选择器不会拦截 Esc,可供外层 Dialog 关闭。Tab / Shift+Tab 关闭面板并保留浏览器的焦点移动,点击外部也可正常转移焦点。禁用状态和输入法组合输入期间不执行上述键盘操作;加载期间不通过 Enter 或上下键选择 / 移动选项,仍可用 Escape 或 Tab 关闭已打开面板。
当前值:未选择;面板:关闭; Select 聚焦:否;父级收到 Escape:0 次
vue
<template>
<div @keydown="handleParentEscape">
<x-select
v-model="value"
v-model:popup-visible="popupVisible"
:options="options"
aria-label="键盘选择城市"
placeholder="Tab 聚焦后按 ↑ / ↓"
@focus="focused = true"
@blur="focused = false"
/>
<x-button>下一个焦点目标</x-button>
<p role="status">
当前值:{{ value || '未选择' }};面板:{{ popupVisible ? '展开' : '关闭' }};
Select 聚焦:{{ focused ? '是' : '否' }};父级收到 Escape:{{ parentEscapeCount }} 次
</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const value = ref('');
const popupVisible = ref(false);
const focused = ref(false);
const parentEscapeCount = ref(0);
const options = [
{ label: '不可用的起始项', value: 'disabled-first', disabled: true },
{ label: '北京', value: 'beijing' },
{ label: '上海', value: 'shanghai' },
{ label: '不可用的末尾项', value: 'disabled-last', disabled: true },
];
const handleParentEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape' && !event.isComposing && event.keyCode !== 229) {
parentEscapeCount.value++;
}
};
</script>默认触发器会自动关联 combobox、listbox 和 option 的 ARIA 状态。使用 trigger 自定义触发器时,插槽会提供 popupVisible、disabled、listboxId 和 activeDescendant;自定义节点需自行实现键盘事件、保持可聚焦,并设置对应的 aria-expanded、aria-controls 与 aria-activedescendant。
在 Form 中校验
组件放进 x-form-item 后会自动继承 Form 的尺寸与禁用状态,并按 FormItem 的校验规则展示错误。默认 validateTrigger 为 change,即值变化时触发校验。
vue
<template>
<x-form ref="formRef" :model="formState" :rules="formRules">
<x-form-item field="city" label="城市">
<x-select v-model="formState.city" :options="cityOptions" placeholder="请选择城市" allow-clear />
</x-form-item>
<x-form-item>
<x-space>
<x-button type="primary" @click="handleValidate">校验</x-button>
<x-button @click="handleReset">重置</x-button>
</x-space>
</x-form-item>
</x-form>
<p role="status">{{ validateResult }}</p>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance, FieldRule } from 'x-next';
const formState = reactive({ city: undefined });
const formRules: Record<string, FieldRule[]> = {
city: [{ required: true, message: '请选择城市' }],
};
const formRef = ref<FormInstance>();
const validateResult = ref('');
const handleValidate = async () => {
const errors = await formRef.value!.validate();
validateResult.value = errors
? `校验失败:${Object.values(errors).map((error) => error.message).join(';')}`
: '校验通过';
};
const handleReset = () => {
formRef.value!.resetFields();
validateResult.value = '已重置';
};
</script>按需导入
ts
import { Select, SelectOption, SelectOptionGroup, SelectDropdown } from 'x-next';样式按需引入(base.css 为共享基础层,多个组件只需引入一次):
ts
import 'x-next/style/base.css';
import 'x-next/style/form-select.css';Select Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
multiple | 是否多选 | boolean | false |
modelValue | 绑定值 | string | number | boolean | object | array | undefined |
defaultValue | 默认值,非受控模式 | string | number | boolean | object | array | 单选 '',多选 [] |
inputValue | 搜索输入值 | string | - |
defaultInputValue | 默认搜索输入值 | string | '' |
size | 选择器尺寸 | 'mini' | 'small' | 'medium' | 'large' | 跟随全局配置 |
placeholder | 占位提示 | string | - |
loading | 是否加载中 | boolean | false |
disabled | 是否禁用 | boolean | false |
error | 是否错误状态 | boolean | false |
allowClear | 是否允许清空 | boolean | false |
allowSearch | 是否允许搜索 | boolean | { retainInputValue?: boolean } | 单选 false,多选 true |
allowCreate | 是否允许创建新选项 | boolean | false |
maxTagCount | 多选最多显示标签数量,0 不限制 | number | 0 |
popupContainer | 弹出层挂载容器 | string | HTMLElement | - |
bordered | 是否显示边框 | boolean | true |
defaultActiveFirstOption | 无有效选中项时是否默认激活首个选项;方向键展开时按方向激活首 / 末项 | boolean | true |
popupVisible | 弹出层显示状态 | boolean | undefined |
defaultPopupVisible | 默认弹出层显示状态 | boolean | false |
unmountOnClose | 关闭时是否销毁弹出层 | boolean | false |
filterOption | 是否过滤选项或自定义过滤方法 | boolean | function | true |
options | 选项数据 | Array<string | number | boolean | SelectOptionData> | [] |
virtualListProps | 虚拟列表配置,传入后开启虚拟滚动 | object | - |
triggerProps | 下拉触发器配置;updateAtScroll 默认开启,可显式关闭 | object | { updateAtScroll: true } |
formatLabel | 格式化显示内容 | (data) => string | - |
fallbackOption | 自定义值不存在时的兜底选项 | boolean | function | true |
showExtraOptions | 是否显示额外选项 | boolean | true |
valueKey | 对象值的唯一键字段 | string | 'value' |
searchDelay | 搜索事件触发延迟 | number | 500 |
limit | 多选最多选择数量,0 不限制 | number | 0 |
fieldNames | 自定义选项字段名 | object | - |
scrollbar | 是否开启滚动条或滚动条配置 | boolean | object | true |
showHeaderOnEmpty | 空状态时是否显示 header | boolean | false |
showFooterOnEmpty | 空状态时是否显示 footer | boolean | false |
tagNowrap | 多选标签内容是否不换行 | boolean | false |
Select Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
update:modelValue | 值更新时触发 | value |
update:inputValue | 搜索输入值更新时触发 | inputValue |
update:popupVisible | 弹出层显示状态更新时触发 | visible |
change | 值变化时触发 | value |
input-value-change | 输入值变化时触发 | inputValue |
popup-visible-change | 弹出层显示状态变化时触发 | visible |
clear | 点击清除按钮时触发 | event |
remove | 删除多选标签时触发 | removed |
search | 用户搜索时触发 | inputValue |
dropdown-scroll | 下拉菜单滚动时触发 | event |
dropdown-reach-bottom | 下拉菜单滚动到底部时触发 | event |
exceed-limit | 多选超过数量限制时触发 | value, event |
Select Slots
| 插槽名 | 说明 |
|---|---|
empty | 空状态内容 |
option | 自定义选项内容 |
label | 自定义选择框显示内容 |
header | 下拉框页头 |
footer | 下拉框页脚 |
arrow-icon | 箭头图标 |
loading-icon | 加载中图标 |
search-icon | 搜索图标 |
prefix | 前缀元素 |
trigger | 自定义触发元素;参数:{ popupVisible, disabled, listboxId, activeDescendant } |
SelectOption Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
value | 选项值,不填时从内容获取 | string | number | boolean | object | undefined |
label | 选项标签,不填时从内容获取 | string | - |
disabled | 是否禁用 | boolean | false |
tagProps | 多选时展示标签的属性 | object | - |
index | 手动指定选项 index | number | - |
SelectOption Slots
| 插槽名 | 说明 |
|---|---|
default | 选项内容 |
icon | 选项图标 |
suffix | 选项后缀 |
SelectOptionGroup Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
label | 选项组标题 | string | - |
SelectOptionGroup Slots
| 插槽名 | 说明 |
|---|---|
default | 选项组内容 |
label | 自定义选项组标题 |