feat(portal-shell): 学生域全页面迁移与规范合规修复

- 学生域 32 页全量迁移(含作答/自动保存/提交/诊断)

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

- 修 4 处 Tailwind 任意值;新增共享组件与路由
This commit is contained in:
SpecialX
2026-08-31 11:25:21 +08:00
parent 039db5efdd
commit 04b7a40bdc
493 changed files with 70985 additions and 2112 deletions

View File

@@ -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,
},
};
}