SwSelect
SwSelect 是下拉选择器:选项收进弹层、触发器收起态。单 / 多选一个组件(multiple 开关),支持可清空(clearable)、分组(选项 group 字段)、可搜索(searchable)与输入即建(creatable)。受控 v-model,单选为单值、多选为数组。
薄壳套 reka-ui:默认基于 Select 原语;searchable 时内部切换 Combobox 原语(触发器变输入框),对外仍是同一套 props / v-model / 插槽。弹层自渲染(不是原生 <select> 的 OS 弹层),故完全可定制;键盘导航 / 弹层定位 / a11y 开箱自带。视觉走「最小必要基础样式」——触发器 / 弹层 / 选中 / 高亮 / 禁用态默认都肉眼可辨,颜色 / 圆角等由 applier 覆盖。
选型:选项多、要省横向空间时用本组件(下拉)。选项少(2–5)、要全部可见、省一次点击时用 SwSegmented(平铺)。二者互补,如 antd 同时有
Select与Segmented。
单选
单选(状态筛选)
<script setup lang="ts">
const status = ref()
</script>
<template>
<SwSelect
v-model="status"
:options="[
{ value: 'all', label: '全部状态' },
{ value: 'ongoing', label: '连载中' },
{ value: 'completed', label: '已完结' },
]"
placeholder="选择状态"
aria-label="状态筛选"
/>
<!-- 整体禁用:父表单 pending / 无权限时锁住选择器 -->
<SwSelect v-model="status" :options="options" disabled />
</template>
多选 + 可清空
multiple 让 v-model 变数组;clearable 在有选中值时显示一键清空按钮。多选时触发器默认显示「已选 N 项」,要展示 tags / chips 走 #trigger 插槽:
多选 + 可清空(触发器显示计数)
分组
选项带 group 字段时,连续同名归一组、组首渲染分组标题;不带 group 则扁平:
选项分组(直辖市 / 浙江)
<template>
<SwSelect
v-model="city"
:options="[
{ value: 'bj', label: '北京', group: '直辖市' },
{ value: 'sh', label: '上海', group: '直辖市' },
{ value: 'hz', label: '杭州', group: '浙江' },
]"
/>
</template>
可搜索(searchable)
searchable 让触发器变输入框:输入按 label 本地过滤(大小写不敏感包含匹配,组内滤空整组隐藏),↑↓ + 回车可选。选项几十上百(演职员 / 标签 / 城市)时用它;集合大到不能一次拉全(作品库 / 全站用户)时用下面的远程搜索。
可搜索单选(输入过滤)
单选时输入框常显选中项 label(弹层打开后未输入前不过滤,避免被选中值锁死列表);多选时已选项渲染为 chips(label + ✕ 移除,Backspace 空输入删末位)。
远程搜索(remote + @search + loading)
三件套组合出「从大集合里搜一个选一个」:remote 关掉本地 label 过滤(options 本身就是远程结果集——服务端按拼音 / 别名 / id 命中的项 label 未必含关键词,本地再滤一遍会把它们藏掉);@search(term) 把用户输入交给业务(按 searchDebounce 防抖,默认 300ms;点清空立即发 '',据此复位列表);loading 在下拉里显示「加载中…」并压掉「无匹配」空态,当前列表保留不清空(防每次击键闪烁)。
结果陈旧期(用户已敲字、结果尚未回来:防抖窗口 ∪ loading)当前列表保留但不可选(选项 disabled + 变淡)——保留防塌陷跳动,不可选防「手快点下去选了与当前查询无关的对象」;creatable 的「创建」项同期藏掉。options 被整换后,已选项即使不在新结果集里,单选回显 / 多选 chips 的 label 也不会丢——组件内部缓存曾被选中值的 label(只记选过的,有界),同 value 的 label 后来变了以最新为准。
两条语义归业务:@search 收到的 '' 只表示「搜索词已清空」——复位 / 回初始列表 / 不搜由你定,不等于搜全部;remote 下 creatable 的「精确命中」只挡已加载页里的重复,真正的去重保证在服务端(唯一约束)。
远程搜索(模拟 500ms 接口,按导演名也能命中)
<SwSelect v-model="mediaId" searchable remote :loading="pending" :options="mediaOptions" @search="onSearch" />
async function onSearch(term: string) {
pending.value = true;
mediaOptions.value = await api.media.options({ keyword: term }); // 整换结果集
pending.value = false;
}
输入即建(creatable,标签选择场景)
creatable(仅 searchable 下生效)在输入无精确命中(大小写不敏感)时于列表尾部追加「创建 "xxx"」项。选中它不改 v-model,只发 @create(payload 为 trim 后输入文本)——业务调后端建项后自行把新 option 推进 options + v-model。multiple + searchable + creatable 组合即 tag-input 形态:
标签选择:多选 + 搜索 + 输入即建
<script setup lang="ts">
async function onCreateTag(name: string) {
const tag = await createTag(name) // 业务调后端建项
tagOptions.value.push({ value: tag.id, label: tag.name })
tagIds.value = [...tagIds.value, tag.id] // 推进 options + v-model
}
</script>
<template>
<SwSelect v-model="tagIds" multiple searchable creatable :options="tagOptions" @create="onCreateTag" />
</template>
Props
| 名称 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
modelValue(v-model) | string | number | (string | number)[] | undefined | 受控值。单选为单值,多选(multiple)为数组 | |
options | SwSelectOption[] | ✓ | — | 选项列表,每项 { value, label, disabled?, group? } |
multiple | boolean | false | 多选模式(v-model 为数组) | |
placeholder | string | '请选择' | 未选时触发器占位文案 | |
disabled | boolean | false | 整体禁用 | |
clearable | boolean | false | 有选中值时显示一键清空按钮 | |
searchable | boolean | false | 可搜索形态:触发器变输入框,输入按 label 本地过滤(大小写不敏感) | |
creatable | boolean | false | 输入无精确命中时显示「创建 "xxx"」项并 emit create;仅 searchable 下生效 | |
remote | boolean | false | 远程搜索:关掉本地 label 过滤(options 即远程结果集),配合 @search;仅 searchable 下有意义 | |
loading | boolean | false | 远程搜索进行中:下拉显示「加载中…」并压掉空态,当前列表保留不清空 | |
searchDebounce | number | 300 | @search 防抖毫秒数,0 = 不防抖 |
SwSelectOption:{ value: string \| number; label: string; disabled?: boolean; group?: string }(可从包名导入:import type { SwSelectOption } from '@swift-web/framework')
Events
| 名称 | 携带 | 说明 |
|---|---|---|
update:modelValue | value | value[] | undefined | 选中变化(清空时单选为 undefined、多选为 []) |
update:open | open: boolean | 弹层开 / 合 |
clear | — | 点击清空按钮触发 |
create | label: string | (searchable + creatable)选中「创建 "xxx"」项时触发;组件不改 v-model,业务建项后自行推 options + v-model |
search | term: string | (searchable)用户输入即发,payload 为 trim 后的搜索词,按 searchDebounce 防抖;点清空立即发 ''。单选回显 label 的程序性同步不发;非 remote 下也发(本地过滤照旧),便于埋点 / 联动 |
Slots
| 名称 | 作用域参数 | 说明 |
|---|---|---|
trigger | { selectedLabel, selectedOptions, count, hasValue } | 自定义触发器内容。不传则裸文案。仅非 searchable 分支有效(searchable 触发器是输入框) |
option | { option, selected } | 自定义单个选项视觉。不传则渲染 option.label |
样式定制 Token
触发器与弹层皮肤经 --sw-select-* token 覆盖(fallback 即默认视觉)。弹层 Portal 到 body,token 在 :root(或任意 body 祖先)覆盖即可,无需 :global 穿透:
| Token | 默认值 | 作用点 |
|---|---|---|
--sw-select-trigger-min-width | 8rem | 触发器最小宽 |
--sw-select-trigger-min-height | var(--control-height) | 触发器最小高 |
--sw-select-trigger-padding | 0.25rem 0.75rem | 触发器内边距 |
--sw-select-trigger-border | var(--input) | 触发器描边色 |
--sw-select-trigger-radius | var(--radius) | 触发器圆角 |
--sw-select-trigger-bg | var(--background) | 触发器底色 |
--sw-select-trigger-color | var(--foreground) | 触发器文字色 |
--sw-select-trigger-font-size | var(--control-font-size) | 触发器字号 |
--sw-select-chip-bg | var(--muted) | 多选 chips 底色(multiple + searchable) |
--sw-select-chip-color | var(--foreground) | 多选 chips 文字色 |
--sw-select-chip-radius | calc(var(--radius) - 0.25rem) | 多选 chips 圆角 |
--sw-select-content-max-height | 18rem | 弹层最大高 |
--sw-select-content-bg | var(--popover) | 弹层底色 |
--sw-select-content-border | var(--border) | 弹层描边色 |
--sw-select-content-radius | var(--radius) | 弹层圆角 |
--sw-select-content-shadow | var(--shadow-md) | 弹层阴影 |
--sw-select-content-font-size | var(--control-font-size) | 弹层字号 |
--sw-select-content-z | 1100 | 弹层层级(fw#332 z 层旋钮)。弹层 Portal 到 body,与其它浮层撞层时改这个 |
高度 / 字号默认吃表单控件族语义 token
--control-height(32px)/--control-font-size(14px)——SwInput / SwSelect / SwSegmented 同行混排默认等高;覆盖这两个 token 全站控件同步缩放,组件级 token 优先级更高。
注意事项
- 弹层经 Portal 传送到 body:applier 要改弹层样式(如字号)须用全局 /
:global选择器(scoped:deep够不到),命中.sw-select-content。 - 无内置视觉品牌:组件只给最小必要基础样式;颜色 / 圆角等由 applier 覆盖。
- 多选触发器默认显示「已选 N 项」:要展示 tags / chips 等用
#trigger插槽自定义。 - searchable 默认是本地过滤:只按
label大小写不敏感包含匹配;远程搜索必须加remote关掉它,否则服务端按别名 / 拼音命中的项会被本地过滤藏掉。数据源仍归业务(组件只交出@search的搜索词、承接options)。 @create不改 v-model:选中「创建 "xxx"」项只发事件——业务建项后必须自己把新 option 推进options并更新v-model,否则界面无变化。- searchable 时
#trigger插槽不生效:搜索形态的触发器是输入框(多选为 chips + 输入框),无自定义触发器插槽。 multiple时 v-model 必须是数组:多选把 v-model 初始化为单值(如ref(''))会让选中态、「已选 N 项」计数全错且类型崩——多选用ref([])、单选用单值。- 分组按「连续同名」归组:同名
group的选项必须相邻排列,散落的同名 group 会渲染成多个同名分组块——options 传入前先按 group 排序。