fix(3d): stabilize timeline dragging

This commit is contained in:
2026-09-14 12:57:26 +08:00
parent 08ddb4453d
commit 331ea3f094
2 changed files with 131 additions and 11 deletions
@@ -9,7 +9,11 @@ import {
useState,
type ReactNode,
} from "react";
import Draggable from "react-draggable";
import {
DraggableCore,
type DraggableData,
type DraggableEvent,
} from "react-draggable";
import {
FiCalendar,
FiChevronDown,
@@ -78,7 +82,15 @@ export function ThreeDimensionalTimeline({
onSelectedDateChange,
onCurrentTimeChange,
}: ThreeDimensionalTimelineProps) {
const timelineRef = useRef<HTMLDivElement>(null);
const timelineRef = useRef<HTMLElement>(null);
const dragShieldRef = useRef<HTMLDivElement>(null);
const dragPositionRef = useRef({ x: 0, y: 0 });
const dragStartRef = useRef<{
pointerX: number;
pointerY: number;
originX: number;
originY: number;
} | null>(null);
const [playing, setPlaying] = useState(false);
const [playIntervalMs, setPlayIntervalMs] = useState(DEFAULT_PLAY_INTERVAL_MS);
const [previewTime, setPreviewTime] = useState<number | null>(null);
@@ -133,6 +145,58 @@ export function ThreeDimensionalTimeline({
? Math.min(100, Math.max(0, (safeCurrentTime / durationMinutes) * 100))
: 0;
const applyDragPosition = useCallback((x: number, y: number) => {
dragPositionRef.current = { x, y };
if (timelineRef.current) {
timelineRef.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;
}
}, []);
const handleDragStart = useCallback(
(_event: DraggableEvent, data: DraggableData) => {
const { x: originX, y: originY } = dragPositionRef.current;
dragStartRef.current = {
pointerX: data.x,
pointerY: data.y,
originX,
originY,
};
if (dragShieldRef.current) {
dragShieldRef.current.style.display = "block";
}
if (timelineRef.current) {
timelineRef.current.style.willChange = "transform";
}
},
[],
);
const handleDrag = useCallback(
(_event: DraggableEvent, data: DraggableData) => {
const start = dragStartRef.current;
if (!start) return;
applyDragPosition(
start.originX + data.x - start.pointerX,
start.originY + data.y - start.pointerY,
);
},
[applyDragPosition],
);
const handleDragStop = useCallback(
(event: DraggableEvent, data: DraggableData) => {
handleDrag(event, data);
dragStartRef.current = null;
if (dragShieldRef.current) {
dragShieldRef.current.style.display = "";
}
if (timelineRef.current) {
timelineRef.current.style.willChange = "";
}
},
[handleDrag],
);
return (
<div
className={clsx(
@@ -140,17 +204,25 @@ export function ThreeDimensionalTimeline({
sidePanelOpen ? "md:right-[416px]" : "md:right-4",
)}
>
<Draggable
<div
ref={dragShieldRef}
aria-hidden="true"
className="pointer-events-auto fixed inset-0 z-0 hidden cursor-move"
/>
<DraggableCore
nodeRef={timelineRef}
handle=".timeline-drag-handle"
cancel="button, input, select, [role='dialog']"
onStart={handleDragStart}
onDrag={handleDrag}
onStop={handleDragStop}
>
<section
ref={timelineRef}
aria-label="三维场景时间轴"
className={clsx(
glassClass,
"pointer-events-auto relative w-full max-w-[950px] rounded-2xl opacity-95 transition-opacity duration-200 hover:opacity-100",
"pointer-events-auto relative z-10 w-full max-w-[950px] rounded-2xl opacity-95 transition-opacity duration-200 hover:opacity-100",
)}
>
<div className="timeline-drag-handle relative flex h-7 cursor-move touch-none items-center justify-center rounded-t-2xl border-b border-white/40 bg-white/10">
@@ -248,7 +320,7 @@ export function ThreeDimensionalTimeline({
</div>
</div>
</section>
</Draggable>
</DraggableCore>
</div>
);
}