Files
omniai-web/src/components/BeforeAfterCompare.tsx
T
ludan a6626beb32
Web Quality / verify (pull_request) Has been cancelled
feat: 多页面UI打磨 — 设置面板、状态反馈与样式升级
本次更新对多个功能页面进行了系统性的 UI/UX 打磨,统一了交互模式并补充了缺失的状态反馈。

## 新增功能
- WorkbenchPage: 图片提示词案例区域新增加载骨架屏、错误回退、空数据三种状态展示
- CharacterMixPage: 新增左侧设置面板(驱动提示词、图像检测开关、水印开关),支持清除已上传的人物图/参考视频
- DigitalHumanPage: 新增左侧设置面板(提示词输入、去水印/保留原声开关),支持清除已上传的人像/音频,增加取消生成按钮
- ImageWorkbenchPage / ResolutionUpscalePage: 新增参数设置面板和资产清除交互
- MorePage: 新增页面入口

## UI 优化
- 统一 Toggle 开关组件: 所有设置页面采用一致的 .studio-toggle 交互模式
- 资产清除: 各上传区域新增清除按钮,含二次确认和提示反馈
- 生成按钮: 统一为带图标的 .studio-generate-btn,增加 disabled/loading 状态
- ConversationSidebar / ProjectSidebar: 侧边栏交互细节优化

## 样式升级
- image-workbench.css: 大幅扩展样式 (+1900 行),覆盖设置面板、上传区、结果展示等
- workbench.css: 新增 666 行样式,含骨架屏动画、案例卡片网格、状态占位等
- subtitle-removal.css: 补充设置面板样式
2026-06-10 17:54:45 +08:00

114 lines
3.4 KiB
TypeScript

import { useRef, useState, type CSSProperties } from "react";
interface BeforeAfterCompareProps {
sourceSrc: string;
resultSrc: string;
sourceLabel?: string;
resultLabel?: string;
sourceAlt?: string;
resultAlt?: string;
className?: string;
aspectRatio?: 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 = "",
aspectRatio,
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}%`,
...(aspectRatio ? { "--compare-aspect-ratio": aspectRatio } : {}),
} 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>
);
}