Element Plus Transfer 穿梭框组件完全指南:从基础用法到源码级原理剖析
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
导读
穿梭框(Transfer)是管理型后台系统中高频使用的数据转移组件,它通过左右两个面板配合中间操作按钮,让用户直观地在"可选数据"与"已选数据"之间移动条目。本文以 Element Plus(Vue 3 组件库)官方文档 Transfer 文档 为主体,逐项讲解其数据模型、筛选、自定义、属性别名、虚拟滚动等能力,并结合仓库内 transfer 组件源码 与各 composables 实现,揭示底层的数据切分、移动策略、事件派发与虚拟列表原理,帮助你既会用、又懂其实现。
一、基础用法:数据模型与 v-model 约定
Transfer 的核心设计是数据源驱动:左侧面板展示"未选中"的条目,右侧面板展示"已选中"的条目,二者共同由一份data数组和绑定到v-model的目标 key 数组推导而来。
1.1 data 的数据结构要求
data必须是对象数组,且每个对象需要包含三个约定字段:
| 字段 | 含义 |
|---|---|
key | 数据项的唯一标识,也是v-model数组中的元素 |
label | 面板中展示的文本 |
disabled | 该数据项是否禁用(禁用项不可勾选、不可移动) |
官方基础示例 basic.vue 展示了完整用法:
<template> <el-transfer v-model="value" :data="data" /> </template> <script lang="ts" setup> import { ref } from 'vue' interface Option { key: number label: string disabled: boolean } const generateData = () => { const data: Option[] = [] for (let i = 1; i <= 15; i++) { data.push({ key: i, label: `Option ${i}`, disabled: i % 4 === 0, // 每 4 项禁用一个,演示 disabled 效果 }) } return data } const data = ref<Option[]>(generateData()) const value = ref([]) // 初始右侧为空 </script>1.2 v-model 的同步语义
右侧列表的条目与v-model绑定的变量保持同步,该变量的值是一个"目标 key 数组"(Array<string | number>),默认值为[]。因此:
- 若希望右侧面板初始就有数据,只需把对应数据项的
key预先放入v-model数组; - 用户点击中间按钮移动条目时,组件会通过
update:model-value事件回写新数组(见 transfer.ts 中modelValue的声明); v-model数组仅存储 key,不存储完整对象,这是保持数据单一来源(data属性)的关键设计。
1.3 源码视角:左右面板数据如何推导
组件并没有维护"左侧数据"和"右侧数据"两份副本,而是通过 useComputedData 实时计算:
// 左侧 = data 中 key 不在 modelValue 里的条目 const sourceData = computed(() => props.data.filter( (item) => !props.modelValue.includes(item[propsAlias.value.key]) ) ) // 右侧 = data 中 key 在 modelValue 里的条目 const targetData = computed(() => { ... })也就是说,data永远是全量数据,左右两栏只是它的两个投影(projection),这保证了"右侧数据变化即 v-model 变化"的单向数据流。该推导逻辑在 transfer.vue 中被接入组件主体。
二、筛选模式(Filterable):内置过滤与自定义过滤
当数据量较大时,可开启filterable属性让用户在面板顶部输入关键字即时过滤。
2.1 内置过滤规则
在 useCheck.ts 中可以看到内置过滤的实现:默认情况下,只要数据项的label包含搜索关键字(不区分大小写)就会被保留;若label缺失,则退回到用key匹配:
const filteredData = computed(() => { return props.data.filter((item) => { if (isFunction(props.filterMethod)) { return props.filterMethod(panelState.query, item) } else { const label = String( item[propsAlias.value.label] || item[propsAlias.value.key] ) return label.toLowerCase().includes(panelState.query.toLowerCase()) } }) })2.2 自定义过滤方法 filter-method
内置规则无法满足需求时,可通过filter-method传入自定义函数。每当关键字变化时,组件会把搜索关键字和每一个数据项传给该方法;对某个数据项返回true即表示命中:
type filterMethod = (query: string, item: Record<string, any>) => boolean例如按编号或组合字段过滤:
<el-transfer v-model="value" :data="data" filterable :filter-method="filterMethod" /> <script lang="ts" setup> // 只匹配 label 以关键字开头的项,或 key 恰好等于数字关键字 const filterMethod = (query: string, item: Record<string, any>) => { return item.label.startsWith(query) || String(item.key) === query } </script>同时可用filter-placeholder自定义搜索输入框的占位文本(未设置时使用 i18n 默认文案el.transfer.filterPlaceholder,见 transfer.vue)。注意:开启虚拟滚动后,每次查询变化组件会调用虚拟列表的scrollToItem(0)将滚动位置重置到顶部(use-check.ts)。
三、自定义面板:标题、按钮、渲染内容与页脚
Transfer 提供了从标题、按钮文本到条目渲染、页脚内容的完整自定义能力。
3.1 标题与按钮文本
titles:[string, string],分别指定左、右面板标题,默认使用 i18n 文案el.transfer.titles.0/el.transfer.titles.1;button-texts:[string, string],分别指定"移到左侧""移到右侧"两个按钮的文字。仅在同时提供两个文本时按钮才会显示文字(源码中以hasButtonTexts = buttonTexts.length === 2判断,见 transfer.vue)。
3.2 自定义条目渲染:render-content 与默认插槽
有两种方式定制每个条目的展示:
方式一:render-content渲染函数。签名在 transfer.ts 中定义:
type renderContent<T extends TransferDataItem = TransferDataItem> = ( h: typeof H, option: T ) => VNode | VNode[]使用 JSX 示例(需要项目正确配置 JSX/TSX 相关依赖):
<script lang="tsx" setup> import { h } from 'vue' const renderContent = (h, option) => { return <span>{option.label} - <i style="color:#909399">{option.key}</i></span> } </script> <template> <el-transfer v-model="value" :data="data" :render-content="renderContent" /> </template>方式二:默认插槽(scoped slot)。插槽作用域为{ option: TransferDataItem }:
<el-transfer v-model="value" :data="data"> <template #default="{ option }"> <span>{{ option.label }}(编号 {{ option.key }})</span> </template> </el-transfer>在 transfer.vue 中,二者优先级是:render-content优先于默认插槽,默认插槽优先于内置的纯文本span渲染。
3.3 列表头部状态文本 format
format对象用于定制面板头部"已勾选/总数"的显示文本,类型为 TransferFormat:
interface TransferFormat { noChecked?: string hasChecked?: string }支持两个占位符:${checked}(已勾选数量)与${total}(面板总数)。实现见 use-check.ts:当同时提供noChecked与hasChecked时,会替换占位符生成文案;否则退化为默认的已勾选数/总数形式。例如:
<el-transfer v-model="value" :data="data" :format="{ noChecked: '共 ${total} 项', hasChecked: '已选 ${checked} / 共 ${total} 项', }" />3.4 页脚插槽与初始勾选
left-footer/right-footer:两个具名插槽分别注入左右面板底部内容(模板中透传给两个transfer-panel,见 transfer.vue);left-default-checked/right-default-checked:Array<string | number>,用于指定左右面板初始勾选的 key 数组。注意源码中对defaultChecked的 watch 会在数据变化时重新计算勾选集合,只保留"可勾选且存在于当前面板"的 key(use-check.ts)。
3.5 空内容自定义插槽 ^(2.9.0)
自 2.9.0 起提供left-empty与right-empty插槽,用于定制面板为空或过滤无结果时的展示内容(对应示例 empty-content.vue):
<el-transfer v-model="value" :data="data"> <template #left-empty>左侧暂无数据</template> <template #right-empty>右侧空空如也,快去选择吧</template> </el-transfer>四、属性别名(Props Aliases):适配任意字段命名
实际业务数据往往不叫key/label/disabled,此时无需改造数据,直接用props属性声明别名即可。类型为 TransferPropsAlias:
interface TransferPropsAlias { label?: string key?: string disabled?: string }官方示例 prop-alias.vue 中,数据项使用value与desc字段:
<el-transfer v-model="value" :props="{ key: 'value', label: 'desc', }" :data="data" /> <script lang="ts" setup> interface Option { value: number desc: string disabled: boolean } // data 中每项形如 { value: 1, desc: 'Option 1', disabled: false } </script>实现上,usePropsAlias 以{ label: 'label', key: 'key', disabled: 'disabled' }为默认值,再用传入的props浅合并覆盖,因此别名只需声明有差异的字段。整个组件内部(数据推导、过滤、勾选、移动)统一通过item[propsAlias.value.key]等表达式访问字段,这是所有自定义都能生效的底层前提。
五、虚拟滚动:大数据量下的性能方案 ^(2.14.3)
当数据量达到上千条(例如 2000 条)时,普通渲染会带来明显卡顿。Transfer 自 2.14.3 起支持虚拟滚动:
<el-transfer v-model="value" :data="data" virtual-scroll :item-size="30" />对应示例 virtual-scroll.vue 中一次渲染了 2000 条数据,开启后仅渲染可视区域内的条目。
5.1 相关属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
virtual-scroll | boolean | false | 是否启用虚拟滚动 |
item-size | number | 30 | 虚拟滚动时每项的高度(px),默认 30px |
5.2 源码实现要点
从 transfer-panel.vue 与 use-check.ts 可以看到,虚拟滚动复用了仓库内 virtual-list 组件 的FixedSizeList(定高列表):
- 面板通过
useElementSize监听勾选容器的实际高度,作为虚拟列表的可视区高度; - 勾选状态仍由面板内部的
checkboxGroupRef管理,保证虚拟滚动下勾选行为与普通模式一致; - 过滤查询变化时调用
virtualListRef.value?.scrollToItem(0)回到顶部(use-check.ts)。
由于虚拟列表依赖item-size精确计算滚动位置,自定义item-size时必须与实际行高保持一致,否则会出现条目错位。
六、目标列表排序策略 target-order
target-order控制右侧(目标)列表条目的排列顺序,取值'original' | 'push' | 'unshift',默认original:
| 取值 | 行为 |
|---|---|
original | 右侧始终按data原始顺序排列(默认) |
push | 新移入的条目追加到右侧底部 |
unshift | 新移入的条目插入到右侧顶部 |
源码层面,targetData的推导在 use-computed-data.ts 中分流:original模式下直接对data按modelValue过滤;push/unshift模式下则按modelValue的顺序逐个从dataObj(以 key 为索引的数据映射表)取值。而移动动作在 use-move.ts 中实现:addToRight先收集勾选且尚未在目标中的 key,再依据targetOrder决定拼接位置,最后调用_emit统一派发事件。
七、API 参考:属性、事件、插槽与 Exposes
以下内容完整对应官方文档 Transfer API 章节。
7.1 Transfer Attributes
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| model-value / v-model | 绑定值(目标 key 数组) | Array<string \| number> | [] |
| data | 数据源 | Record<string, any>[] | [] |
| filterable | 是否可筛选 | boolean | false |
| filter-placeholder | 筛选输入框占位符 | string | — |
| filter-method | 自定义筛选方法 | (query: string, item: Record<string, any>) => boolean | — |
| target-order | 目标列表排序策略:original(保持数据源顺序)、push(新项追加到底部)、unshift(新项插入到顶部) | 'original' \| 'push' \| 'unshift' | original |
| titles | 自定义面板标题 | [string, string] | [] |
| button-texts | 自定义按钮文本 | [string, string] | [] |
| render-content | 自定义数据项渲染函数 | renderContent | — |
| format | 列表头部勾选状态文本 | TransferFormat | {} |
| props | 数据源属性别名 | TransferPropsAlias | — |
| left-default-checked | 左侧面板初始勾选的 key 数组 | Array<string \| number> | [] |
| right-default-checked | 右侧面板初始勾选的 key 数组 | Array<string \| number> | [] |
| validate-event | 是否触发表单校验 | boolean | true |
| virtual-scroll ^(2.14.3) | 是否启用虚拟滚动 | boolean | false |
| item-size ^(2.14.3) | 虚拟滚动时每项高度(px) | number | 30 |
以上默认值在 transfer.ts 的buildProps中均有明确定义(如targetOrder通过values约束枚举、itemSize默认 30)。
7.2 Transfer Events
| 名称 | 说明 | 类型 |
|---|---|---|
| change | 右侧列表数据变化时触发 | (value: TransferKey[], direction: TransferDirection, movedKeys: TransferKey[]) => void |
| left-check-change | 左侧任意条目勾选状态变化时触发 | (value: TransferKey[], movedKeys?: TransferKey[]) => void |
| right-check-change | 右侧任意条目勾选状态变化时触发 | (value: TransferKey[], movedKeys?: TransferKey[]) => void |
事件校验器定义在 transfer.ts:change事件中direction只能是'left' | 'right',movedKeys为本次实际移动的 key 数组。勾选类事件的movedKeys是"新选中集合与旧选中集合的对称差",由面板在 use-check.ts 的 watch 中计算,再经 use-checked-change.ts 转发给上层。
<el-transfer v-model="value" :data="data" @change="(value, direction, movedKeys) => console.log(value, direction, movedKeys)" @left-check-change="(value, movedKeys) => console.log('left', value, movedKeys)" />7.3 Transfer Slots
| 名称 | 说明 | 作用域类型 |
|---|---|---|
| default | 自定义数据项内容 | { option: TransferDataItem } |
| left-footer | 左侧面板页脚内容 | — |
| right-footer | 右侧面板页脚内容 | — |
| left-empty ^(2.9.0) | 左侧面板为空或过滤无结果时内容 | — |
| right-empty ^(2.9.0) | 右侧面板为空或过滤无结果时内容 | — |
7.4 Transfer Exposes
通过模板 ref 可访问组件实例的公开方法/引用:
| 名称 | 说明 | 类型 |
|---|---|---|
| clearQuery | 清除指定面板的筛选关键字 | (which: TransferDirection) => void |
| leftPanel | 左侧面板 ref | Ref<TransferPanelInstance> |
| rightPanel | 右侧面板 ref | Ref<TransferPanelInstance> |
用法示例:
<script lang="ts" setup> import { ref } from 'vue' import type { TransferInstance } from 'element-plus' const transferRef = ref<TransferInstance>() // 清除左侧面板的搜索关键字 transferRef.value?.clearQuery('left') </script> <template> <el-transfer ref="transferRef" v-model="value" :data="data" filterable /> </template>7.5 Transfer Panel Exposes
leftPanel/rightPanel指向的TransferPanelInstance暴露一个query属性,即该面板当前的筛选关键字(string),可通过它读取或设置面板搜索词。
八、类型声明与表单集成
8.1 完整类型声明
组件对外公开的类型在 transfer.ts 中集中定义,可直接从element-plus导入使用:
import type { h as H, VNode } from 'vue' type TransferKey = string | number type TransferDirection = 'left' | 'right' type TransferDataItem = Record<string, any> type renderContent<T extends TransferDataItem = TransferDataItem> = ( h: typeof H, option: T ) => VNode | VNode[] interface TransferFormat { noChecked?: string hasChecked?: string } interface TransferPropsAlias { label?: string key?: string disabled?: string }8.2 与表单校验的集成
Transfer 实现了validate-event(默认true),可无缝嵌入el-form。在 transfer.vue 中,组件通过useFormItem取得父级表单上下文,并在modelValue变化时调用formItem?.validate?.('change'):
watch( () => props.modelValue, () => { if (props.validateEvent) { formItem?.validate?.('change').catch(NOOP) } } )因此,你可以在表单 rules 中为穿梭框配置校验规则(如"必须至少选择一项"),当用户在面板间移动条目时校验会自动触发。
九、测试与可验证依据
组件行为在 transfer.test.tsx 中有完整覆盖(含筛选、移动、事件、属性别名、虚拟滚动等用例),是理解各 API 实际语义的最佳参考。安装与引入方式与其他 Element Plus 组件一致:
import { ElTransfer } from 'element-plus'并按需引入样式(@element-plus/theme-chalk中的 transfer 样式或使用完整样式包)。
结语
Transfer 是一个"小身材、大能力"的组件:单向数据流 + 数据源投影的设计让v-model语义简单可靠;filter-method、render-content、props别名、format与多具名插槽共同撑起高自由度的定制空间;而虚拟滚动与target-order则分别解决了大数据渲染和排序策略两个实战痛点。掌握这些 API 及其源码实现,你就能在复杂业务中灵活驾驭数据穿梭场景。
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考