fix: stabilize enhance preview compare
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-21 01:31:38 +08:00
parent 82dda143eb
commit 8a722ed849

View File

@ -160,13 +160,6 @@ function clampNumber(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function isEnhanceCompareControlTarget(target: EventTarget | null) {
return (
target instanceof HTMLElement &&
Boolean(target.closest("[data-enhance-pro-compare-control='true']"))
);
}
function fitDimensions(width: number, height: number, maxOutputEdge: number) {
const safeWidth = Math.max(1, Math.round(width || 1024));
const safeHeight = Math.max(1, Math.round(height || 1024));
@ -403,16 +396,8 @@ export function EnhanceProWorkspace({
const [settingsOpen, setSettingsOpen] = useState(true);
const [adjustmentsOpen, setAdjustmentsOpen] = useState(true);
const [faceOpen, setFaceOpen] = useState(true);
const [previewOffset, setPreviewOffset] = useState({ x: 0, y: 0 });
const [comparePosition, setComparePosition] = useState(50);
const [isPreviewDragging, setIsPreviewDragging] = useState(false);
const previewDragStateRef = useRef<{
pointerId: number;
startX: number;
startY: number;
originX: number;
originY: number;
} | null>(null);
const compareDragPointerIdRef = useRef<number | null>(null);
const { data: configPayload, isLoading } = useSWR<{
success?: boolean;
@ -523,12 +508,6 @@ export function EnhanceProWorkspace({
setLastErrorMessage("");
}, [sourceImageUrl]);
useEffect(() => {
setPreviewOffset({ x: 0, y: 0 });
setIsPreviewDragging(false);
previewDragStateRef.current = null;
}, [activePreviewUrl]);
useEffect(() => {
setComparePosition(50);
}, [selectedResultIndex]);
@ -682,67 +661,43 @@ export function EnhanceProWorkspace({
return items;
}, [generatedImages, sourceImageUrl]);
const handlePreviewPointerDown = (
const updateComparePositionFromPointer = (
event: ReactPointerEvent<HTMLDivElement>
) => {
if (!activePreviewUrl || event.button !== 0) {
return;
}
if (isEnhanceCompareControlTarget(event.target)) {
return;
}
const rect = event.currentTarget.getBoundingClientRect();
if (rect.width <= 0) return;
const nextPosition = ((event.clientX - rect.left) / rect.width) * 100;
setComparePosition(clampNumber(nextPosition, 0, 100));
};
previewDragStateRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
originX: previewOffset.x,
originY: previewOffset.y,
};
setIsPreviewDragging(true);
const handleComparePointerDown = (
event: ReactPointerEvent<HTMLDivElement>
) => {
if (event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
compareDragPointerIdRef.current = event.pointerId;
event.currentTarget.setPointerCapture(event.pointerId);
updateComparePositionFromPointer(event);
};
const handlePreviewPointerMove = (
const handleComparePointerMove = (
event: ReactPointerEvent<HTMLDivElement>
) => {
const dragState = previewDragStateRef.current;
if (!dragState || dragState.pointerId !== event.pointerId) {
return;
}
const maxOffsetX = Math.max(48, event.currentTarget.clientWidth * 0.18);
const maxOffsetY = Math.max(48, event.currentTarget.clientHeight * 0.18);
setPreviewOffset({
x: clampNumber(
dragState.originX + event.clientX - dragState.startX,
-maxOffsetX,
maxOffsetX
),
y: clampNumber(
dragState.originY + event.clientY - dragState.startY,
-maxOffsetY,
maxOffsetY
),
});
if (compareDragPointerIdRef.current !== event.pointerId) return;
event.preventDefault();
event.stopPropagation();
updateComparePositionFromPointer(event);
};
const finishPreviewDrag = (event?: ReactPointerEvent<HTMLDivElement>) => {
const dragState = previewDragStateRef.current;
if (!dragState) {
return;
const finishCompareDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
if (compareDragPointerIdRef.current !== event.pointerId) return;
event.preventDefault();
event.stopPropagation();
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
if (
event &&
event.currentTarget.hasPointerCapture(dragState.pointerId)
) {
event.currentTarget.releasePointerCapture(dragState.pointerId);
}
previewDragStateRef.current = null;
setIsPreviewDragging(false);
compareDragPointerIdRef.current = null;
};
return (
@ -751,24 +706,9 @@ export function EnhanceProWorkspace({
<div className="pointer-events-none absolute inset-0 bg-[#ffffff]" />
<div className="pattern-grid-lg pointer-events-none absolute inset-0 opacity-[0.025] [mask-image:radial-gradient(circle_at_center,black_38%,transparent_82%)]" />
{activePreviewUrl ? (
<div
className={cn(
"relative flex h-full w-full min-w-0 touch-none select-none items-center justify-center overflow-hidden",
isPreviewDragging ? "cursor-grabbing" : "cursor-grab"
)}
onDoubleClick={() => setPreviewOffset({ x: 0, y: 0 })}
onPointerCancel={finishPreviewDrag}
onPointerDown={handlePreviewPointerDown}
onPointerMove={handlePreviewPointerMove}
onPointerUp={finishPreviewDrag}
>
<div className="relative flex h-full w-full min-w-0 select-none items-center justify-center overflow-hidden">
<div className="relative flex h-full w-full min-w-0 items-center justify-center overflow-hidden px-4 py-4">
<div
className="relative h-full w-full max-w-[min(1320px,calc(100vw-30rem))] overflow-hidden"
style={{
transform: `translate(${previewOffset.x}px, ${previewOffset.y}px)`,
}}
>
<div className="relative h-full w-full max-w-[min(1320px,calc(100vw-30rem))] overflow-hidden">
<img
alt={activePreviewLabel}
className="pointer-events-none absolute inset-0 h-full w-full object-contain transition-transform duration-75"
@ -792,6 +732,25 @@ export function EnhanceProWorkspace({
className="pointer-events-none absolute inset-y-0 w-px bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.32),0_0_18px_rgba(0,0,0,0.35)]"
style={{ left: `${comparePosition}%` }}
/>
<div
className="pointer-events-auto absolute inset-0 cursor-col-resize touch-none"
data-enhance-pro-compare-control="true"
onClick={(event) => event.stopPropagation()}
onPointerCancel={finishCompareDrag}
onPointerDown={handleComparePointerDown}
onPointerMove={handleComparePointerMove}
onPointerUp={finishCompareDrag}
>
<div
className="pointer-events-none absolute inset-y-0 w-12 -translate-x-1/2"
style={{ left: `${comparePosition}%` }}
>
<div className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.25),0_0_18px_rgba(0,0,0,0.3)]" />
<div className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 flex h-9 w-9 items-center justify-center rounded-full border border-white/80 bg-black/70 text-white shadow-[0_12px_24px_rgba(0,0,0,0.28)] backdrop-blur-sm">
<SlidersHorizontal className="h-4 w-4" />
</div>
</div>
</div>
</>
) : null}
{showComparisonPreview ? (
@ -802,38 +761,6 @@ export function EnhanceProWorkspace({
<div className="pointer-events-none absolute top-3 right-3 rounded-full bg-black/65 px-2.5 py-1 font-medium text-[11px] text-white backdrop-blur-sm">
增强后
</div>
<div
className="pointer-events-auto absolute right-6 bottom-5 left-6 h-8 cursor-col-resize rounded-full"
data-enhance-pro-compare-control="true"
onClick={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
onPointerMove={(event) => event.stopPropagation()}
>
<div className="-translate-y-1/2 pointer-events-none absolute top-1/2 right-0 left-0 h-1 rounded-full bg-white/55 shadow-[0_0_0_1px_rgba(0,0,0,0.2)]">
<div
className="h-full rounded-full bg-[#111111]"
style={{ width: `${comparePosition}%` }}
/>
</div>
<div
className="-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute top-1/2 h-5 w-5 rounded-full border-2 border-white bg-[#111111] shadow-[0_8px_18px_rgba(0,0,0,0.28)]"
style={{ left: `${comparePosition}%` }}
/>
<input
aria-label="原图和增强结果对比滑块"
className="absolute inset-0 h-full w-full cursor-col-resize opacity-0"
max={100}
min={0}
onChange={(event) =>
setComparePosition(Number(event.target.value) || 0)
}
onInput={(event) =>
setComparePosition(Number(event.currentTarget.value) || 0)
}
type="range"
value={comparePosition}
/>
</div>
</>
) : null}
</div>
@ -843,7 +770,7 @@ export function EnhanceProWorkspace({
<span>{activePreviewLabel}</span>
</div>
<div className="pointer-events-none absolute top-6 right-8 hidden items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/96 px-3 py-1.5 text-[#666666] text-xs shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_10px_24px_rgba(0,0,0,0.04)] lg:inline-flex">
双击回正
图片固定显示
</div>
</div>
) : (