transfer —— 穿梭框
Ce contenu n’est pas encore disponible dans votre langue.
穿梭框解決的是雙向的多選搬運:左列是「待選」,右列是「已選」,中間兩個箭頭按鈕來回搬。它比 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 單一資料來源:元件的「值」就是右列的 key 陣列。左列不單獨存狀態——partitionTransferData(data, sourceKeys) 每次從全量資料切出來。這避免了「左列和右列狀態不同步」的經典 bug:任何搬運只需改一個陣列,兩側檢視自動重算。
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 不提供屬性包函式(沒有彈層或複雜語義):兩個面板各是 role="listbox",清單項 role="option" + aria-selected;中間的搬運按鈕是普通 <button>,用 aria-label 說明方向(如「向右移動 3 項」);全選框用 checkbox 語義,indeterminate 態透過 getTransferCheckAllState 的 some 觸發 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是「右列有哪些」(元件值)。搬運後要手動把已搬走的 key 從checked裡清掉,否則它們會殘留在勾選態裡(moveTransferKeys不會幫你改 checked)。- 幽靈鍵的來源:資料非同步載入時,使用者先勾選了若干項,然後資料重新整理導致某些 key 消失——這些 key 留在
checked裡就是幽靈鍵。moveTransferKeys清理它們,但toggleTransferCheckAll的全選計數(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