Initial commit: OmniAI Web Frontend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { useRef, useState, type CSSProperties } from "react";
|
||||
|
||||
interface BeforeAfterCompareProps {
|
||||
sourceSrc: string;
|
||||
resultSrc: string;
|
||||
sourceLabel?: string;
|
||||
resultLabel?: string;
|
||||
sourceAlt?: string;
|
||||
resultAlt?: string;
|
||||
className?: string;
|
||||
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 = "",
|
||||
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}`}
|
||||
style={{ "--compare-position": `${position}%` } as CSSProperties}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user