Skip to content

Transfer ​

Двухпанельный список для перемещения элементов между исходной и целевой коллекциями.

Source10
Item 1
Item 3
Item 4
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target2
Item 2
Item 5
vue
<script setup lang="ts">
import { ref } from 'vue'
import { VxTransfer } from 'veloxui'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
  disabled: i === 3 || i === 7,
}))

const value = ref([2, 5])
</script>

<template>
  <VxTransfer v-model="value" :data="data" />
</template>

Filterable ​

Source10
Item 2
Item 3
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target2
Item 1
Item 4
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1, 4])
</script>

<template>
  <VxTransfer v-model="value" :data="data" filterable />
</template>

Custom Titles ​

Available Cities6
Los Angeles
Houston
Phoenix
Philadelphia
San Antonio
San Diego
Selected Cities2
New York
Chicago
vue
<script setup lang="ts">
import { ref } from 'vue'

const cities = [
  { key: 'nyc', label: 'New York' },
  { key: 'la', label: 'Los Angeles' },
  { key: 'chi', label: 'Chicago' },
  { key: 'hou', label: 'Houston' },
  { key: 'phx', label: 'Phoenix' },
]
const value = ref(['nyc', 'chi'])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="cities"
    :titles="['Available Cities', 'Selected Cities']"
    filterable
  />
</template>

Field Mapping ​

Используйте key-field и label-field, чтобы сопоставить произвольные свойства данных без их преобразования.

Users4
Alice
Charlie
Diana
Eve
Team1
Bob
vue
<script setup lang="ts">
import { ref } from 'vue'

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' },
  { id: 4, name: 'Diana' },
  { id: 5, name: 'Eve' },
]
const value = ref([2])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="users"
    key-field="id"
    label-field="name"
    :titles="['Users', 'Team']"
  />
</template>

Panel Width ​

Source4
Item 2
Item 4
Item 5
Item 6
Target2
Item 1
Item 3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1, 3])
</script>

<template>
  <!-- Number = pixels -->
  <VxTransfer v-model="value" :data="data" :panel-width="300" />

  <!-- String = any CSS value -->
  <VxTransfer v-model="value" :data="data" panel-width="50%" />
</template>

Sort ​

Элементы в обеих панелях сортируются переданной функцией-компаратором.

Sorted A-Z10
Item 10
Item 11
Item 12
Item 2
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Sorted A-Z2
Item 1
Item 3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([3, 1])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="data"
    :sort-method="(a, b) => a.label.localeCompare(b.label)"
    :titles="['Sorted A-Z', 'Sorted A-Z']"
  />
</template>

Move All ​

Показать кнопки перемещения всего (двойные стрелки), чтобы разом перенести все доступные элементы.

Source12
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
  disabled: i === 3 || i === 7,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data" show-move-all />
</template>

Button Texts ​

Source12
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data" :button-texts="['Add', 'Remove']" />
</template>

Custom Button Slots ​

Каждый слот получает move, disabled и checkedCount.

Source7
Item 1
Item 2
Item 3
Item 5
Item 6
Item 7
Item 8
Target1
Item 4
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 8 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([4])
</script>

<template>
  <VxTransfer v-model="value" :data="data">
    <template #to-target-button="{ move, disabled, checkedCount }">
      <button :disabled="disabled" @click="move">
        Add {{ checkedCount || '' }} →
      </button>
    </template>
    <template #to-source-button="{ move, disabled, checkedCount }">
      <button :disabled="disabled" @click="move">
        ← Remove {{ checkedCount || '' }}
      </button>
    </template>
  </VxTransfer>
</template>

Custom Item Slot ​

Source11
Item 1#1
Item 2#2
Item 4#4
Item 5#5
Item 6#6
Item 7#7
Item 8#8
Item 9#9
Item 10#10
Item 11#11
Item 12#12
Target1
Item 3#3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([3])
</script>

<template>
  <VxTransfer v-model="value" :data="data">
    <template #item="{ item }">
      <span>{{ item.label }}</span>
      <span style="margin-left: auto; font-size: 11px; opacity: 0.5">
        #{{ item.key }}
      </span>
    </template>
  </VxTransfer>
</template>

Настраивайте заголовки и футеры панелей. Слоты получают { checked, total, query }.

Pick items (6)
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data">
    <template #source-header="{ total }">
      <div class="vx-transfer__header" style="background: var(--muted);">
        <span style="font-size: 13px; font-weight: 600;">
          Pick items ({{ total }})
        </span>
      </div>
    </template>
    <template #target-footer="{ total }">
      <div class="vx-transfer__footer">
        <span class="vx-transfer__footer-info">
          {{ total }} selected
        </span>
      </div>
    </template>
  </VxTransfer>
</template>

Drag and Drop ​

Перетаскивайте элементы между панелями. Включается через draggable. Целевая панель подсвечивается при наведении с перетаскиваемым элементом.

Source10
Item 1
Item 3
Item 4
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target2
Item 2
Item 5
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
  disabled: i === 3 || i === 7,
}))
const value = ref([2, 5])
</script>

<template>
  <VxTransfer v-model="value" :data="data" draggable />
</template>

Keyboard Navigation ​

Кликните по телу панели, чтобы сфокусироваться, затем используйте Arrow Up/Down для навигации по элементам и Space или Enter для переключения выбора. Сфокусированный элемент получает кольцо-обводку.

Source12
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <!-- Click inside a panel, then use Arrow keys + Space/Enter -->
  <VxTransfer v-model="value" :data="data" />
</template>

Scroll Pagination ​

Установите page-size, чтобы подгружать элементы по мере прокрутки. Отрисовывается только видимая страница — новые элементы появляются автоматически при приближении к низу.

All Records (100)95
Record 1
Record 2
Record 3
Record 4
Record 6
Record 7
Record 8
Record 9
Record 11
Record 12
Record 13
Record 14
Record 15
Record 16
Record 17
Selected5
Record 5
Record 10
Record 20
Record 30
Record 40
vue
<script setup lang="ts">
import { ref } from 'vue'

const bigData = Array.from({ length: 100 }, (_, i) => ({
  key: i + 1,
  label: `Record ${i + 1}`,
  disabled: i === 9,
}))
const value = ref([5, 10, 20, 30, 40])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="bigData"
    :page-size="15"
    filterable
    :titles="['All Records (100)', 'Selected']"
  />
</template>

Virtual Scroll ​

Для очень больших наборов данных (1000+ элементов) включите virtual-scroll вместе с item-height для плавной прокрутки на 60fps. В DOM отрисовываются только видимые строки.

5,000 Items4997
Virtual item 1
Virtual item 2
Virtual item 3
Virtual item 4
Virtual item 5
Virtual item 6
Virtual item 7
Virtual item 8
Virtual item 9
Virtual item 10
Virtual item 11
Virtual item 12
Virtual item 13
Virtual item 14
Virtual item 15
Virtual item 16
Virtual item 17
Virtual item 18
Virtual item 19
Selected3
Virtual item 100
Virtual item 200
Virtual item 500
vue
<script setup lang="ts">
import { ref } from 'vue'

const hugeData = Array.from({ length: 5000 }, (_, i) => ({
  key: i + 1,
  label: `Virtual item ${i + 1}`,
}))
const value = ref([100, 200, 500])
</script>

<template>
  <VxTransfer
    v-model="value"
    :data="hugeData"
    virtual-scroll
    :item-height="36"
    filterable
    :titles="['5,000 Items', 'Selected']"
  />
</template>

Remote Data (fetchOptions) ​

Загружайте исходные данные из API с постраничной пагинацией при бесконечной прокрутке. Передайте асинхронную функцию fetchOptions(query, page), возвращающую TransferItem[]. Прокрутите до низа, чтобы загрузить следующую страницу. Поле поиска работает с debounce и запускает новую загрузку.

Comments (remote)0
No data
Selected0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

async function fetchComments(query: string, page: number) {
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/comments?_limit=10&_page=${page}`
  )
  const data = await res.json()
  return data.map((c: any) => ({ key: c.id, label: c.name }))
}

const value = ref([])
</script>

<template>
  <VxTransfer
    v-model="value"
    :fetch-options="fetchComments"
    filterable
    :titles="['Comments (remote)', 'Selected']"
  />
</template>

Loading ​

Внешнее состояние загрузки — показывает индикатор внизу исходной панели.

Source3
Item 2
Item 3
Item 4
Target1
Item 1
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 4 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1])
const loading = ref(true)
</script>

<template>
  <VxTransfer v-model="value" :data="data" :loading="loading" />
</template>

Target Order ​

По умолчанию элементы сохраняют исходный порядок данных. Используйте target-order="push", чтобы добавлять элементы в порядке их выбора.

Source11
Item 1
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Target1
Item 2
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 12 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([2])
</script>

<template>
  <VxTransfer v-model="value" :data="data" target-order="push" />
</template>

Button Visibility ​

Скрывайте одну или обе кнопки перемещения.

Source4
Item 2
Item 4
Item 5
Item 6
Target2
Item 1
Item 3
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([1, 3])
</script>

<template>
  <!-- Only allow adding to target (no remove button) -->
  <VxTransfer v-model="value" :data="data" :show-to-source-button="false" />

  <!-- Only allow removing from target (no add button) -->
  <VxTransfer v-model="value" :data="data" :show-to-target-button="false" />
</template>

Custom Empty State ​

Используйте слоты source-empty и target-empty, чтобы настроить отображение при отсутствии элементов в панели.

Source0
📭
Nothing here yet
Add items to get started
Target0
🎯
No selections
Move items from the left
vue
<template>
  <VxTransfer :data="[]">
    <template #source-empty>
      <div style="text-align: center; padding: 20px;">
        <div style="font-size: 24px;">📭</div>
        <div>Nothing here yet</div>
        <div style="opacity: 0.7;">Add items to get started</div>
      </div>
    </template>
    <template #target-empty>
      <div style="text-align: center; padding: 20px;">
        <div style="font-size: 24px;">🎯</div>
        <div>No selections</div>
        <div style="opacity: 0.7;">Move items from the left</div>
      </div>
    </template>
  </VxTransfer>
</template>

Disabled ​

Source6
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Target0
No data
vue
<script setup lang="ts">
import { ref } from 'vue'

const data = Array.from({ length: 6 }, (_, i) => ({
  key: i + 1,
  label: `Item ${i + 1}`,
}))
const value = ref([])
</script>

<template>
  <VxTransfer v-model="value" :data="data" disabled />
</template>

Props ​

PropTypeDefaultDescription
modelValue(string | number)[][]Ключи в целевом списке (v-model)
dataTransferItem[][]Полный источник данных
titles[string, string]['Source', 'Target']Заголовки панелей
filterablebooleanfalseПоказать поле поиска
filterPlaceholderstring'Search...'Плейсхолдер поиска
filterMethod(query, item) => boolean—Пользовательская функция фильтрации
disabledbooleanfalseОтключить всё взаимодействие
loadingbooleanfalseВнешнее состояние загрузки
targetOrder'original' | 'push''original'Порядок целевых элементов
buttonTexts[string?, string?][]Подписи кнопок перемещения
showToTargetButtonbooleantrueПоказать кнопку перемещения в цель
showToSourceButtonbooleantrueПоказать кнопку перемещения в источник
showMoveAllbooleanfalseПоказать кнопки перемещения всего
pageSizenumber0Элементов на страницу (0 = все)
scrollDistancenumber30Пикселей от низа для запуска загрузки
fetchOptions(query, page) => Promise<TransferItem[]>—Асинхронный удалённый источник
debouncenumber300Debounce удалённого поиска (мс)
keyFieldstring'key'Имя свойства для ключа элемента
labelFieldstring'label'Имя свойства для подписи элемента
panelWidthnumber | string240Ширина панели (px или CSS-значение)
sortMethod(a, b) => number—Компаратор сортировки элементов
draggablebooleanfalseВключить drag and drop между панелями
virtualScrollbooleanfalseВключить виртуальную прокрутку для больших списков
itemHeightnumber36Высота строки в px (для virtual scroll)

Events ​

EventTypeDescription
update:modelValue(value)Целевые ключи изменены
change(value, direction, movedKeys)Элементы перемещены
load-more(page)Запрошена следующая удалённая страница

Slots ​

SlotPropsDescription
item{ item }Пользовательская отрисовка элемента
source-header{ checked, total, query }Пользовательский заголовок исходной панели
target-header{ checked, total, query }Пользовательский заголовок целевой панели
source-footer{ checked, total }Пользовательский футер исходной панели
target-footer{ checked, total }Пользовательский футер целевой панели
source-empty—Пустое состояние источника
target-empty—Пустое состояние цели
to-target-button{ move, disabled, checkedCount }Пользовательская кнопка перемещения в цель
to-source-button{ move, disabled, checkedCount }Пользовательская кнопка перемещения в источник

Exposed Methods ​

MethodDescription
moveRight()Переместить отмеченные элементы в цель
moveLeft()Переместить отмеченные элементы в источник
moveAllRight()Переместить все доступные элементы в цель
moveAllLeft()Переместить все доступные элементы в источник

TransferItem Interface ​

ts
interface TransferItem {
  key: string | number
  label: string
  disabled?: boolean
  [k: string]: any
}