2026-06-02 12:38:01 +08:00
|
|
|
import { useRef, useState, type CSSProperties } from "react";
|
|
|
|
|
|
|
|
|
|
interface BeforeAfterCompareProps {
|
|
|
|
|
sourceSrc: string;
|
|
|
|
|
resultSrc: string;
|
|
|
|
|
sourceLabel?: string;
|
|
|
|
|
resultLabel?: string;
|
|
|
|
|
sourceAlt?: string;
|
|
|
|
|
resultAlt?: string;
|
|
|
|
|
className?: string;
|
2026-06-10 17:54:45 +08:00
|
|
|
aspectRatio?: string;
|
2026-06-02 12:38:01 +08:00
|
|
|
onSourceLoad?: (width: number, height: number) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MIN_POSITION = 5;
|
|
|
|
|
const MAX_POSITION = 95;
|
|
|
|
|
|
|
|
|
|
function clamp(value: number) {
|
|
|
|
|
return Math.min(MAX_POSITION, Math.max(MIN_POSITION, value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function BeforeAfterCompare({
|
|
|
|
|
sourceSrc,
|
|
|
|
|
resultSrc,
|
|
|
|
|
sourceLabel,
|
|
|
|
|
resultLabel,
|
|
|
|
|
sourceAlt = "原图",
|
|
|
|
|
resultAlt = "结果",
|
|
|
|
|
className = "",
|
2026-06-10 17:54:45 +08:00
|
|
|
aspectRatio,
|
2026-06-02 12:38:01 +08:00
|
|
|
onSourceLoad,
|
|
|
|
|
}: BeforeAfterCompareProps) {
|
|
|
|
|
const stageRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [position, setPosition] = useState(50);
|
|
|
|
|
|
|
|
|
|
const updatePosition = (clientX: number) => {
|
|
|
|
|
const stage = stageRef.current;
|
|
|
|
|
if (!stage) return;
|
|
|
|
|
const rect = stage.getBoundingClientRect();
|
|
|
|
|
if (!rect.width) return;
|
|
|
|
|
setPosition(clamp(((clientX - rect.left) / rect.width) * 100));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={stageRef}
|
|
|
|
|
className={`before-after-compare ${className}`}
|
2026-06-10 17:54:45 +08:00
|
|
|
style={{
|
|
|
|
|
"--compare-position": `${position}%`,
|
|
|
|
|
...(aspectRatio ? { "--compare-aspect-ratio": aspectRatio } : {}),
|
|
|
|
|
} as CSSProperties}
|
2026-06-02 12:38:01 +08:00
|
|
|
aria-label="前后对比"
|
|
|
|
|
>
|
|
|
|
|
<div className="before-after-compare__layer before-after-compare__layer--source">
|
|
|
|
|
<img
|
|
|
|
|
src={sourceSrc}
|
|
|
|
|
alt={sourceAlt}
|
|
|
|
|
onLoad={(event) => {
|
|
|
|
|
onSourceLoad?.(event.currentTarget.naturalWidth, event.currentTarget.naturalHeight);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="before-after-compare__layer before-after-compare__layer--result">
|
|
|
|
|
<img src={resultSrc} alt={resultAlt} />
|
|
|
|
|
</div>
|
|
|
|
|
{sourceLabel && (
|
|
|
|
|
<div className="before-after-compare__label before-after-compare__label--source">{sourceLabel}</div>
|
|
|
|
|
)}
|
|
|
|
|
{resultLabel && (
|
|
|
|
|
<div className="before-after-compare__label before-after-compare__label--result">{resultLabel}</div>
|
|
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
className="before-after-compare__divider"
|
|
|
|
|
role="slider"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
aria-label="拖动对比"
|
|
|
|
|
aria-valuemin={MIN_POSITION}
|
|
|
|
|
aria-valuemax={MAX_POSITION}
|
|
|
|
|
aria-valuenow={Math.round(position)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "ArrowLeft") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setPosition((current) => clamp(current - 2));
|
|
|
|
|
}
|
|
|
|
|
if (event.key === "ArrowRight") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setPosition((current) => clamp(current + 2));
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onPointerDown={(event) => {
|
|
|
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
|
|
|
updatePosition(event.clientX);
|
|
|
|
|
}}
|
|
|
|
|
onPointerMove={(event) => {
|
|
|
|
|
if (!event.currentTarget.hasPointerCapture(event.pointerId)) return;
|
|
|
|
|
updatePosition(event.clientX);
|
|
|
|
|
}}
|
|
|
|
|
onPointerUp={(event) => {
|
|
|
|
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
|
|
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onPointerCancel={(event) => {
|
|
|
|
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
|
|
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|