Transfer
Двухпанельный список для перемещения элементов между исходной и целевой коллекциями.
<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
<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
<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, чтобы сопоставить произвольные свойства данных без их преобразования.
<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
<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
Элементы в обеих панелях сортируются переданной функцией-компаратором.
<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
Показать кнопки перемещения всего (двойные стрелки), чтобы разом перенести все доступные элементы.
<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
<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.
<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
<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>Header & Footer Slots
Настраивайте заголовки и футеры панелей. Слоты получают { checked, total, query }.
<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. Целевая панель подсвечивается при наведении с перетаскиваемым элементом.
<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 для переключения выбора. Сфокусированный элемент получает кольцо-обводку.
<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, чтобы подгружать элементы по мере прокрутки. Отрисовывается только видимая страница — новые элементы появляются автоматически при приближении к низу.
<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 отрисовываются только видимые строки.
<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 и запускает новую загрузку.
<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
Внешнее состояние загрузки — показывает индикатор внизу исходной панели.
<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", чтобы добавлять элементы в порядке их выбора.
<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
Скрывайте одну или обе кнопки перемещения.
<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, чтобы настроить отображение при отсутствии элементов в панели.
<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
<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
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | (string | number)[] | [] | Ключи в целевом списке (v-model) |
data | TransferItem[] | [] | Полный источник данных |
titles | [string, string] | ['Source', 'Target'] | Заголовки панелей |
filterable | boolean | false | Показать поле поиска |
filterPlaceholder | string | 'Search...' | Плейсхолдер поиска |
filterMethod | (query, item) => boolean | — | Пользовательская функция фильтрации |
disabled | boolean | false | Отключить всё взаимодействие |
loading | boolean | false | Внешнее состояние загрузки |
targetOrder | 'original' | 'push' | 'original' | Порядок целевых элементов |
buttonTexts | [string?, string?] | [] | Подписи кнопок перемещения |
showToTargetButton | boolean | true | Показать кнопку перемещения в цель |
showToSourceButton | boolean | true | Показать кнопку перемещения в источник |
showMoveAll | boolean | false | Показать кнопки перемещения всего |
pageSize | number | 0 | Элементов на страницу (0 = все) |
scrollDistance | number | 30 | Пикселей от низа для запуска загрузки |
fetchOptions | (query, page) => Promise<TransferItem[]> | — | Асинхронный удалённый источник |
debounce | number | 300 | Debounce удалённого поиска (мс) |
keyField | string | 'key' | Имя свойства для ключа элемента |
labelField | string | 'label' | Имя свойства для подписи элемента |
panelWidth | number | string | 240 | Ширина панели (px или CSS-значение) |
sortMethod | (a, b) => number | — | Компаратор сортировки элементов |
draggable | boolean | false | Включить drag and drop между панелями |
virtualScroll | boolean | false | Включить виртуальную прокрутку для больших списков |
itemHeight | number | 36 | Высота строки в px (для virtual scroll) |
Events
| Event | Type | Description |
|---|---|---|
update:modelValue | (value) | Целевые ключи изменены |
change | (value, direction, movedKeys) | Элементы перемещены |
load-more | (page) | Запрошена следующая удалённая страница |
Slots
| Slot | Props | Description |
|---|---|---|
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
| Method | Description |
|---|---|
moveRight() | Переместить отмеченные элементы в цель |
moveLeft() | Переместить отмеченные элементы в источник |
moveAllRight() | Переместить все доступные элементы в цель |
moveAllLeft() | Переместить все доступные элементы в источник |
TransferItem Interface
interface TransferItem {
key: string | number
label: string
disabled?: boolean
[k: string]: any
}