Files
Edu/apps/portal-shell/src/features/teacher/ai/hooks/use-drag-position.ts
SpecialX 04b7a40bdc feat(portal-shell): 学生域全页面迁移与规范合规修复
- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断)

- 补齐 4 个 MSW mock 缺口,修 diagnostic case 名

- 修 4 处 Tailwind 任意值;新增共享组件与路由
2026-08-31 11:25:21 +08:00

131 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
},
};
}