Files
omniai-web/src/features/ecommerce/panels/EcommerceClonePanel.tsx
T

798 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
CloudUploadOutlined,
CloseOutlined,
FileImageOutlined,
LoadingOutlined,
QuestionCircleOutlined,
ReloadOutlined,
SettingOutlined,
} from "@ant-design/icons";
import { createPortal } from "react-dom";
import type { CSSProperties, ChangeEvent, DragEvent, MutableRefObject, RefObject } from "react";
import { useRef, useState } from "react";
type ProductSetOutputKey = "set" | "detail" | "model" | "video";
type CloneOutputKey = ProductSetOutputKey | "hot" | "video-outfit";
type CloneSetCountKey = "selling" | "white" | "scene";
type CloneModelPanelTab = "scene" | "model";
type CloneReferenceMode = "upload" | "link";
type CloneReplicateLevelKey = "style" | "high";
type CloneVideoQualityKey = "standard" | "high" | "ultra";
type CloneBasicSelectKey = "platform" | "market" | "language" | "ratio";
type CloneModelSelectKey = "gender" | "age" | "ethnicity" | "body";
interface CloneImageItem {
id: string;
src: string;
name: string;
}
interface CloneBasicSelectItem {
key: CloneBasicSelectKey;
label: string;
value: string;
options: string[];
onChange: (value: string) => void;
}
interface CloneModelSelectItem {
key: CloneModelSelectKey;
label: string;
value: string;
options: string[];
onChange: (value: string) => void;
}
interface CloneSetCountOption {
key: CloneSetCountKey;
title: string;
desc: string;
}
interface CloneOutputOption {
key: CloneOutputKey;
label: string;
}
interface CloneReplicateLevelOption {
key: CloneReplicateLevelKey;
title: string;
desc: string;
}
interface CloneVideoQualityOption {
key: CloneVideoQualityKey;
label: string;
desc: string;
}
interface CloneDetailModule {
id: string;
title: string;
desc: string;
}
interface EcommerceClonePanelProps {
productInputRef: RefObject<HTMLInputElement>;
cloneReferenceInputRef: RefObject<HTMLInputElement>;
productImages: CloneImageItem[];
isProductUploadDragging: boolean;
cloneOutput: CloneOutputKey;
cloneOutputOptions: CloneOutputOption[];
cloneBasicSelects: CloneBasicSelectItem[];
openCloneBasicSelect: CloneBasicSelectKey | null;
cloneReferenceMode: CloneReferenceMode;
cloneReferenceImages: CloneImageItem[];
maxCloneReferenceImages: number;
cloneReplicateLevel: CloneReplicateLevelKey;
cloneReplicateLevelOptions: CloneReplicateLevelOption[];
cloneSetCounts: Record<CloneSetCountKey, number>;
cloneSetCountOptions: CloneSetCountOption[];
cloneSetTotal: number;
minCloneSetTotal: number;
maxCloneSetTotal: number;
selectedCloneDetailModules: string[];
cloneDetailModules: CloneDetailModule[];
cloneModelPanelTab: CloneModelPanelTab;
tryOnScenes: string[];
selectedCloneModelScenes: string[];
cloneModelCustomScene: string;
cloneModelSelects: CloneModelSelectItem[];
openCloneModelSelect: CloneModelSelectKey | null;
cloneModelSelectDropUp: boolean;
cloneVideoQuality: CloneVideoQualityKey;
cloneVideoQualityOptions: CloneVideoQualityOption[];
cloneVideoDuration: number;
cloneVideoDurationMin: number;
cloneVideoDurationMax: number;
cloneVideoDurationStyle: CSSProperties;
cloneVideoSmart: boolean;
canGenerate: boolean;
status: string;
lastFailedActionRef: MutableRefObject<(() => void) | null>;
setIsProductUploadDragging: (value: boolean) => void;
handleProductDrop: (event: DragEvent<HTMLDivElement>) => void;
removeProductImage: (id: string) => void;
handleProductUpload: (event: ChangeEvent<HTMLInputElement>) => void;
handleCloneOutputChange: (value: CloneOutputKey) => void;
setOpenCloneBasicSelect: (value: CloneBasicSelectKey | null) => void;
setCloneReferenceMode: (value: CloneReferenceMode) => void;
handleCloneReferenceUpload: (event: ChangeEvent<HTMLInputElement>) => void;
isCloneReferenceDragging: boolean;
handleCloneReferenceDragOver: (event: DragEvent<HTMLButtonElement>) => void;
handleCloneReferenceDragLeave: (event: DragEvent<HTMLButtonElement>) => void;
handleCloneReferenceDrop: (event: DragEvent<HTMLButtonElement>) => void;
setCloneReplicateLevel: (value: CloneReplicateLevelKey) => void;
startCloneSetCountHold: (key: CloneSetCountKey, delta: -1 | 1, disabled: boolean) => void;
clearCloneSetCountHold: () => void;
toggleCloneDetailModule: (id: string) => void;
setCloneModelPanelTab: (value: CloneModelPanelTab) => void;
toggleCloneModelScene: (scene: string) => void;
setCloneModelCustomScene: (value: string) => void;
setOpenCloneModelSelect: (value: CloneModelSelectKey | null) => void;
setCloneModelSelectDropUp: (value: boolean) => void;
setCloneVideoQuality: (value: CloneVideoQualityKey) => void;
setCloneVideoDuration: (value: number) => void;
clampCloneVideoDuration: (value: number) => number;
setCloneVideoSmart: (updater: (current: boolean) => boolean) => void;
handleGenerate: () => void;
onCancelGenerate: () => void;
formatRatioDisplayValue: (value: string) => string;
setVideoOutfitFiles?: (video: File | null, ref: File | null) => void;
onStartVideoPlan?: () => void;
}
export default function EcommerceClonePanel({
productInputRef,
cloneReferenceInputRef,
productImages,
isProductUploadDragging,
cloneOutput,
cloneOutputOptions,
cloneBasicSelects,
openCloneBasicSelect,
cloneReferenceMode,
cloneReferenceImages,
maxCloneReferenceImages,
cloneReplicateLevel,
cloneReplicateLevelOptions,
cloneSetCounts,
cloneSetCountOptions,
cloneSetTotal,
minCloneSetTotal,
maxCloneSetTotal,
selectedCloneDetailModules,
cloneDetailModules,
cloneModelPanelTab,
tryOnScenes,
selectedCloneModelScenes,
cloneModelCustomScene,
cloneModelSelects,
openCloneModelSelect,
cloneModelSelectDropUp,
cloneVideoQuality,
cloneVideoQualityOptions,
cloneVideoDuration,
cloneVideoDurationMin,
cloneVideoDurationMax,
cloneVideoDurationStyle,
cloneVideoSmart,
canGenerate,
status,
lastFailedActionRef,
setIsProductUploadDragging,
handleProductDrop,
removeProductImage,
handleProductUpload,
handleCloneOutputChange,
setOpenCloneBasicSelect,
setCloneReferenceMode,
handleCloneReferenceUpload,
isCloneReferenceDragging,
handleCloneReferenceDragOver,
handleCloneReferenceDragLeave,
handleCloneReferenceDrop,
setCloneReplicateLevel,
startCloneSetCountHold,
clearCloneSetCountHold,
toggleCloneDetailModule,
setCloneModelPanelTab,
toggleCloneModelScene,
setCloneModelCustomScene,
setOpenCloneModelSelect,
setCloneModelSelectDropUp,
setCloneVideoQuality,
setCloneVideoDuration,
clampCloneVideoDuration,
setCloneVideoSmart,
handleGenerate,
onCancelGenerate,
formatRatioDisplayValue,
setVideoOutfitFiles,
onStartVideoPlan,
}: EcommerceClonePanelProps) {
const videoOutfitVideoRef = useRef<HTMLInputElement>(null);
const videoOutfitRefRef = useRef<HTMLInputElement>(null);
const [videoOutfitVideoUrl, setVideoOutfitVideoUrl] = useState<string | null>(null);
const [videoOutfitRefUrl, setVideoOutfitRefUrl] = useState<string | null>(null);
const [zoomImage, setZoomImage] = useState<{ src: string; x: number; y: number } | null>(null);
const handleFileMouseEnter = (src: string, event: React.MouseEvent<HTMLElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
setZoomImage({ src, x: rect.left + rect.width / 2, y: rect.top });
};
const handleFileMouseLeave = () => setZoomImage(null);
const handleVideoOutfitVideoChange = () => {
const file = videoOutfitVideoRef.current?.files?.[0] || null;
if (file) setVideoOutfitVideoUrl(URL.createObjectURL(file));
setVideoOutfitFiles?.(file, videoOutfitRefRef.current?.files?.[0] || null);
};
const handleVideoOutfitRefChange = () => {
const file = videoOutfitRefRef.current?.files?.[0] || null;
if (file) setVideoOutfitRefUrl(URL.createObjectURL(file));
setVideoOutfitFiles?.(videoOutfitVideoRef.current?.files?.[0] || null, file);
};
return (
<>
<div className="product-clone-panel__scroll clone-ai-panel">
<header className="clone-ai-logo">
<span className="clone-ai-logo__mark">AI</span>
<strong></strong>
</header>
<section className="clone-ai-card">
<h2>
<CloudUploadOutlined />
</h2>
<div
role="button"
tabIndex={0}
className={`clone-ai-upload-zone${isProductUploadDragging ? " is-dragging" : ""}`}
onClick={() => productInputRef.current?.click()}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return;
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
productInputRef.current?.click();
}
}}
onDragEnter={(event) => {
event.preventDefault();
setIsProductUploadDragging(true);
}}
onDragOver={(event) => event.preventDefault()}
onDragLeave={() => setIsProductUploadDragging(false)}
onDrop={handleProductDrop}
>
<div className="clone-ai-upload-main">
<span className="clone-ai-upload-icon">
<FileImageOutlined />
</span>
<span className="clone-ai-upload-title"></span>
<strong>
<span aria-hidden="true">+</span>
</strong>
<span className="clone-ai-upload-hint"> 7 </span>
</div>
{productImages.length ? (
<div className="clone-ai-uploaded-files" aria-label="已上传商品原图">
{productImages.map((item) => (
<figure key={item.id} className="clone-ai-uploaded-file">
<img src={item.src} alt={item.name} />
<span className="uploaded-image-zoom" aria-hidden="true">
<img src={item.src} alt="" />
</span>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
removeProductImage(item.id);
}}
aria-label={`删除${item.name}`}
>
<CloseOutlined />
</button>
</figure>
))}
</div>
) : null}
</div>
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} />
</section>
<section className="clone-ai-card">
<h2>
<SettingOutlined />
</h2>
<div className="clone-ai-settings-section">
<span className="clone-ai-settings-label"></span>
<div className="clone-ai-tag-group" role="radiogroup" aria-label="生成内容">
{cloneOutputOptions.map((option) => (
<button
key={option.key}
type="button"
className={cloneOutput === option.key ? "is-active" : ""}
aria-pressed={cloneOutput === option.key}
onClick={() => handleCloneOutputChange(option.key)}
>
{option.label}
</button>
))}
</div>
</div>
<div className="clone-ai-settings-section">
<span className="clone-ai-settings-label"></span>
<div className="clone-ai-select-group">
{cloneBasicSelects.map((item) => {
const hasMultipleOptions = item.options.length > 1;
const isOpen = hasMultipleOptions && openCloneBasicSelect === item.key;
return (
<div key={item.key} className="clone-ai-basic-select" data-clone-basic-select>
<button
type="button"
className={`${isOpen ? "is-open" : ""}${hasMultipleOptions ? "" : " is-static"}`}
aria-expanded={hasMultipleOptions ? isOpen : undefined}
aria-haspopup={hasMultipleOptions ? "listbox" : undefined}
aria-controls={hasMultipleOptions ? `clone-basic-select-${item.key}` : undefined}
onClick={() => setOpenCloneBasicSelect(hasMultipleOptions ? (isOpen ? null : item.key) : null)}
>
<span>{item.label}</span>
<strong>{item.key === "ratio" ? formatRatioDisplayValue(item.value) : item.value}</strong>
{hasMultipleOptions ? <i aria-hidden="true" /> : null}
</button>
{hasMultipleOptions && isOpen ? (
<div id={`clone-basic-select-${item.key}`} className="clone-ai-basic-select__menu" role="listbox">
{item.options.map((option) => (
<button
key={option}
type="button"
className={item.value === option ? "is-active" : ""}
role="option"
aria-selected={item.value === option}
onClick={() => {
item.onChange(option);
setOpenCloneBasicSelect(null);
}}
>
{item.key === "ratio" ? formatRatioDisplayValue(option) : option}
</button>
))}
</div>
) : null}
</div>
);
})}
</div>
</div>
</section>
{cloneOutput === "hot" ? (
<section className="clone-ai-replicate-panel" aria-label="爆款图复刻设置">
<div className="clone-ai-replicate-section">
<span className="clone-ai-replicate-title"></span>
<div className="clone-ai-replicate-tabs" role="tablist" aria-label="参考内容来源">
<button
type="button"
className={cloneReferenceMode === "upload" ? "is-active" : ""}
aria-selected={cloneReferenceMode === "upload"}
onClick={() => setCloneReferenceMode("upload")}
>
</button>
<button
type="button"
className={cloneReferenceMode === "link" ? "is-active" : ""}
aria-selected={cloneReferenceMode === "link"}
onClick={() => setCloneReferenceMode("link")}
>
</button>
</div>
{cloneReferenceMode === "upload" ? (
<button
type="button"
className={`clone-ai-replicate-upload${isCloneReferenceDragging ? " is-dragging" : ""}${cloneReferenceImages.length ? " has-files" : ""}`}
onClick={() => cloneReferenceInputRef.current?.click()}
onDragOver={handleCloneReferenceDragOver}
onDragLeave={handleCloneReferenceDragLeave}
onDrop={handleCloneReferenceDrop}
>
{cloneReferenceImages.length ? (
<>
<div className="clone-ai-replicate-files">
{cloneReferenceImages.map((item) => (
<figure
key={item.id}
className="clone-ai-replicate-file"
onMouseEnter={(e) => handleFileMouseEnter(item.src, e)}
onMouseLeave={handleFileMouseLeave}
>
<img src={item.src} alt="" />
</figure>
))}
</div>
<span className="clone-ai-replicate-add-more">
<CloudUploadOutlined />
</span>
</>
) : (
<span>
<CloudUploadOutlined />
<span className="clone-ai-replicate-upload-text"></span>
</span>
)}
<em>{cloneReferenceImages.length ? `已选 ${cloneReferenceImages.length}/${maxCloneReferenceImages}` : `最多 ${maxCloneReferenceImages}`}</em>
{isCloneReferenceDragging ? (
<div className="clone-ai-replicate-upload-overlay">
<CloudUploadOutlined />
<span></span>
</div>
) : null}
</button>
) : (
<label className="clone-ai-replicate-link">
<input placeholder="粘贴商品图或详情页链接" />
</label>
)}
<input
ref={cloneReferenceInputRef}
type="file"
accept="image/jpeg,image/png,image/webp"
multiple
onChange={handleCloneReferenceUpload}
/>
</div>
<div className="clone-ai-replicate-section">
<span className="clone-ai-replicate-title"></span>
<div className="clone-ai-replicate-levels" role="radiogroup" aria-label="复刻程度">
{cloneReplicateLevelOptions.map((option) => (
<button
key={option.key}
type="button"
className={cloneReplicateLevel === option.key ? "is-active" : ""}
aria-pressed={cloneReplicateLevel === option.key}
onClick={() => setCloneReplicateLevel(option.key)}
>
<strong>{option.title}</strong>
<span>{option.desc}</span>
</button>
))}
</div>
</div>
</section>
) : null}
{cloneOutput === "set" ? (
<section className="clone-ai-count-panel" aria-label="套图图片数量">
<p> 1-16 </p>
<div className="clone-ai-count-list">
{cloneSetCountOptions.map((item) => {
const count = cloneSetCounts[item.key];
const decrementDisabled = count <= 0 || cloneSetTotal <= minCloneSetTotal;
const incrementDisabled = cloneSetTotal >= maxCloneSetTotal;
return (
<div key={item.key} className="clone-ai-count-row">
<div className="clone-ai-count-copy">
<strong>{item.title}</strong>
<span>{item.desc}</span>
</div>
<div className="clone-ai-count-stepper" aria-label={`${item.title}数量`}>
<button
type="button"
disabled={decrementDisabled}
onPointerDown={(event) => {
event.preventDefault();
startCloneSetCountHold(item.key, -1, decrementDisabled);
}}
onPointerUp={clearCloneSetCountHold}
onPointerLeave={clearCloneSetCountHold}
onPointerCancel={clearCloneSetCountHold}
onBlur={clearCloneSetCountHold}
aria-label={`减少${item.title}`}
>
-
</button>
<b>{count}</b>
<button
type="button"
disabled={incrementDisabled}
onPointerDown={(event) => {
event.preventDefault();
startCloneSetCountHold(item.key, 1, incrementDisabled);
}}
onPointerUp={clearCloneSetCountHold}
onPointerLeave={clearCloneSetCountHold}
onPointerCancel={clearCloneSetCountHold}
onBlur={clearCloneSetCountHold}
aria-label={`增加${item.title}`}
>
+
</button>
</div>
</div>
);
})}
</div>
</section>
) : null}
{cloneOutput === "detail" ? (
<section className="clone-ai-module-panel" aria-label="详情图包含模块">
<p>
<QuestionCircleOutlined />
</p>
<div className="clone-ai-module-list">
{cloneDetailModules.map((module) => {
const isSelected = selectedCloneDetailModules.includes(module.id);
return (
<button
key={module.id}
type="button"
className={isSelected ? "is-active" : ""}
aria-pressed={isSelected}
onClick={() => toggleCloneDetailModule(module.id)}
>
<strong>{module.title}</strong>
<span>{module.desc}</span>
</button>
);
})}
</div>
</section>
) : null}
{cloneOutput === "model" ? (
<section className="clone-ai-model-panel" aria-label="模特图设置">
<div className="clone-ai-model-tabs" role="tablist" aria-label="模特图设置类型">
<button
type="button"
className={cloneModelPanelTab === "scene" ? "is-active" : ""}
aria-selected={cloneModelPanelTab === "scene"}
onClick={() => setCloneModelPanelTab("scene")}
>
</button>
<button
type="button"
className={cloneModelPanelTab === "model" ? "is-active" : ""}
aria-selected={cloneModelPanelTab === "model"}
onClick={() => setCloneModelPanelTab("model")}
>
</button>
</div>
<div className="clone-ai-model-scroll">
{cloneModelPanelTab === "scene" ? (
<div className="clone-ai-model-scenes">
<div className="clone-ai-model-scene-grid">
{tryOnScenes.map((scene) => {
const isSelected = selectedCloneModelScenes.includes(scene);
return (
<button
key={scene}
type="button"
className={isSelected ? "is-active" : ""}
aria-pressed={isSelected}
onClick={() => toggleCloneModelScene(scene)}
>
<span aria-hidden="true" />
{scene}
</button>
);
})}
</div>
<label className="clone-ai-model-textarea">
<strong></strong>
<textarea
value={cloneModelCustomScene}
onChange={(event) => setCloneModelCustomScene(event.target.value)}
placeholder="描述你想要的场景:如秋季枫叶小径、暖色调午后阳光、模特倚靠树干..."
/>
</label>
</div>
) : (
<div className="clone-ai-model-profile">
<div className="clone-ai-model-select-grid">
{cloneModelSelects.map((item) => {
const isOpen = openCloneModelSelect === item.key;
return (
<div
key={item.key}
className={`clone-ai-model-select${isOpen ? " is-open" : ""}${
isOpen && cloneModelSelectDropUp ? " is-drop-up" : ""
}`}
data-clone-model-select
>
<span>{item.label}</span>
<button
type="button"
className={isOpen ? "is-open" : ""}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-controls={`clone-model-select-${item.key}`}
onClick={(event) => {
setOpenCloneBasicSelect(null);
if (!isOpen) {
event.currentTarget.scrollIntoView({ block: "center", inline: "nearest" });
const triggerRect = event.currentTarget.getBoundingClientRect();
const scrollRect = event.currentTarget.closest(".clone-ai-model-scroll")?.getBoundingClientRect();
const lowerBoundary = Math.min(window.innerHeight, scrollRect?.bottom ?? window.innerHeight);
const upperBoundary = Math.max(0, scrollRect?.top ?? 0);
const estimatedMenuHeight = Math.min(150, item.options.length * 36 + 12);
const belowSpace = lowerBoundary - triggerRect.bottom;
const aboveSpace = triggerRect.top - upperBoundary;
setCloneModelSelectDropUp(belowSpace < estimatedMenuHeight && aboveSpace > belowSpace);
} else {
setCloneModelSelectDropUp(false);
}
setOpenCloneModelSelect(isOpen ? null : item.key);
}}
>
<strong>{item.value}</strong>
<i aria-hidden="true" />
</button>
{isOpen ? (
<div id={`clone-model-select-${item.key}`} className="clone-ai-model-select__menu" role="listbox">
{item.options.map((option) => (
<button
key={option}
type="button"
className={item.value === option ? "is-active" : ""}
role="option"
aria-selected={item.value === option}
onClick={() => {
item.onChange(option);
setOpenCloneModelSelect(null);
setCloneModelSelectDropUp(false);
}}
>
{option}
</button>
))}
</div>
) : null}
</div>
);
})}
</div>
</div>
)}
</div>
</section>
) : null}
{cloneOutput === "video" ? (
<section className="clone-ai-video-panel" aria-label="短视频设置">
<div className="clone-ai-video-section">
<span className="clone-ai-video-title"></span>
<div className="clone-ai-video-options">
{cloneVideoQualityOptions.map((option) => (
<button
key={option.key}
type="button"
className={cloneVideoQuality === option.key ? "is-active" : ""}
aria-pressed={cloneVideoQuality === option.key}
onClick={() => setCloneVideoQuality(option.key)}
>
<strong>{option.label}</strong>
<span>{option.desc}</span>
</button>
))}
</div>
</div>
<div className="clone-ai-video-section">
<div className="clone-ai-video-title-row">
<span className="clone-ai-video-title"></span>
<strong>{cloneVideoDuration}</strong>
</div>
<div className="clone-ai-duration-control" style={cloneVideoDurationStyle}>
<input
type="range"
min={cloneVideoDurationMin}
max={cloneVideoDurationMax}
step={5}
value={cloneVideoDuration}
onChange={(event) => setCloneVideoDuration(clampCloneVideoDuration(Number(event.target.value)))}
aria-label="短视频时长"
/>
<div className="clone-ai-duration-scale" aria-hidden="true">
<span>5</span>
<span>15</span>
<span>30</span>
<span>45</span>
</div>
</div>
</div>
<button
type="button"
className={`clone-ai-video-smart${cloneVideoSmart ? " is-on" : ""}`}
aria-pressed={cloneVideoSmart}
onClick={() => setCloneVideoSmart((current) => !current)}
>
<span>
<strong></strong>
<em></em>
</span>
<i aria-hidden="true" />
</button>
</section>
) : null}
{cloneOutput === "video" && onStartVideoPlan ? (
<button type="button" className="clone-ai-generate" onClick={onStartVideoPlan}>
</button>
) : null}
{cloneOutput === "video-outfit" ? (
<section className="clone-ai-video-panel" aria-label="视频换装">
<div className="clone-ai-video-section">
<span className="clone-ai-video-title"></span>
<div className="clone-ai-video-outfit-upload">
<input
ref={videoOutfitVideoRef}
type="file"
accept="video/*"
onChange={handleVideoOutfitVideoChange}
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitVideoRef.current?.click()}>
{videoOutfitVideoUrl ? "重新上传视频" : "点击上传视频"}
</button>
{videoOutfitVideoUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
</div>
<div className="clone-ai-video-section">
<span className="clone-ai-video-title">/</span>
<div className="clone-ai-video-outfit-upload">
<input
ref={videoOutfitRefRef}
type="file"
accept="image/*"
onChange={handleVideoOutfitRefChange}
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitRefRef.current?.click()}>
{videoOutfitRefUrl ? "重新上传参考图" : "点击上传参考图"}
</button>
{videoOutfitRefUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
</div>
</section>
) : null}
<button type="button" className="clone-ai-generate" disabled={!canGenerate || cloneOutput === "video"} onClick={status === "failed" && lastFailedActionRef.current ? lastFailedActionRef.current : handleGenerate} style={cloneOutput === "video" ? { display: "none" } : undefined}>
{status === "generating" ? <LoadingOutlined /> : status === "failed" ? <ReloadOutlined /> : null}
{status === "generating" ? "生成中..." : status === "failed" ? "重新生成" : cloneOutput === "video-outfit" ? "✦ 开始换装" : "✦ 开始生成"}
</button>
{status === "generating" && cloneOutput !== "video" ? (
<button type="button" className="clone-ai-generate clone-ai-generate--cancel" onClick={onCancelGenerate}>
{"\u53d6\u6d88\u751f\u6210"}
</button>
) : null}
</div>
{zoomImage
? createPortal(
<div
className="clone-ai-zoom-portal"
style={{ left: zoomImage.x, top: zoomImage.y } as CSSProperties}
onMouseLeave={handleFileMouseLeave}
>
<img src={zoomImage.src} alt="" />
</div>,
document.body,
)
: null}
</>
);
}