feat(portal-shell): 学生域全页面迁移与规范合规修复
- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断) - 补齐 4 个 MSW mock 缺口,修 diagnostic case 名 - 修 4 处 Tailwind 任意值;新增共享组件与路由
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"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,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user