VetNet-LCPS / vetcopilot-app /src /hooks /useDragSort.ts
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
3.93 kB
/**
* 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) => (
* <div key={item.id} {...dragHandlers(index)}>
* {item.name}
* </div>
* ))
*/
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<number | null>(null)
const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(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 }
}