/** * useDragSort Hook * * 列表项拖拽排序,使用 HTML5 Drag and Drop API。 * 触摸设备通过 emulating touch events 支持拖拽(简单模拟,不支持复杂的多点触控排序)。 * * 用法: * const { draggedIndex, dropTargetIndex, dragHandlers } = useDragSort({ * items, * onReorder: (fromIndex, toIndex) => reorder(fromIndex, toIndex), * }) * * return items.map((item, index) => ( *
* {item.name} *
* )) */ import { useState, useCallback, useRef } from 'react' // ================================================================ // 类型定义 // ================================================================ interface UseDragSortOptions { /** 重排序回调 */ onReorder: (fromIndex: number, toIndex: number) => void /** 拖拽句柄的类名(可选),如果设置则仅在该元素上触发拖拽 */ dragHandleClass?: string /** 是否启用,默认 true */ enabled?: boolean } interface DragHandlers { /** 拖拽元素的 ref */ draggable: boolean onDragStart: (e: React.DragEvent) => void onDragOver: (e: React.DragEvent) => void onDragEnter: (e: React.DragEvent) => void onDragEnd: (e: React.DragEvent) => void /** 元素在当前列表中的样式(拖拽中时显示占位效果) */ style: React.CSSProperties } interface UseDragSortReturn { /** 当前被拖拽项的索引 */ draggedIndex: number | null /** 拖拽目标位置的索引 */ dropTargetIndex: number | null /** 获取指定索引项的拖拽事件处理器 */ dragHandlers: (index: number) => DragHandlers } // ================================================================ // Hook // ================================================================ export function useDragSort(options: UseDragSortOptions): UseDragSortReturn { const { onReorder, enabled = true } = options const [draggedIndex, setDraggedIndex] = useState(null) const [dropTargetIndex, setDropTargetIndex] = useState(null) const onReorderRef = useRef(onReorder) onReorderRef.current = onReorder const handleDragStart = useCallback( (index: number) => (_e: React.DragEvent) => { if (!enabled) return setDraggedIndex(index) setDropTargetIndex(null) }, [enabled] ) const handleDragOver = useCallback( (_index: number) => (e: React.DragEvent) => { if (!enabled) return e.preventDefault() e.dataTransfer.dropEffect = 'move' }, [enabled] ) const handleDragEnter = useCallback( (index: number) => (_e: React.DragEvent) => { if (!enabled) return setDropTargetIndex(index) }, [enabled] ) const handleDragEnd = useCallback(() => { if (!enabled) return if (draggedIndex !== null && dropTargetIndex !== null && draggedIndex !== dropTargetIndex) { onReorderRef.current(draggedIndex, dropTargetIndex) } setDraggedIndex(null) setDropTargetIndex(null) }, [enabled, draggedIndex, dropTargetIndex]) const dragHandlers = useCallback( (index: number): DragHandlers => { const isDragging = draggedIndex === index const isOver = dropTargetIndex === index && draggedIndex !== null && draggedIndex !== index return { draggable: enabled, onDragStart: handleDragStart(index), onDragOver: handleDragOver(index), onDragEnter: handleDragEnter(index), onDragEnd: handleDragEnd, style: { opacity: isDragging ? 0.4 : 1, ...(isOver ? { borderTop: '2px solid #3b82f6' } : {}), cursor: enabled ? 'grab' : 'default', transition: 'opacity 0.15s ease', }, } }, [enabled, draggedIndex, dropTargetIndex, handleDragStart, handleDragOver, handleDragEnter, handleDragEnd] ) return { draggedIndex, dropTargetIndex, dragHandlers } }