fix(intake): 支持拖动查看放大的 AI 白底图原图

This commit is contained in:
WuYongJie 2026-06-16 10:09:00 +08:00
parent b2fea563ca
commit ce6c403637
2 changed files with 127 additions and 10 deletions

View File

@ -148,19 +148,36 @@ describe("NonIntegratedAiWhiteBackgroundPanel", () => {
"src",
"/api/intake/non-integrated/assets/DNZ0001/产品实拍图/front-source.webp",
);
expect(previewImage).toHaveStyle({ transform: "scale(1)" });
expect(previewImage).toHaveStyle({
transform: "translate3d(0px, 0px, 0) scale(1)",
});
fireEvent.click(screen.getByRole("button", { name: "放大原图" }));
expect(previewImage).toHaveStyle({ transform: "scale(1.25)" });
expect(previewImage).toHaveStyle({
transform: "translate3d(0px, 0px, 0) scale(1.25)",
});
fireEvent.mouseDown(previewImage, { clientX: 100, clientY: 120 });
fireEvent.mouseMove(window, { clientX: 136, clientY: 150 });
expect(previewImage).toHaveStyle({
transform: "translate3d(36px, 30px, 0) scale(1.25)",
});
fireEvent.mouseUp(window);
fireEvent.wheel(lightbox, { deltaY: -100 });
expect(previewImage).toHaveStyle({ transform: "scale(1.5)" });
expect(previewImage).toHaveStyle({
transform: "translate3d(36px, 30px, 0) scale(1.5)",
});
fireEvent.wheel(lightbox, { deltaY: 100 });
expect(previewImage).toHaveStyle({ transform: "scale(1.25)" });
expect(previewImage).toHaveStyle({
transform: "translate3d(36px, 30px, 0) scale(1.25)",
});
fireEvent.click(screen.getByRole("button", { name: "重置原图缩放" }));
expect(previewImage).toHaveStyle({ transform: "scale(1)" });
expect(previewImage).toHaveStyle({
transform: "translate3d(0px, 0px, 0) scale(1)",
});
fireEvent.click(screen.getByRole("button", { name: "收起原图" }));
expect(

View File

@ -1,6 +1,11 @@
"use client";
import { type WheelEvent, useEffect, useState } from "react";
import {
type MouseEvent as ReactMouseEvent,
type WheelEvent,
useEffect,
useState,
} from "react";
import {
AlertCircle,
ImageIcon,
@ -181,6 +186,19 @@ const AI_WHITE_BACKGROUND_PREVIEW_ZOOM_MAX = 3;
const AI_WHITE_BACKGROUND_PREVIEW_ZOOM_STEP = 0.25;
const AI_WHITE_BACKGROUND_PREVIEW_WHEEL_ZOOM_FACTOR =
AI_WHITE_BACKGROUND_PREVIEW_ZOOM_STEP / 100;
const AI_WHITE_BACKGROUND_PREVIEW_PAN_ORIGIN = { x: 0, y: 0 };
type AiWhiteBackgroundPreviewPan = {
x: number;
y: number;
};
type AiWhiteBackgroundPreviewDragState = {
originX: number;
originY: number;
startX: number;
startY: number;
};
function formatPreviewZoom(value: number) {
return `${Math.round(value * 100)}%`;
@ -212,6 +230,13 @@ function AiWhiteBackgroundImageLightbox({
onResetZoom: () => void;
onClose: () => void;
}) {
const [previewPan, setPreviewPan] = useState<AiWhiteBackgroundPreviewPan>(
AI_WHITE_BACKGROUND_PREVIEW_PAN_ORIGIN,
);
const [dragState, setDragState] =
useState<AiWhiteBackgroundPreviewDragState | null>(null);
const canPanPreview = zoom > 1;
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
@ -223,6 +248,31 @@ function AiWhiteBackgroundImageLightbox({
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
useEffect(() => {
if (!dragState) {
return undefined;
}
function handlePreviewMouseMove(event: MouseEvent) {
event.preventDefault();
setPreviewPan({
x: dragState.originX + event.clientX - dragState.startX,
y: dragState.originY + event.clientY - dragState.startY,
});
}
function handlePreviewMouseUp() {
setDragState(null);
}
window.addEventListener("mousemove", handlePreviewMouseMove);
window.addEventListener("mouseup", handlePreviewMouseUp);
return () => {
window.removeEventListener("mousemove", handlePreviewMouseMove);
window.removeEventListener("mouseup", handlePreviewMouseUp);
};
}, [dragState]);
const handleWheelZoom = (event: WheelEvent<HTMLDivElement>) => {
if (Math.abs(event.deltaY) < 0.5) {
return;
@ -231,8 +281,49 @@ function AiWhiteBackgroundImageLightbox({
event.preventDefault();
event.stopPropagation();
onWheelZoom(event.deltaY);
const nextZoom = clampPreviewZoom(
zoom - event.deltaY * AI_WHITE_BACKGROUND_PREVIEW_WHEEL_ZOOM_FACTOR,
);
if (nextZoom <= 1) {
setPreviewPan(AI_WHITE_BACKGROUND_PREVIEW_PAN_ORIGIN);
setDragState(null);
}
};
const handleZoomOutPreview = () => {
if (clampPreviewZoom(zoom - AI_WHITE_BACKGROUND_PREVIEW_ZOOM_STEP) <= 1) {
setPreviewPan(AI_WHITE_BACKGROUND_PREVIEW_PAN_ORIGIN);
setDragState(null);
}
onZoomOut();
};
const handlePreviewMouseDown = (
event: ReactMouseEvent<HTMLImageElement>,
) => {
if (!canPanPreview) {
return;
}
event.preventDefault();
event.stopPropagation();
setDragState({
originX: previewPan.x,
originY: previewPan.y,
startX: event.clientX,
startY: event.clientY,
});
};
const handleResetPreview = () => {
setPreviewPan(AI_WHITE_BACKGROUND_PREVIEW_PAN_ORIGIN);
setDragState(null);
onResetZoom();
};
const appliedPreviewPan = canPanPreview
? previewPan
: AI_WHITE_BACKGROUND_PREVIEW_PAN_ORIGIN;
return (
<div
data-testid="ai-white-background-lightbox"
@ -254,7 +345,7 @@ function AiWhiteBackgroundImageLightbox({
<button
type="button"
aria-label="缩小原图"
onClick={onZoomOut}
onClick={handleZoomOutPreview}
disabled={zoom <= AI_WHITE_BACKGROUND_PREVIEW_ZOOM_MIN}
className="flex h-8 w-8 items-center justify-center rounded-[5px] text-white/82 transition hover:bg-white/12 hover:text-white disabled:cursor-not-allowed disabled:opacity-35"
>
@ -263,7 +354,7 @@ function AiWhiteBackgroundImageLightbox({
<button
type="button"
aria-label="重置原图缩放"
onClick={onResetZoom}
onClick={handleResetPreview}
className="h-8 min-w-14 rounded-[5px] px-2 font-mono text-[11px] font-black text-white/82 transition hover:bg-white/12 hover:text-white"
>
{formatPreviewZoom(zoom)}
@ -298,9 +389,17 @@ function AiWhiteBackgroundImageLightbox({
data-testid="ai-white-background-preview-image"
src={src}
alt={alt}
className="max-h-[82vh] max-w-[92vw] select-none object-contain shadow-[0_26px_90px_rgba(0,0,0,0.5)] transition-transform duration-150"
draggable={false}
onMouseDown={handlePreviewMouseDown}
className={`max-h-[82vh] max-w-[92vw] select-none object-contain shadow-[0_26px_90px_rgba(0,0,0,0.5)] ${
canPanPreview
? dragState
? "cursor-grabbing transition-none"
: "cursor-grab transition-transform duration-150"
: "transition-transform duration-150"
}`}
style={{
transform: `scale(${zoom})`,
transform: `translate3d(${appliedPreviewPan.x}px, ${appliedPreviewPan.y}px, 0) scale(${zoom})`,
transformOrigin: "center center",
}}
/>
@ -797,6 +896,7 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
{previewImage ? (
<AiWhiteBackgroundImageLightbox
key={previewImage.src}
src={previewImage.src}
alt={previewImage.alt}
zoom={previewZoom}