transfer —— Трансфер
Трансфер решает задачу двустороннего перемещения множественного выбора: левая колонка — «кандидаты», правая — «выбранные», а между ними две кнопки-стрелки переносят элементы туда-обратно. Он подходит для больших списков лучше, чем группа checkbox (есть пагинация + поиск), и нагляднее, чем несколько select (видны обе стороны). core/transfer моделирует состояние через единый источник данных sourceKeys — левая колонка это «элементы, не входящие в sourceKeys», правая — «элементы из sourceKeys», а любое действие сводится к преобразованию этого массива.
Когда использовать
Заголовок раздела «Когда использовать»- На всех платформах реализован
LhTransfer: назначение прав, выбор членов роли, фильтрация по тегам, отметка полей данных. - Элементов много (>10), нужен поиск с фильтрацией, и важно одновременно видеть обе стороны — «выбрано/не выбрано».
- Не использовать: элементов меньше 6 (группа checkbox или segmented компактнее); для одностороннего выбора, где не нужно «переносить обратно» (подойдёт select).
import { partitionTransferData, toggleTransferChecked, toggleTransferCheckAll, moveTransferKeys, getTransferCheckAllState, filterTransferItems, getTransferClassNames, getTransferPanelClassNames,} from '@lh-ui/share/core/transfer'import type { LhTransferItem, LhTransferSide } from '@lh-ui/share/core/transfer'Замысел дизайна
Заголовок раздела «Замысел дизайна»sourceKeys — единый источник данных: «значением» компонента является массив ключей правой колонки. Левая колонка не хранит состояние отдельно — partitionTransferData(data, sourceKeys) каждый раз нарезает её из полного набора данных. Это исключает классический баг «состояния левой и правой колонок рассинхронизированы»: любой перенос меняет только один массив, а обе стороны пересчитываются автоматически.
Очистка фантомных ключей в moveTransferKeys: среди checked могут оказаться ключи, которых нет в data (устаревшие отметки, оставшиеся после обновления данных, — фантомные ключи). Их нельзя переносить, но и нельзя оставлять в исходной зоне, засоряя следующий кадр, — поэтому функция попутно отфильтровывает фантомные ключи из исходной зоны. В комментарии кода расписаны оба соображения, заложенные в это решение.
toggleTransferCheckAll пропускает отключённые элементы: «выбрать все» отмечает только «доступные для выбора» (!disabled), и «снять все» сбрасывает только их — состояние отключённых элементов не трогается, что соответствует интуиции «отключено = не подлежит действиям».
Три состояния getTransferCheckAllState: all / some / none управляют признаком indeterminate чекбокса «выбрать все» — это стандартное поведение такого чекбокса (при частичном выборе отображается горизонтальная черта).
Таблица функций
Заголовок раздела «Таблица функций»| Функция | Сигнатура | Возвращаемое значение | Назначение |
|---|---|---|---|
partitionTransferData |
(data, sourceKeys) => { left, right } |
списки обеих сторон | разбиение по keySet |
toggleTransferChecked |
(checked, key) => Array |
новый массив отметок | есть — убрать, нет — добавить |
toggleTransferCheckAll |
(checked, sideItems) => Array |
новый массив отметок | выбрать все / снять все (мимо отключённых) |
moveTransferKeys |
(sourceKeys, data, checked, direction) => Array |
новый sourceKeys | перенос с очисткой фантомных ключей |
getTransferCheckAllState |
(checked, sideItems) => 'all' | 'some' | 'none' |
три состояния | управление indeterminate |
filterTransferItems |
(items, query) => LhTransferItem[] |
отфильтрованный список | нечувствителен к регистру |
getTransferClassNames |
(state: { disabled }) => string[] |
классы | отключено |
getTransferPanelClassNames |
(side) => string[] |
классы | сторона панели |
Определения типов
Заголовок раздела «Определения типов»export interface LhTransferItem { key: string | number label?: string // рабочий контракт допускает отсутствие (UI-слой показывает key) disabled?: boolean // отключено (нельзя перемещать и не участвует в «выбрать все»)}
export type LhTransferSide = 'left' | 'right'Доступность
Заголовок раздела «Доступность»Структуру доступности трансфера собирает UI-слой: core не предоставляет функций-пакетов aria (нет всплывающих слоёв или сложной семантики). Обе панели — это role="listbox", элементы списка — role="option" + aria-selected; кнопки переноса между панелями — обычные <button> с aria-label, поясняющим направление (например «Переместить 3 элемента вправо»); чекбокс «выбрать все» использует семантику checkbox, а состояние indeterminate активируется значением some из getTransferCheckAllState через DOM-атрибут.
Клавиатурное соглашение: после фокуса на элементе списка Space его отмечает, ↑/↓ перемещают фокус; после фокуса на кнопке переноса Enter выполняет действие.
Примеры использования
Заголовок раздела «Примеры использования»import { partitionTransferData, moveTransferKeys, getTransferCheckAllState, filterTransferItems,} from '@lh-ui/share/core/transfer'
const data = [ { key: 'a', label: 'яблоко' }, { key: 'b', label: 'банан', disabled: true }, { key: 'c', label: 'вишня' },]
partitionTransferData(data, ['c'])// { left: [яблоко, банан], right: [вишня] }
moveTransferKeys(['c'], data, ['a', 'b'], 'left-to-right')// ['c', 'a'] — b отключён и не переносится; a перенесён слева направо
moveTransferKeys(['c'], data, ['a', 'ghost'], 'left-to-right')// ['c', 'a'] — фантомный ключ 'ghost' отброшен и не попадает в результат
getTransferCheckAllState(['a'], [{ key: 'a' }, { key: 'b', disabled: true }, { key: 'c' }])// 'some' (доступны для выбора a и c, отмечен только a → частично; b отключён и в расчёт не идёт)
filterTransferItems(data, 'бан') // [{ key: 'b', label: 'банан' }]filterTransferItems(data, '') // исходный массив (пустой query не фильтрует)<!-- Vue3 --><div :class="getTransferClassNames({ disabled })"> <transfer-panel side="left" :items="partition.left" :checked="leftChecked" @toggle="onToggle" @check-all="onCheckAll" /> <div class="lh-transfer__buttons"> <button type="button" class="lh-transfer__button" :disabled="leftChecked.length === 0" :aria-label="`Переместить ${leftChecked.length} элем. вправо`" @click="move('left-to-right')" >→</button> <button type="button" class="lh-transfer__button" :disabled="rightChecked.length === 0" :aria-label="`Переместить ${rightChecked.length} элем. влево`" @click="move('right-to-left')" >←</button> </div> <transfer-panel side="right" :items="partition.right" :checked="rightChecked" /></div>// подключение состояния (разбиение через computed, checked — локальные отметки)const partition = computed(() => partitionTransferData(props.data, props.modelValue))const leftChecked = computed(() => state.checked.filter((k) => !props.modelValue.includes(k)))const rightChecked = computed(() => state.checked.filter((k) => props.modelValue.includes(k)))
function move(direction) { const checked = direction === 'left-to-right' ? leftChecked.value : rightChecked.value emit('update:modelValue', moveTransferKeys(props.modelValue, props.data, checked, direction)) state.checked = state.checked.filter((k) => !checked.includes(k)) // снять отметки с перенесённых}// React: sourceKeys — это управляемое значение; отметки хранятся в локальном useState,// а после переноса соответствующие отметки сбрасываются// uniapp: две колонки на <view> + прокрутка через <scroll-view>;// фильтрацию в поле поиска делает filterTransferItemsСтили и токены
Заголовок раздела «Стили и токены»| Имя класса | Семантика |
|---|---|
.lh-transfer |
базовый класс (горизонтальный flex) |
.lh-transfer--disabled |
отключено |
.lh-transfer-panel |
панель одной стороны |
.lh-transfer-panel--left / --right |
сторона |
.lh-transfer-panel__header |
заголовок панели (чекбокс «выбрать все» + счётчик) |
.lh-transfer-panel__search |
поле поиска |
.lh-transfer-panel__body |
область списка |
.lh-transfer-panel__item |
элемент списка |
.lh-transfer-panel__item--disabled |
отключённый элемент |
.lh-transfer-panel__footer |
футер (пустое состояние / счётчик) |
.lh-transfer__buttons / __button |
группа кнопок переноса |
Состояние indeterminate чекбокса в заголовке панели управляется getTransferCheckAllState.
Замечания
Заголовок раздела «Замечания»checkedиsourceKeys— два независимых состояния:checked— «что сейчас отмечено» (возможно, с обеих сторон),sourceKeys— «что лежит в правой колонке» (значение компонента). После переноса нужно вручную убрать перенесённые ключи изchecked, иначе они останутся в состоянии отметок (moveTransferKeysне очищает checked за вас).- Происхождение фантомных ключей: при асинхронной загрузке данных пользователь успевает отметить несколько элементов, затем данные обновляются и часть ключей исчезает — эти ключи и становятся фантомными в
checked.moveTransferKeysих очищает, но на подсчёт «выбрать все» (getTransferCheckAllState) они тоже могут влиять, поэтому при рендере отметок на UI-слое надёжнее дополнительно фильтровать черезbyKey.has(key). - Границы отключённых элементов: элемент с
disabled: trueне перемещается, не участвует в «выбрать все» и не снимается при «снять все» — он сохраняет своё состояние отметки. Если бизнес-логика требует «отключённый элемент всегда неотмечен», обрабатывайте это на уровне данных, не опираясь на core. labelможет отсутствовать: в типахlabel?необязателен, UI-слой должен показыватьkeyкак запасной вариант (item.label ?? item.key).- Производительность на больших данных:
partitionTransferDataкаждый раз полностью обходит data, аfilterTransferItemsвыполняется при изменении query. При тысячах элементов добавьте виртуальную прокрутку (core/shared/virtual); сама функция разбиения в core имеет сложность O(n) и не является узким местом. - Порядок наложения поиска и разбиения: сначала
partitionTransferData, затемfilterTransferItemsдля каждой стороны — если поменять порядок, поиск будет фильтровать неразбитый полный массив.
@lh-ui/share/core/transfer → moveTransferKeys( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue