Transfer穿梭框 - 图1

Transfer 穿梭框

双栏穿梭选择框。

何时使用

  • 需要在多个可选项中进行多选时。
  • 比起 Select 和 TreeSelect,穿梭框占据更大的空间,可以展示可选项的更多信息。

代码演示

Transfer穿梭框 - 图2

基本用法

最基本的用法,展示了 dataSourcetargetKeys、每行的渲染函数 render 以及回调函数 change selectChange scroll 的用法。

  1. <template>
  2. <div>
  3. <a-transfer
  4. :data-source="mockData"
  5. :titles="['Source', 'Target']"
  6. :target-keys="targetKeys"
  7. :selected-keys="selectedKeys"
  8. :render="item => item.title"
  9. :disabled="disabled"
  10. @change="handleChange"
  11. @selectChange="handleSelectChange"
  12. @scroll="handleScroll"
  13. />
  14. <a-switch
  15. un-checked-children="enabled"
  16. checked-children="disabled"
  17. v-model:checked="disabled"
  18. style="margin-top: 16px"
  19. />
  20. </div>
  21. </template>
  22. <script lang="ts">
  23. import { defineComponent, ref } from 'vue';
  24. interface MockData {
  25. key: string;
  26. title: string;
  27. description: string;
  28. disabled: boolean;
  29. }
  30. const mockData: MockData[] = [];
  31. for (let i = 0; i < 20; i++) {
  32. mockData.push({
  33. key: i.toString(),
  34. title: `content${i + 1}`,
  35. description: `description of content${i + 1}`,
  36. disabled: i % 3 < 1,
  37. });
  38. }
  39. const oriTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
  40. export default defineComponent({
  41. data() {
  42. const disabled = ref<boolean>(false);
  43. const targetKeys = ref<string[]>(oriTargetKeys);
  44. const selectedKeys = ref<string[]>(['1', '4']);
  45. const handleChange = (nextTargetKeys: string[], direction: string, moveKeys: string[]) => {
  46. targetKeys.value = nextTargetKeys;
  47. console.log('targetKeys: ', nextTargetKeys);
  48. console.log('direction: ', direction);
  49. console.log('moveKeys: ', moveKeys);
  50. };
  51. const handleSelectChange = (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => {
  52. selectedKeys.value = [...sourceSelectedKeys, ...targetSelectedKeys];
  53. console.log('sourceSelectedKeys: ', sourceSelectedKeys);
  54. console.log('targetSelectedKeys: ', targetSelectedKeys);
  55. };
  56. const handleScroll = (direction: string, e: Event) => {
  57. console.log('direction:', direction);
  58. console.log('target:', e.target);
  59. };
  60. return {
  61. mockData,
  62. targetKeys,
  63. selectedKeys,
  64. disabled,
  65. handleChange,
  66. handleSelectChange,
  67. handleScroll,
  68. };
  69. },
  70. });
  71. </script>

Transfer穿梭框 - 图3

带搜索框

带搜索框的穿梭框,可以自定义搜索函数。

  1. <template>
  2. <a-transfer
  3. :data-source="mockData"
  4. show-search
  5. :filter-option="filterOption"
  6. :target-keys="targetKeys"
  7. :render="item => item.title"
  8. @change="handleChange"
  9. @search="handleSearch"
  10. />
  11. </template>
  12. <script lang="ts">
  13. import { defineComponent, onMounted, ref } from 'vue';
  14. interface MockData {
  15. key: string;
  16. title: string;
  17. description: string;
  18. chosen: boolean;
  19. }
  20. export default defineComponent({
  21. setup() {
  22. const mockData = ref<MockData[]>([]);
  23. const targetKeys = ref<string[]>([]);
  24. onMounted(() => {
  25. getMock();
  26. });
  27. const getMock = () => {
  28. const keys = [];
  29. const mData = [];
  30. for (let i = 0; i < 20; i++) {
  31. const data = {
  32. key: i.toString(),
  33. title: `content${i + 1}`,
  34. description: `description of content${i + 1}`,
  35. chosen: Math.random() * 2 > 1,
  36. };
  37. if (data.chosen) {
  38. keys.push(data.key);
  39. }
  40. mData.push(data);
  41. }
  42. mockData.value = mData;
  43. targetKeys.value = keys;
  44. };
  45. const filterOption = (inputValue: string, option: MockData) => {
  46. return option.description.indexOf(inputValue) > -1;
  47. };
  48. const handleChange = (keys: string[], direction: string, moveKeys: string[]) => {
  49. console.log(keys, direction, moveKeys);
  50. targetKeys.value = keys;
  51. };
  52. const handleSearch = (dir: string, value: string) => {
  53. console.log('search:', dir, value);
  54. };
  55. return {
  56. mockData,
  57. targetKeys,
  58. filterOption,
  59. handleChange,
  60. handleSearch,
  61. };
  62. },
  63. });
  64. </script>

Transfer穿梭框 - 图4

高级用法

穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。

  1. <template>
  2. <a-transfer
  3. :data-source="mockData"
  4. show-search
  5. :list-style="{
  6. width: '250px',
  7. height: '300px',
  8. }"
  9. :operations="['to right', 'to left']"
  10. :target-keys="targetKeys"
  11. :render="item => `${item.title}-${item.description}`"
  12. @change="handleChange"
  13. >
  14. <template #footer>
  15. <a-button size="small" style="float: right; margin: 5px" @click="getMock">reload</a-button>
  16. </template>
  17. <template #notFoundContent>
  18. <span>没数据</span>
  19. </template>
  20. </a-transfer>
  21. </template>
  22. <script lang="ts">
  23. import { defineComponent, ref, onMounted } from 'vue';
  24. interface MockData {
  25. key: string;
  26. title: string;
  27. description: string;
  28. chosen: boolean;
  29. }
  30. export default defineComponent({
  31. setup() {
  32. const mockData = ref<MockData[]>([]);
  33. const targetKeys = ref<string[]>([]);
  34. onMounted(() => {
  35. getMock();
  36. });
  37. const getMock = () => {
  38. const keys = [];
  39. const mData = [];
  40. for (let i = 0; i < 20; i++) {
  41. const data = {
  42. key: i.toString(),
  43. title: `content${i + 1}`,
  44. description: `description of content${i + 1}`,
  45. chosen: Math.random() * 2 > 1,
  46. };
  47. if (data.chosen) {
  48. keys.push(data.key);
  49. }
  50. mData.push(data);
  51. }
  52. mockData.value = mData;
  53. targetKeys.value = keys;
  54. };
  55. const handleChange = (keys: string[], direction: string, moveKeys: string[]) => {
  56. targetKeys.value = keys;
  57. console.log(keys, direction, moveKeys);
  58. };
  59. return {
  60. mockData,
  61. targetKeys,
  62. handleChange,
  63. getMock,
  64. };
  65. },
  66. });
  67. </script>

Transfer穿梭框 - 图5

自定义渲染行数据

自定义渲染每一个 Transfer Item,可用于渲染复杂数据。

  1. <template>
  2. <a-transfer
  3. :data-source="mockData"
  4. :list-style="{
  5. width: '300px',
  6. height: '300px',
  7. }"
  8. :target-keys="targetKeys"
  9. @change="handleChange"
  10. >
  11. <template #render="item">
  12. <span class="custom-item" style="color: red">{{ item.title }} - {{ item.description }}</span>
  13. </template>
  14. </a-transfer>
  15. </template>
  16. <script lang="ts">
  17. import { defineComponent, ref, onMounted } from 'vue';
  18. interface MockData {
  19. key: string;
  20. title: string;
  21. description: string;
  22. chosen: boolean;
  23. }
  24. export default defineComponent({
  25. setup() {
  26. const mockData = ref<MockData[]>([]);
  27. const targetKeys = ref<string[]>([]);
  28. onMounted(() => {
  29. getMock();
  30. });
  31. const getMock = () => {
  32. const keys = [];
  33. const mData = [];
  34. for (let i = 0; i < 20; i++) {
  35. const data = {
  36. key: i.toString(),
  37. title: `content${i + 1}`,
  38. description: `description of content${i + 1}`,
  39. chosen: Math.random() * 2 > 1,
  40. };
  41. if (data.chosen) {
  42. keys.push(data.key);
  43. }
  44. mData.push(data);
  45. }
  46. mockData.value = mData;
  47. targetKeys.value = keys;
  48. };
  49. const handleChange = (keys: string[], direction: string, moveKeys: string[]) => {
  50. targetKeys.value = keys;
  51. console.log(keys, direction, moveKeys);
  52. };
  53. return {
  54. mockData,
  55. targetKeys,
  56. handleChange,
  57. getMock,
  58. };
  59. },
  60. });
  61. </script>

Transfer穿梭框 - 图6

表格穿梭框

使用 Table 组件作为自定义渲染列表。

  1. <template>
  2. <div>
  3. <a-transfer
  4. :data-source="mockData"
  5. :target-keys="targetKeys"
  6. :disabled="disabled"
  7. :show-search="showSearch"
  8. :filter-option="(inputValue, item) => item.title.indexOf(inputValue) !== -1"
  9. :show-select-all="false"
  10. @change="onChange"
  11. >
  12. <template
  13. #children="{
  14. direction,
  15. filteredItems,
  16. selectedKeys,
  17. disabled: listDisabled,
  18. onItemSelectAll,
  19. onItemSelect,
  20. }"
  21. >
  22. <a-table
  23. :row-selection="
  24. getRowSelection({
  25. disabled: listDisabled,
  26. selectedKeys,
  27. onItemSelectAll,
  28. onItemSelect,
  29. })
  30. "
  31. :columns="direction === 'left' ? leftColumns : rightColumns"
  32. :data-source="filteredItems"
  33. size="small"
  34. :style="{ pointerEvents: listDisabled ? 'none' : null }"
  35. :custom-row="
  36. ({ key, disabled: itemDisabled }) => ({
  37. onClick: () => {
  38. if (itemDisabled || listDisabled) return;
  39. onItemSelect(key, !selectedKeys.includes(key));
  40. },
  41. })
  42. "
  43. />
  44. </template>
  45. </a-transfer>
  46. <a-switch
  47. un-checked-children="disabled"
  48. checked-children="disabled"
  49. v-model:checked="disabled"
  50. style="margin-top: 16px"
  51. />
  52. <a-switch
  53. un-checked-children="showSearch"
  54. checked-children="showSearch"
  55. v-model:checked="showSearch"
  56. style="margin-top: 16px"
  57. />
  58. </div>
  59. </template>
  60. <script lang="ts">
  61. import { difference } from 'lodash-es';
  62. import { defineComponent, ref } from 'vue';
  63. interface MockData {
  64. key: string;
  65. title: string;
  66. description: string;
  67. disabled: boolean;
  68. }
  69. type tableColumn = Record<string, string>;
  70. const mockData: MockData[] = [];
  71. for (let i = 0; i < 20; i++) {
  72. mockData.push({
  73. key: i.toString(),
  74. title: `content${i + 1}`,
  75. description: `description of content${i + 1}`,
  76. disabled: i % 4 === 0,
  77. });
  78. }
  79. const originTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
  80. const leftTableColumns = [
  81. {
  82. dataIndex: 'title',
  83. title: 'Name',
  84. },
  85. {
  86. dataIndex: 'description',
  87. title: 'Description',
  88. },
  89. ];
  90. const rightTableColumns = [
  91. {
  92. dataIndex: 'title',
  93. title: 'Name',
  94. },
  95. ];
  96. export default defineComponent({
  97. setup() {
  98. const targetKeys = ref<string[]>(originTargetKeys);
  99. const disabled = ref<boolean>(false);
  100. const showSearch = ref<boolean>(false);
  101. const leftColumns = ref<tableColumn[]>(leftTableColumns);
  102. const rightColumns = ref<tableColumn[]>(rightTableColumns);
  103. const onChange = (nextTargetKeys: string[]) => {
  104. targetKeys.value = nextTargetKeys;
  105. };
  106. const getRowSelection = ({
  107. disabled,
  108. selectedKeys,
  109. onItemSelectAll,
  110. onItemSelect,
  111. }: Record<string, any>) => {
  112. return {
  113. getCheckboxProps: (item: Record<string, string | boolean>) => ({
  114. disabled: disabled || item.disabled,
  115. }),
  116. onSelectAll(selected: boolean, selectedRows: Record<string, string | boolean>[]) {
  117. const treeSelectedKeys = selectedRows
  118. .filter(item => !item.disabled)
  119. .map(({ key }) => key);
  120. const diffKeys = selected
  121. ? difference(treeSelectedKeys, selectedKeys)
  122. : difference(selectedKeys, treeSelectedKeys);
  123. onItemSelectAll(diffKeys, selected);
  124. },
  125. onSelect({ key }: Record<string, string>, selected: boolean) {
  126. onItemSelect(key, selected);
  127. },
  128. selectedRowKeys: selectedKeys,
  129. };
  130. };
  131. return {
  132. mockData,
  133. targetKeys,
  134. disabled,
  135. showSearch,
  136. leftColumns,
  137. rightColumns,
  138. onChange,
  139. getRowSelection,
  140. };
  141. },
  142. });
  143. </script>

Transfer穿梭框 - 图7

树穿梭框

使用 Tree 组件作为自定义渲染列表。

  1. <template>
  2. <div>
  3. <a-transfer
  4. class="tree-transfer"
  5. :data-source="dataSource"
  6. :target-keys="targetKeys"
  7. :render="item => item.title"
  8. :show-select-all="false"
  9. @change="onChange"
  10. >
  11. <template #children="{ direction, selectedKeys, onItemSelect }">
  12. <a-tree
  13. v-if="direction === 'left'"
  14. blockNode
  15. checkable
  16. checkStrictly
  17. defaultExpandAll
  18. :checkedKeys="[...selectedKeys, ...targetKeys]"
  19. :treeData="treeData"
  20. @check="
  21. (_, props) => {
  22. onChecked(_, props, [...selectedKeys, ...targetKeys], onItemSelect);
  23. }
  24. "
  25. @select="
  26. (_, props) => {
  27. onChecked(_, props, [...selectedKeys, ...targetKeys], onItemSelect);
  28. }
  29. "
  30. />
  31. </template>
  32. </a-transfer>
  33. </div>
  34. </template>
  35. <script lang="ts">
  36. import { CheckEvent } from 'ant-design-vue/es/tree/Tree';
  37. import { computed, defineComponent, ref } from 'vue';
  38. interface TreeDataItem {
  39. key: string;
  40. title: string;
  41. disabled?: boolean;
  42. children?: TreeDataItem[];
  43. }
  44. const tData: TreeDataItem[] = [
  45. { key: '0-0', title: '0-0' },
  46. {
  47. key: '0-1',
  48. title: '0-1',
  49. children: [
  50. { key: '0-1-0', title: '0-1-0' },
  51. { key: '0-1-1', title: '0-1-1' },
  52. ],
  53. },
  54. { key: '0-2', title: '0-3' },
  55. ];
  56. const transferDataSource: TreeDataItem[] = [];
  57. function flatten(list: TreeDataItem[] = []) {
  58. list.forEach(item => {
  59. transferDataSource.push(item);
  60. flatten(item.children);
  61. });
  62. }
  63. flatten(JSON.parse(JSON.stringify(tData)));
  64. function isChecked(selectedKeys: string[], eventKey: string) {
  65. return selectedKeys.indexOf(eventKey) !== -1;
  66. }
  67. function handleTreeData(data: TreeDataItem[], targetKeys: string[] = []): TreeDataItem[] {
  68. data.forEach(item => {
  69. item['disabled'] = targetKeys.includes(item.key);
  70. if (item.children) {
  71. handleTreeData(item.children, targetKeys);
  72. }
  73. });
  74. return data;
  75. }
  76. export default defineComponent({
  77. setup() {
  78. const targetKeys = ref<string[]>([]);
  79. const dataSource = ref<TreeDataItem[]>(transferDataSource);
  80. const treeData = computed<TreeDataItem[]>(() => {
  81. return handleTreeData(tData, targetKeys.value);
  82. });
  83. const onChange = (keys: string[]) => {
  84. targetKeys.value = keys;
  85. };
  86. const onChecked = (
  87. _: Record<string, string[]>,
  88. e: CheckEvent,
  89. checkedKeys: string[],
  90. onItemSelect: (n: any, c: boolean) => void,
  91. ) => {
  92. const { eventKey } = e.node;
  93. onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
  94. };
  95. return {
  96. targetKeys,
  97. dataSource,
  98. treeData,
  99. onChange,
  100. onChecked,
  101. };
  102. },
  103. });
  104. </script>
  105. <style scoped>
  106. .tree-transfer .ant-transfer-list:first-child {
  107. width: 50%;
  108. flex: none;
  109. }
  110. </style>

API

参数说明类型默认值版本
dataSource数据源,其中的数据将会被渲染到左边一栏中,targetKeys 中指定的除外。[{key: string.isRequired,title: string.isRequired,description: string,disabled: bool}][][]
disabled是否禁用booleanfalse
filterOption接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false(inputValue, option): boolean
footer可以设置为一个 作用域插槽slot=”footer” slot-scope=”props”
lazyTransfer 使用了 [vc-lazy-load]优化性能,这里可以设置相关参数。设为 false 可以关闭懒加载。object|boolean{ height: 32, offset: 32 }
listStyle两个穿梭框的自定义样式object
locale各种语言object{ itemUnit: ‘项’, itemsUnit: ‘项’, notFoundContent: ‘列表为空’, searchPlaceholder: ‘请输入搜索内容’ }
operations操作文案集合,顺序从上至下string[][‘>’, ‘<’]
render每行数据渲染函数,该函数的入参为 dataSource 中的项,返回值为 element。或者返回一个普通对象,其中 label 字段为 element,value 字段为 titleFunction(record)| slot
selectedKeys设置哪些项应该被选中string[][]
showSearch是否显示搜索框booleanfalse
showSelectAll是否展示全选勾选框booleantrue
targetKeys显示在右侧框数据的 key 集合string[][]
titles标题集合,顺序从左至右string[][‘’, ‘’]

事件

事件名称说明回调参数版本
change选项在两栏之间转移时的回调函数(targetKeys, direction, moveKeys): void
scroll选项列表滚动时的回调函数(direction, event): void
search搜索框内容时改变时的回调函数(direction: ‘left’|’right’, value: string): void-
selectChange选中项发生改变时的回调函数(sourceSelectedKeys, targetSelectedKeys): void

Render Props

Transfer 支持接收 children 自定义渲染列表,并返回以下参数:

  1. {
  2. "direction": String,
  3. "disabled": Boolean,
  4. "filteredItems": Array,
  5. "selectedKeys": Array,
  6. "onItemSelect": Function,
  7. "onItemSelectAll": Function
  8. }
参数说明类型版本
direction渲染列表的方向‘left’ | ‘right’
disabled是否禁用列表boolean
filteredItems过滤后的数据TransferItem[]
selectedKeys选中的条目string[]
itemSelect勾选条目(key: string, selected: boolean)
itemSelectAll勾选一组条目(keys: string[], selected: boolean)

参考示例

  1. <a-transfer>
  2. <template
  3. #children="{
  4. direction,
  5. filteredItems,
  6. selectedKeys,
  7. disabled: listDisabled,
  8. onItemSelectAll,
  9. onItemSelect,
  10. }"
  11. >
  12. <your-component />
  13. <template>
  14. </a-transfer>

注意

按照 Vue 最新的规范,所有的组件数组最好绑定 key。在 Transfer 中,dataSource里的数据值需要指定 key 值。对于 dataSource 默认将每列数据的 key 属性作为唯一的标识。

如果你的数据没有这个属性,务必使用 rowKey 来指定数据列的主键。

  1. // 比如你的数据主键是 uid
  2. return <Transfer :rowKey="record => record.uid" />;