Naive UI Legacy Transfer 组件完整指南:双向穿梭选择器的 Props、过滤与虚拟滚动实战

发布时间:2026/9/21 18:36:23
Naive UI Legacy Transfer 组件完整指南:双向穿梭选择器的 Props、过滤与虚拟滚动实战 Naive UI Legacy Transfer 组件完整指南双向穿梭选择器的 Props、过滤与虚拟滚动实战【免费下载链接】naive-uiA Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.项目地址: https://gitcode.com/gh_mirrors/na/naive-ui本指南以 Naive UI 仓库中legacy-transfer组件旧版穿梭框的官方文档 index.demo-entry.md 为主体结合其源码实现与单元测试系统讲解该组件的全部 Props、TransferOption数据结构、过滤、尺寸与虚拟滚动等核心能力并说明其已被新版 Transfer 取代的迁移背景。读完本文你将能够熟练使用n-legacy-transfer完成左右列表的数据穿梭并在超大数据量场景下正确开启虚拟滚动。组件定位与弃用声明n-legacy-transfer旧版穿梭框是一个典型的左、右、左、右双向列表选择组件用户在左侧Source列表中勾选选项点击按钮将其移动到右侧Target列表也可反向移回。仓库文档明确给出WarningThe transfer component is deprecated. It wont have any new feature and will be removed in the next major version. Its recommended to use new Transfer.这意味着该组件不再增加任何新功能将在下一个大版本中被移除官方推荐改用新版 Transfer 穿梭框组件。因此本文介绍的内容主要适用于维护存量代码、迁移老项目或阅读源码的场景新项目建议直接使用新版 Transfer。在源码层面组件注册名为LegacyTransfer对外导出为NLegacyTransfer其导出入口见 legacy-transfer/index.tsexport type { Option as LegacyTransferOption } from ./src/interface export { transferProps as legacyTransferProps, default as NLegacyTransfer } from ./src/Transfer基本用法与数据驱动模型与大多数受控组件一致n-legacy-transfer由options全部选项和value当前已选到右侧的值数组两个数据源驱动用户勾选并通过中间按钮移动后触发on-update:value更新值。参考官方示例 basic.demo.vue一个最基础的用法如下script langts setup import { ref } from vue function createOptions() { return Array.from({ length: 100 }).map((v, i) ({ label: Option ${i}, value: i, disabled: i % 5 0 })) } function createValues() { return Array.from({ length: 50 }).map((v, i) i) } const options createOptions() const value ref(createValues()) /script template n-legacy-transfer v-model:valuevalue :optionsoptions / /template要点说明v-model:value双向绑定当前已选值数组即右侧列表内容:options传入全部可选列表组件会依据value自动将选项切分为左侧未选与右侧已选两部分选项中的disabled: i % 5 0演示了按规则禁用部分选项。Transfer Props 完整 API 对照官方文档给出的 Props 表如下这是使用该组件的权威参考NameTypeDefaultDescriptiondefault-valueArraystring \| number \| nullnull默认值非受控模式下的初始值。disabledbooleantrue禁用状态。filterablebooleanfalse是否开启过滤筛选功能。filterfunction(pattern: string, option: TransferOption, from: source \| target) boolean默认是一个基础的 label 字符串匹配函数。optionsTransferOption[][]配置选项结构见下文 TransferOption Type。sizesmall \| medium \| largemedium尺寸。source-filter-placeholderstringundefined左侧Source搜索框占位文案。source-titlestringSource左侧列表标题。target-filter-placeholderstringundefined右侧Target搜索框占位文案。target-titlestringTarget右侧列表标题。valueArraystring \| number \| nullundefined手动设置时的当前值受控模式。on-update:value(value: Arraystring \| number) voidundefined值变化时的回调。virtual-scrollbooleanfalse是否启用虚拟滚动。受控与非受控模式从 use-transfer-data.ts 的源码可以看出组件同时支持受控与非受控两种模式const uncontrolledValueRef ref(props.defaultValue) const controlledValueRef toRef(props, value) const mergedValueRef useMergedState( controlledValueRef, uncontrolledValueRef )不传value、只传default-value时组件内部自行维护状态非受控传了value时组件完全由外部状态驱动受控并在用户操作时通过on-update:value通知外部更新。数据切分逻辑mergedValueRef即当前已选值会与options共同决定左右两侧列表const tgtValueSetRef computed(() new Set(mergedValueRef.value || [])) const srcOptsRef computed(() props.options.filter(option !tgtValueSetRef.value.has(option.value)) ) const tgtOptsRef computed(() { const optMap optMapRef.value return (mergedValueRef.value || []).map(v optMap.get(v)) })即右侧列表 按 value 顺序从 options 中取出已选中的选项左侧列表 全部选项减去已选中项。右侧列表的顺序由value数组的顺序决定而不是 options 中的原始顺序。移动按钮的状态机use-transfer-data.ts中通过两个useMemo控制中间按钮的可用性const fromButtonDisabledRef useMemo(() { if (mergedDisabledRef.value) return true return tgtCheckedValuesRef.value.length 0 }) const toButtonDisabledRef useMemo(() { if (mergedDisabledRef.value) return true return srcCheckedValuesRef.value.length 0 })也就是说左侧没有勾选任何选项时移到右侧按钮禁用右侧没有勾选任何选项时移回左侧按钮禁用。移动逻辑本身在 Transfer.tsx 中function handleToTgtClick(): void { doUpdateValue( srcCheckedValuesRef.value.concat(mergedValueRef.value || []) ) srcCheckedValuesRef.value [] } function handleToSrcClick(): void { const tgtCheckedValueSet new Set(tgtCheckedValuesRef.value) doUpdateValue( (mergedValueRef.value || []).filter(v !tgtCheckedValueSet.has(v)) ) tgtCheckedValuesRef.value [] }向右侧移动 左侧勾选值追加到当前值尾部移回左侧 从当前值中过滤掉右侧勾选值。关于 disabled 默认值的说明文档表格中disabled的默认值标注为true但 Transfer.tsx 中其 prop 声明为default: undefined随后通过useFormItem(props)与表单上下文合并得到mergedDisabledRef。因此实际禁用状态是组件自身 disabled 与所在表单/表单项禁用状态取并集的结果。若组件未放置于表单上下文中实际表现以disabled属性传入值为准。TransferOption Type选项数据结构n-legacy-transfer的选项类型在 interface.ts 中定义export type OptionValue string | number export interface Option { label: string value: OptionValue disabled?: boolean }官方文档对应的属性表PropertyTypeDescriptionlabelstring选项显示文本。valuestring \| number选项唯一值。disabledboolean选项禁用状态。三个字段的含义label直接决定列表项展示的文字也是默认filter的匹配对象value作为选项的唯一标识被用于Map索引optMapRef和Set去重必须保持唯一否则切分与移动逻辑会出错disabled为可选字段禁用的选项不可被勾选也不参与表头全选/半选的可用数量统计。过滤Filterable与自定义 filter开启filterable后左右两个列表顶部会各出现一个搜索框输入内容会即时过滤列表项。参考官方示例 filterable.demo.vuescript langts setup import { ref } from vue function createOptions() { return Array.from({ length: 100 }).map((v, i) ({ label: Option ${i}, value: i, disabled: i % 5 0 })) } function createValues() { return Array.from({ length: 50 }).map((v, i) i) } const options createOptions() const value ref(createValues()) /script template n-legacy-transfer v-model:valuevalue virtual-scroll :optionsoptions filterable / /template默认 filter 实现如果不传filter组件使用内置的大小写不敏感的子串匹配源码位于 Transfer.tsxfilter: { type: Function as PropTypeFilter, default: (pattern: string, option: Option) { if (!pattern) return true return ~${option.label} .toLowerCase() .indexOf(${pattern}.toLowerCase()) } }行为特征pattern为空字符串时放行所有选项否则将option.label与输入模式都转小写后做indexOf子串匹配匹配仅针对label字段不匹配value。自定义 filter 签名filter的类型为export type Filter ( pattern: string, option: Option, from: source | target ) boolean第三个参数from指示当前过滤发生在左侧source还是右侧target可用于实现两侧不同的过滤规则。过滤的计算逻辑在 use-transfer-data.tsconst filteredSrcOptsRef computed(() { if (!props.filterable) return srcOptsRef.value const { filter } props return srcOptsRef.value.filter(opt filter(srcPatternRef.value, opt, source) ) }) const filteredTgtOptsRef computed(() { if (!props.filterable) return tgtOptsRef.value const { filter } props return tgtOptsRef.value.filter(opt filter(tgtPatternRef.value, opt as Option, target) ) })两个列表的搜索关键词srcPatternRef/tgtPatternRef相互独立可分别过滤。自定义 filter 示例例如希望同时匹配 label 与 value并忽略大小写script langts setup import { ref } from vue import type { LegacyTransferOption } from naive-ui function customFilter( pattern: string, option: LegacyTransferOption, from: source | target ) { if (!pattern) return true const p pattern.toLowerCase() return ( option.label.toLowerCase().includes(p) || String(option.value).toLowerCase().includes(p) ) } const options refLegacyTransferOption[]([]) const value refArraystring | number([]) /script template n-legacy-transfer v-model:valuevalue :optionsoptions filterable :filtercustomFilter / /template过滤下的全选状态过滤开启时表头的全选仅针对**当前过滤结果中可用未禁用**的选项生效这一点由 use-transfer-data.ts 中的avlSrcValueSetRef/avlTgtValueSetRef与srcCheckedStatusRef/tgtCheckedStatusRef保证——它们基于filteredSrcOpts/filteredTgtOpts计算并区分checked全选、indeterminate半选与disabled无可选项三种状态。尺寸Size与占位文案定制size支持small | medium | large三档默认medium。参考官方示例 size.demo.vuescript langts setup import { ref } from vue function createOptions() { return Array.from({ length: 100 }).map((v, i) ({ label: Option ${i}, value: i, disabled: i % 5 0 })) } function createValues() { return Array.from({ length: 50 }).map((v, i) i) } const options createOptions() const value ref(createValues()) /script template n-space vertical n-legacy-transfer v-model:valuevalue :optionsoptions sizesmall / n-legacy-transfer v-model:valuevalue :optionsoptions sizelarge / /n-space /template示例中同时渲染 small 与 large 两个实例并提示Mixing sizes does not look harmonious——同一界面内混用不同尺寸会显得不协调实际项目中应保持尺寸统一。尺寸的底层实现尺寸并非简单地切换 class而是驱动主题变量的切换。Transfer.tsx 中根据当前尺寸从主题中取对应变量并注入 CSS 变量const itemSizeRef computed(() { const { value: size } mergedSizeRef const { self: { [createKey(itemHeight, size)]: itemSize } } themeRef.value return depx(itemSize) })主题中为每种尺寸分别定义了fontSize与itemHeight见 styles/light.ts 等主题文件最终通过cssVars输出为--n-font-size、--n-item-height等 CSS 变量供 styles/index.cssr.ts 中的样式使用。标题与占位文案source-title/target-title分别定制左右列表标题默认值为Source/Targetsource-filter-placeholder/target-filter-placeholder定制两侧搜索框占位文字需同时开启filterable才可见。标题同样支持 i18n 默认值useLocale(LegacyTransfer)会从 locale 配置中读取sourceTitle/targetTitle各语言包见 locales/common 下的LegacyTransfer配置项因此不传source-title/target-title时会使用当前 locale 的本地化文案。大数据量与虚拟滚动virtual-scroll当选项数量庞大时普通渲染会显著拖慢穿梭操作。官方示例 large-data.demo.vue 直接构造了42000 个选项来演示该场景script langts setup import { ref } from vue function createOptions() { return Array.from({ length: 42000 }).map((v, i) ({ label: Option${i}, value: i, disabled: i % 5 0 })) } function createValues() { return Array.from({ length: 50 }).map((v, i) i) } const options createOptions() const value ref(createValues()) /script template n-legacy-transfer v-model:valuevalue :optionsoptions virtual-scroll / /template示例说明原文If you have tons of data, you may need to speed the transfer up! Setvirtual-scrollon transfer to use a blazing fast transfer (which turns the ridiculous animation off).数据量巨大时请开启virtual-scroll获得极快的穿梭体验同时关闭了繁琐的动画。虚拟滚动内部实现从 TransferList.tsx 可以看出virtual-scroll开启与否对应两套完全不同的渲染策略开启虚拟滚动渲染vueuc的VirtualList仅渲染可视区域内的行配合NScrollbar通过scrollContainer/scrollContent同步滚动条并用keyFieldvalue指定虚拟列表的 key 字段关闭虚拟滚动渲染普通div容器 TransitionGroup为列表项移动提供入场/离场动画。virtualScroll ? ( VirtualList refvlInstRef style{{ height: 100% }} class{${mergedClsPrefix}-legacy-transfer-list-content} items{this.options} itemSize{this.itemSize} showScrollbar{false} onResize{syncVLScroller} onScroll{syncVLScroller} keyFieldvalue ... /VirtualList ) : ( div class{${mergedClsPrefix}-legacy-transfer-list-content} TransitionGroup nameitem appear{this.isMounted} css{!this.isInputing} ... /TransitionGroup /div )这也解释了示例中which turns the ridiculous animation off虚拟滚动模式下用TransitionGroup的动画被移除换来了大数据量下的性能。空列表渲染当某侧列表为空时组件默认渲染NEmpty空状态若通过全局配置注入了Transfer.renderEmptyuseConfig的mergedComponentPropsRef.value?.Transfer?.renderEmpty则会优先使用自定义的空状态渲染函数。表单联动与事件n-legacy-transfer通过useFormItem(props)接入 Naive UI 表单体系见 Transfer.tsxconst formItem useFormItem(props) const { mergedSizeRef, mergedDisabledRef } formItemsize、disabled会与所在n-form-item的配置合并值变化时通过doUpdateValue触发nTriggerFormInput()与nTriggerFormChange()从而驱动表单的校验与提交逻辑。事件上首选使用on-update:value或v-model:value兼容on-update:value的onUpdateValue写法旧事件on-change仍被支持但在开发环境下会打印弃用警告提示改用on-update:value见源码if (props.onChange ! undefined) { warnOnce( legacy-transfer, on-change is deprecated, please use on-update:value instead. ) }单元测试覆盖情况组件仓库内的测试 Transfer.spec.ts 覆盖了以下行为可作为使用与排错参考按需引入mount(NLegacyTransfer)不报错disabled禁用时根节点带有n-legacy-transfer--disabledclassfilterable开启后根节点带有n-legacy-transfer--filterableclassfilter在filterable与自定义filter下向输入框输入内容onFilter会被调用sizesmall / medium / large三种尺寸的 style 均与快照匹配快照文件placeholdersource-filter-placeholder/target-filter-placeholder会分别写入两个搜索框的placeholder属性titlesource-title/target-title会渲染到对应列表头文本中。此外 server.spec.tsx 还验证了该组件在 SSR服务端渲染场景下的可用性。总结与迁移建议n-legacy-transfer提供了一套完整且经过测试的双向穿梭选择能力受控/非受控双模式、默认子串过滤与自定义 filter、三档尺寸、全选/半选状态机以及针对数万级数据量的虚拟滚动。其数据切分、过滤计算、按钮状态等核心逻辑集中在 use-transfer-data.ts渲染结构见 Transfer.tsx整体实现清晰、可读性高非常适合作为 Vue 3 复杂受控组件的学习范本。最后再次提醒该组件已进入弃用状态官方明确不会再增加新功能并将在下个主版本移除。对于存量项目建议规划向新版 Transfer 组件 的平滑迁移对于新项目请直接使用新版 Transfer。【免费下载链接】naive-uiA Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.项目地址: https://gitcode.com/gh_mirrors/na/naive-ui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考