a6626beb32
Web Quality / verify (pull_request) Has been cancelled
本次更新对多个功能页面进行了系统性的 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: 补充设置面板样式
114 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|