- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断) - 补齐 4 个 MSW mock 缺口,修 diagnostic case 名 - 修 4 处 Tailwind 任意值;新增共享组件与路由
131 lines
3.3 KiB
TypeScript
131 lines
3.3 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useRef, useState } from "react";
|
||
|
||
import type { Position } from "./use-position-persistence";
|
||
import { clampPosition } from "./use-position-persistence";
|
||
|
||
type DragState = {
|
||
active: boolean;
|
||
moved: boolean;
|
||
startX: number;
|
||
startY: number;
|
||
originX: number;
|
||
originY: number;
|
||
pointerId: number;
|
||
};
|
||
|
||
type DragCallbacks = {
|
||
/** 拖拽开始时触发(pointer down 后) */
|
||
onDragStart: () => void;
|
||
/** 拖拽释放时触发,moved 表示是否发生了实际移动 */
|
||
onRelease: (moved: boolean) => void;
|
||
};
|
||
|
||
/**
|
||
* 拖拽位置 Hook
|
||
*
|
||
* 处理 pointer 事件,跟踪拖拽状态与位置变化。
|
||
* 不处理边缘吸附、持久化等业务逻辑,通过回调委托给调用方。
|
||
*/
|
||
export function useDragPosition(
|
||
position: Position,
|
||
setPosition: React.Dispatch<React.SetStateAction<Position>>,
|
||
callbacks: DragCallbacks,
|
||
): {
|
||
dragging: boolean;
|
||
handlers: {
|
||
onPointerDown: (e: React.PointerEvent<HTMLButtonElement>) => void;
|
||
onPointerMove: (e: React.PointerEvent<HTMLButtonElement>) => void;
|
||
onPointerUp: (e: React.PointerEvent<HTMLButtonElement>) => void;
|
||
onPointerCancel: () => void;
|
||
};
|
||
} {
|
||
const [dragging, setDragging] = useState(false);
|
||
const dragStateRef = useRef<DragState>({
|
||
active: false,
|
||
moved: false,
|
||
startX: 0,
|
||
startY: 0,
|
||
originX: 0,
|
||
originY: 0,
|
||
pointerId: -1,
|
||
});
|
||
const callbacksRef = useRef(callbacks);
|
||
useEffect(() => {
|
||
callbacksRef.current = callbacks;
|
||
}, [callbacks]);
|
||
|
||
const onPointerDown = useCallback(
|
||
(e: React.PointerEvent<HTMLButtonElement>): void => {
|
||
// 仅主键响应拖拽
|
||
if (e.button !== 0 && e.pointerType === "mouse") return;
|
||
const s = dragStateRef.current;
|
||
s.active = true;
|
||
s.moved = false;
|
||
s.startX = e.clientX;
|
||
s.startY = e.clientY;
|
||
s.originX = position.x;
|
||
s.originY = position.y;
|
||
s.pointerId = e.pointerId;
|
||
try {
|
||
e.currentTarget.setPointerCapture(e.pointerId);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
setDragging(true);
|
||
callbacksRef.current.onDragStart();
|
||
},
|
||
[position],
|
||
);
|
||
|
||
const onPointerMove = useCallback(
|
||
(e: React.PointerEvent<HTMLButtonElement>): void => {
|
||
const s = dragStateRef.current;
|
||
if (!s.active || e.pointerId !== s.pointerId) return;
|
||
const dx = e.clientX - s.startX;
|
||
const dy = e.clientY - s.startY;
|
||
// 阈值过滤微抖动
|
||
if (!s.moved && Math.abs(dx) + Math.abs(dy) < 4) return;
|
||
s.moved = true;
|
||
const next = clampPosition({
|
||
x: s.originX + dx,
|
||
y: s.originY + dy,
|
||
});
|
||
setPosition(next);
|
||
},
|
||
[setPosition],
|
||
);
|
||
|
||
const onPointerUp = useCallback(
|
||
(e: React.PointerEvent<HTMLButtonElement>): void => {
|
||
const s = dragStateRef.current;
|
||
if (!s.active || e.pointerId !== s.pointerId) return;
|
||
s.active = false;
|
||
setDragging(false);
|
||
try {
|
||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
callbacksRef.current.onRelease(s.moved);
|
||
},
|
||
[],
|
||
);
|
||
|
||
const onPointerCancel = useCallback((): void => {
|
||
dragStateRef.current.active = false;
|
||
setDragging(false);
|
||
}, []);
|
||
|
||
return {
|
||
dragging,
|
||
handlers: {
|
||
onPointerDown,
|
||
onPointerMove,
|
||
onPointerUp,
|
||
onPointerCancel,
|
||
},
|
||
};
|
||
}
|