fix(intake): 支持拖动查看放大的 AI 白底图原图
This commit is contained in:
parent
b2fea563ca
commit
ce6c403637
@ -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(
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user