Skip to content

Transfer 数据穿梭框 ​

在两列列表之间移动数据,用于权限分配、成员分组和资源授权。

何时使用 ​

  • 同时查看候选与已分配数据,进行批量分配或取消分配。
  • 选项包含较多业务信息,Select 展开空间不足。
  • 自定义列表可以接入表格或树;层级关联与列表键盘逻辑由业务组合实现。

基础用法与移动事件 ​

先勾选,再点击方向按钮。change 的第二参数是移动目的侧,第三参数仅包含实际移动值。

0 / 3
0 / 1

请勾选权限并移动

目标值:[ "audit" ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const basicValue = ref<TransferValue[]>(['audit']);
  const basicData: TransferItem[] = [
    { value: 'read', label: '查看数据' },
    { value: 'edit', label: '编辑数据' },
    { value: 'audit', label: '审核数据' },
    { value: 'admin', label: '管理配置(不可分配)', disabled: true },
  ];
  const basicLog = ref('请勾选权限并移动');
  function basicChange(value: TransferValue[], type: TransferType, moved: TransferValue[]) {
    basicLog.value = `移至 ${type}:${moved.join(', ')};当前值 ${JSON.stringify(value)}`;
  }
</script>

<template>
  <Transfer
    v-model="basicValue"
    :data="basicData"
    :title="['可分配权限', '已分配权限']"
    @change="basicChange"
  />
  <p aria-live="polite">{{ basicLog }}</p>
  <p>目标值:{{ basicValue }}</p>
</template>

受控勾选与目标排序 ​

v-model:selected-keys 控制两侧临时勾选,与目标值独立。target-order="append" 以模型顺序展示目标项。

1 / 5
0 / 1

目标:[ 2 ];勾选:[ 1 ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer, Button } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const checkedValue = ref<TransferValue[]>([2]);
  const checkedKeys = ref<TransferValue[]>([1]);
  const checkedData: TransferItem[] = Array.from({ length: 6 }, (_, index) => ({
    value: index + 1,
    label: `成员 ${index + 1}`,
  }));
  const checkedLog = ref('');
  function checkedSelect(source: TransferValue[], target: TransferValue[]) {
    checkedLog.value = `左侧 ${JSON.stringify(source)};右侧 ${JSON.stringify(target)}`;
  }
</script>

<template>
  <p
    ><Button @click="checkedKeys = [1, 2, 3]">勾选成员 1 / 2 / 3</Button>
    <Button @click="checkedKeys = []">取消所有勾选</Button></p
  >
  <Transfer
    v-model="checkedValue"
    v-model:selected-keys="checkedKeys"
    :data="checkedData"
    target-order="append"
    @select="checkedSelect"
  />
  <p>目标:{{ checkedValue }};勾选:{{ checkedKeys }}</p>
  <p aria-live="polite">{{ checkedLog }}</p>
</template>

搜索与过滤后的全选 ​

全选只作用于当前搜索结果,禁用项不参与;已勾选的隐藏项仍可以移动。自定义过滤可搜索附加业务字段。

0 / 4
0 / 0
暂无数据

尝试搜索“华东”,然后全选

目标值:[]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const searchValue = ref<TransferValue[]>([]);
  const searchData: TransferItem[] = [
    { value: 'east-1', label: '杭州', region: '华东' },
    { value: 'east-2', label: '上海', region: '华东' },
    { value: 'north', label: '北京', region: '华北' },
    { value: 'south', label: '深圳', region: '华南', disabled: true },
  ];
  const searchLog = ref('尝试搜索“华东”,然后全选');
  function searchFilter(input: string, item: TransferItem) {
    return `${item.label} ${String(item.region ?? '')}`.includes(input.trim());
  }
  function searchChanged(input: string, type: TransferType) {
    searchLog.value = `${type} 搜索:${input || '已清空'}`;
  }
</script>

<template>
  <Transfer
    v-model="searchValue"
    :data="searchData"
    show-search
    search-placeholder="搜索城市或大区"
    :filter-option="searchFilter"
    @search="searchChanged"
  />
  <p aria-live="polite">{{ searchLog }}</p>
  <p>目标值:{{ searchValue }}</p>
</template>

禁用、加载与错误状态 ​

加载时显示真实加载组件并阻止操作。单项禁用只影响该项;error 同时支持从 Form 继承。

禁用

0 / 3
0 / 1
vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer, Checkbox } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const stateValue = ref<TransferValue[]>([1]);
  const stateDisabled = ref(false);
  const stateLoading = ref(false);
  const stateError = ref(false);
  const stateData: TransferItem[] = [
    { value: 1, label: '业务成员' },
    { value: 2, label: '运维成员' },
    { value: 3, label: '只读成员(禁用)', disabled: true },
    { value: 4, label: '这是一个用于检查溢出省略和原生标题的超长成员名称' },
  ];
</script>

<template>
  <p
    ><Checkbox v-model="stateDisabled">禁用</Checkbox>
    <Checkbox v-model="stateLoading">加载中</Checkbox>
    <Checkbox v-model="stateError">错误</Checkbox></p
  >
  <Transfer
    v-model="stateValue"
    :data="stateData"
    :disabled="stateDisabled"
    :loading="stateLoading"
    :error="stateError"
    show-search
  />
</template>

单向分配与逐项移除 ​

只保留向右按钮,目标侧使用可通过键盘到达的移除按钮。已分配的禁用项不能移除。

0 / 1
已选项
0 / 2
固定负责人
查看成员

已分配:[ "owner", "viewer" ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const oneWayValue = ref<TransferValue[]>(['owner', 'viewer']);
  const oneWayData: TransferItem[] = [
    { value: 'owner', label: '固定负责人', disabled: true },
    { value: 'viewer', label: '查看成员' },
    { value: 'editor', label: '编辑成员' },
  ];
</script>

<template>
  <Transfer v-model="oneWayValue" :data="oneWayData" one-way :operations="['分配']" />
  <p>已分配:{{ oneWayValue }}</p>
</template>

简单模式 ​

点击选项直接移动,也支持 Tab 与 Enter/Space。与 one-way 组合时,目标侧仍使用移除按钮。

可选项
0 / 3
已选项
0 / 1

点击组名即可移动:[ "b" ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const simpleValue = ref<TransferValue[]>(['b']);
  const simpleData: TransferItem[] = [
    { value: 'a', label: '开发组' },
    { value: 'b', label: '产品组' },
    { value: 'c', label: '测试组' },
    { value: 'd', label: '归档组', disabled: true },
  ];
</script>

<template>
  <Transfer v-model="simpleValue" :data="simpleData" simple />
  <p>点击组名即可移动:{{ simpleValue }}</p>
</template>

自定义选项、标题、图标和页脚 ​

item 仅替换内容,保留原生勾选交互。标题通过 onSelectAllChange 调用当前搜索结果/页内的全选。

0 / 3
可选 3 人,勾选 0 人
0 / 0
暂无数据
已选 0 人,勾选 0 人
vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer, Checkbox } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const slotValue = ref<TransferValue[]>([]);
  const slotData: TransferItem[] = [
    { value: 1, label: '林老师', role: '产品设计' },
    { value: 2, label: '陈同学', role: '前端研发' },
    { value: 3, label: '张同学', role: '质量保障' },
  ];
</script>

<template>
  <Transfer
    v-model="slotValue"
    :data="slotData"
    show-search
    :title="['团队成员', '项目成员']"
    :list-style="{ width: '260px' }"
  >
    <template #item="{ item }"
      ><span>{{ item.label }} · {{ item.role }}</span></template
    >
    <template #source-title="{ title, checked, indeterminate, disabled, onSelectAllChange }">
      <Checkbox
        :model-value="checked"
        :indeterminate="indeterminate"
        :disabled="disabled"
        @change="onSelectAllChange($event === true)"
        >{{ title }}</Checkbox
      >
    </template>
    <template #to-target-icon><span aria-hidden="true">→</span></template>
    <template #to-source-icon><span aria-hidden="true">←</span></template>
    <template #footer="{ type, data, selectedKeys }"
      ><small
        >{{ type === 'source' ? '可选' : '已选' }} {{ data.length }} 人,勾选
        {{ selectedKeys.length }} 人</small
      ></template
    >
  </Transfer>
</template>

自定义列表 ​

source/target 替换列表内容。回调接入表格、树或业务卡片,保持共享移动规则。示例按钮支持键盘,并用 aria-pressed 表达勾选状态。

0 / 3
0 / 1

当前项目任务:[ "release" ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const customValue = ref<TransferValue[]>(['release']);
  const customData: TransferItem[] = [
    { value: 'design', label: '设计评审' },
    { value: 'develop', label: '研发联调' },
    { value: 'release', label: '上线验证' },
    { value: 'archived', label: '已归档', disabled: true },
  ];
</script>

<template>
  <Transfer v-model="customValue" :data="customData" show-search>
    <template #source="{ pageData, selectedKeys, disabled, onSelect }">
      <div style="padding: 12px; display: grid; gap: 8px">
        <button
          v-for="item in pageData"
          :key="item.value"
          type="button"
          :disabled="disabled || item.disabled"
          :aria-pressed="selectedKeys.includes(item.value)"
          @click="onSelect(item.value, !selectedKeys.includes(item.value))"
        >
          {{ selectedKeys.includes(item.value) ? '✓ ' : '+ ' }}{{ item.label }}
        </button>
        <span v-if="!pageData.length">没有可选任务</span>
      </div>
    </template>
    <template #target="{ pageData, selectedKeys, disabled, onSelect }">
      <div style="padding: 12px; display: grid; gap: 8px">
        <button
          v-for="item in pageData"
          :key="item.value"
          type="button"
          :disabled="disabled || item.disabled"
          :aria-pressed="selectedKeys.includes(item.value)"
          @click="onSelect(item.value, !selectedKeys.includes(item.value))"
        >
          {{ selectedKeys.includes(item.value) ? '✓ ' : '+ ' }}{{ item.label }}
        </button>
        <span v-if="!pageData.length">尚未分配任务</span>
      </div>
    </template>
  </Transfer>
  <p>当前项目任务:{{ customValue }}</p>
</template>

分页 ​

两侧页码独立,全选仅当前页。搜索或每页条数变化后回到第一页,移动使页码超出范围时自动收敛。

0 / 28
0 / 2

已分配:[ 2, 4 ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const pagedValue = ref<TransferValue[]>([2, 4]);
  const pagedData: TransferItem[] = Array.from({ length: 30 }, (_, index) => ({
    value: index + 1,
    label: `候选成员 ${index + 1}`,
    disabled: index === 8,
  }));
</script>

<template>
  <Transfer v-model="pagedValue" :data="pagedData" :pagination="{ pageSize: 5 }" show-search />
  <p>已分配:{{ pagedValue }}</p>
</template>

虚拟滚动 ​

大数据列表复用库内 VirtualList。分页优先于虚拟滚动;自定义列表自行决定渲染方式。

0 / 998
0 / 2

尝试滚动列表或搜索数据项 999

目标值:[ 100, 500 ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const virtualValue = ref<TransferValue[]>([100, 500]);
  const virtualData: TransferItem[] = Array.from({ length: 1000 }, (_, index) => ({
    value: index + 1,
    label: `数据项 ${index + 1}`,
  }));
  const virtualLog = ref('尝试滚动列表或搜索数据项 999');
  function virtualScroll(_event: Event, type: TransferType) {
    virtualLog.value = `${type} 列表正在滚动`;
  }
</script>

<template>
  <Transfer
    v-model="virtualValue"
    :data="virtualData"
    show-search
    :virtual-list-props="{
      height: 240,
      threshold: 30,
      fixedSize: true,
      estimatedSize: 36,
      buffer: 5,
    }"
    @scroll="virtualScroll"
  />
  <p aria-live="polite">{{ virtualLog }}</p>
  <p>目标值:{{ virtualValue }}</p>
</template>

Form 校验联动 ​

勾选和搜索不改变字段值;实际移动才触发 change 校验。标签/必填/错误说明绑定到整个组,内部控件不会重复注册。

0 / 3
0 / 0
暂无数据

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer, Button, FormItem, Form } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const transferFormRef = ref<FormInstance>();
  const transferForm = reactive({ members: [] as TransferValue[] });
  const transferRules: Record<string, FieldRule[]> = {
    members: [{ type: 'array', required: true, minLength: 2, message: '请至少分配 2 个成员' }],
  };
  const transferFormData: TransferItem[] = [
    { value: 'lin', label: '林老师' },
    { value: 'chen', label: '陈同学' },
    { value: 'zhang', label: '张同学' },
  ];
  const transferFormLog = ref('');
  async function validateTransfer() {
    transferFormLog.value = (await transferFormRef.value?.validate()) ? '请补齐成员' : '校验通过';
  }
</script>

<template>
  <Form ref="transferFormRef" :model="transferForm" :rules="transferRules" layout="vertical">
    <FormItem field="members" label="项目成员" required>
      <Transfer v-model="transferForm.members" :data="transferFormData" show-search />
    </FormItem>
    <Button type="primary" @click="validateTransfer">校验成员</Button>
    <Button @click="transferFormRef?.resetFields()">重置</Button>
    <p aria-live="polite">{{ transferFormLog }}</p>
  </Form>
</template>

空态与异步数据 ​

模型中暂时没有对应数据的值会保留,数据到达后展示。空态插槽能区分源侧、目标侧和搜索条件。

0 / 0
暂无候选成员
0 / 0
暂无已加载成员

模型值保留:[ "remote" ]

vue
<script setup lang="ts">
  import { reactive, ref } from 'vue';
  import { Transfer, Button } from 'x-next';
  import type { TransferItem, TransferValue, TransferType, FormInstance, FieldRule } from 'x-next';
  const emptyValue = ref<TransferValue[]>(['remote']);
  const emptyData = ref<TransferItem[]>([]);
  function loadTransferData() {
    emptyData.value = [
      { value: 'remote', label: '已分配的远程成员' },
      { value: 'new', label: '新加载的候选成员' },
    ];
  }
</script>

<template>
  <p
    ><Button @click="loadTransferData">加载模拟数据</Button>
    <Button @click="emptyData = []">清空数据源</Button></p
  >
  <Transfer v-model="emptyValue" :data="emptyData" show-search>
    <template #empty="{ type, searchValue }"
      ><span>{{
        searchValue ? '没有匹配的成员' : type === 'source' ? '暂无候选成员' : '暂无已加载成员'
      }}</span></template
    >
  </Transfer>
  <p>模型值保留:{{ emptyValue }}</p>
</template>

键盘与行为边界 ​

  • Tab 到达搜索、全选、选项和方向按钮;Space 勾选复选框,Enter/Space 激活方向/移除/简单模式按钮。
  • 默认列表支持 ↑ / ↓、Home / End 定位,跳过禁用项。自定义列表由业务实现键盘语义。
  • 勾选值与目标值分别受控;传入模型而不回写时保持原值。默认值只在创建时初始化。
  • value 必须全局唯一,数字 1 和字符串 "1" 不同。重复项保留首项,非法值不展示;不原地修改输入数组。
  • 默认搜索不区分大小写。全选仅当前搜索结果,分页时仅当前页;已勾选的搜索隐藏项仍参与移动。
  • 禁用项不能全选、移入或移除。缺失数据的目标值在移动其他项时保留。
  • targetOrder="original" 按数据源排序;append 按模型顺序,移入值追加在已有目标值之后。
  • pagination 优先于 virtualListProps。source/target 插槽接收过滤数据和当前页数据,自行实现虚拟渲染与键盘。
  • 数组控件没有 readonly 属性;只读展示可使用 disabled。字段映射、远程分页、拖拽排序需业务适配。

按需导入 ​

ts
import { Transfer } from 'x-next';
import 'x-next/style/transfer';

样式入口自动包含 Checkbox、Input、Button、Empty、Spin、Pagination 和滚动条的运行时样式。

Props ​

参数说明类型默认值
data数据源,value 唯一,支持附加字段TransferItem[][]
modelValue / v-model目标侧的值(string | number)[]undefined
defaultValue非受控目标初始值(string | number)[][]
selectedKeys / v-model:selected-keys两侧临时勾选值(string | number)[]undefined
defaultSelectedKeys非受控勾选初始值(string | number)[][]
disabled禁用,继承 Formbooleanfalse
loading加载并阻止操作booleanfalse
error错误,继承 Formbooleanfalse
simple点击立即移动booleanfalse
oneWay单向分配,目标逐项移除booleanfalse
showSearch两侧搜索booleanfalse
showSelectAll显示全选;simple/oneWay 目标侧不显示booleantrue
title两侧标题,缺项按语言包回退string[]undefined
operations向右、向左操作文案string[][]
searchPlaceholder搜索占位string语言包
filterOption非空搜索的过滤函数(input: string, item: TransferItem, type: TransferType) => boolean标签搜索
targetOrder目标排序'original' | 'append''original'
listStyle面板样式,可设置 width 或组件 CSS tokenCSSPropertiesundefined
pagination分页,非法 pageSize 回退 10boolean | { pageSize?: number }false
virtualListPropsVirtualList 配置,默认高度 240VirtualListPropsundefined

Events ​

事件说明参数
update:modelValue请求更新目标模型(values: TransferValue[])
update:selectedKeys请求更新临时勾选模型(values: TransferValue[])
change实际移动,type 为目的侧(values, type, movedValues)
select临时勾选变化或移动清除勾选(sourceSelected, targetSelected)
search输入/清空/隐藏搜索重置(inputValue: string, type: TransferType)
scroll列表滚动(event: Event, type: TransferType)

Slots ​

插槽说明作用域
item选项内容{ item, type, selected, disabled }
source / target替换对应侧列表{ type, data, pageData, selectedKeys, disabled, onSelect, onSelectAll, onMove }
source-title / target-title替换标题{ type, title, countTotal, countSelected, searchValue, checked, indeterminate, disabled, onSelectAllChange }
footer两侧页脚与列表作用域相同
empty默认列表空态,自定义列表自行处理{ type, searchValue }
to-target-icon / to-source-icon方向图标无

onSelect(value, checked) 选择单项;onSelectAll(values, checked) 选择指定项;onMove(values) 移动同侧项。回调校验数据存在、所属侧、禁用和加载状态。onSelectAllChange(checked) 仅选择当前搜索/页内可用项。selectedKeys 是本侧存在且未禁用的勾选值,包含搜索隐藏项。

Methods ​

没有额外公开方法。通过模型、props 与作用域回调控制状态。

数据类型 ​

ts
type TransferValue = string | number;
type TransferType = 'source' | 'target';
interface TransferItem {
  value: TransferValue;
  label: string;
  disabled?: boolean;
  [key: string]: unknown;
}

组合参见 Form 表单、Checkbox 复选框、Table 表格、TreeSelect 树选择器。