import type { Key } from 'react' import { useCallback, useMemo } from 'react' interface UseDndReorderParams { /** 原始的、完整的数据列表 */ originalList: T[] /** 当前在界面上渲染的、可能被过滤的列表 */ filteredList: T[] /** 用于更新原始列表状态的函数 */ onUpdate: (newList: T[]) => void /** 用于从列表项中获取唯一ID的属性名或函数 */ itemKey: keyof T | ((item: T) => Key) } /** * 增强拖拽排序能力,处理“过滤后列表”与“原始列表”的索引映射问题。 * * @template T 列表项的类型 * @param params - { originalList, filteredList, onUpdate, idKey } * @returns 返回可以直接传递给 Sortable 的 onSortEnd 回调 */ export function useDndReorder({ originalList, filteredList, onUpdate, itemKey }: UseDndReorderParams) { const getId = useCallback( (item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)), [itemKey] ) // 创建从 item ID 到其在 *原始列表* 中索引的映射 const itemIndexMap = useMemo(() => { const map = new Map() originalList.forEach((item, index) => { map.set(getId(item), index) }) return map }, [originalList, getId]) // 创建一个函数,将 *过滤后列表* 的视图索引转换为 *原始列表* 的数据索引 const getItemKey = useCallback( (index: number): Key => { const item = filteredList[index] // 如果找不到item,返回视图索引兜底 if (!item) return index const originalIndex = itemIndexMap.get(getId(item)) return originalIndex ?? index }, [filteredList, itemIndexMap, getId] ) // 创建 onSortEnd 回调,封装了所有重排逻辑 const onSortEnd = useCallback( ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => { // 使用 getItemKey 将视图索引转换为数据索引 const sourceOriginalIndex = getItemKey(oldIndex) as number const destOriginalIndex = getItemKey(newIndex) as number // 如果索引转换失败,不进行任何操作 if (sourceOriginalIndex === undefined || destOriginalIndex === undefined) { return } if (sourceOriginalIndex === destOriginalIndex) { return } // 操作原始列表的副本 const newList = [...originalList] const [movedItem] = newList.splice(sourceOriginalIndex, 1) newList.splice(destOriginalIndex, 0, movedItem) // 调用外部更新函数 onUpdate(newList) }, [getItemKey, originalList, onUpdate] ) return { onSortEnd, itemKey: getItemKey } }